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
137 changes: 38 additions & 99 deletions generic_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,123 +32,82 @@ func (m *GenericManager) Warnings() []string {
return m.warnings
}

func (m *GenericManager) Init(ctx context.Context) (*Result, error) {
input := CommandInput{
Args: map[string]string{},
Flags: map[string]any{},
}

cmd, err := m.translator.BuildCommand(m.def.Name, "init", input)
// run builds the command chain for an operation (base command plus any
// then: entries) and executes them in order. It returns the first
// command's result with subsequent results attached in Then. Execution
// stops after the first non-zero exit; Success() on the returned result
// reflects the whole chain.
func (m *GenericManager) run(ctx context.Context, operation string, input CommandInput) (*Result, error) {
cmds, err := m.translator.BuildCommands(m.def.Name, operation, input)
if err != nil {
return nil, err
}
var first *Result
for i, cmd := range cmds {
res, err := m.runner.Run(ctx, m.dir, cmd...)
if i == 0 {
first = res
} else if first != nil && res != nil {
first.Then = append(first.Then, res)
}
if err != nil {
return first, err
}
if res == nil || res.ExitCode != 0 {
return first, nil
}
}
return first, nil
}

return m.runner.Run(ctx, m.dir, cmd...)
func (m *GenericManager) Init(ctx context.Context) (*Result, error) {
return m.run(ctx, "init", CommandInput{})
}

func (m *GenericManager) Install(ctx context.Context, opts InstallOptions) (*Result, error) {
input := CommandInput{
Args: map[string]string{},
return m.run(ctx, "install", CommandInput{
Flags: map[string]any{
"frozen": opts.Frozen,
"clean": opts.Clean,
"production": opts.Production,
},
}

cmd, err := m.translator.BuildCommand(m.def.Name, "install", input)
if err != nil {
return nil, err
}

return m.runner.Run(ctx, m.dir, cmd...)
})
}

func (m *GenericManager) Add(ctx context.Context, pkg string, opts AddOptions) (*Result, error) {
input := CommandInput{
Args: map[string]string{
argPackage: pkg,
},
Args: map[string]string{argPackage: pkg},
Flags: map[string]any{
"dev": opts.Dev,
"optional": opts.Optional,
"exact": opts.Exact,
"workspace": opts.Workspace,
},
}

if opts.Version != "" {
input.Args["version"] = opts.Version
}

cmd, err := m.translator.BuildCommand(m.def.Name, "add", input)
if err != nil {
return nil, err
}

return m.runner.Run(ctx, m.dir, cmd...)
return m.run(ctx, "add", input)
}

func (m *GenericManager) Remove(ctx context.Context, pkg string) (*Result, error) {
input := CommandInput{
Args: map[string]string{
argPackage: pkg,
},
Flags: map[string]any{},
}

cmd, err := m.translator.BuildCommand(m.def.Name, "remove", input)
if err != nil {
return nil, err
}

return m.runner.Run(ctx, m.dir, cmd...)
return m.run(ctx, "remove", CommandInput{Args: map[string]string{argPackage: pkg}})
}

func (m *GenericManager) List(ctx context.Context) (*Result, error) {
input := CommandInput{
Args: map[string]string{},
Flags: map[string]any{},
}

cmd, err := m.translator.BuildCommand(m.def.Name, "list", input)
if err != nil {
return nil, err
}

return m.runner.Run(ctx, m.dir, cmd...)
return m.run(ctx, "list", CommandInput{})
}

func (m *GenericManager) Outdated(ctx context.Context) (*Result, error) {
input := CommandInput{
Args: map[string]string{},
Flags: map[string]any{},
}

cmd, err := m.translator.BuildCommand(m.def.Name, "outdated", input)
if err != nil {
return nil, err
}

return m.runner.Run(ctx, m.dir, cmd...)
return m.run(ctx, "outdated", CommandInput{})
}

func (m *GenericManager) Update(ctx context.Context, pkg string) (*Result, error) {
input := CommandInput{
Args: map[string]string{},
Flags: map[string]any{},
}

input := CommandInput{}
if pkg != "" {
input.Args[argPackage] = pkg
input.Args = map[string]string{argPackage: pkg}
}

cmd, err := m.translator.BuildCommand(m.def.Name, "update", input)
if err != nil {
return nil, err
}

return m.runner.Run(ctx, m.dir, cmd...)
return m.run(ctx, "update", input)
}

func (m *GenericManager) Supports(cap Capability) bool {
Expand All @@ -172,31 +131,11 @@ func (m *GenericManager) Capabilities() []Capability {
}

func (m *GenericManager) Vendor(ctx context.Context) (*Result, error) {
input := CommandInput{
Args: map[string]string{},
Flags: map[string]any{},
}

cmd, err := m.translator.BuildCommand(m.def.Name, "vendor", input)
if err != nil {
return nil, err
}

return m.runner.Run(ctx, m.dir, cmd...)
return m.run(ctx, "vendor", CommandInput{})
}

func (m *GenericManager) Resolve(ctx context.Context) (*Result, error) {
input := CommandInput{
Args: map[string]string{},
Flags: map[string]any{},
}

cmd, err := m.translator.BuildCommand(m.def.Name, "resolve", input)
if err != nil {
return nil, err
}

return m.runner.Run(ctx, m.dir, cmd...)
return m.run(ctx, "resolve", CommandInput{})
}

func (m *GenericManager) Path(ctx context.Context, pkg string) (*PathResult, error) {
Expand Down
145 changes: 145 additions & 0 deletions generic_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,151 @@ func TestGenericManager_Add_CargoVersion(t *testing.T) {
}
}

func TestGenericManager_Add_RunsThenChain(t *testing.T) {
runner := NewMockRunner()
mgr := newTestManager(embeddedDef(t, "gomod"), runner)
res, err := mgr.Add(context.Background(), "github.com/pkg/errors", AddOptions{})
if err != nil {
t.Fatalf("Add: %v", err)
}
if len(runner.Captured) != 2 {
t.Fatalf("captured %d commands, want 2 (go get + go mod tidy)", len(runner.Captured))
}
if !slicesEqual(runner.Captured[0], []string{"go", "get", "github.com/pkg/errors"}) {
t.Errorf("cmd[0] = %v", runner.Captured[0])
}
if !slicesEqual(runner.Captured[1], []string{"go", "mod", "tidy"}) {
t.Errorf("cmd[1] = %v", runner.Captured[1])
}
if !slicesEqual(res.Command, runner.Captured[0]) {
t.Errorf("res.Command = %v, want the primary go get", res.Command)
}
if len(res.Then) != 1 || !slicesEqual(res.Then[0].Command, runner.Captured[1]) {
t.Errorf("res.Then = %+v, want the go mod tidy result", res.Then)
}
if !res.Success() {
t.Error("Success() should be true when all chain commands exit zero")
}
}

func TestGenericManager_Add_ChainNilFollowupError(t *testing.T) {
// A Runner that returns (nil, err) for the second call, as
// PolicyRunner does on a policy violation.
runner := NewMockRunner()
runner.Results = []*Result{{Command: []string{"go", "get", "x"}, ExitCode: 0}}
runner.Errors = []error{nil, errors.New("policy denied")}
mgr := newTestManager(embeddedDef(t, "gomod"), runner)
res, err := mgr.Add(context.Background(), "example.com/x", AddOptions{})
if err == nil {
t.Fatal("expected error from follow-up")
}
if res == nil {
t.Fatal("expected first command's result")
}
for _, th := range res.Then {
if th == nil {
t.Fatal("Then must not contain nil entries")
}
}
// Success() must not panic; its value is not asserted since the
// failure is signalled via the returned error, not the Result.
_ = res.Success()
}

func TestGenericManager_Add_ChainStopsOnFailure(t *testing.T) {
runner := NewMockRunner()
runner.Results = []*Result{
{Command: []string{"go", "get", "x"}, ExitCode: 1, Stderr: "boom"},
}
mgr := newTestManager(embeddedDef(t, "gomod"), runner)
res, err := mgr.Add(context.Background(), "example.com/x", AddOptions{})
if err != nil {
t.Fatalf("Add: %v", err)
}
if res.Success() {
t.Error("Success() should be false when first command exits non-zero")
}
if len(runner.Captured) != 1 {
t.Errorf("captured %d commands, want 1 (chain should stop)", len(runner.Captured))
}
}

func TestGenericManager_Add_ChainThenFails(t *testing.T) {
runner := NewMockRunner()
runner.Results = []*Result{
{Command: []string{"go", "get", "x"}, ExitCode: 0},
{Command: []string{"go", "mod", "tidy"}, ExitCode: 1, Stderr: "tidy failed"},
}
mgr := newTestManager(embeddedDef(t, "gomod"), runner)
res, err := mgr.Add(context.Background(), "example.com/x", AddOptions{})
if err != nil {
t.Fatalf("Add: %v", err)
}
if res.Success() {
t.Error("Success() should be false when a then: command exits non-zero")
}
if !slicesEqual(res.Command, []string{"go", "get", "x"}) {
t.Errorf("res.Command = %v, want the primary command", res.Command)
}
if len(res.Then) != 1 {
t.Fatalf("res.Then = %d, want 1", len(res.Then))
}
if res.Then[0].ExitCode != 1 || res.Then[0].Stderr != "tidy failed" {
t.Errorf("res.Then[0] = %+v", res.Then[0])
}
}

func TestGenericManager_Replace_RunsChain(t *testing.T) {
runner := NewMockRunner()
mgr := newTestManager(embeddedDef(t, "gomod"), runner)
res, err := mgr.Replace(context.Background(), "example.com/x", ReplaceOptions{Path: "../x"})
if err != nil {
t.Fatalf("Replace: %v", err)
}
// gomod Replace runs `go mod edit -replace ...` then `go mod tidy`
// as two separate operations; both must be reachable via Then.
if len(runner.Captured) != 2 {
t.Fatalf("captured %d commands, want 2", len(runner.Captured))
}
if !slicesEqual(runner.Captured[1], []string{"go", "mod", "tidy"}) {
t.Errorf("cmd[1] = %v, want go mod tidy", runner.Captured[1])
}
if len(res.Then) != 1 || !slicesEqual(res.Then[0].Command, runner.Captured[1]) {
t.Errorf("res.Then = %+v, want the tidy result", res.Then)
}
}

func TestGenericManager_Replace_RunsOperationThen(t *testing.T) {
// A synthetic definition where the replace operation itself has a
// then: entry, to prove Replace routes through the chain executor.
def := &definitions.Definition{
Name: "gomod",
Binary: "go",
Commands: map[string]definitions.Command{
"replace": {
Base: []string{"mod", "edit"},
Args: map[string]definitions.Arg{"spec": {Flag: "-replace"}},
Then: []definitions.Command{{Base: []string{"post"}}},
},
"tidy": {Base: []string{"mod", "tidy"}},
},
Capabilities: []string{"replace_path"},
}
runner := NewMockRunner()
mgr := newTestManager(def, runner)
_, err := mgr.Replace(context.Background(), "example.com/x", ReplaceOptions{Path: "../x"})
if err != nil {
t.Fatalf("Replace: %v", err)
}
// replace + its then: (post) + tidy = 3
if len(runner.Captured) != 3 {
t.Fatalf("captured %d commands, want 3: %v", len(runner.Captured), runner.Captured)
}
if !slicesEqual(runner.Captured[1], []string{"go", "post"}) {
t.Errorf("then: command not run: %v", runner.Captured)
}
}

func TestGenericManager_Path_Raw(t *testing.T) {
def := &definitions.Definition{
Name: "testpkg",
Expand Down
16 changes: 15 additions & 1 deletion manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,24 @@ type Result struct {
Duration time.Duration
Cwd string
Context ExecContext
// Then holds the results of any follow-up commands the operation
// ran after this one, in order. It is empty for single-command
// operations and when this command failed.
Then []*Result
}

// Success reports whether this command and every follow-up in Then
// exited zero.
func (r *Result) Success() bool {
return r.ExitCode == 0
if r.ExitCode != 0 {
return false
}
for _, t := range r.Then {
if !t.Success() {
return false
}
}
return true
}

type PathResult struct {
Expand Down
Loading
Loading