Skip to content

Commit 95548fe

Browse files
committed
Get rid of MiseExecutor, make ExecEnv mockable instead
MiseExecutor as an interface was created for testing, but I relized this is redundant and ExecEnv should be mockable instead.
1 parent 9aaccad commit 95548fe

File tree

5 files changed

+149
-124
lines changed

5 files changed

+149
-124
lines changed

toolprovider/mise/execenv/execenv.go

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,33 @@ const (
1616

1717
// ExecEnv contains everything needed to run mise commands in a specific environment
1818
// that is installed and pre-configured.
19-
type ExecEnv struct {
19+
type ExecEnv interface {
2020
// InstallDir is the directory where mise is installed. This is not necessarily the same as the data directory.
21-
InstallDir string
21+
InstallDir() string
22+
RunMise(args ...string) (string, error)
23+
RunMiseWithTimeout(timeout time.Duration, args ...string) (string, error)
24+
RunMisePlugin(args ...string) (string, error)
25+
}
26+
27+
type MiseExecEnv struct {
28+
installDir string
2229

23-
// Additional env vars that configure mise and are required for its operation.
24-
ExtraEnvs map[string]string
30+
extraEnvs map[string]string
2531
}
2632

27-
func (e *ExecEnv) RunMise(args ...string) (string, error) {
33+
// extraEnvs: additional env vars that configure mise and are required for its operation.
34+
func NewMiseExecEnv(installDir string, extraEnvs map[string]string) MiseExecEnv {
35+
return MiseExecEnv{
36+
installDir: installDir,
37+
extraEnvs: extraEnvs,
38+
}
39+
}
40+
41+
func (e MiseExecEnv) RunMise(args ...string) (string, error) {
2842
return e.RunMiseWithTimeout(0, args...)
2943
}
3044

31-
func (e *ExecEnv) RunMisePlugin(args ...string) (string, error) {
45+
func (e MiseExecEnv) RunMisePlugin(args ...string) (string, error) {
3246
cmdWithArgs := append([]string{"plugin"}, args...)
3347

3448
// Use timeout for all plugin operations as they involve unknown code execution
@@ -37,7 +51,7 @@ func (e *ExecEnv) RunMisePlugin(args ...string) (string, error) {
3751

3852
// RunMiseWithTimeout runs mise commands that involve untrusted operations (plugin execution, remote network calls)
3953
// with a timeout to prevent hanging
40-
func (e *ExecEnv) RunMiseWithTimeout(timeout time.Duration, args ...string) (string, error) {
54+
func (e MiseExecEnv) RunMiseWithTimeout(timeout time.Duration, args ...string) (string, error) {
4155
var ctx context.Context
4256
if timeout == 0 {
4357
ctx = context.Background()
@@ -47,10 +61,10 @@ func (e *ExecEnv) RunMiseWithTimeout(timeout time.Duration, args ...string) (str
4761
defer cancel()
4862
}
4963

50-
executable := filepath.Join(e.InstallDir, "bin", "mise")
64+
executable := filepath.Join(e.installDir, "bin", "mise")
5165
cmd := exec.CommandContext(ctx, executable, args...)
5266
cmd.Env = os.Environ()
53-
for k, v := range e.ExtraEnvs {
67+
for k, v := range e.extraEnvs {
5468
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
5569
}
5670
output, err := cmd.CombinedOutput()
@@ -63,3 +77,7 @@ func (e *ExecEnv) RunMiseWithTimeout(timeout time.Duration, args ...string) (str
6377

6478
return string(output), nil
6579
}
80+
81+
func (e MiseExecEnv) InstallDir() string {
82+
return e.installDir
83+
}

toolprovider/mise/helpers_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package mise
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
)
8+
9+
type fakeExecEnv struct {
10+
// responses maps command strings to their outputs
11+
responses map[string]string
12+
// errors maps command strings to errors
13+
errors map[string]error
14+
}
15+
16+
func newFakeExecEnv() *fakeExecEnv {
17+
return &fakeExecEnv{
18+
responses: make(map[string]string),
19+
errors: make(map[string]error),
20+
}
21+
}
22+
23+
func (m *fakeExecEnv) setResponse(cmdKey string, output string) {
24+
m.responses[cmdKey] = output
25+
}
26+
27+
func (m *fakeExecEnv) setError(cmdKey string, err error) {
28+
m.errors[cmdKey] = err
29+
}
30+
31+
func (m *fakeExecEnv) InstallDir() string {
32+
return "/fake/mise/install/dir"
33+
}
34+
35+
func (m *fakeExecEnv) RunMise(args ...string) (string, error) {
36+
return m.runCommand(args...)
37+
}
38+
39+
func (m *fakeExecEnv) RunMisePlugin(args ...string) (string, error) {
40+
return m.runCommand(append([]string{"plugin"}, args...)...)
41+
}
42+
43+
func (m *fakeExecEnv) RunMiseWithTimeout(timeout time.Duration, args ...string) (string, error) {
44+
return m.runCommand(args...)
45+
}
46+
47+
func (m *fakeExecEnv) runCommand(args ...string) (string, error) {
48+
cmdKey := strings.Join(args, " ")
49+
50+
if err, ok := m.errors[cmdKey]; ok {
51+
return "", err
52+
}
53+
54+
if output, ok := m.responses[cmdKey]; ok {
55+
return output, nil
56+
}
57+
58+
return "", fmt.Errorf("no mock response configured for command: %s", cmdKey)
59+
}

toolprovider/mise/mise.go

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,20 @@ func NewToolProvider(installDir string, dataDir string) (*MiseToolProvider, erro
6363
}
6464

6565
return &MiseToolProvider{
66-
ExecEnv: execenv.ExecEnv{
67-
InstallDir: installDir,
68-
66+
ExecEnv: execenv.NewMiseExecEnv(installDir, map[string]string{
6967
// https://mise.jdx.dev/configuration.html#environment-variables
70-
ExtraEnvs: map[string]string{
71-
"MISE_DATA_DIR": dataDir,
72-
73-
// Isolate this mise instance's "global" config from system-wide config
74-
"MISE_CONFIG_DIR": filepath.Join(dataDir),
75-
"MISE_GLOBAL_CONFIG_FILE": filepath.Join(dataDir, "config.toml"),
76-
"MISE_GLOBAL_CONFIG_ROOT": dataDir,
77-
78-
// Enable corepack by default for Node.js installations. This mirrors the preinstalled Node versions on Bitrise stacks.
79-
// https://mise.jdx.dev/lang/node.html#environment-variables
80-
"MISE_NODE_COREPACK": "1",
81-
},
68+
"MISE_DATA_DIR": dataDir,
69+
70+
// Isolate this mise instance's "global" config from system-wide config
71+
"MISE_CONFIG_DIR": filepath.Join(dataDir),
72+
"MISE_GLOBAL_CONFIG_FILE": filepath.Join(dataDir, "config.toml"),
73+
"MISE_GLOBAL_CONFIG_ROOT": dataDir,
74+
75+
// Enable corepack by default for Node.js installations. This mirrors the preinstalled Node versions on Bitrise stacks.
76+
// https://mise.jdx.dev/lang/node.html#environment-variables
77+
"MISE_NODE_COREPACK": "1",
8278
},
79+
),
8380
}, nil
8481
}
8582

@@ -88,12 +85,12 @@ func (m *MiseToolProvider) ID() string {
8885
}
8986

9087
func (m *MiseToolProvider) Bootstrap() error {
91-
if isMiseInstalled(m.ExecEnv.InstallDir) {
88+
if isMiseInstalled(m.ExecEnv.InstallDir()) {
9289
log.Debugf("[TOOLPROVIDER] Mise already installed in %s, skipping bootstrap", m.ExecEnv.InstallDir)
9390
return nil
9491
}
9592

96-
err := installReleaseBinary(GetMiseVersion(), GetMiseChecksums(), m.ExecEnv.InstallDir)
93+
err := installReleaseBinary(GetMiseVersion(), GetMiseChecksums(), m.ExecEnv.InstallDir())
9794
if err != nil {
9895
return fmt.Errorf("bootstrap mise: %w", err)
9996
}
@@ -102,13 +99,9 @@ func (m *MiseToolProvider) Bootstrap() error {
10299
}
103100

104101
func (m *MiseToolProvider) InstallTool(tool provider.ToolRequest) (provider.ToolInstallResult, error) {
105-
useNix, err := m.canBeInstalledWithNix(tool)
106-
if err != nil {
107-
return provider.ToolInstallResult{}, err
108-
}
109-
102+
useNix := canBeInstalledWithNix(tool, m.ExecEnv)
110103
if !useNix {
111-
err = m.InstallPlugin(tool)
104+
err := m.InstallPlugin(tool)
112105
if err != nil {
113106
return provider.ToolInstallResult{}, fmt.Errorf("install tool plugin %s: %w", tool.ToolName, err)
114107
}
@@ -162,7 +155,7 @@ func (m *MiseToolProvider) ActivateEnv(result provider.ToolInstallResult) (provi
162155

163156
activationResult := processEnvOutput(envs)
164157
// Some core plugins create shims to executables (e.g. npm). These shims call `mise reshim` and require the `mise` binary to be in $PATH.
165-
miseExecPath := filepath.Join(m.ExecEnv.InstallDir, "bin")
158+
miseExecPath := filepath.Join(m.ExecEnv.InstallDir(), "bin")
166159
activationResult.ContributedPaths = append(activationResult.ContributedPaths, miseExecPath)
167160
return activationResult, nil
168161
}

toolprovider/mise/resolve.go

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,13 @@ import (
55
"errors"
66
"fmt"
77
"strings"
8-
"time"
98

109
"github.com/bitrise-io/bitrise/v2/toolprovider/mise/execenv"
1110
"github.com/bitrise-io/bitrise/v2/toolprovider/provider"
1211
)
1312

1413
var errNoMatchingVersion = errors.New("no matching version found")
1514

16-
// MiseExecutor defines the interface for executing mise commands
17-
type MiseExecutor interface {
18-
RunMiseWithTimeout(timeout time.Duration, args ...string) (string, error)
19-
}
20-
2115
func (m *MiseToolProvider) resolveToConcreteVersionAfterInstall(tool provider.ToolRequest) (string, error) {
2216
// Mise doesn't tell us what version it resolved to when installing the user-provided (and potentially fuzzy) version.
2317
// But we can use `mise latest` to find out the concrete version.
@@ -35,12 +29,12 @@ func (m *MiseToolProvider) resolveToConcreteVersionAfterInstall(tool provider.To
3529
}
3630

3731
func (m *MiseToolProvider) resolveToLatestReleased(toolName provider.ToolID, version string) (string, error) {
38-
return resolveToLatestReleased(&m.ExecEnv, toolName, version)
32+
return resolveToLatestReleased(m.ExecEnv, toolName, version)
3933
}
4034

41-
func resolveToLatestReleased(executor MiseExecutor, toolName provider.ToolID, version string) (string, error) {
35+
func resolveToLatestReleased(execEnv execenv.ExecEnv, toolName provider.ToolID, version string) (string, error) {
4236
// Even if version is empty string "sometool@" will not cause an error.
43-
output, err := executor.RunMiseWithTimeout(execenv.DefaultTimeout, "latest", fmt.Sprintf("%s@%s", toolName, version))
37+
output, err := execEnv.RunMiseWithTimeout(execenv.DefaultTimeout, "latest", fmt.Sprintf("%s@%s", toolName, version))
4438
if err != nil {
4539
return "", fmt.Errorf("mise latest %s@%s: %w", toolName, version, err)
4640
}
@@ -54,18 +48,18 @@ func resolveToLatestReleased(executor MiseExecutor, toolName provider.ToolID, ve
5448
}
5549

5650
func (m *MiseToolProvider) resolveToLatestInstalled(toolName provider.ToolID, version string) (string, error) {
57-
return resolveToLatestInstalled(&m.ExecEnv, toolName, version)
51+
return resolveToLatestInstalled(m.ExecEnv, toolName, version)
5852
}
5953

60-
func resolveToLatestInstalled(executor MiseExecutor, toolName provider.ToolID, version string) (string, error) {
54+
func resolveToLatestInstalled(execEnv execenv.ExecEnv, toolName provider.ToolID, version string) (string, error) {
6155
// Even if version is empty string "sometool@" will not cause an error.
6256
var toolString = string(toolName)
6357
if version != "" && version != "installed" {
6458
// tool@installed is not valid, so only append version when it's not "installed"
6559
toolString = fmt.Sprintf("%s@%s", toolName, version)
6660
}
6761

68-
output, err := executor.RunMiseWithTimeout(execenv.DefaultTimeout, "latest", "--installed", "--quiet", toolString)
62+
output, err := execEnv.RunMiseWithTimeout(execenv.DefaultTimeout, "latest", "--installed", "--quiet", toolString)
6963
if err != nil {
7064
return "", fmt.Errorf("mise latest --installed %s: %w", toolString, err)
7165
}
@@ -79,13 +73,13 @@ func resolveToLatestInstalled(executor MiseExecutor, toolName provider.ToolID, v
7973
}
8074

8175
func (m *MiseToolProvider) versionExists(toolName provider.ToolID, version string) (bool, error) {
82-
return versionExists(&m.ExecEnv, toolName, version)
76+
return versionExists(m.ExecEnv, toolName, version)
8377
}
8478

85-
func versionExists(executor MiseExecutor, toolName provider.ToolID, version string) (bool, error) {
79+
func versionExists(execEnv execenv.ExecEnv, toolName provider.ToolID, version string) (bool, error) {
8680
if version == "installed" {
8781
// List all installed versions to see if there is at least one version available.
88-
output, err := executor.RunMiseWithTimeout(execenv.DefaultTimeout, "ls", "--installed", "--json", "--quiet", string(toolName))
82+
output, err := execEnv.RunMiseWithTimeout(execenv.DefaultTimeout, "ls", "--installed", "--json", "--quiet", string(toolName))
8983
if err != nil {
9084
return false, fmt.Errorf("mise ls --installed %s: %w", toolName, err)
9185
}
@@ -113,7 +107,7 @@ func versionExists(executor MiseExecutor, toolName provider.ToolID, version stri
113107
// - it can return multiple versions (one per line) when a fuzzy version is provided
114108
// - in case of no matching version, the exit code is still 0, just there is no output
115109
// - in case of a non-existing tool, the exit code is 1, but a non-existing tool ID fails earlier than this check
116-
output, err := executor.RunMiseWithTimeout(execenv.DefaultTimeout, "ls-remote", "--quiet", versionString)
110+
output, err := execEnv.RunMiseWithTimeout(execenv.DefaultTimeout, "ls-remote", "--quiet", versionString)
117111
if err != nil {
118112
return false, fmt.Errorf("mise ls-remote %s: %w", versionString, err)
119113
}

0 commit comments

Comments
 (0)