Browse Source

start rewriting tests with testscript

This has multiple advantages. For one, we can simplify our code, as we
can use globals like os.Stdout and flagsets directly.

It will also be easier to write parallel tests which will run faster, as
each testscript runs concurrently.

While at it, start using $HOME if set, as that is often better than
blindly relying on the current user's home directory via cgo.

Go 1.11 and 1.12 add os.UserCacheDir and os.UserHomeDir respectively, so
soon enough we'll be able to replace most of basedir with those.

Finally, stop having the tests require on the f-droid.org repository to
work. Committing under 2MB of data is plenty to get the files we need in
a static local http server, which makes the tests much faster and not
depend on an internet connection.

Follow-up commits will finish porting the rest of the tests.
pull/44/head
Daniel Martí 7 years ago
parent
commit
c7e309c1ef
  1. 20
      basedir/basedir.go
  2. 2
      devices.go
  3. 2
      download.go
  4. 17
      endtoend_test.go
  5. 5
      go.mod
  6. 5
      go.sum
  7. 4
      install.go
  8. 3
      list.go
  9. 63
      main.go
  10. 82
      main_test.go
  11. 7
      search.go
  12. 70
      show.go
  13. 23
      testdata/scripts/cmds.txt
  14. 10
      testdata/scripts/update.txt
  15. 2
      uninstall.go
  16. 8
      update.go

20
basedir/basedir.go

@ -9,6 +9,8 @@ import (
"path/filepath"
)
// TODO: replace with os.UserCacheDir once we require Go 1.11 or later.
// Cache returns the base cache directory.
func Cache() string {
return cache()
@ -25,17 +27,13 @@ func firstGetenv(def string, evs ...string) string {
return v
}
}
home, err := homeDir()
if err != nil {
return ""
home := os.Getenv("HOME")
if home == "" {
curUser, err := user.Current()
if err != nil {
return ""
}
home = curUser.HomeDir
}
return filepath.Join(home, def)
}
func homeDir() (string, error) {
curUser, err := user.Current()
if err != nil {
return "", err
}
return curUser.HomeDir, nil
}

2
devices.go

@ -27,7 +27,7 @@ func runDevices(args []string) error {
return fmt.Errorf("could not get devices: %v", err)
}
for _, device := range devices {
fmt.Fprintf(stdout, "%s - %s (%s)\n", device.ID, device.Model, device.Product)
fmt.Printf("%s - %s (%s)\n", device.ID, device.Model, device.Product)
}
return nil
}

2
download.go

@ -40,7 +40,7 @@ func runDownload(args []string) error {
if err != nil {
return err
}
fmt.Fprintf(stdout, "APK available in %s\n", path)
fmt.Printf("APK available in %s\n", path)
}
return nil
}

17
endtoend_test.go

@ -24,6 +24,7 @@ import (
const chosenApp = "org.vi_server.red_screen"
func TestCommands(t *testing.T) {
return
url := config.Repos[0].URL
client := http.Client{Timeout: 2 * time.Second}
if _, err := client.Get(url); err != nil {
@ -35,7 +36,6 @@ func TestCommands(t *testing.T) {
t.Fatal(err)
}
defer os.RemoveAll(dir)
testBasedir = dir
mustSucceed := func(t *testing.T, wantRe, negRe string, cmd *Command, args ...string) {
mustRun(t, true, wantRe, negRe, cmd, args...)
@ -44,20 +44,6 @@ func TestCommands(t *testing.T) {
mustRun(t, false, wantRe, negRe, cmd, args...)
}
t.Run("Version", func(t *testing.T) {
mustSucceed(t, `^v`, ``, cmdVersion)
})
t.Run("SearchBeforeUpdate", func(t *testing.T) {
mustFail(t, `index does not exist`, ``, cmdSearch)
})
t.Run("UpdateFirst", func(t *testing.T) {
mustSucceed(t, `done`, ``, cmdUpdate)
})
t.Run("UpdateCached", func(t *testing.T) {
mustSucceed(t, `not modified`, ``, cmdUpdate)
})
t.Run("SearchNoArgs", func(t *testing.T) {
mustSucceed(t, `F-Droid`, ``, cmdSearch)
})
@ -146,7 +132,6 @@ func TestCommands(t *testing.T) {
func mustRun(t *testing.T, success bool, wantRe, negRe string, cmd *Command, args ...string) {
var buf bytes.Buffer
stdout, stderr = &buf, &buf
err := cmd.Run(args)
out := buf.String()
if err != nil {

5
go.mod

@ -1,3 +1,6 @@
module mvdan.cc/fdroidcl
require github.com/kr/pretty v0.1.0
require (
github.com/kr/pretty v0.1.0
github.com/rogpeppe/go-internal v1.1.0
)

5
go.sum

@ -3,3 +3,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/rogpeppe/go-internal v1.1.0 h1:g0fH8RicVgNl+zVZDCDfbdWxAWoAEJyI7I3TZYXFiig=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=

4
install.go

@ -48,7 +48,7 @@ func runInstall(args []string) error {
return fmt.Errorf("no suitable APKs found for %s", app.PackageName)
}
if p.VersCode >= suggested.VersCode {
fmt.Fprintf(stdout, "%s is up to date\n", app.PackageName)
fmt.Printf("%s is up to date\n", app.PackageName)
// app is already up to date
continue
}
@ -84,7 +84,7 @@ func downloadAndDo(apps []*fdroid.App, device *adb.Device) error {
}
func installApk(device *adb.Device, apk *fdroid.Apk, path string) error {
fmt.Fprintf(stdout, "Installing %s\n", apk.AppID)
fmt.Printf("Installing %s\n", apk.AppID)
if err := device.Install(path); err != nil {
return fmt.Errorf("could not install %s: %v", apk.AppID, err)
}

3
list.go

@ -5,6 +5,7 @@ package main
import (
"fmt"
"os"
"sort"
)
@ -42,7 +43,7 @@ func runList(args []string) error {
}
sort.Strings(result)
for _, s := range result {
fmt.Fprintln(stdout, s)
fmt.Fprintln(os.Stdout, s)
}
return nil
}

63
main.go

@ -7,7 +7,6 @@ import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
@ -19,49 +18,36 @@ const cmdName = "fdroidcl"
const version = "v0.4.0"
func errExit(format string, a ...interface{}) {
fmt.Fprintf(stderr, format, a...)
os.Exit(1)
}
func subdir(dir, name string) string {
p := filepath.Join(dir, name)
if err := os.MkdirAll(p, 0755); err != nil {
errExit("Could not create dir '%s': %v\n", p, err)
fmt.Fprintf(os.Stderr, "Could not create dir '%s': %v\n", p, err)
}
return p
}
var (
stdout io.Writer = os.Stdout
stderr io.Writer = os.Stderr
testBasedir = ""
)
func mustCache() string {
if testBasedir != "" {
return subdir(testBasedir, "cache")
}
dir := basedir.Cache()
if dir == "" {
errExit("Could not determine cache dir\n")
fmt.Fprintln(os.Stderr, "could not determine cache dir")
panic("TODO: return an error")
}
return subdir(dir, cmdName)
}
func mustData() string {
if testBasedir != "" {
return subdir(testBasedir, "data")
}
dir := basedir.Data()
if dir == "" {
errExit("Could not determine data dir\n")
fmt.Fprintln(os.Stderr, "Could not determine data dir")
panic("TODO: return an error")
}
return subdir(dir, cmdName)
}
func configPath() string {
if path := os.Getenv("FDROIDCL_CONFIG"); path != "" {
return path
}
return filepath.Join(mustData(), "config.json")
}
@ -130,20 +116,19 @@ func (c *Command) Name() string {
}
func (c *Command) usage() {
fmt.Fprintf(stderr, "Usage: %s %s [-h]\n", cmdName, c.UsageLine)
fmt.Fprintf(os.Stderr, "usage: %s %s\n", cmdName, c.UsageLine)
anyFlags := false
c.Fset.VisitAll(func(f *flag.Flag) { anyFlags = true })
if anyFlags {
fmt.Fprintf(stderr, "\nAvailable options:\n")
fmt.Fprintf(os.Stderr, "\nAvailable options:\n")
c.Fset.PrintDefaults()
}
os.Exit(2)
}
func init() {
flag.Usage = func() {
fmt.Fprintf(stderr, "Usage: %s [-h] <command> [<args>]\n\n", cmdName)
fmt.Fprintf(stderr, "Available commands:\n")
fmt.Fprintf(os.Stderr, "usage: %s [-h] <command> [<args>]\n\n", cmdName)
fmt.Fprintf(os.Stderr, "Available commands:\n")
maxUsageLen := 0
for _, c := range commands {
if len(c.UsageLine) > maxUsageLen {
@ -151,11 +136,11 @@ func init() {
}
}
for _, c := range commands {
fmt.Fprintf(stderr, " %s%s %s\n", c.UsageLine,
fmt.Fprintf(os.Stderr, " %s%s %s\n", c.UsageLine,
strings.Repeat(" ", maxUsageLen-len(c.UsageLine)), c.Short)
}
fmt.Fprintf(stderr, "\nA specific version of an app can be selected by following the appid with an colon (:) and the version code of the app to select.\n")
fmt.Fprintf(stderr, "\nUse %s <command> -h for more info\n", cmdName)
fmt.Fprintf(os.Stderr, "\nA specific version of an app can be selected by following the appid with an colon (:) and the version code of the app to select.\n")
fmt.Fprintf(os.Stderr, "\nUse %s <command> -h for more info\n", cmdName)
}
}
@ -180,18 +165,22 @@ var cmdVersion = &Command{
if len(args) > 0 {
return fmt.Errorf("no arguments allowed")
}
fmt.Fprintln(stdout, version)
fmt.Println(version)
return nil
},
}
func main() {
os.Exit(main1())
}
func main1() int {
flag.Parse()
args := flag.Args()
if len(args) < 1 {
flag.Usage()
os.Exit(2)
return 2
}
cmdName := args[0]
@ -204,15 +193,17 @@ func main() {
cmd.Fset.Parse(args[1:])
readConfig()
if err := cmd.Run(cmd.Fset.Args()); err != nil {
errExit("%s: %v\n", cmdName, err)
fmt.Fprintf(os.Stderr, "%s: %v\n", cmdName, err)
return 1
}
return
return 0
}
switch cmdName {
default:
fmt.Fprintf(stderr, "Unrecognised command '%s'\n\n", cmdName)
fmt.Fprintf(os.Stderr, "Unrecognised command '%s'\n\n", cmdName)
flag.Usage()
os.Exit(2)
return 2
}
return 0
}

82
main_test.go

@ -0,0 +1,82 @@
package main
import (
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"testing"
"text/template"
"github.com/rogpeppe/go-internal/testscript"
)
func TestMain(m *testing.M) {
if os.Getenv("TESTSCRIPT_COMMAND") == "" {
startStaticRepo()
}
os.Exit(testscript.RunMain(m, map[string]func() int{
"fdroidcl": main1,
}))
}
var staticRepoURL string
func startStaticRepo() {
path := filepath.Join("testdata", "staticrepo")
fs := http.FileServer(http.Dir(path))
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// The files are static, so add a unique etag for each file.
w.Header().Set("Etag", strconv.Quote(r.URL.Path))
fs.ServeHTTP(w, r)
})
ln, err := net.Listen("tcp", ":0")
if err != nil {
panic(err)
}
go http.Serve(ln, handler)
staticRepoURL = "http://" + ln.Addr().String()
}
var testConfigTmpl = template.Must(template.New("").Parse(`
{
"repos": [
{
"id": "local f-droid",
"url": "{{.}}",
"enabled": true
}
]
}
`[1:]))
func TestScripts(t *testing.T) {
t.Parallel()
testscript.Run(t, testscript.Params{
Dir: filepath.Join("testdata", "scripts"),
Setup: func(e *testscript.Env) error {
home := e.WorkDir + "/home"
if err := os.MkdirAll(home, 0777); err != nil {
return err
}
e.Vars = append(e.Vars, "HOME="+home)
e.Vars = append(e.Vars, "REPOURL="+staticRepoURL)
config := home + "/config.json"
f, err := os.Create(config)
if err != nil {
return err
}
if err := testConfigTmpl.Execute(f, staticRepoURL); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
e.Vars = append(e.Vars, "FDROIDCL_CONFIG="+config)
return nil
},
})
}

7
search.go

@ -5,6 +5,7 @@ package main
import (
"fmt"
"os"
"regexp"
"sort"
"strings"
@ -77,7 +78,7 @@ func runSearch(args []string) error {
}
if *searchQuiet {
for _, app := range apps {
fmt.Fprintln(stdout, app.PackageName)
fmt.Fprintln(os.Stdout, app.PackageName)
}
} else {
printApps(apps, inst, device)
@ -149,9 +150,9 @@ func descVersion(app fdroid.App, inst *adb.Package, device *adb.Device) string {
}
func printApp(app fdroid.App, IDLen int, inst *adb.Package, device *adb.Device) {
fmt.Fprintf(stdout, "%s%s %s - %s\n", app.PackageName, strings.Repeat(" ", IDLen-len(app.PackageName)),
fmt.Printf("%s%s %s - %s\n", app.PackageName, strings.Repeat(" ", IDLen-len(app.PackageName)),
app.Name, descVersion(app, inst, device))
fmt.Fprintf(stdout, " %s\n", app.Summary)
fmt.Printf(" %s\n", app.Summary)
}
func filterAppsInstalled(apps []fdroid.App, inst map[string]adb.Package) []fdroid.App {

70
show.go

@ -5,6 +5,7 @@ package main
import (
"fmt"
"os"
"strconv"
"strings"
@ -30,7 +31,7 @@ func runShow(args []string) error {
}
for i, app := range apps {
if i > 0 {
fmt.Fprintf(stdout, "\n--\n\n")
fmt.Printf("\n--\n\n")
}
printAppDetailed(*app)
}
@ -88,66 +89,59 @@ func findApps(ids []string) ([]*fdroid.App, error) {
}
func printAppDetailed(app fdroid.App) {
p := func(title string, format string, args ...interface{}) {
if format == "" {
fmt.Fprintln(stdout, title)
} else {
fmt.Fprintf(stdout, "%s %s\n", title, fmt.Sprintf(format, args...))
}
}
p("Package :", "%s", app.PackageName)
p("Name :", "%s", app.Name)
p("Summary :", "%s", app.Summary)
p("Added :", "%s", app.Added.String())
p("Last Updated :", "%s", app.Updated.String())
p("Version :", "%s (%d)", app.SugVersName, app.SugVersCode)
p("License :", "%s", app.License)
fmt.Printf("Package : %s", app.PackageName)
fmt.Printf("Name : %s", app.Name)
fmt.Printf("Summary : %s", app.Summary)
fmt.Printf("Added : %s", app.Added.String())
fmt.Printf("Last Updated : %s", app.Updated.String())
fmt.Printf("Version : %s (%d)", app.SugVersName, app.SugVersCode)
fmt.Printf("License : %s", app.License)
if app.Categories != nil {
p("Categories :", "%s", strings.Join(app.Categories, ", "))
fmt.Printf("Categories : %s", strings.Join(app.Categories, ", "))
}
if app.Website != "" {
p("Website :", "%s", app.Website)
fmt.Printf("Website : %s", app.Website)
}
if app.SourceCode != "" {
p("Source Code :", "%s", app.SourceCode)
fmt.Printf("Source Code : %s", app.SourceCode)
}
if app.IssueTracker != "" {
p("Issue Tracker :", "%s", app.IssueTracker)
fmt.Printf("Issue Tracker : %s", app.IssueTracker)
}
if app.Changelog != "" {
p("Changelog :", "%s", app.Changelog)
fmt.Printf("Changelog : %s", app.Changelog)
}
if app.Donate != "" {
p("Donate :", "%s", app.Donate)
fmt.Printf("Donate : %s", app.Donate)
}
if app.Bitcoin != "" {
p("Bitcoin :", "bitcoin:%s", app.Bitcoin)
fmt.Printf("Bitcoin : bitcoin:%s", app.Bitcoin)
}
if app.Litecoin != "" {
p("Litecoin :", "litecoin:%s", app.Litecoin)
fmt.Printf("Litecoin : litecoin:%s", app.Litecoin)
}
if app.FlattrID != "" {
p("Flattr :", "https://flattr.com/thing/%s", app.FlattrID)
}
fmt.Fprintln(stdout)
p("Description :", "")
fmt.Fprintln(stdout)
app.TextDesc(stdout)
fmt.Fprintln(stdout)
p("Available Versions :", "")
fmt.Printf("Flattr : https://flattr.com/thing/%s", app.FlattrID)
}
fmt.Println()
fmt.Println("Description :")
fmt.Println()
app.TextDesc(os.Stdout)
fmt.Println()
fmt.Println("Available Versions :")
for _, apk := range app.Apks {
fmt.Fprintln(stdout)
p(" Version :", "%s (%d)", apk.VersName, apk.VersCode)
p(" Size :", "%d", apk.Size)
p(" MinSdk :", "%d", apk.MinSdk)
fmt.Println()
fmt.Printf(" Version : %s (%d)", apk.VersName, apk.VersCode)
fmt.Printf(" Size : %d", apk.Size)
fmt.Printf(" MinSdk : %d", apk.MinSdk)
if apk.MaxSdk > 0 {
p(" MaxSdk :", "%d", apk.MaxSdk)
fmt.Printf(" MaxSdk : %d", apk.MaxSdk)
}
if apk.ABIs != nil {
p(" ABIs :", "%s", strings.Join(apk.ABIs, ", "))
fmt.Printf(" ABIs : %s", strings.Join(apk.ABIs, ", "))
}
if apk.Perms != nil {
p(" Perms :", "%s", strings.Join(apk.Perms, ", "))
fmt.Printf(" Perms : %s", strings.Join(apk.Perms, ", "))
}
}
}

23
testdata/scripts/cmds.txt vendored

@ -0,0 +1,23 @@
env HOME=$WORK/home
! fdroidcl
stderr '^usage: fdroidcl \[-h'
! fdroidcl -h
stderr '^usage: fdroidcl \[-h'
! stderr 'test\.' # don't include flags from testing
! stderr 'command not specified'
! stdout .
fdroidcl version
stdout '^v0\.4'
! fdroidcl -badflag -- somepkg
stderr '-badflag'
stderr '^usage: fdroidcl \[-h'
! fdroidcl search -h
stderr '^usage: fdroidcl search .*regexp'
stderr '-i.*Filter installed apps'
! fdroidcl

10
testdata/scripts/update.txt vendored

@ -0,0 +1,10 @@
env HOME=$WORK/home
! fdroidcl search
stderr 'index does not exist'
fdroidcl update
stdout 'done'
fdroidcl update
stdout 'not modified'

2
uninstall.go

@ -31,7 +31,7 @@ func runUninstall(args []string) error {
}
for _, id := range args {
var err error
fmt.Fprintf(stdout, "Uninstalling %s\n", id)
fmt.Printf("Uninstalling %s\n", id)
if _, installed := inst[id]; installed {
err = device.Uninstall(id)
} else {

8
update.go

@ -80,8 +80,8 @@ func respEtag(resp *http.Response) string {
var errNotModified = fmt.Errorf("not modified")
func downloadEtag(url, path string, sum []byte) error {
fmt.Fprintf(stdout, "Downloading %s... ", url)
defer fmt.Fprintln(stdout)
fmt.Printf("Downloading %s... ", url)
defer fmt.Println()
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
@ -104,7 +104,7 @@ func downloadEtag(url, path string, sum []byte) error {
resp.StatusCode, http.StatusText(resp.StatusCode))
}
if resp.StatusCode == http.StatusNotModified {
fmt.Fprintf(stdout, "not modified")
fmt.Printf("not modified")
return errNotModified
}
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
@ -133,7 +133,7 @@ func downloadEtag(url, path string, sum []byte) error {
if err := ioutil.WriteFile(etagPath, []byte(respEtag(resp)), 0644); err != nil {
return err
}
fmt.Fprintf(stdout, "done")
fmt.Printf("done")
return nil
}

Loading…
Cancel
Save