-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add cache.Go interface, add client tests (#3)
* chore: Fix restoring shed cache in CI * feat: Add cache.Go interface to allow specifying go functionality through dependency injection * chore: Implement cache.mockGo and create tests for client * refactor: cache clean command should use shed.CleanCache
- Loading branch information
Showing
6 changed files
with
549 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
package cache | ||
|
||
import ( | ||
"bytes" | ||
"go/build" | ||
"io/ioutil" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"sort" | ||
"strings" | ||
|
||
"github.com/getshiphub/shed/internal/util" | ||
"github.com/getshiphub/shed/tool" | ||
"github.com/pkg/errors" | ||
"golang.org/x/mod/modfile" | ||
"golang.org/x/mod/module" | ||
"golang.org/x/mod/semver" | ||
) | ||
|
||
// createGoModFile creates and writes an empty go.mod file at the path referenced by dir. | ||
// mod is used as the module name. This functions similar to 'go mod init'. | ||
func createGoModFile(mod, dir string) error { | ||
modFile := &modfile.File{} | ||
modFile.AddComment("// Autogenerated by https://github.com/getshiphub/shed. DO NOT EDIT") | ||
// AddModuleStmt never actually returns an error, not sure why it's in the signature | ||
modFile.AddModuleStmt(mod) //nolint:errcheck | ||
|
||
// Add go statement | ||
tags := build.Default.ReleaseTags | ||
version := tags[len(tags)-1] | ||
if !strings.HasPrefix(version, "go") || !modfile.GoVersionRE.MatchString(version[2:]) { | ||
return errors.Errorf("unrecognized default go version %q", version) | ||
} | ||
if err := modFile.AddGoStmt(version[2:]); err != nil { | ||
return errors.Wrap(err, "failed to add go statement to modfile") | ||
} | ||
|
||
data, err := modFile.Format() | ||
if err != nil { | ||
return errors.Wrapf(err, "failed to create modfile") | ||
} | ||
modfilePath := filepath.Join(dir, "go.mod") | ||
err = ioutil.WriteFile(modfilePath, data, 0o644) | ||
if err != nil { | ||
return errors.Wrapf(err, "failed to write modfile %s", modfilePath) | ||
} | ||
return nil | ||
} | ||
|
||
// Go represents the core functionality provided by the go command. It allows for downloading | ||
// and building of modules. | ||
type Go interface { | ||
// Build builds pkg and outputs the binary at outPath. dir is used as the working directory | ||
// when building. pkg must be a valid import path. | ||
// Build functions like 'go build -o'. | ||
Build(pkg, outPath, dir string) error | ||
// GetD downloads the source code for the module mod. dir is used as the working directory | ||
// and is expected to contain a go.mod file which will be updated with the installed module. | ||
// mod must be a valid module name, that is an import path, optionally with a version. | ||
// If no version is provided, the latest version will be downloaded. | ||
// GetD functions like 'got get -d' in module aware mode. | ||
GetD(mod, dir string) error | ||
} | ||
|
||
// realGo is the main implementation of the Go interface. | ||
// It is a wrapper around the go command. | ||
type realGo struct{} | ||
|
||
// NewGo returns a new Go instance which allows for downloading and building modules. | ||
func NewGo() Go { | ||
return realGo{} | ||
} | ||
|
||
func (realGo) Build(pkg, outPath, dir string) error { | ||
return execGo(dir, "build", "-o", outPath, pkg) | ||
} | ||
|
||
func (realGo) GetD(mod, dir string) error { | ||
return execGo(dir, "get", "-d", mod) | ||
} | ||
|
||
func execGo(dir string, args ...string) error { | ||
cmd := exec.Command("go", args...) | ||
cmd.Dir = dir | ||
stderr := &bytes.Buffer{} | ||
cmd.Stderr = stderr | ||
|
||
err := cmd.Run() | ||
if err != nil { | ||
argsStr := strings.Join(args, " ") | ||
return errors.Wrapf(err, "failed to run 'go %s', stderr: %s", argsStr, stderr.String()) | ||
} | ||
return nil | ||
} | ||
|
||
// mockGo provides a implementation of the Go interface that is suitable for testing. | ||
type mockGo struct { | ||
// Tool import path to module | ||
registry map[string]mockModule | ||
} | ||
|
||
type mockModule struct { | ||
// Name of the module, i.e. the import path | ||
name string | ||
// List of semver versions, must be sorted from earliest to latest version | ||
versions []string | ||
} | ||
|
||
// NewMockGo returns a new Go instance that is suitable for testing. | ||
// Tools is a list of tools which contains the import path and version. | ||
func NewMockGo(tools []string) (Go, error) { | ||
registry := make(map[string]mockModule) | ||
for _, tn := range tools { | ||
t, err := tool.Parse(tn) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Module already exists, add new version | ||
m, ok := registry[t.ImportPath] | ||
if ok { | ||
m.versions = append(m.versions, t.Version) | ||
registry[t.ImportPath] = m | ||
continue | ||
} | ||
|
||
// Take first 3 parts as the module name | ||
// This should be could enough for testing purposes | ||
modName := strings.Join(strings.Split(t.ImportPath, "/")[:3], "/") | ||
m = mockModule{name: modName, versions: []string{t.Version}} | ||
registry[t.ImportPath] = m | ||
} | ||
|
||
for _, m := range registry { | ||
// Sort versions so we can easily find the latest version | ||
sort.Slice(m.versions, func(i, j int) bool { | ||
return semver.Compare(m.versions[i], m.versions[j]) == -1 | ||
}) | ||
} | ||
return &mockGo{registry: registry}, nil | ||
} | ||
|
||
func (mg *mockGo) Build(pkg, outPath, dir string) error { | ||
if _, ok := mg.registry[pkg]; !ok { | ||
return errors.Errorf("unknown package %s", pkg) | ||
} | ||
if !util.FileOrDirExists(dir) { | ||
return errors.Errorf("directory %s does not exist", dir) | ||
} | ||
// Can just write an empty file to outPath so the binary "exists" | ||
err := ioutil.WriteFile(outPath, nil, 0o644) | ||
if err != nil { | ||
return errors.Wrapf(err, "failed to write build to %s", outPath) | ||
} | ||
return nil | ||
} | ||
|
||
func (mg *mockGo) GetD(mod, dir string) error { | ||
t, err := tool.Parse(mod) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
m, ok := mg.registry[t.ImportPath] | ||
if !ok { | ||
return errors.Errorf("unknown package %s", mod) | ||
} | ||
|
||
modver := module.Version{Path: m.name} | ||
if t.Version != "" { | ||
// If version is provided, see if it exists | ||
found := false | ||
for _, v := range m.versions { | ||
if v == t.Version { | ||
modver.Version = v | ||
found = true | ||
break | ||
} | ||
} | ||
if !found { | ||
return errors.Errorf("module %s has no version %s", t.ImportPath, t.Version) | ||
} | ||
} else { | ||
// Find latest version | ||
modver.Version = m.versions[len(m.versions)-1] | ||
} | ||
|
||
modfilePath := filepath.Join(dir, "go.mod") | ||
data, err := ioutil.ReadFile(modfilePath) | ||
if os.IsNotExist(err) { | ||
// Treat no go.mod as an error because it is required for shed to work properly. | ||
// If there is no go.mod then go get will function in non-module mode which we don't want. | ||
return errors.Errorf("failed to download %s, no go.mod file found at %s", mod, dir) | ||
} else if err != nil { | ||
return errors.Wrapf(err, "failed to read %s", modfilePath) | ||
} | ||
|
||
modFile, err := modfile.Parse(modfilePath, data, nil) | ||
if err != nil { | ||
return errors.Wrapf(err, "failed to parse go.mod file %s", modfilePath) | ||
} | ||
|
||
// Add resolved module to go.mod | ||
modFile.AddNewRequire(modver.Path, modver.Version, true) | ||
newData, err := modFile.Format() | ||
if err != nil { | ||
return errors.Wrapf(err, "failed to update modfile %s", modfilePath) | ||
} | ||
err = ioutil.WriteFile(modfilePath, newData, 0o644) | ||
if err != nil { | ||
return errors.Wrapf(err, "failed to write modfile %s", modfilePath) | ||
} | ||
return nil | ||
} |
Oops, something went wrong.