diff --git a/README.md b/README.md index fe806a192..827dc2376 100644 --- a/README.md +++ b/README.md @@ -150,16 +150,24 @@ creds_helpers: additional_directories: # the path of each additional directory. - path: /Volumes -# vmType (Experimental): sets which Hypervisor to use to launch the VM. (optional) +# vmType: sets which Hypervisor to use to launch the VM. (optional) # Only takes effect when a new VM is launched (only on vm init). # One of: "qemu", "vz". -# - "qemu" (default): Uses QEMU as the Hypervisor. -# - "vz": Uses Virtualization.framework as the Hypervisor. -vmType: "qemu" -# rosetta (Experimental): sets whether to enable Rosetta as the binfmt_misc handler inside the VM. (optional) +# - "qemu": Uses QEMU as the Hypervisor. +# - "vz" (default): Uses Virtualization.framework as the Hypervisor. +# +# NOTE: prior to version 1.2.0, "qemu" was the default, and it will still be the default for +# macOS versions that do not support Virtualization.framework (pre-13.0.0). +vmType: "vz" +# rosetta: sets whether to enable Rosetta as the binfmt_misc handler for x86_64 +# binaries inside the VM, as an alternative to qemu user mode emulation. (optional) # Only takes effect when a new VM is launched (only on vm init). # Only available when using vmType "vz" on Apple Silicon running macOS 13+. # If true, also sets vmType to "vz". +# +# NOTE: while Rosetta is generally faster than qemu user mode emulation, it causes +# some performance regressions, as noted in this issue: +# https://github.com/lima-vm/lima/issues/1269 rosetta: false ``` diff --git a/cmd/finch/main.go b/cmd/finch/main.go index a4d016474..9d3f0b952 100644 --- a/cmd/finch/main.go +++ b/cmd/finch/main.go @@ -56,12 +56,29 @@ func xmain(logger flog.Logger, if err != nil { return fmt.Errorf("failed to get finch root path: %w", err) } - fc, err := config.Load(fs, fp.ConfigFilePath(finchRootPath), logger, loadCfgDeps, mem) + ecc := command.NewExecCmdCreator() + fc, err := config.Load( + fs, + fp.ConfigFilePath(finchRootPath), + logger, + loadCfgDeps, + mem, + ecc, + ) if err != nil { return fmt.Errorf("failed to load config: %w", err) } - return newApp(logger, fp, fs, fc, stdOut, home, finchRootPath).Execute() + return newApp( + logger, + fp, + fs, + fc, + stdOut, + home, + finchRootPath, + ecc, + ).Execute() } var newApp = func( @@ -72,6 +89,7 @@ var newApp = func( stdOut io.Writer, home, finchRootPath string, + ecc command.Creator, ) *cobra.Command { usage := fmt.Sprintf("%v ", finchRootCmd) rootCmd := &cobra.Command{ @@ -93,7 +111,6 @@ var newApp = func( return nil } - ecc := command.NewExecCmdCreator() lcc := command.NewLimaCmdCreator(ecc, logger, fp.LimaHomePath(), @@ -129,7 +146,7 @@ var newApp = func( func initializeNerdctlCommands( lcc command.LimaCmdCreator, - ecc *command.ExecCmdCreator, + ecc command.Creator, logger flog.Logger, fs afero.Fs, fc *config.Finch, diff --git a/cmd/finch/main_darwin.go b/cmd/finch/main_darwin.go index 3decb1fe3..e6614a404 100644 --- a/cmd/finch/main_darwin.go +++ b/cmd/finch/main_darwin.go @@ -19,7 +19,7 @@ import ( ) func dependencies( - ecc *command.ExecCmdCreator, + ecc command.Creator, fc *config.Finch, fp path.Finch, fs afero.Fs, diff --git a/cmd/finch/main_test.go b/cmd/finch/main_test.go index d21918292..9e693c041 100644 --- a/cmd/finch/main_test.go +++ b/cmd/finch/main_test.go @@ -193,10 +193,11 @@ func TestNewApp(t *testing.T) { fp := path.Finch("") fs := afero.NewMemMapFs() stdOut := os.Stdout + ecc := mocks.NewCommandCreator(ctrl) require.NoError(t, afero.WriteFile(fs, "/real/config.yaml", []byte(configStr), 0o600)) - cmd := newApp(l, fp, fs, &config.Finch{}, stdOut, "", "") + cmd := newApp(l, fp, fs, &config.Finch{}, stdOut, "", "", ecc) assert.Equal(t, cmd.Name(), finchRootCmd) assert.Equal(t, cmd.Version, version.Version) diff --git a/cmd/finch/main_windows.go b/cmd/finch/main_windows.go index d5351c045..af78a5679 100644 --- a/cmd/finch/main_windows.go +++ b/cmd/finch/main_windows.go @@ -20,7 +20,7 @@ import ( ) func dependencies( - ecc *command.ExecCmdCreator, + ecc command.Creator, fc *config.Finch, fp path.Finch, fs afero.Fs, diff --git a/cmd/finch/virtual_machine.go b/cmd/finch/virtual_machine.go index 506179896..f35769391 100644 --- a/cmd/finch/virtual_machine.go +++ b/cmd/finch/virtual_machine.go @@ -100,7 +100,7 @@ func virtualMachineCommands( logger flog.Logger, fp path.Finch, lcc command.LimaCmdCreator, - ecc *command.ExecCmdCreator, + ecc command.Creator, fs afero.Fs, fc *config.Finch, home string, diff --git a/e2e/vm/virtualization_framework_rosetta_darwin_test.go b/e2e/vm/virtualization_framework_rosetta_darwin_test.go index 2bc0d1c7d..5fd42704b 100644 --- a/e2e/vm/virtualization_framework_rosetta_darwin_test.go +++ b/e2e/vm/virtualization_framework_rosetta_darwin_test.go @@ -18,20 +18,20 @@ import ( "github.com/runfinch/finch/pkg/config" ) -var testVirtualizationFrameworkAndRosetta = func(o *option.Option, installed bool) { - ginkgo.Describe("Virtualization framework", ginkgo.Ordered, func() { +var testNonDefaultOptions = func(o *option.Option, installed bool) { + ginkgo.Describe("Non-default options", ginkgo.Ordered, func() { supportsVz, supportsVzErr := config.SupportsVirtualizationFramework(finch_cmd.NewExecCmdCreator()) gomega.Expect(supportsVzErr).ShouldNot(gomega.HaveOccurred()) - ginkgo.Describe("Virtualization framework", ginkgo.Ordered, func() { + ginkgo.Describe("QEMU", ginkgo.Ordered, func() { ginkgo.BeforeAll(func() { if !supportsVz { - ginkgo.Skip("Skipping because system does not support Virtualization.framework") + ginkgo.Skip("Skipping because default for this system is already QEMU") } resetVM(o) resetDisks(o, installed) - writeFile(finchConfigFilePath, []byte("memory: 4GiB\ncpus: 6\nvmType: vz\nrosetta: false")) + writeFile(finchConfigFilePath, []byte("memory: 4GiB\ncpus: 6\nvmType: qemu\nrosetta: false")) // vm init with VZ set sometimes takes 2 minutes just to convert the disk to raw command.New(o, virtualMachineRootCmd, "init").WithoutCheckingExitCode().WithTimeoutInSeconds(240).Run() tests.SetupLocalRegistry(o) @@ -46,7 +46,7 @@ var testVirtualizationFrameworkAndRosetta = func(o *option.Option, installed boo tests.Port(o) }) - ginkgo.Describe("Virtualization framework and Rosetta", ginkgo.Ordered, func() { + ginkgo.Describe("Virtualization framework with Rosetta", ginkgo.Ordered, func() { ginkgo.BeforeAll(func() { if !supportsVz || runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" { ginkgo.Skip("Skipping because system does not support Rosetta") diff --git a/e2e/vm/vm_darwin_test.go b/e2e/vm/vm_darwin_test.go index 3d7206ad8..4652cd96e 100644 --- a/e2e/vm/vm_darwin_test.go +++ b/e2e/vm/vm_darwin_test.go @@ -66,7 +66,7 @@ func TestVM(t *testing.T) { testConfig(o, *e2e.Installed) testFinchConfigFile(o) testVersion(o) - testVirtualizationFrameworkAndRosetta(o, *e2e.Installed) + testNonDefaultOptions(o, *e2e.Installed) testSupportBundle(o) testCredHelper(o, *e2e.Installed, *e2e.Registry) testSoci(o, *e2e.Installed) diff --git a/pkg/config/config.go b/pkg/config/config.go index bd9dd3511..714181f64 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -18,6 +18,7 @@ import ( "github.com/spf13/afero" "gopkg.in/yaml.v3" + "github.com/runfinch/finch/pkg/command" "github.com/runfinch/finch/pkg/flog" "github.com/runfinch/finch/pkg/fmemory" "github.com/runfinch/finch/pkg/system" @@ -135,12 +136,19 @@ func ensureConfigDir(fs afero.Fs, path string, log flog.Logger) error { } // Load loads Finch's configuration from a YAML file and initializes default values. -func Load(fs afero.Fs, cfgPath string, log flog.Logger, systemDeps LoadSystemDeps, mem fmemory.Memory) (*Finch, error) { +func Load( + fs afero.Fs, + cfgPath string, + log flog.Logger, + systemDeps LoadSystemDeps, + mem fmemory.Memory, + ecc command.Creator, +) (*Finch, error) { b, err := afero.ReadFile(fs, cfgPath) if err != nil { if errors.Is(err, afero.ErrFileNotFound) { log.Infof("Using default values due to missing config file at %q", cfgPath) - defCfg := applyDefaults(&Finch{}, systemDeps, mem) + defCfg := applyDefaults(&Finch{}, systemDeps, mem, ecc) if err := ensureConfigDir(fs, filepath.Dir(cfgPath), log); err != nil { return nil, fmt.Errorf("failed to ensure %q directory: %w", cfgPath, err) } @@ -157,7 +165,7 @@ func Load(fs afero.Fs, cfgPath string, log flog.Logger, systemDeps LoadSystemDep return nil, fmt.Errorf("failed to unmarshal config file: %w", err) } - defCfg := applyDefaults(&cfg, systemDeps, mem) + defCfg := applyDefaults(&cfg, systemDeps, mem, ecc) if err := writeConfig(defCfg, fs, cfgPath); err != nil { return nil, err } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index d711c77fe..bf8cd0713 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -23,14 +23,28 @@ func TestLoad(t *testing.T) { testCases := []struct { name string path string - mockSvc func(fs afero.Fs, l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) + mockSvc func( + fs afero.Fs, + l *mocks.Logger, + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) want *Finch wantErr error }{ { name: "config file does not contain valid YAML", path: "/config.yaml", - mockSvc: func(fs afero.Fs, _ *mocks.Logger, _ *mocks.LoadSystemDeps, _ *mocks.Memory) { + mockSvc: func( + fs afero.Fs, + _ *mocks.Logger, + _ *mocks.LoadSystemDeps, + _ *mocks.Memory, + _ *mocks.CommandCreator, + _ *gomock.Controller, + ) { require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte("this isn't YAML"), 0o600)) }, want: nil, @@ -44,14 +58,28 @@ func TestLoad(t *testing.T) { darwinTestCases := []struct { name string path string - mockSvc func(fs afero.Fs, l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) + mockSvc func( + fs afero.Fs, + l *mocks.Logger, + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) want *Finch wantErr error }{ { name: "happy path", path: "/config.yaml", - mockSvc: func(fs afero.Fs, _ *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + mockSvc: func( + fs afero.Fs, + _ *mocks.Logger, + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) { data := ` memory: 4GiB cpus: 8 @@ -60,11 +88,14 @@ cpus: 8 deps.EXPECT().NumCPU().Return(8) // 12_884_901_888 == 12GiB mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)) + c := mocks.NewCommand(ctrl) + ecc.EXPECT().Create("sw_vers", "-productVersion").Return(c) + c.EXPECT().Output().Return([]byte("14.0.0"), nil) }, want: &Finch{ Memory: pointer.String("4GiB"), CPUs: pointer.Int(8), - VMType: pointer.String("qemu"), + VMType: pointer.String("vz"), Rosetta: pointer.Bool(false), }, wantErr: nil, @@ -72,15 +103,25 @@ cpus: 8 { name: "config file exists, but is empty", path: "/config.yaml", - mockSvc: func(fs afero.Fs, _ *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + mockSvc: func( + fs afero.Fs, + _ *mocks.Logger, + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) { require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte(""), 0o600)) deps.EXPECT().NumCPU().Return(4).Times(2) mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)).Times(2) + c := mocks.NewCommand(ctrl) + ecc.EXPECT().Create("sw_vers", "-productVersion").Return(c) + c.EXPECT().Output().Return([]byte("14.0.0"), nil) }, want: &Finch{ Memory: pointer.String("3GiB"), CPUs: pointer.Int(2), - VMType: pointer.String("qemu"), + VMType: pointer.String("vz"), Rosetta: pointer.Bool(false), }, wantErr: nil, @@ -88,31 +129,51 @@ cpus: 8 { name: "config file exists, but contains only some fields", path: "/config.yaml", - mockSvc: func(fs afero.Fs, _ *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { - require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte("memory: 2GiB"), 0o600)) + mockSvc: func( + fs afero.Fs, + _ *mocks.Logger, + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) { + require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte("memory: 2GiB\nrosetta: true"), 0o600)) deps.EXPECT().NumCPU().Return(4).Times(2) mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)).Times(1) + c := mocks.NewCommand(ctrl) + ecc.EXPECT().Create("sw_vers", "-productVersion").Return(c) + c.EXPECT().Output().Return([]byte("14.0.0"), nil) }, want: &Finch{ Memory: pointer.String("2GiB"), CPUs: pointer.Int(2), - VMType: pointer.String("qemu"), - Rosetta: pointer.Bool(false), + VMType: pointer.String("vz"), + Rosetta: pointer.Bool(true), }, wantErr: nil, }, { name: "config file exists, but contains an unknown field", path: "/config.yaml", - mockSvc: func(fs afero.Fs, _ *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + mockSvc: func( + fs afero.Fs, + _ *mocks.Logger, + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) { require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte("unknownField: 2GiB"), 0o600)) deps.EXPECT().NumCPU().Return(4).Times(2) mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)).Times(2) + c := mocks.NewCommand(ctrl) + ecc.EXPECT().Create("sw_vers", "-productVersion").Return(c) + c.EXPECT().Output().Return([]byte("14.0.0"), nil) }, want: &Finch{ Memory: pointer.String("3GiB"), CPUs: pointer.Int(2), - VMType: pointer.String("qemu"), + VMType: pointer.String("vz"), Rosetta: pointer.Bool(false), }, wantErr: nil, @@ -120,15 +181,25 @@ cpus: 8 { name: "config file does not exist", path: "/config.yaml", - mockSvc: func(_ afero.Fs, l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + mockSvc: func( + _ afero.Fs, + l *mocks.Logger, + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) { l.EXPECT().Infof("Using default values due to missing config file at %q", "/config.yaml") deps.EXPECT().NumCPU().Return(4).Times(1) mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)).Times(1) + c := mocks.NewCommand(ctrl) + ecc.EXPECT().Create("sw_vers", "-productVersion").Return(c) + c.EXPECT().Output().Return([]byte("14.0.0"), nil) }, want: &Finch{ Memory: pointer.String("3GiB"), CPUs: pointer.Int(2), - VMType: pointer.String("qemu"), + VMType: pointer.String("vz"), Rosetta: pointer.Bool(false), }, wantErr: nil, @@ -138,14 +209,28 @@ cpus: 8 windowsTestCases := []struct { name string path string - mockSvc func(fs afero.Fs, l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) + mockSvc func( + fs afero.Fs, + l *mocks.Logger, + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) want *Finch wantErr error }{ { name: "happy path", path: "/config.yaml", - mockSvc: func(fs afero.Fs, _ *mocks.Logger, _ *mocks.LoadSystemDeps, _ *mocks.Memory) { + mockSvc: func( + fs afero.Fs, + _ *mocks.Logger, + _ *mocks.LoadSystemDeps, + _ *mocks.Memory, + _ *mocks.CommandCreator, + _ *gomock.Controller, + ) { data := ` memory: 4GiB cpus: 8 @@ -162,7 +247,14 @@ cpus: 8 { name: "config file exists, but is empty", path: "/config.yaml", - mockSvc: func(fs afero.Fs, _ *mocks.Logger, _ *mocks.LoadSystemDeps, _ *mocks.Memory) { + mockSvc: func( + fs afero.Fs, + _ *mocks.Logger, + _ *mocks.LoadSystemDeps, + _ *mocks.Memory, + _ *mocks.CommandCreator, + _ *gomock.Controller, + ) { require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte(""), 0o600)) }, want: &Finch{ @@ -173,7 +265,14 @@ cpus: 8 { name: "config file exists, but contains only some fields", path: "/config.yaml", - mockSvc: func(fs afero.Fs, _ *mocks.Logger, _ *mocks.LoadSystemDeps, _ *mocks.Memory) { + mockSvc: func( + fs afero.Fs, + _ *mocks.Logger, + _ *mocks.LoadSystemDeps, + _ *mocks.Memory, + _ *mocks.CommandCreator, + _ *gomock.Controller, + ) { require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte("memory: 2GiB"), 0o600)) }, want: &Finch{ @@ -185,7 +284,14 @@ cpus: 8 { name: "config file exists, but contains an unknown field", path: "/config.yaml", - mockSvc: func(fs afero.Fs, _ *mocks.Logger, _ *mocks.LoadSystemDeps, _ *mocks.Memory) { + mockSvc: func( + fs afero.Fs, + _ *mocks.Logger, + _ *mocks.LoadSystemDeps, + _ *mocks.Memory, + _ *mocks.CommandCreator, + _ *gomock.Controller, + ) { require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte("unknownField: 2GiB"), 0o600)) }, want: &Finch{ @@ -196,7 +302,14 @@ cpus: 8 { name: "config file does not exist", path: "/config.yaml", - mockSvc: func(_ afero.Fs, l *mocks.Logger, _ *mocks.LoadSystemDeps, _ *mocks.Memory) { + mockSvc: func( + _ afero.Fs, + l *mocks.Logger, + _ *mocks.LoadSystemDeps, + _ *mocks.Memory, + _ *mocks.CommandCreator, + _ *gomock.Controller, + ) { l.EXPECT().Infof("Using default values due to missing config file at %q", "/config.yaml") }, want: &Finch{ @@ -224,11 +337,12 @@ cpus: 8 l := mocks.NewLogger(ctrl) deps := mocks.NewLoadSystemDeps(ctrl) mem := mocks.NewMemory(ctrl) + ecc := mocks.NewCommandCreator(ctrl) fs := afero.NewMemMapFs() - tc.mockSvc(fs, l, deps, mem) + tc.mockSvc(fs, l, deps, mem, ecc, ctrl) - got, gotErr := Load(fs, tc.path, l, deps, mem) + got, gotErr := Load(fs, tc.path, l, deps, mem, ecc) require.Equal(t, tc.wantErr, gotErr) assert.Equal(t, tc.want, got) }) diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go deleted file mode 100644 index e77c8c049..000000000 --- a/pkg/config/defaults.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package config - -import ( - "github.com/runfinch/finch/pkg/fmemory" -) - -// applyDefaults sets default configuration options if they are not already set. -func applyDefaults(cfg *Finch, deps LoadSystemDeps, mem fmemory.Memory) *Finch { - cpuDefault(cfg, deps) - - memoryDefault(cfg, mem) - - vmDefault(cfg) - - rosettaDefault(cfg) - - return cfg -} diff --git a/pkg/config/defaults_darwin.go b/pkg/config/defaults_darwin.go index 3dffabcca..6cc7ef38a 100644 --- a/pkg/config/defaults_darwin.go +++ b/pkg/config/defaults_darwin.go @@ -11,6 +11,7 @@ import ( "github.com/docker/go-units" "github.com/xorcare/pointer" + "github.com/runfinch/finch/pkg/command" "github.com/runfinch/finch/pkg/fmemory" ) @@ -20,9 +21,13 @@ const ( fallbackCPUs int = 2 ) -func vmDefault(cfg *Finch) { +func vmDefault(cfg *Finch, supportsVz bool) { if cfg.VMType == nil { - cfg.VMType = pointer.String("qemu") + if supportsVz { + cfg.VMType = pointer.String("vz") + } else { + cfg.VMType = pointer.String("qemu") + } } } @@ -53,3 +58,23 @@ func cpuDefault(cfg *Finch, deps LoadSystemDeps) { } } } + +// applyDefaults sets default configuration options if they are not already set. +func applyDefaults( + cfg *Finch, + deps LoadSystemDeps, + mem fmemory.Memory, + ecc command.Creator, +) *Finch { + cpuDefault(cfg, deps) + memoryDefault(cfg, mem) + supportsVz := false + vz, err := SupportsVirtualizationFramework(ecc) + if err == nil && vz { + supportsVz = true + } + vmDefault(cfg, supportsVz) + rosettaDefault(cfg) + + return cfg +} diff --git a/pkg/config/defaults_test.go b/pkg/config/defaults_test.go index 9535c0f8c..570f489a7 100644 --- a/pkg/config/defaults_test.go +++ b/pkg/config/defaults_test.go @@ -4,6 +4,7 @@ package config import ( + "fmt" "runtime" "testing" @@ -20,27 +21,45 @@ func Test_applyDefaults(t *testing.T) { var testCases []struct { name string cfg *Finch - mockSvc func(deps *mocks.LoadSystemDeps, mem *mocks.Memory) - want *Finch + mockSvc func( + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) + want *Finch } darwinTestCases := []struct { name string cfg *Finch - mockSvc func(deps *mocks.LoadSystemDeps, mem *mocks.Memory) - want *Finch + mockSvc func( + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) + want *Finch }{ { name: "happy path", cfg: &Finch{}, - mockSvc: func(deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + mockSvc: func( + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) { deps.EXPECT().NumCPU().Return(8) // 12,884,901,888 == 12GiB mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)) + c := mocks.NewCommand(ctrl) + ecc.EXPECT().Create("sw_vers", "-productVersion").Return(c) + c.EXPECT().Output().Return([]byte("14.0.0"), nil) }, want: &Finch{ CPUs: pointer.Int(2), Memory: pointer.String("3GiB"), - VMType: pointer.String("qemu"), + VMType: pointer.String("vz"), Rosetta: pointer.Bool(false), }, }, @@ -49,13 +68,21 @@ func Test_applyDefaults(t *testing.T) { cfg: &Finch{ Memory: pointer.String("4GiB"), }, - mockSvc: func(deps *mocks.LoadSystemDeps, _ *mocks.Memory) { + mockSvc: func( + deps *mocks.LoadSystemDeps, + _ *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) { deps.EXPECT().NumCPU().Return(8) + c := mocks.NewCommand(ctrl) + ecc.EXPECT().Create("sw_vers", "-productVersion").Return(c) + c.EXPECT().Output().Return([]byte("14.0.0"), nil) }, want: &Finch{ CPUs: pointer.Int(2), Memory: pointer.String("4GiB"), - VMType: pointer.String("qemu"), + VMType: pointer.String("vz"), Rosetta: pointer.Bool(false), }, }, @@ -64,28 +91,115 @@ func Test_applyDefaults(t *testing.T) { cfg: &Finch{ CPUs: pointer.Int(6), }, - mockSvc: func(_ *mocks.LoadSystemDeps, mem *mocks.Memory) { + mockSvc: func( + _ *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) { // 12,884,901,888 == 12GiB mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)) + c := mocks.NewCommand(ctrl) + ecc.EXPECT().Create("sw_vers", "-productVersion").Return(c) + c.EXPECT().Output().Return([]byte("14.0.0"), nil) }, want: &Finch{ CPUs: pointer.Int(6), Memory: pointer.String("3GiB"), - VMType: pointer.String("qemu"), + VMType: pointer.String("vz"), Rosetta: pointer.Bool(false), }, }, { name: "fills with fallbacks when defaults are too low", cfg: &Finch{}, - mockSvc: func(deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + mockSvc: func( + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) { deps.EXPECT().NumCPU().Return(4) // 1,073,741,824 == 1GiB mem.EXPECT().TotalMemory().Return(uint64(1_073_741_824)) + c := mocks.NewCommand(ctrl) + ecc.EXPECT().Create("sw_vers", "-productVersion").Return(c) + c.EXPECT().Output().Return([]byte("14.0.0"), nil) }, want: &Finch{ CPUs: pointer.Int(2), Memory: pointer.String("2GiB"), + VMType: pointer.String("vz"), + Rosetta: pointer.Bool(false), + }, + }, + { + name: "doesn't override existing values", + cfg: &Finch{ + VMType: pointer.String("qemu"), + }, + mockSvc: func( + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) { + deps.EXPECT().NumCPU().Return(8) + // 12,884,901,888 == 12GiB + mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)) + c := mocks.NewCommand(ctrl) + ecc.EXPECT().Create("sw_vers", "-productVersion").Return(c) + c.EXPECT().Output().Return([]byte("14.0.0"), nil) + }, + want: &Finch{ + CPUs: pointer.Int(2), + Memory: pointer.String("3GiB"), + VMType: pointer.String("qemu"), + Rosetta: pointer.Bool(false), + }, + }, + { + name: "falls back to qemu on old versions", + cfg: &Finch{}, + mockSvc: func( + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) { + deps.EXPECT().NumCPU().Return(8) + // 12,884,901,888 == 12GiB + mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)) + c := mocks.NewCommand(ctrl) + ecc.EXPECT().Create("sw_vers", "-productVersion").Return(c) + c.EXPECT().Output().Return([]byte("12.0.0"), nil) + }, + want: &Finch{ + CPUs: pointer.Int(2), + Memory: pointer.String("3GiB"), + VMType: pointer.String("qemu"), + Rosetta: pointer.Bool(false), + }, + }, + { + name: "falls back to qemu if there's an error", + cfg: &Finch{}, + mockSvc: func( + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) { + deps.EXPECT().NumCPU().Return(8) + // 12,884,901,888 == 12GiB + mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)) + c := mocks.NewCommand(ctrl) + ecc.EXPECT().Create("sw_vers", "-productVersion").Return(c) + c.EXPECT().Output().Return([]byte("12.0.0"), fmt.Errorf("an error")) + }, + want: &Finch{ + CPUs: pointer.Int(2), + Memory: pointer.String("3GiB"), VMType: pointer.String("qemu"), Rosetta: pointer.Bool(false), }, @@ -95,13 +209,23 @@ func Test_applyDefaults(t *testing.T) { windowsTestCases := []struct { name string cfg *Finch - mockSvc func(deps *mocks.LoadSystemDeps, mem *mocks.Memory) - want *Finch + mockSvc func( + deps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ecc *mocks.CommandCreator, + ctrl *gomock.Controller, + ) + want *Finch }{ { name: "happy path", cfg: &Finch{}, - mockSvc: func(_ *mocks.LoadSystemDeps, _ *mocks.Memory) { + mockSvc: func( + _ *mocks.LoadSystemDeps, + _ *mocks.Memory, + _ *mocks.CommandCreator, + _ *gomock.Controller, + ) { }, want: &Finch{ VMType: pointer.String("wsl2"), @@ -112,7 +236,12 @@ func Test_applyDefaults(t *testing.T) { cfg: &Finch{ VMType: pointer.String("wsl"), }, - mockSvc: func(_ *mocks.LoadSystemDeps, _ *mocks.Memory) { + mockSvc: func( + _ *mocks.LoadSystemDeps, + _ *mocks.Memory, + _ *mocks.CommandCreator, + _ *gomock.Controller, + ) { }, want: &Finch{ VMType: pointer.String("wsl"), @@ -136,10 +265,11 @@ func Test_applyDefaults(t *testing.T) { ctrl := gomock.NewController(t) deps := mocks.NewLoadSystemDeps(ctrl) mem := mocks.NewMemory(ctrl) + ecc := mocks.NewCommandCreator(ctrl) - tc.mockSvc(deps, mem) + tc.mockSvc(deps, mem, ecc, ctrl) - got := applyDefaults(tc.cfg, deps, mem) + got := applyDefaults(tc.cfg, deps, mem, ecc) require.Equal(t, tc.want, got) }) } diff --git a/pkg/config/defaults_windows.go b/pkg/config/defaults_windows.go index 88a9eb204..397a987c8 100644 --- a/pkg/config/defaults_windows.go +++ b/pkg/config/defaults_windows.go @@ -8,23 +8,23 @@ package config import ( "github.com/xorcare/pointer" + "github.com/runfinch/finch/pkg/command" "github.com/runfinch/finch/pkg/fmemory" ) -// Does not matter if Rosetta is set, no-op. -func rosettaDefault(_ *Finch) { -} - func vmDefault(cfg *Finch) { if cfg.VMType == nil { cfg.VMType = pointer.String("wsl2") } } -// no-op , not configurable in wsl. -func memoryDefault(_ *Finch, _ fmemory.Memory) { -} - -// no-op , not configurable in wsl. -func cpuDefault(_ *Finch, _ LoadSystemDeps) { +// applyDefaults sets default configuration options if they are not already set. +func applyDefaults( + cfg *Finch, + _ LoadSystemDeps, + _ fmemory.Memory, + _ command.Creator, +) *Finch { + vmDefault(cfg) + return cfg }