Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f86369e
Add more tests for InstalledVersion
leofleeo Jun 22, 2025
bb35948
Table test for get plugin info and replace ErrNotFound with a shared …
leofleeo Jun 23, 2025
47f2b1f
Add invalid version test for getplugininfo and remove temporary funct…
leofleeo Jun 23, 2025
300b6e9
Tests for shared.NotFoundError.Error() and shared.Source.String()
leofleeo Jun 23, 2025
a36a04c
Use testify assertations
leofleeo Jun 23, 2025
2639f08
Use fatal and error when they should be used and use more testify whe…
leofleeo Jun 25, 2025
85dcd07
Use testify for AssertIsNotFoundError
leofleeo Jun 25, 2025
ac9814d
Forgot to format
leofleeo Jun 25, 2025
37eb914
Use testify mock for TestingT in AssertIsNotFoundError
leofleeo Jun 25, 2025
f11e7d5
Tests for AssertIsNotFoundError
leofleeo Jun 25, 2025
1986313
Reduce duplication in tests for AssertIsNotFoundError part 1
leofleeo Jun 25, 2025
d7f25c2
Reduce duplication in tests for AssertIsNotFoundError part 2
leofleeo Jun 25, 2025
f425657
Merge branch 'main' into moretests
leofleeo Jun 25, 2025
0d5dfd7
Reduce duplication part 3
leofleeo Jun 25, 2025
3a0800b
Some tests for startDownload and other changes
leofleeo Jun 26, 2025
3457e8f
Use testify Error and NoError functions when appropriate
leofleeo Jun 26, 2025
82e5d32
Invalid URL and no content length tests, and switch content length ch…
leofleeo Jun 26, 2025
a4abf82
Make shared.IsVersionInstalled use afero
leofleeo Jun 26, 2025
0c2d9aa
Format and tests for IsVersionInstalled
leofleeo Jun 26, 2025
b6d8fad
Tests for IsNotFound
leofleeo Jun 26, 2025
ed8e7ec
Remove ancient commented code and remove unused parameter in loadplugin
leofleeo Jun 26, 2025
8709256
Stricter linter and modify code to meet new requirements
leofleeo Jun 26, 2025
608acbf
Fix defer parameter
leofleeo Jul 23, 2025
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
22 changes: 4 additions & 18 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,7 @@ version: "2"
linters:
enable:
- gocritic
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
- dupl
settings:
dupl:
threshold: 100
5 changes: 3 additions & 2 deletions cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ If you run "mtvm install go latest" it will install the latest version of go`,
Args: cobra.ExactArgs(2),
Aliases: []string{"i", "in"},
Run: func(cmd *cobra.Command, args []string) {
err := createInstallDir(afero.NewOsFs())
fs := afero.NewOsFs()
err := createInstallDir(fs)
if err != nil {
log.Fatal(err)
}
Expand All @@ -79,7 +80,7 @@ If you run "mtvm install go latest" it will install the latest version of go`,
log.Fatal(err)
}
}
installed, err := shared.IsVersionInstalled(args[0], version)
installed, err := shared.IsVersionInstalled(args[0], version, fs)
if err != nil {
log.Fatal(err)
}
Expand Down
9 changes: 9 additions & 0 deletions cmd/plugincmds/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ func getPluginInfoCmd(metadata plugin.Metadata) tea.Cmd {
url = v.Url
}
}
if url == "" {
return shared.NotFoundError{
Thing: "download",
Source: shared.Source{
File: "cmd/plugincmds/install.go",
Function: "getPluginInfoCmd(metadata plugin.Metadata) tea.Cmd",
},
}
}
return pluginDownloadInfo{
Url: url,
Name: metadata.Name,
Expand Down
194 changes: 108 additions & 86 deletions cmd/plugincmds/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package plugincmds

import (
"context"
"errors"
"fmt"
"runtime"
"testing"

"github.com/stretchr/testify/require"

"github.com/stretchr/testify/assert"

"github.com/MTVersionManager/mtvm/components/downloader"
"github.com/MTVersionManager/mtvm/plugin"
"github.com/MTVersionManager/mtvm/shared"
Expand All @@ -15,98 +17,118 @@ import (
)

func TestGetPluginInfo(t *testing.T) {
msg := getPluginInfoCmd(plugin.Metadata{
Name: "loremIpsum",
Version: "0.0.0",
Downloads: []plugin.Download{
{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
Url: "https://example.com",
},
type test struct {
metadata plugin.Metadata
testFunc func(t *testing.T, msg tea.Msg)
}
exampleUsableDownloads := []plugin.Download{
{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
Url: "https://example.com",
},
})()
if downloadInfo, ok := msg.(pluginDownloadInfo); ok {
if downloadInfo.Name != "loremIpsum" {
t.Fatalf("want name to be 'loremIpsum', got name '%v'", downloadInfo.Name)
}
if downloadInfo.Url != "https://example.com" {
t.Fatalf("want url to be 'https://example.com', got url '%v'", downloadInfo.Url)
}
compareVersionTo := semver.New(0, 0, 0, "", "")
if !compareVersionTo.Equal(downloadInfo.Version) {
t.Fatalf("Want version 0.0.0 got %v", downloadInfo.Version.String())
}
} else if err, ok := msg.(error); ok {
t.Fatalf("want no error, got %v", err)
} else {
t.Fatalf("want pluginDownloadInfo returned, got %T with content %v", msg, msg)
}
}

func TestGetPluginInfoInvalidVersion(t *testing.T) {
msg := getPluginInfoCmd(plugin.Metadata{
Name: "loremIpsum",
Version: "loremIpsum",
Downloads: []plugin.Download{
{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
Url: "https://example.com",
tests := map[string]test{
"existing download": {
metadata: plugin.Metadata{
Name: "loremIpsum",
Version: "0.0.0",
Downloads: exampleUsableDownloads,
},
testFunc: func(t *testing.T, msg tea.Msg) {
if downloadInfo, ok := msg.(pluginDownloadInfo); ok {
assert.Equalf(t, "loremIpsum", downloadInfo.Name, "want name to be 'loremIpsum', got name '%v'", downloadInfo.Name)
assert.Equalf(t, "https://example.com", downloadInfo.Url, "want url to be 'https://example.com', got url '%v'", downloadInfo.Url)
compareVersionTo := semver.New(0, 0, 0, "", "")
if !compareVersionTo.Equal(downloadInfo.Version) {
t.Errorf("Want version 0.0.0 got %v", downloadInfo.Version.String())
}
} else if err, ok := msg.(error); ok {
assert.NoError(t, err)
} else {
t.Errorf("want pluginDownloadInfo returned, got %T with content %v", msg, msg)
}
},
},
"no download": {
metadata: plugin.Metadata{
Name: "loremIpsum",
Version: "0.0.0",
Downloads: []plugin.Download{
{
OS: func() string {
if runtime.GOOS == "imaginaryOS" {
return "fakeOS"
}
return "imaginaryOS"
}(),
Arch: func() string {
if runtime.GOARCH == "imaginaryArch" {
return "fakeArch"
}
return "imaginaryArch"
}(),
Url: "https://example.com",
},
},
},
testFunc: func(t *testing.T, msg tea.Msg) {
if err, ok := msg.(error); ok {
shared.AssertIsNotFoundError(t, err, "download", shared.Source{
File: "cmd/plugincmds/install.go",
Function: "getPluginInfoCmd(metadata plugin.Metadata) tea.Cmd",
})
} else {
t.Errorf("want error, got %T with content %v", msg, msg)
}
},
},
"invalid version": {
metadata: plugin.Metadata{
Name: "loremIpsum",
Version: "IAmAnInvalidVersion",
Downloads: exampleUsableDownloads,
},
testFunc: func(t *testing.T, msg tea.Msg) {
if err, ok := msg.(error); ok {
assert.ErrorIs(t, err, semver.ErrInvalidSemVer)
} else {
t.Errorf("want error, got %T with content %v", msg, msg)
}
},
},
})()
if err, ok := msg.(error); !ok {
t.Fatalf("want error, got %T with contents %v", msg, msg)
} else if !errors.Is(err, semver.ErrInvalidSemVer) {
t.Fatalf("want error containing ErrInvalidSemVer, got %v", err)
}
}

func TestInstallUpdateCancelQ(t *testing.T) {
err := CancelTest(tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune{'q'},
})
if err != nil {
t.Fatal(err)
}
}

func TestPluginInstallUpdateCancelCtrlC(t *testing.T) {
err := CancelTest(tea.KeyMsg{
Type: tea.KeyCtrlC,
})
if err != nil {
t.Fatal(err)
}
}

func CancelTest(keyPress tea.KeyMsg) error {
model := initialInstallModel("https://example.com")
_, cancel := context.WithCancel(context.Background())
modelUpdated, _ := model.Update(downloader.DownloadStartedMsg{
Cancel: cancel,
})
_, cmd := modelUpdated.Update(keyPress)
if cmd == nil {
return errors.New("want not nil command, got nil")
}
msg := cmd()
if _, ok := msg.(downloader.DownloadCanceledMsg); !ok {
return fmt.Errorf("expected returned command to return downloader.DownloadCanceledMsg, returned %v with type %T", msg, msg)
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
msg := getPluginInfoCmd(tt.metadata)()
tt.testFunc(t, msg)
})
}
return nil
}

func TestPluginInstallUpdateEntriesSuccess(t *testing.T) {
model := initialInstallModel("https://example.com")
_, cmd := model.Update(shared.SuccessMsg("UpdateEntries"))
if cmd == nil {
t.Fatal("want not nil command, got nil")
func TestInstallUpdateCancel(t *testing.T) {
tests := map[string]tea.KeyMsg{
"ctrl+c": {
Type: tea.KeyCtrlC,
},
"q": {
Type: tea.KeyRunes,
Runes: []rune{'q'},
},
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Fatalf("want command to return tea.QuitMsg, returned %T with content %v", msg, msg)
for name, keyPress := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
model := initialInstallModel("https://example.com")
_, cancel := context.WithCancel(context.Background())
modelUpdated, _ := model.Update(downloader.DownloadStartedMsg{
Cancel: cancel,
})
_, cmd := modelUpdated.Update(keyPress)
require.NotNil(t, cmd, "want not nil command, got nil")
msg := cmd()
assert.IsType(t, downloader.DownloadCanceledMsg{}, msg)
})
}
}
5 changes: 4 additions & 1 deletion cmd/remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"os"
"path/filepath"

"github.com/spf13/afero"

"github.com/MTVersionManager/mtvm/shared"
"github.com/MTVersionManager/mtvmplugin"
"github.com/charmbracelet/bubbles/spinner"
Expand Down Expand Up @@ -94,7 +96,8 @@ For example:
log.Fatal(err)
}
}
installed, err := shared.IsVersionInstalled(args[0], version)
fs := afero.NewOsFs()
installed, err := shared.IsVersionInstalled(args[0], version, fs)
if err != nil {
log.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/use.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ So if you run go version it will print the version number 1.23.3`,
log.Fatal(err)
}
}
versionInstalled, err := shared.IsVersionInstalled(args[0], version)
versionInstalled, err := shared.IsVersionInstalled(args[0], version, fs)
if err != nil {
log.Fatal(err)
}
Expand Down
20 changes: 12 additions & 8 deletions components/downloader/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ func (dw *downloadWriter) Start() {
// This sends a signal to the update function that it is safe to close the response body
dw.copyDone <- true
if err != nil && !errors.Is(err, context.Canceled) {
fmt.Println("Error from copying")
log.Fatal(err)
log.Fatal(fmt.Errorf("from copying: %v", err))
}
}

Expand Down Expand Up @@ -132,13 +131,18 @@ func (m Model) startDownload() tea.Msg {
return fmt.Errorf("%v %v", resp.StatusCode, http.StatusText(resp.StatusCode))
}
contentLengthKnown := true
if resp.ContentLength <= 0 {
if resp.ContentLength == -1 {
contentLengthKnown = false
} else {
cancel()
return errors.New("error when getting content length")
switch resp.ContentLength {
case -1:
contentLengthKnown = false
case 0:
cancel()
return errors.New("content length is 0")
default:
if resp.ContentLength > 0 {
break
}
cancel()
return errors.New("unexpected error when getting content length")
}
m.writer.totalSize = resp.ContentLength
m.writer.resp = resp
Expand Down
Loading