Skip to content

Commit

Permalink
feat: add basic CLI tests using Go Test
Browse files Browse the repository at this point in the history
This is intended as a replacement for sharness. These are vanilla Go
tests which can be run in your IDE for quick iteration on end-to-end
CLI tests.

This also removes IPTB by duplicating its functionality in the test
harness. This isn't a big deal...IPTB's complexity is mostly around
the fact that its state needs to be saved to disk in between `iptb`
command invocations, and that it uses Go plugins to inject
functionality, neither of which are relevant here.

If we merge this, we'll have to live with bifurcated tests for a while
until they are all migrated. I'd recommend we self-enforce a rule
that, if we need to touch a sharness test, we migrate it and one more
test over to Go tests first. Then eventually we will have migrated
everything.
  • Loading branch information
guseggert committed Dec 12, 2022
1 parent 08e9bb3 commit 579175f
Show file tree
Hide file tree
Showing 22 changed files with 1,657 additions and 609 deletions.
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
linters:
enable:
- stylecheck

linters-settings:
stylecheck:
dot-import-whitelist:
- github.com/ipfs/kubo/test/cli/testutils
2 changes: 1 addition & 1 deletion coverage/Rules.mk
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ include mk/header.mk

GOCC ?= go

$(d)/coverage_deps: $$(DEPS_GO)
$(d)/coverage_deps: $$(DEPS_GO) cmd/ipfs/ipfs
rm -rf $(@D)/unitcover && mkdir $(@D)/unitcover
rm -rf $(@D)/sharnesscover && mkdir $(@D)/sharnesscover

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ require (
go.uber.org/fx v1.18.2
go.uber.org/zap v1.24.0
golang.org/x/crypto v0.3.0
golang.org/x/mod v0.7.0
golang.org/x/sync v0.1.0
golang.org/x/sys v0.3.0
)
Expand Down Expand Up @@ -233,7 +234,6 @@ require (
go.uber.org/multierr v1.8.0 // indirect
go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.3.0 // indirect
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
golang.org/x/term v0.3.0 // indirect
Expand Down
238 changes: 238 additions & 0 deletions test/cli/basic_commands_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package cli

import (
"fmt"
"regexp"
"strings"
"testing"

"github.com/blang/semver/v4"
"github.com/ipfs/kubo/test/cli/harness"
. "github.com/ipfs/kubo/test/cli/testutils"
"github.com/stretchr/testify/assert"
gomod "golang.org/x/mod/module"
)

var versionRegexp = regexp.MustCompile(`^ipfs version (.+)$`)

func parseVersionOutput(s string) semver.Version {
versString := versionRegexp.FindStringSubmatch(s)[1]
v, err := semver.Parse(versString)
if err != nil {
panic(err)
}
return v
}

func TestCurDirIsWritable(t *testing.T) {
t.Parallel()
h := harness.NewT(t)
h.WriteFile("test.txt", "It works!")
}

func TestIPFSVersionCommandMatchesFlag(t *testing.T) {
t.Parallel()
node := harness.NewT(t).NewNode()
commandVersionStr := node.IPFS("version").Stdout.String()
commandVersionStr = strings.TrimSpace(commandVersionStr)
commandVersion := parseVersionOutput(commandVersionStr)

flagVersionStr := node.IPFS("--version").Stdout.String()
flagVersionStr = strings.TrimSpace(flagVersionStr)
flagVersion := parseVersionOutput(flagVersionStr)

assert.Equal(t, commandVersion, flagVersion)
}

func TestIPFSVersionAll(t *testing.T) {
t.Parallel()
node := harness.NewT(t).NewNode()
res := node.IPFS("version", "--all").Stdout.String()
res = strings.TrimSpace(res)
assert.Contains(t, res, "Kubo version")
assert.Contains(t, res, "Repo version")
assert.Contains(t, res, "System version")
assert.Contains(t, res, "Golang version")
}

func TestIPFSVersionDeps(t *testing.T) {
t.Parallel()
node := harness.NewT(t).NewNode()
res := node.IPFS("version", "deps").Stdout.String()
res = strings.TrimSpace(res)
lines := SplitLines(res)

assert.Equal(t, "github.com/ipfs/kubo@(devel)", lines[0])

for _, depLine := range lines[1:] {
split := strings.Split(depLine, " => ")
for _, moduleVersion := range split {
splitModVers := strings.Split(moduleVersion, "@")
modPath := splitModVers[0]
modVers := splitModVers[1]
assert.NoError(t, gomod.Check(modPath, modVers), "path: %s, version: %s", modPath, modVers)
}
}
}

func TestIPFSCommands(t *testing.T) {
t.Parallel()
node := harness.NewT(t).NewNode()
cmds := node.IPFSCommands()
assert.Contains(t, cmds, "ipfs add")
assert.Contains(t, cmds, "ipfs daemon")
assert.Contains(t, cmds, "ipfs update")
}

func TestAllSubcommandsAcceptHelp(t *testing.T) {
t.Parallel()
node := harness.NewT(t).NewNode()
for _, cmd := range node.IPFSCommands() {
t.Run(fmt.Sprintf("command %q accepts help", cmd), func(t *testing.T) {
t.Parallel()
splitCmd := strings.Split(cmd, " ")[1:]
node.IPFS(StrCat("help", splitCmd)...)
node.IPFS(StrCat(splitCmd, "--help")...)
})
}
}

func TestAllRootCommandsAreMentionedInHelpText(t *testing.T) {
t.Parallel()
node := harness.NewT(t).NewNode()
cmds := node.IPFSCommands()
var rootCmds []string
for _, cmd := range cmds {
splitCmd := strings.Split(cmd, " ")
if len(splitCmd) == 2 {
rootCmds = append(rootCmds, splitCmd[1])
}
}

// a few base commands are not expected to be in the help message
// but we default to requiring them to be in the help message, so that we
// have to make an conscious decision to exclude them
notInHelp := map[string]bool{
"object": true,
"shutdown": true,
"tar": true,
"urlstore": true,
"dns": true,
}

helpMsg := strings.TrimSpace(node.IPFS("--help").Stdout.String())
for _, rootCmd := range rootCmds {
if _, ok := notInHelp[rootCmd]; ok {
continue
}
assert.Contains(t, helpMsg, fmt.Sprintf(" %s", rootCmd))
}
}

func TestCommandDocsWidth(t *testing.T) {
t.Parallel()
node := harness.NewT(t).NewNode()

// require new commands to explicitly opt in to longer lines
allowList := map[string]bool{
"ipfs add": true,
"ipfs block put": true,
"ipfs daemon": true,
"ipfs config profile": true,
"ipfs pin remote service": true,
"ipfs name pubsub": true,
"ipfs object patch": true,
"ipfs swarm connect": true,
"ipfs p2p forward": true,
"ipfs p2p close": true,
"ipfs swarm disconnect": true,
"ipfs swarm addrs listen": true,
"ipfs dag resolve": true,
"ipfs dag get": true,
"ipfs object stat": true,
"ipfs pin remote add": true,
"ipfs config show": true,
"ipfs config edit": true,
"ipfs pin remote rm": true,
"ipfs pin remote ls": true,
"ipfs pin verify": true,
"ipfs dht get": true,
"ipfs pin remote service add": true,
"ipfs file ls": true,
"ipfs pin update": true,
"ipfs pin rm": true,
"ipfs p2p": true,
"ipfs resolve": true,
"ipfs dag stat": true,
"ipfs name publish": true,
"ipfs object diff": true,
"ipfs object patch add-link": true,
"ipfs name": true,
"ipfs object patch append-data": true,
"ipfs object patch set-data": true,
"ipfs dht put": true,
"ipfs diag profile": true,
"ipfs diag cmds": true,
"ipfs swarm addrs local": true,
"ipfs files ls": true,
"ipfs stats bw": true,
"ipfs urlstore add": true,
"ipfs swarm peers": true,
"ipfs pubsub sub": true,
"ipfs repo fsck": true,
"ipfs files write": true,
"ipfs swarm limit": true,
"ipfs commands completion fish": true,
"ipfs key export": true,
"ipfs routing get": true,
"ipfs refs": true,
"ipfs refs local": true,
"ipfs cid base32": true,
"ipfs pubsub pub": true,
"ipfs repo ls": true,
"ipfs routing put": true,
"ipfs key import": true,
"ipfs swarm peering add": true,
"ipfs swarm peering rm": true,
"ipfs swarm peering ls": true,
"ipfs update": true,
"ipfs swarm stats": true,
}
for _, cmd := range node.IPFSCommands() {
if _, ok := allowList[cmd]; ok {
continue
}
t.Run(fmt.Sprintf("command %q conforms to docs width limit", cmd), func(t *testing.T) {
splitCmd := strings.Split(cmd, " ")
resStr := node.IPFS(StrCat(splitCmd[1:], "--help")...)
res := strings.TrimSpace(resStr.Stdout.String())
for _, line := range SplitLines(res) {
assert.LessOrEqualf(t, len(line), 80, "expected width %d < 80 for %q", len(line), cmd)
}

})
}
}

func TestAllCommandsFailWhenPassedBadFlag(t *testing.T) {
t.Parallel()
node := harness.NewT(t).NewNode()

for _, cmd := range node.IPFSCommands() {
t.Run(fmt.Sprintf("command %q fails when passed a bad flag", cmd), func(t *testing.T) {
splitCmd := strings.Split(cmd, " ")
res := node.RunIPFS(StrCat(splitCmd, "--badflag")...)
assert.Equal(t, 1, res.Cmd.ProcessState.ExitCode())
})
}

}

func TestCommandsFlags(t *testing.T) {
t.Parallel()
node := harness.NewT(t).NewNode()
resStr := node.IPFS("commands", "--flags").Stdout.String()
assert.Contains(t, resStr, "ipfs pin add --recursive / ipfs pin add -r")
assert.Contains(t, resStr, "ipfs id --format / ipfs id -f")
assert.Contains(t, resStr, "ipfs repo gc --quiet / ipfs repo gc -q")
}
31 changes: 31 additions & 0 deletions test/cli/completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cli

import (
"fmt"
"testing"

"github.com/ipfs/kubo/test/cli/harness"
. "github.com/ipfs/kubo/test/cli/testutils"
"github.com/stretchr/testify/assert"
)

func TestBashCompletion(t *testing.T) {
t.Parallel()
h := harness.NewT(t)
node := h.NewNode()

res := node.IPFS("commands", "completion", "bash")

length := len(res.Stdout.String())
if length < 100 {
t.Fatalf("expected a long Bash completion file, but got one of length %d", length)
}

t.Run("completion file can be loaded in bash", func(t *testing.T) {
RequiresLinux(t)

completionFile := h.WriteToTemp(res.Stdout.String())
res = h.Sh(fmt.Sprintf("source %s && type -t _ipfs", completionFile))
assert.NoError(t, res.Err)
})
}
Loading

0 comments on commit 579175f

Please sign in to comment.