Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .cursor/rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Project Rules

## Before making code changes

- Discuss the design with the user
- Show example changes to make sure we are in the right direction
- If the user request is not clear, ask for more details
- List possible alternatives to make the change

## Making code changes

- Do the minimal change needed, no extra code that is not needed right now
- Avoid unrelated changes (spelling, whitespace, etc.)
- Keep changes small to make human review easy and avoid mistakes

## After making code changes

- Run `make fmt` to format code with golangci-lint formatters
- Run `make test` to verify all tests pass
- Running specific package tests (e.g., `go test ./pkg/test/...`) is fine for quick local verification

## File organization

- Keep files focused - separate files for different concerns (e.g., `html.go`, `yaml.go`, `summary.go`)
- All files need SPDX license headers - check existing files for the format

## Error handling

- Check existing code for error formatting conventions

## Testing

- Use `helpers.FakeTime(t)` for time-dependent tests to ensure reproducibility

## Commit messages

When the user wants to commit changes, suggest a commit message.

The main purpose is to explain why the change was made - what are we trying to do.

Content guidelines:
- Explain how the change affects the user - what is the new or modified behavior
- If the change affects performance, include measurements and description of how we measured
- If the change modifies the output, include example output with and without the change
- If the change introduces new logs, show example logs including the changed or new logs
- If several alternatives were considered, explain why we chose the particular solution
- Discuss the negative effects of the change if any
- If the change includes new APIs, describe the new APIs and how they are used
- Avoid describing details that are best seen in the diff

Footer:
- Include `Assisted-by: Cursor/{model-name}` footer
33 changes: 26 additions & 7 deletions pkg/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import (
e2eenv "github.com/ramendr/ramen/e2e/env"
"github.com/ramendr/ramen/e2e/types"
"go.uber.org/zap"
"sigs.k8s.io/yaml"

"github.com/ramendr/ramenctl/pkg/console"
"github.com/ramendr/ramenctl/pkg/report"
)

// Command is a ramenctl generic command used by all ramenctl commands. Note that the config is not
Expand Down Expand Up @@ -134,14 +134,33 @@ func (c *Command) Close() {
c.closeLog()
}

// WriteReport writes report in yaml format to the command output directory.
func (c *Command) WriteReport(report any) error {
data, err := yaml.Marshal(report)
// OpenReport opens a report file for writing in the specified format.
func (c *Command) OpenReport(format string) (*os.File, error) {
path := filepath.Join(c.outputDir, c.name+"."+format)
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("failed to marshal report: %w", err)
return nil, fmt.Errorf("failed to create report file %s: %w", path, err)
}
return file, nil
}

// WriteYAMLReport writes any report as YAML to the command output directory.
func (c *Command) WriteYAMLReport(r any) {
file, err := c.OpenReport("yaml")
if err != nil {
console.Error("failed to open report file: %s", err)
return
}
defer file.Close()

if err := report.WriteYAML(file, r); err != nil {
console.Error("failed to write report: %s", err)
return
}

if err := file.Close(); err != nil {
console.Error("failed to close report file: %s", err)
}
path := filepath.Join(c.outputDir, c.name+".yaml")
return os.WriteFile(path, data, 0o640)
}

func logName(commandName string) string {
Expand Down
8 changes: 2 additions & 6 deletions pkg/gather/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,16 +280,12 @@ func (c Command) withTimeout(d stdtime.Duration) (*Command, context.CancelFunc)
}

func (c *Command) failed() error {
if err := c.command.WriteReport(c.report); err != nil {
console.Error("failed to write report: %s", err)
}
c.command.WriteYAMLReport(c.report)
return fmt.Errorf("Gather %s", c.report.Status)
}

func (c *Command) passed() {
if err := c.command.WriteReport(c.report); err != nil {
console.Error("failed to write report: %s", err)
}
c.command.WriteYAMLReport(c.report)
console.Completed("Gather completed")
}

Expand Down
13 changes: 11 additions & 2 deletions pkg/report/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ type Base struct {
Status Status `json:"status,omitempty"`
Duration float64 `json:"duration,omitempty"`
Steps []*Step `json:"steps"`

// Summary is set by `validate` and `test` commands.
Summary *Summary `json:"summary,omitempty"`
}

// Application is application info.
Expand Down Expand Up @@ -128,9 +131,15 @@ func (r *Base) Equal(o *Base) bool {
if r.Duration != o.Duration {
return false
}
return slices.EqualFunc(r.Steps, o.Steps, func(a *Step, b *Step) bool {
if !slices.EqualFunc(r.Steps, o.Steps, func(a *Step, b *Step) bool {
return a.Equal(b)
})
}) {
return false
}
if !r.Summary.Equal(o.Summary) {
return false
}
return true
}

// AddStep adds a step to the report.
Expand Down
17 changes: 17 additions & 0 deletions pkg/report/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,23 @@ func TestBaseNotEqual(t *testing.T) {
t.Error("reports with different step should not be equal")
}
})
t.Run("summary", func(t *testing.T) {
r1 := report.NewBase("name")
r1.Summary = &report.Summary{report.SummaryKey("ok"): 5}
r2 := report.NewBase("name")
r2.Summary = &report.Summary{report.SummaryKey("ok"): 3}
if r1.Equal(r2) {
t.Error("reports with different summary should not be equal")
}
})
t.Run("nil vs non-nil summary", func(t *testing.T) {
r1 := report.NewBase("name")
r2 := report.NewBase("name")
r2.Summary = &report.Summary{}
if r1.Equal(r2) {
t.Error("reports with nil vs non-nil summary should not be equal")
}
})
}

func TestBaseDuration(t *testing.T) {
Expand Down
35 changes: 35 additions & 0 deletions pkg/report/summary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: The RamenDR authors
// SPDX-License-Identifier: Apache-2.0

package report

import "maps"

// SummaryKey is a typed key for Summary counters.
// Each package defines its own keys (e.g., validate.OK, test.Passed).
type SummaryKey string

// Summary is a counter for report summaries.
type Summary map[SummaryKey]int

// Add increments the counter for the given key.
func (s Summary) Add(key SummaryKey) {
s[key]++
}

// Get returns the count for the given key.
func (s Summary) Get(key SummaryKey) int {
return s[key]
}

// Equal returns true if both summaries are equal.
// Handles nil pointers: two nil summaries are equal, nil and non-nil are not.
func (s *Summary) Equal(o *Summary) bool {
if s == o {
return true
}
if s == nil || o == nil {
return false
}
return maps.Equal(*s, *o)
}
58 changes: 58 additions & 0 deletions pkg/report/summary_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: The RamenDR authors
// SPDX-License-Identifier: Apache-2.0

package report_test

import (
"maps"
"testing"

"sigs.k8s.io/yaml"

"github.com/ramendr/ramenctl/pkg/report"
)

func TestTestSummaryYAMLRoundtrip(t *testing.T) {
s1 := report.Summary{
report.SummaryKey("passed"): 5,
report.SummaryKey("failed"): 2,
report.SummaryKey("skipped"): 1,
report.SummaryKey("canceled"): 0,
}

data, err := yaml.Marshal(s1)
if err != nil {
t.Fatalf("failed to marshal summary: %v", err)
}

var s2 report.Summary
if err := yaml.Unmarshal(data, &s2); err != nil {
t.Fatalf("failed to unmarshal summary: %v", err)
}

if !maps.Equal(s1, s2) {
t.Errorf("expected %v, got %v", s1, s2)
}
}

func TestValidationSummaryYAMLRoundtrip(t *testing.T) {
s1 := report.Summary{
report.SummaryKey("ok"): 10,
report.SummaryKey("stale"): 1,
report.SummaryKey("problem"): 2,
}

data, err := yaml.Marshal(s1)
if err != nil {
t.Fatalf("failed to marshal summary: %v", err)
}

var s2 report.Summary
if err := yaml.Unmarshal(data, &s2); err != nil {
t.Fatalf("failed to unmarshal summary: %v", err)
}

if !maps.Equal(s1, s2) {
t.Errorf("expected %v, got %v", s1, s2)
}
}
21 changes: 21 additions & 0 deletions pkg/report/yaml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: The RamenDR authors
// SPDX-License-Identifier: Apache-2.0

package report

import (
"fmt"
"io"

"sigs.k8s.io/yaml"
)

// WriteYAML writes YAML for any report to the writer.
func WriteYAML(w io.Writer, report any) error {
data, err := yaml.Marshal(report)
if err != nil {
return fmt.Errorf("failed to marshal report: %w", err)
}
_, err = w.Write(data)
return err
}
12 changes: 4 additions & 8 deletions pkg/test/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,17 +274,13 @@ func (c *Command) gatherS3Data() {
}

func (c *Command) failed() error {
if err := c.command.WriteReport(c.report); err != nil {
console.Error("failed to write report: %s", err)
}
return fmt.Errorf("%s (%s)", c.report.Status, c.report.Summary)
c.command.WriteYAMLReport(c.report)
return fmt.Errorf("%s (%s)", c.report.Status, summaryString(c.report.Summary))
}

func (c *Command) passed() {
if err := c.command.WriteReport(c.report); err != nil {
console.Error("failed to write report: %s", err)
}
console.Completed("%s (%s)", c.report.Status, c.report.Summary)
c.command.WriteYAMLReport(c.report)
console.Completed("%s (%s)", c.report.Status, summaryString(c.report.Summary))
}

func (c *Command) startStep(name string) {
Expand Down
Loading