mirror of
https://github.com/asdf-vm/asdf.git
synced 2024-12-23 20:05:09 -07:00
feat(golang-rewrite): offline Go tests
Some of the Go tests actually cloned remote repos, which meant slow tests, and tests failures if I was working offline. These changes allow all Go tests to run offline. The test code now clones local Git repos instead of remotes ones. * Add otiai10/copy as a dependency * Create repotest package for test code that needs to work with Git repos * Add back missing test script for execute package * Update git, pluginindex, and plugins package tests so they work offline
This commit is contained in:
parent
2e185a0e5b
commit
f74efbf1bf
3
execute/testdata/script
vendored
Executable file
3
execute/testdata/script
vendored
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
echo $@
|
@ -5,35 +5,27 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"asdf/repotest"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Switch to local repo so tests don't go over the network
|
|
||||||
const (
|
|
||||||
testRepo = "https://github.com/Stratus3D/asdf-lua"
|
|
||||||
testPluginName = "lua"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPluginClone(t *testing.T) {
|
func TestPluginClone(t *testing.T) {
|
||||||
t.Run("when plugin name is valid but URL is invalid prints an error", func(t *testing.T) {
|
t.Run("when plugin name is valid but URL is invalid prints an error", func(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
plugin := NewRepo(t.TempDir())
|
||||||
directory := filepath.Join(tempDir, testPluginName)
|
|
||||||
|
|
||||||
plugin := NewRepo(directory)
|
|
||||||
err := plugin.Clone("foobar")
|
err := plugin.Clone("foobar")
|
||||||
|
|
||||||
assert.ErrorContains(t, err, "unable to clone plugin: repository not found")
|
assert.ErrorContains(t, err, "unable to clone plugin: repository not found")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("clones provided Git URL to plugin directory when URL is valid", func(t *testing.T) {
|
t.Run("clones provided Git URL to plugin directory when URL is valid", func(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
repoDir := generateRepo(t)
|
||||||
directory := filepath.Join(tempDir, testPluginName)
|
directory := t.TempDir()
|
||||||
|
|
||||||
plugin := NewRepo(directory)
|
plugin := NewRepo(directory)
|
||||||
err := plugin.Clone(testRepo)
|
|
||||||
|
|
||||||
|
err := plugin.Clone(repoDir)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
// Assert plugin directory contains Git repo with bin directory
|
// Assert plugin directory contains Git repo with bin directory
|
||||||
@ -42,17 +34,17 @@ func TestPluginClone(t *testing.T) {
|
|||||||
|
|
||||||
entries, err := os.ReadDir(directory + "/bin")
|
entries, err := os.ReadDir(directory + "/bin")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, 5, len(entries))
|
assert.Equal(t, 12, len(entries))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPluginHead(t *testing.T) {
|
func TestPluginHead(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
repoDir := generateRepo(t)
|
||||||
directory := filepath.Join(tempDir, testPluginName)
|
directory := t.TempDir()
|
||||||
|
|
||||||
plugin := NewRepo(directory)
|
plugin := NewRepo(directory)
|
||||||
|
|
||||||
err := plugin.Clone(testRepo)
|
err := plugin.Clone(repoDir)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
head, err := plugin.Head()
|
head, err := plugin.Head()
|
||||||
@ -61,12 +53,12 @@ func TestPluginHead(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPluginRemoteURL(t *testing.T) {
|
func TestPluginRemoteURL(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
repoDir := generateRepo(t)
|
||||||
directory := filepath.Join(tempDir, testPluginName)
|
directory := t.TempDir()
|
||||||
|
|
||||||
plugin := NewRepo(directory)
|
plugin := NewRepo(directory)
|
||||||
|
|
||||||
err := plugin.Clone(testRepo)
|
err := plugin.Clone(repoDir)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
url, err := plugin.RemoteURL()
|
url, err := plugin.RemoteURL()
|
||||||
@ -75,16 +67,16 @@ func TestPluginRemoteURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPluginUpdate(t *testing.T) {
|
func TestPluginUpdate(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
repoDir := generateRepo(t)
|
||||||
directory := filepath.Join(tempDir, testPluginName)
|
directory := t.TempDir()
|
||||||
|
|
||||||
plugin := NewRepo(directory)
|
plugin := NewRepo(directory)
|
||||||
|
|
||||||
err := plugin.Clone(testRepo)
|
err := plugin.Clone(repoDir)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
t.Run("returns error when plugin with name does not exist", func(t *testing.T) {
|
t.Run("returns error when plugin with name does not exist", func(t *testing.T) {
|
||||||
nonexistantPath := filepath.Join(tempDir, "nonexistant")
|
nonexistantPath := filepath.Join(directory, "nonexistant")
|
||||||
nonexistantPlugin := NewRepo(nonexistantPath)
|
nonexistantPlugin := NewRepo(nonexistantPath)
|
||||||
updatedToRef, err := nonexistantPlugin.Update("")
|
updatedToRef, err := nonexistantPlugin.Update("")
|
||||||
|
|
||||||
@ -96,7 +88,7 @@ func TestPluginUpdate(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("returns error when plugin repo does not exist", func(t *testing.T) {
|
t.Run("returns error when plugin repo does not exist", func(t *testing.T) {
|
||||||
badPluginName := "badplugin"
|
badPluginName := "badplugin"
|
||||||
badPluginDir := filepath.Join(tempDir, badPluginName)
|
badPluginDir := filepath.Join(directory, badPluginName)
|
||||||
err := os.MkdirAll(badPluginDir, 0o777)
|
err := os.MkdirAll(badPluginDir, 0o777)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
@ -198,3 +190,12 @@ func checkoutPreviousCommit(path string) (string, error) {
|
|||||||
|
|
||||||
return previousHash.String(), nil
|
return previousHash.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateRepo(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
path, err := repotest.GeneratePlugin("dummy_plugin", tempDir, "lua")
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
2
go.mod
2
go.mod
@ -5,6 +5,7 @@ go 1.21.5
|
|||||||
require (
|
require (
|
||||||
github.com/go-git/go-git/v5 v5.11.0
|
github.com/go-git/go-git/v5 v5.11.0
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
|
github.com/otiai10/copy v1.14.0
|
||||||
github.com/sethvargo/go-envconfig v1.0.0
|
github.com/sethvargo/go-envconfig v1.0.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/urfave/cli/v2 v2.27.1
|
github.com/urfave/cli/v2 v2.27.1
|
||||||
@ -35,6 +36,7 @@ require (
|
|||||||
golang.org/x/crypto v0.16.0 // indirect
|
golang.org/x/crypto v0.16.0 // indirect
|
||||||
golang.org/x/mod v0.12.0 // indirect
|
golang.org/x/mod v0.12.0 // indirect
|
||||||
golang.org/x/net v0.19.0 // indirect
|
golang.org/x/net v0.19.0 // indirect
|
||||||
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
golang.org/x/sys v0.15.0 // indirect
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
golang.org/x/tools v0.13.0 // indirect
|
golang.org/x/tools v0.13.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -52,6 +52,10 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
|
|||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
||||||
|
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
|
||||||
|
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
|
||||||
|
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
||||||
|
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
||||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
@ -63,7 +63,7 @@ func (p PluginIndex) Refresh() (bool, error) {
|
|||||||
// directory empty, clone down repo
|
// directory empty, clone down repo
|
||||||
err := p.repo.Clone(p.url)
|
err := p.repo.Clone(p.url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, fmt.Errorf("unable to initialize index: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return touchFS(p.directory)
|
return touchFS(p.directory)
|
||||||
@ -90,7 +90,7 @@ func (p PluginIndex) doUpdate() (bool, error) {
|
|||||||
// commit is
|
// commit is
|
||||||
_, err := p.repo.Update("")
|
_, err := p.repo.Update("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, fmt.Errorf("unable to update plugin index: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Touch update file
|
// Touch update file
|
||||||
|
@ -9,13 +9,15 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"asdf/git"
|
"asdf/git"
|
||||||
|
"asdf/repotest"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
realIndexURL = "https://github.com/asdf-vm/asdf-plugins.git"
|
mockIndexURL = "https://github.com/asdf-vm/asdf-plugins.git"
|
||||||
badIndexURL = "http://asdf-vm.com/non-existent"
|
badIndexURL = "http://asdf-vm.com/non-existent"
|
||||||
|
fooPluginURL = "http://example.com/foo"
|
||||||
elixirPluginURL = "https://github.com/asdf-vm/asdf-elixir.git"
|
elixirPluginURL = "https://github.com/asdf-vm/asdf-elixir.git"
|
||||||
erlangPluginURL = "https://github.com/asdf-vm/asdf-erlang.git"
|
erlangPluginURL = "https://github.com/asdf-vm/asdf-erlang.git"
|
||||||
)
|
)
|
||||||
@ -84,15 +86,22 @@ func writeMockPluginFile(dir, pluginName, pluginURL string) error {
|
|||||||
func TestGetPluginSourceURL(t *testing.T) {
|
func TestGetPluginSourceURL(t *testing.T) {
|
||||||
t.Run("with Git returns a plugin url when provided name of existing plugin", func(t *testing.T) {
|
t.Run("with Git returns a plugin url when provided name of existing plugin", func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pluginIndex := New(dir, realIndexURL, true, 0, &git.Repo{Directory: dir})
|
indexDir := filepath.Join(dir, "index")
|
||||||
url, err := pluginIndex.GetPluginSourceURL("elixir")
|
err := os.Mkdir(indexDir, 0o777)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, url, elixirPluginURL)
|
|
||||||
|
repoPath, err := repotest.GeneratePluginIndex(dir)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
pluginIndex := New(indexDir, repoPath, true, 0, &git.Repo{Directory: indexDir})
|
||||||
|
url, err := pluginIndex.GetPluginSourceURL("foo")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, url, fooPluginURL)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("returns a plugin url when provided name of existing plugin", func(t *testing.T) {
|
t.Run("returns a plugin url when provided name of existing plugin", func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pluginIndex := New(dir, realIndexURL, true, 0, &MockIndex{Directory: dir})
|
pluginIndex := New(dir, mockIndexURL, true, 0, &MockIndex{Directory: dir})
|
||||||
url, err := pluginIndex.GetPluginSourceURL("elixir")
|
url, err := pluginIndex.GetPluginSourceURL("elixir")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, url, elixirPluginURL)
|
assert.Equal(t, url, elixirPluginURL)
|
||||||
@ -100,7 +109,7 @@ func TestGetPluginSourceURL(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("returns a plugin url when provided name of existing plugin when loading from cache", func(t *testing.T) {
|
t.Run("returns a plugin url when provided name of existing plugin when loading from cache", func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pluginIndex := New(dir, realIndexURL, false, 10, &MockIndex{Directory: dir})
|
pluginIndex := New(dir, mockIndexURL, false, 10, &MockIndex{Directory: dir})
|
||||||
url, err := pluginIndex.GetPluginSourceURL("elixir")
|
url, err := pluginIndex.GetPluginSourceURL("elixir")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, url, elixirPluginURL)
|
assert.Equal(t, url, elixirPluginURL)
|
||||||
@ -112,7 +121,7 @@ func TestGetPluginSourceURL(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("returns an error when given a name that isn't in the index", func(t *testing.T) {
|
t.Run("returns an error when given a name that isn't in the index", func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pluginIndex := New(dir, realIndexURL, false, 10, &MockIndex{Directory: dir})
|
pluginIndex := New(dir, mockIndexURL, false, 10, &MockIndex{Directory: dir})
|
||||||
url, err := pluginIndex.GetPluginSourceURL("foobar")
|
url, err := pluginIndex.GetPluginSourceURL("foobar")
|
||||||
assert.EqualError(t, err, "plugin foobar not found in repository")
|
assert.EqualError(t, err, "plugin foobar not found in repository")
|
||||||
assert.Equal(t, url, "")
|
assert.Equal(t, url, "")
|
||||||
@ -130,7 +139,7 @@ func TestGetPluginSourceURL(t *testing.T) {
|
|||||||
pluginIndex := New(dir, badIndexURL, false, 10, &repo)
|
pluginIndex := New(dir, badIndexURL, false, 10, &repo)
|
||||||
|
|
||||||
url, err := pluginIndex.GetPluginSourceURL("lua")
|
url, err := pluginIndex.GetPluginSourceURL("lua")
|
||||||
assert.EqualError(t, err, "unable to clone: repository not found")
|
assert.EqualError(t, err, "unable to update plugin index: unable to clone: repository not found")
|
||||||
assert.Equal(t, url, "")
|
assert.Equal(t, url, "")
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -138,7 +147,7 @@ func TestGetPluginSourceURL(t *testing.T) {
|
|||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pluginIndex := New(dir, badIndexURL, false, 10, &MockIndex{Directory: dir})
|
pluginIndex := New(dir, badIndexURL, false, 10, &MockIndex{Directory: dir})
|
||||||
url, err := pluginIndex.GetPluginSourceURL("lua")
|
url, err := pluginIndex.GetPluginSourceURL("lua")
|
||||||
assert.EqualError(t, err, "unable to clone: repository not found")
|
assert.EqualError(t, err, "unable to initialize index: unable to clone: repository not found")
|
||||||
assert.Equal(t, url, "")
|
assert.Equal(t, url, "")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -146,10 +155,17 @@ func TestGetPluginSourceURL(t *testing.T) {
|
|||||||
func TestRefresh(t *testing.T) {
|
func TestRefresh(t *testing.T) {
|
||||||
t.Run("with Git updates repo when called once", func(t *testing.T) {
|
t.Run("with Git updates repo when called once", func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pluginIndex := New(dir, realIndexURL, false, 0, &git.Repo{Directory: dir})
|
indexDir := filepath.Join(dir, "index")
|
||||||
url, err := pluginIndex.GetPluginSourceURL("elixir")
|
err := os.Mkdir(indexDir, 0o777)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, url, elixirPluginURL)
|
|
||||||
|
repoPath, err := repotest.GeneratePluginIndex(dir)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
pluginIndex := New(indexDir, repoPath, false, 0, &git.Repo{Directory: indexDir})
|
||||||
|
url, err := pluginIndex.GetPluginSourceURL("foo")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, url, fooPluginURL)
|
||||||
|
|
||||||
updated, err := pluginIndex.Refresh()
|
updated, err := pluginIndex.Refresh()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -158,7 +174,7 @@ func TestRefresh(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("updates repo when called once", func(t *testing.T) {
|
t.Run("updates repo when called once", func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pluginIndex := New(dir, realIndexURL, false, 0, &MockIndex{Directory: dir})
|
pluginIndex := New(dir, mockIndexURL, false, 0, &MockIndex{Directory: dir})
|
||||||
|
|
||||||
updated, err := pluginIndex.Refresh()
|
updated, err := pluginIndex.Refresh()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -171,7 +187,7 @@ func TestRefresh(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("does not update index when time has not elaspsed", func(t *testing.T) {
|
t.Run("does not update index when time has not elaspsed", func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pluginIndex := New(dir, realIndexURL, false, 10, &MockIndex{Directory: dir})
|
pluginIndex := New(dir, mockIndexURL, false, 10, &MockIndex{Directory: dir})
|
||||||
|
|
||||||
// Call Refresh twice, the second call should not perform an update
|
// Call Refresh twice, the second call should not perform an update
|
||||||
updated, err := pluginIndex.Refresh()
|
updated, err := pluginIndex.Refresh()
|
||||||
@ -185,7 +201,7 @@ func TestRefresh(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("updates plugin index when time has elaspsed", func(t *testing.T) {
|
t.Run("updates plugin index when time has elaspsed", func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pluginIndex := New(dir, realIndexURL, false, 0, &MockIndex{Directory: dir})
|
pluginIndex := New(dir, mockIndexURL, false, 0, &MockIndex{Directory: dir})
|
||||||
|
|
||||||
// Call Refresh twice, the second call should perform an update
|
// Call Refresh twice, the second call should perform an update
|
||||||
updated, err := pluginIndex.Refresh()
|
updated, err := pluginIndex.Refresh()
|
||||||
@ -200,10 +216,10 @@ func TestRefresh(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("returns error when plugin index repo doesn't exist", func(t *testing.T) {
|
t.Run("returns error when plugin index repo doesn't exist", func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pluginIndex := New(dir, badIndexURL, false, 0, &MockIndex{Directory: dir})
|
|
||||||
|
|
||||||
|
pluginIndex := New(dir, badIndexURL, false, 0, &MockIndex{Directory: dir})
|
||||||
updated, err := pluginIndex.Refresh()
|
updated, err := pluginIndex.Refresh()
|
||||||
assert.EqualError(t, err, "unable to clone: repository not found")
|
assert.EqualError(t, err, "unable to initialize index: unable to clone: repository not found")
|
||||||
assert.False(t, updated)
|
assert.False(t, updated)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,23 @@
|
|||||||
package plugins
|
package plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"asdf/config"
|
"asdf/config"
|
||||||
|
"asdf/repotest"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Switch to local repo so tests don't go over the network
|
const testPluginName = "lua"
|
||||||
const (
|
|
||||||
testRepo = "https://github.com/Stratus3D/asdf-lua"
|
|
||||||
testPluginName = "lua"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestList(t *testing.T) {
|
func TestList(t *testing.T) {
|
||||||
testDataDir := t.TempDir()
|
testDataDir := t.TempDir()
|
||||||
conf := config.Config{DataDir: testDataDir}
|
conf := config.Config{DataDir: testDataDir}
|
||||||
testRepo, err := installMockPluginRepo(testDataDir, testPluginName)
|
testRepo, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
err = Add(conf, testPluginName, testRepo)
|
err = Add(conf, testPluginName, testRepo)
|
||||||
@ -93,7 +88,7 @@ func TestAdd(t *testing.T) {
|
|||||||
|
|
||||||
for _, invalid := range invalids {
|
for _, invalid := range invalids {
|
||||||
t.Run(invalid, func(t *testing.T) {
|
t.Run(invalid, func(t *testing.T) {
|
||||||
err := Add(config.Config{}, invalid, testRepo)
|
err := Add(config.Config{}, invalid, "never-cloned")
|
||||||
|
|
||||||
expectedErrMsg := "is invalid. Name may only contain lowercase letters, numbers, '_', and '-'"
|
expectedErrMsg := "is invalid. Name may only contain lowercase letters, numbers, '_', and '-'"
|
||||||
if !strings.Contains(err.Error(), expectedErrMsg) {
|
if !strings.Contains(err.Error(), expectedErrMsg) {
|
||||||
@ -107,13 +102,16 @@ func TestAdd(t *testing.T) {
|
|||||||
conf := config.Config{DataDir: testDataDir}
|
conf := config.Config{DataDir: testDataDir}
|
||||||
|
|
||||||
// Add plugin
|
// Add plugin
|
||||||
err := Add(conf, testPluginName, testRepo)
|
repoPath, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
err = Add(conf, testPluginName, repoPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Expected to be able to add plugin")
|
t.Fatal("Expected to be able to add plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add it again to trigger error
|
// Add it again to trigger error
|
||||||
err = Add(conf, testPluginName, testRepo)
|
err = Add(conf, testPluginName, repoPath)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error got nil")
|
t.Fatal("expected error got nil")
|
||||||
@ -136,8 +134,10 @@ func TestAdd(t *testing.T) {
|
|||||||
t.Run("when plugin name and URL are valid installs plugin", func(t *testing.T) {
|
t.Run("when plugin name and URL are valid installs plugin", func(t *testing.T) {
|
||||||
testDataDir := t.TempDir()
|
testDataDir := t.TempDir()
|
||||||
conf := config.Config{DataDir: testDataDir}
|
conf := config.Config{DataDir: testDataDir}
|
||||||
|
pluginPath, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
err := Add(conf, testPluginName, testRepo)
|
err = Add(conf, testPluginName, pluginPath)
|
||||||
|
|
||||||
assert.Nil(t, err, "Expected to be able to add plugin")
|
assert.Nil(t, err, "Expected to be able to add plugin")
|
||||||
|
|
||||||
@ -149,14 +149,17 @@ func TestAdd(t *testing.T) {
|
|||||||
|
|
||||||
entries, err := os.ReadDir(pluginDir + "/bin")
|
entries, err := os.ReadDir(pluginDir + "/bin")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, 5, len(entries))
|
assert.Equal(t, 12, len(entries))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("when parameters are valid creates plugin download dir", func(t *testing.T) {
|
t.Run("when parameters are valid creates plugin download dir", func(t *testing.T) {
|
||||||
testDataDir := t.TempDir()
|
testDataDir := t.TempDir()
|
||||||
conf := config.Config{DataDir: testDataDir}
|
conf := config.Config{DataDir: testDataDir}
|
||||||
|
|
||||||
err := Add(conf, testPluginName, testRepo)
|
repoPath, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
err = Add(conf, testPluginName, repoPath)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
// Assert download dir exists
|
// Assert download dir exists
|
||||||
@ -170,7 +173,10 @@ func TestRemove(t *testing.T) {
|
|||||||
testDataDir := t.TempDir()
|
testDataDir := t.TempDir()
|
||||||
conf := config.Config{DataDir: testDataDir}
|
conf := config.Config{DataDir: testDataDir}
|
||||||
|
|
||||||
err := Add(conf, testPluginName, testRepo)
|
repoPath, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
err = Add(conf, testPluginName, repoPath)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
t.Run("returns error when plugin with name does not exist", func(t *testing.T) {
|
t.Run("returns error when plugin with name does not exist", func(t *testing.T) {
|
||||||
@ -197,7 +203,7 @@ func TestRemove(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("removes plugin download dir when passed name of installed plugin", func(t *testing.T) {
|
t.Run("removes plugin download dir when passed name of installed plugin", func(t *testing.T) {
|
||||||
err := Add(conf, testPluginName, testRepo)
|
err := Add(conf, testPluginName, repoPath)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
err = Remove(conf, testPluginName)
|
err = Remove(conf, testPluginName)
|
||||||
@ -214,7 +220,10 @@ func TestUpdate(t *testing.T) {
|
|||||||
testDataDir := t.TempDir()
|
testDataDir := t.TempDir()
|
||||||
conf := config.Config{DataDir: testDataDir}
|
conf := config.Config{DataDir: testDataDir}
|
||||||
|
|
||||||
err := Add(conf, testPluginName, testRepo)
|
repoPath, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
err = Add(conf, testPluginName, repoPath)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
badPluginName := "badplugin"
|
badPluginName := "badplugin"
|
||||||
@ -359,7 +368,7 @@ func TestRunCallback(t *testing.T) {
|
|||||||
|
|
||||||
testDataDir := t.TempDir()
|
testDataDir := t.TempDir()
|
||||||
conf := config.Config{DataDir: testDataDir}
|
conf := config.Config{DataDir: testDataDir}
|
||||||
testRepo, err := installMockPluginRepo(testDataDir, testPluginName)
|
testRepo, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
err = Add(conf, testPluginName, testRepo)
|
err = Add(conf, testPluginName, testRepo)
|
||||||
@ -412,125 +421,3 @@ func touchFile(name string) error {
|
|||||||
}
|
}
|
||||||
return file.Close()
|
return file.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func installMockPluginRepo(dataDir, name string) (string, error) {
|
|
||||||
// Because the legacy dummy plugin directory is relative to the root of this
|
|
||||||
// project I cannot use the usual testing functions to locate it. To
|
|
||||||
// determine the location of it we compute the module root, which also
|
|
||||||
// happens to be the root of the repo.
|
|
||||||
modRootDir, err := moduleRoot()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
location := dataDir + "/repo-" + name
|
|
||||||
|
|
||||||
// Then we specify the path to the dummy plugin relative to the module root
|
|
||||||
err = runCmd("cp", "-r", filepath.Join(modRootDir, "test/fixtures/dummy_plugin"), location)
|
|
||||||
if err != nil {
|
|
||||||
return location, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Definitely some opportunities to refactor here. This code might be
|
|
||||||
// simplified by switching to the Go git library
|
|
||||||
err = runCmd("git", "-C", location, "init", "-q")
|
|
||||||
if err != nil {
|
|
||||||
return location, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = runCmd("git", "-C", location, "config", "user.name", "\"Test\"")
|
|
||||||
if err != nil {
|
|
||||||
return location, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = runCmd("git", "-C", location, "config", "user.email", "\"test@example.com\"")
|
|
||||||
if err != nil {
|
|
||||||
return location, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = runCmd("git", "-C", location, "add", "-A")
|
|
||||||
if err != nil {
|
|
||||||
return location, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = runCmd("git", "-C", location, "commit", "-q", "-m", fmt.Sprintf("\"asdf %s plugin init\"", name))
|
|
||||||
if err != nil {
|
|
||||||
return location, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = runCmd("touch", filepath.Join(location, "README.md"))
|
|
||||||
if err != nil {
|
|
||||||
return location, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = runCmd("git", "-C", location, "add", "-A")
|
|
||||||
if err != nil {
|
|
||||||
return location, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = runCmd("git", "-C", location, "commit", "-q", "-m", fmt.Sprintf("\"asdf %s plugin readme \"", name))
|
|
||||||
if err != nil {
|
|
||||||
return location, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// kind of ugly but I want a remote with a valid path so I use the same
|
|
||||||
// location as the remote. Probably should refactor
|
|
||||||
err = runCmd("git", "-C", location, "remote", "add", "origin", location)
|
|
||||||
if err != nil {
|
|
||||||
return location, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return location, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func moduleRoot() (string, error) {
|
|
||||||
currentDir, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return findModuleRoot(currentDir), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Taken from https://github.com/golang/go/blob/9e3b1d53a012e98cfd02de2de8b1bd53522464d4/src/cmd/go/internal/modload/init.go#L1504C1-L1522C2 because that function is in an internal module
|
|
||||||
// and I can't rely on it.
|
|
||||||
func findModuleRoot(dir string) (roots string) {
|
|
||||||
if dir == "" {
|
|
||||||
panic("dir not set")
|
|
||||||
}
|
|
||||||
dir = filepath.Clean(dir)
|
|
||||||
|
|
||||||
// Look for enclosing go.mod.
|
|
||||||
for {
|
|
||||||
if fi, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil && !fi.IsDir() {
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
d := filepath.Dir(dir)
|
|
||||||
if d == dir {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
dir = d
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function to make running commands easier
|
|
||||||
func runCmd(cmdName string, args ...string) error {
|
|
||||||
cmd := exec.Command(cmdName, args...)
|
|
||||||
|
|
||||||
// Capture stdout and stderr
|
|
||||||
var stdout strings.Builder
|
|
||||||
var stderr strings.Builder
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
// If command fails print both stderr and stdout
|
|
||||||
fmt.Println("stdout:", stdout.String())
|
|
||||||
fmt.Println("stderr:", stderr.String())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
200
repotest/repotest.go
Normal file
200
repotest/repotest.go
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
// Package repotest contains various test helpers for tests that work with code
|
||||||
|
// relying on plugin Git repos and the asdf plugin index
|
||||||
|
//
|
||||||
|
// Three main actions:
|
||||||
|
//
|
||||||
|
// * Install plugin index repo into asdf (index contains records that point to
|
||||||
|
// local plugins defined by this package)
|
||||||
|
// * Install plugin into asdf data dir
|
||||||
|
// * Create local plugin repo that can be cloned into asdf
|
||||||
|
package repotest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
cp "github.com/otiai10/copy"
|
||||||
|
)
|
||||||
|
|
||||||
|
const fixturesDir = "fixtures"
|
||||||
|
|
||||||
|
// Setup copies all files into place and initializes all repos for any Go test
|
||||||
|
// that needs either plugin repos or the plugin index repo.
|
||||||
|
func Setup(asdfDataDir string) error {
|
||||||
|
if err := InstallPluginIndex(asdfDataDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePlugin copies in the specified plugin fixture into a test directory
|
||||||
|
// and initializes a Git repo for it so it can be installed by asdf.
|
||||||
|
func GeneratePlugin(fixtureName, asdfDataDir, pluginName string) (string, error) {
|
||||||
|
root, err := getModuleRoot()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
fixturesDir := filepath.Join(asdfDataDir, fixturesDir)
|
||||||
|
return generatePluginInDir(root, fixtureName, fixturesDir, pluginName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallPluginIndex generates and installs a plugin index Git repo inside of
|
||||||
|
// the provided asdf data directory.
|
||||||
|
func InstallPluginIndex(asdfDataDir string) error {
|
||||||
|
root, err := getModuleRoot()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy in plugin index
|
||||||
|
source := filepath.Join(root, "test/fixtures/dummy_plugins_repo")
|
||||||
|
return cp.Copy(source, filepath.Join(asdfDataDir, "plugin-index"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePluginIndex generates a mock plugin index Git repo inside the given
|
||||||
|
// directory.
|
||||||
|
func GeneratePluginIndex(asdfDataDir string) (string, error) {
|
||||||
|
root, err := getModuleRoot()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy in plugin index
|
||||||
|
source := filepath.Join(root, "test/fixtures/dummy_plugins_repo")
|
||||||
|
destination := filepath.Join(asdfDataDir, fixturesDir, "plugin-index")
|
||||||
|
err = cp.Copy(source, destination)
|
||||||
|
if err != nil {
|
||||||
|
return destination, fmt.Errorf("unable to copy in plugin index: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate git repo for plugin
|
||||||
|
return createGitRepo(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatePluginInDir(root, fixtureName, outputDir, pluginName string) (string, error) {
|
||||||
|
// Copy in plugin files into output dir
|
||||||
|
pluginPath, err := copyInPlugin(root, fixtureName, outputDir, pluginName)
|
||||||
|
if err != nil {
|
||||||
|
return pluginPath, fmt.Errorf("unable to copy in plugin files: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate git repo for plugin
|
||||||
|
return createGitRepo(pluginPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getModuleRoot() (string, error) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to get current working directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
root := findModuleRoot(cwd)
|
||||||
|
return root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createGitRepo(location string) (string, error) {
|
||||||
|
// Definitely some opportunities to refactor here. This code might be
|
||||||
|
// simplified by switching to the Go git library
|
||||||
|
err := runCmd("git", "-C", location, "init", "-q")
|
||||||
|
if err != nil {
|
||||||
|
return location, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = runCmd("git", "-C", location, "config", "user.name", "\"Test\"")
|
||||||
|
if err != nil {
|
||||||
|
return location, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = runCmd("git", "-C", location, "config", "user.email", "\"test@example.com\"")
|
||||||
|
if err != nil {
|
||||||
|
return location, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = runCmd("git", "-C", location, "add", "-A")
|
||||||
|
if err != nil {
|
||||||
|
return location, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = runCmd("git", "-C", location, "commit", "-q", "-m", "init repo")
|
||||||
|
if err != nil {
|
||||||
|
return location, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = runCmd("touch", filepath.Join(location, "README.md"))
|
||||||
|
if err != nil {
|
||||||
|
return location, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = runCmd("git", "-C", location, "add", "-A")
|
||||||
|
if err != nil {
|
||||||
|
return location, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = runCmd("git", "-C", location, "commit", "-q", "-m", "add readme")
|
||||||
|
if err != nil {
|
||||||
|
return location, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// kind of ugly but I want a remote with a valid path so I use the same
|
||||||
|
// location as the remote. Probably should refactor
|
||||||
|
err = runCmd("git", "-C", location, "remote", "add", "origin", location)
|
||||||
|
if err != nil {
|
||||||
|
return location, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return location, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyInPlugin(root, name, destination, newName string) (string, error) {
|
||||||
|
source := filepath.Join(root, "test/fixtures/", name)
|
||||||
|
dest := filepath.Join(destination, newName)
|
||||||
|
return dest, cp.Copy(source, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taken from https://github.com/golang/go/blob/9e3b1d53a012e98cfd02de2de8b1bd53522464d4/src/cmd/go/internal/modload/init.go#L1504C1-L1522C2 because that function is in an internal module
|
||||||
|
// and I can't rely on it.
|
||||||
|
func findModuleRoot(dir string) (roots string) {
|
||||||
|
if dir == "" {
|
||||||
|
panic("dir not set")
|
||||||
|
}
|
||||||
|
dir = filepath.Clean(dir)
|
||||||
|
|
||||||
|
// Look for enclosing go.mod.
|
||||||
|
for {
|
||||||
|
if fi, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil && !fi.IsDir() {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
d := filepath.Dir(dir)
|
||||||
|
if d == dir {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dir = d
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to make running commands easier
|
||||||
|
func runCmd(cmdName string, args ...string) error {
|
||||||
|
cmd := exec.Command(cmdName, args...)
|
||||||
|
|
||||||
|
// Capture stdout and stderr
|
||||||
|
var stdout strings.Builder
|
||||||
|
var stderr strings.Builder
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
// If command fails print both stderr and stdout
|
||||||
|
fmt.Println("stdout:", stdout.String())
|
||||||
|
fmt.Println("stderr:", stderr.String())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user