Skip to content
Open
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
36 changes: 30 additions & 6 deletions internal/app/azldev/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"path/filepath"
"runtime"
"strings"
"sync"
"time"

"github.com/charmbracelet/gum/confirm"
Expand Down Expand Up @@ -92,13 +93,34 @@ type Env struct {

// Fix suggestion: a list of human readable hints that will be printed after an error to help the user
// resolve the issue. Printed in FIFO order.
fixSuggestions []string
fixSuggestions *fixSuggestionState

// lockStore provides cached access to per-component lock files.
// Nil when no project directory is configured.
lockStore *lockfile.Store
}

type fixSuggestionState struct {
mu sync.Mutex
suggestions []string
}

// Add appends a fix suggestion in FIFO order.
func (suggestions *fixSuggestionState) Add(suggestion string) {
suggestions.mu.Lock()
defer suggestions.mu.Unlock()

suggestions.suggestions = append(suggestions.suggestions, suggestion)
}

// All returns a copy of all collected fix suggestions.
func (suggestions *fixSuggestionState) All() []string {
suggestions.mu.Lock()
defer suggestions.mu.Unlock()

return append([]string(nil), suggestions.suggestions...)
}

// Constructs a new [Env] using specified options.
func NewEnv(ctx context.Context, options EnvOptions) *Env {
var workDir, logDir, outputDir string
Expand Down Expand Up @@ -150,7 +172,7 @@ func NewEnv(ctx context.Context, options EnvOptions) *Env {
constructionTime: time.Now(),

// No fix suggestions to start.
fixSuggestions: []string{},
fixSuggestions: &fixSuggestionState{},

// Lock store: created when we have a project directory.
lockStore: newLockStore(options.ProjectDir, options.Config, options.Interfaces.FileSystemFactory),
Expand Down Expand Up @@ -300,12 +322,14 @@ func (env *Env) OutputDir() string {
// AddFixSuggestion records a human-readable hint that will be printed after an
// error to help the user resolve the issue. Suggestions are printed in FIFO order.
func (env *Env) AddFixSuggestion(suggestion string) {
env.fixSuggestions = append(env.fixSuggestions, suggestion)
env.fixSuggestions.Add(suggestion)
}

// PrintFixSuggestions prints the current fix suggestions, if any.
func (env *Env) PrintFixSuggestions() {
if len(env.fixSuggestions) == 0 {
suggestions := env.fixSuggestions.All()

if len(suggestions) == 0 {
return
}

Expand All @@ -324,7 +348,7 @@ func (env *Env) PrintFixSuggestions() {
paddingSize := len(padding)

maxMsgLength := 0
for _, suggestion := range env.fixSuggestions {
for _, suggestion := range suggestions {
if len(suggestion) > maxMsgLength {
maxMsgLength = len(suggestion)
}
Expand All @@ -335,7 +359,7 @@ func (env *Env) PrintFixSuggestions() {

slog.Warn(boxEdgeString)

for _, suggestion := range env.fixSuggestions {
for _, suggestion := range suggestions {
slog.Warn(padding + suggestion)
}

Expand Down
82 changes: 82 additions & 0 deletions internal/app/azldev/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
package azldev_test

import (
"bytes"
"context"
"fmt"
"log/slog"
"os"
"sync"
"testing"
"time"

Expand All @@ -16,6 +20,19 @@ import (
"github.com/stretchr/testify/require"
)

func setupTestLogger(logBuffer *bytes.Buffer) func() {
logger := slog.New(slog.NewTextHandler(logBuffer, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))

previousDefaultLogger := slog.Default()
slog.SetDefault(logger)

return func() {
slog.SetDefault(previousDefaultLogger)
}
}

func TestNewEnv(t *testing.T) {
const (
testProjectRoot = "/non/existent/dir"
Expand Down Expand Up @@ -168,4 +185,69 @@ func TestFixSuggestions(t *testing.T) {
testEnv.Env.PrintFixSuggestions()
})
})

t.Run("suggestions added from child envs are visible on the parent env", func(t *testing.T) {
testEnv := testutils.NewTestEnv(t)

const suggestionCount = 32

var waitGroup sync.WaitGroup
for suggestionIndex := range suggestionCount {
waitGroup.Add(1)

go func(index int) {
defer waitGroup.Done()

childEnv, cancel := testEnv.Env.WithCancel()
defer cancel()

childEnv.AddFixSuggestion(fmt.Sprintf("child suggestion %d", index))
}(suggestionIndex)
}

waitGroup.Wait()

var logBuffer bytes.Buffer

restoreLogger := setupTestLogger(&logBuffer)
defer restoreLogger()

testEnv.Env.PrintFixSuggestions()

output := logBuffer.String()
for suggestionIndex := range suggestionCount {
assert.Contains(t, output, fmt.Sprintf("child suggestion %d", suggestionIndex))
}
})

t.Run("concurrent suggestions on the same env are all preserved", func(t *testing.T) {
testEnv := testutils.NewTestEnv(t)

const suggestionCount = 128

var waitGroup sync.WaitGroup
for suggestionIndex := range suggestionCount {
waitGroup.Add(1)

go func(index int) {
defer waitGroup.Done()

testEnv.Env.AddFixSuggestion(fmt.Sprintf("shared suggestion %d", index))
}(suggestionIndex)
}

waitGroup.Wait()

var logBuffer bytes.Buffer

restoreLogger := setupTestLogger(&logBuffer)
defer restoreLogger()

testEnv.Env.PrintFixSuggestions()

output := logBuffer.String()
for suggestionIndex := range suggestionCount {
assert.Contains(t, output, fmt.Sprintf("shared suggestion %d", suggestionIndex))
}
})
}
Loading