feat: lite-mode - CLI tests for lotus client commands

This commit is contained in:
Dirk McCormick 2020-10-20 17:08:25 +02:00
parent 92942d44d1
commit 906286fdbe
15 changed files with 381 additions and 162 deletions

View File

@ -34,5 +34,6 @@ type GatewayAPI interface {
StateMarketStorageDeal(ctx context.Context, dealId abi.DealID, tsk types.TipSetKey) (*MarketDeal, error) StateMarketStorageDeal(ctx context.Context, dealId abi.DealID, tsk types.TipSetKey) (*MarketDeal, error)
StateMinerInfo(ctx context.Context, actor address.Address, tsk types.TipSetKey) (miner.MinerInfo, error) StateMinerInfo(ctx context.Context, actor address.Address, tsk types.TipSetKey) (miner.MinerInfo, error)
StateNetworkVersion(context.Context, types.TipSetKey) (network.Version, error) StateNetworkVersion(context.Context, types.TipSetKey) (network.Version, error)
StateVerifiedClientStatus(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*abi.StoragePower, error)
StateWaitMsg(ctx context.Context, msg cid.Cid, confidence uint64) (*MsgLookup, error) StateWaitMsg(ctx context.Context, msg cid.Cid, confidence uint64) (*MsgLookup, error)
} }

View File

@ -396,6 +396,7 @@ type GatewayStruct struct {
StateMarketBalance func(ctx context.Context, addr address.Address, tsk types.TipSetKey) (api.MarketBalance, error) StateMarketBalance func(ctx context.Context, addr address.Address, tsk types.TipSetKey) (api.MarketBalance, error)
StateMarketStorageDeal func(ctx context.Context, dealId abi.DealID, tsk types.TipSetKey) (*api.MarketDeal, error) StateMarketStorageDeal func(ctx context.Context, dealId abi.DealID, tsk types.TipSetKey) (*api.MarketDeal, error)
StateNetworkVersion func(ctx context.Context, tsk types.TipSetKey) (stnetwork.Version, error) StateNetworkVersion func(ctx context.Context, tsk types.TipSetKey) (stnetwork.Version, error)
StateVerifiedClientStatus func(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*abi.StoragePower, error)
StateWaitMsg func(ctx context.Context, msg cid.Cid, confidence uint64) (*api.MsgLookup, error) StateWaitMsg func(ctx context.Context, msg cid.Cid, confidence uint64) (*api.MsgLookup, error)
} }
} }
@ -1547,6 +1548,10 @@ func (g GatewayStruct) StateNetworkVersion(ctx context.Context, tsk types.TipSet
return g.Internal.StateNetworkVersion(ctx, tsk) return g.Internal.StateNetworkVersion(ctx, tsk)
} }
func (g GatewayStruct) StateVerifiedClientStatus(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*abi.StoragePower, error) {
return g.Internal.StateVerifiedClientStatus(ctx, addr, tsk)
}
func (g GatewayStruct) StateWaitMsg(ctx context.Context, msg cid.Cid, confidence uint64) (*api.MsgLookup, error) { func (g GatewayStruct) StateWaitMsg(ctx context.Context, msg cid.Cid, confidence uint64) (*api.MsgLookup, error) {
return g.Internal.StateWaitMsg(ctx, msg, confidence) return g.Internal.StateWaitMsg(ctx, msg, confidence)
} }

View File

@ -106,21 +106,7 @@ func TestDoubleDealFlow(t *testing.T, b APIBuilder, blocktime time.Duration) {
} }
func MakeDeal(t *testing.T, ctx context.Context, rseed int, client api.FullNode, miner TestStorageNode, carExport, fastRet bool) { func MakeDeal(t *testing.T, ctx context.Context, rseed int, client api.FullNode, miner TestStorageNode, carExport, fastRet bool) {
data := make([]byte, 1600) res, data, err := CreateClientFile(ctx, client, rseed)
rand.New(rand.NewSource(int64(rseed))).Read(data)
dir, err := ioutil.TempDir(os.TempDir(), "test-make-deal-")
if err != nil {
t.Fatal(err)
}
path := filepath.Join(dir, "sourcefile.dat")
err = ioutil.WriteFile(path, data, 0644)
if err != nil {
t.Fatal(err)
}
res, err := client.ClientImport(ctx, api.FileRef{Path: path})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -141,6 +127,28 @@ func MakeDeal(t *testing.T, ctx context.Context, rseed int, client api.FullNode,
testRetrieval(t, ctx, client, fcid, &info.PieceCID, carExport, data) testRetrieval(t, ctx, client, fcid, &info.PieceCID, carExport, data)
} }
func CreateClientFile(ctx context.Context, client api.FullNode, rseed int) (*api.ImportRes, []byte, error) {
data := make([]byte, 1600)
rand.New(rand.NewSource(int64(rseed))).Read(data)
dir, err := ioutil.TempDir(os.TempDir(), "test-make-deal-")
if err != nil {
return nil, nil, err
}
path := filepath.Join(dir, "sourcefile.dat")
err = ioutil.WriteFile(path, data, 0644)
if err != nil {
return nil, nil, err
}
res, err := client.ClientImport(ctx, api.FileRef{Path: path})
if err != nil {
return nil, nil, err
}
return res, data, nil
}
func TestFastRetrievalDealFlow(t *testing.T, b APIBuilder, blocktime time.Duration) { func TestFastRetrievalDealFlow(t *testing.T, b APIBuilder, blocktime time.Duration) {
ctx := context.Background() ctx := context.Background()

View File

@ -343,6 +343,7 @@ var clientDealCmd = &cli.Command{
} }
defer closer() defer closer()
ctx := ReqContext(cctx) ctx := ReqContext(cctx)
afmt := NewAppFmt(cctx.App)
if cctx.NArg() != 4 { if cctx.NArg() != 4 {
return xerrors.New("expected 4 args: dataCid, miner, price, duration") return xerrors.New("expected 4 args: dataCid, miner, price, duration")
@ -462,7 +463,7 @@ var clientDealCmd = &cli.Command{
return err return err
} }
fmt.Println(encoder.Encode(*proposal)) afmt.Println(encoder.Encode(*proposal))
return nil return nil
}, },
@ -477,6 +478,7 @@ func interactiveDeal(cctx *cli.Context) error {
ctx := ReqContext(cctx) ctx := ReqContext(cctx)
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
afmt := NewAppFmt(cctx.App)
state := "import" state := "import"
gib := types.NewInt(1 << 30) gib := types.NewInt(1 << 30)
@ -517,10 +519,10 @@ func interactiveDeal(cctx *cli.Context) error {
} }
printErr := func(err error) { printErr := func(err error) {
fmt.Printf("%s %s\n", color.RedString("Error:"), err.Error()) afmt.Printf("%s %s\n", color.RedString("Error:"), err.Error())
} }
cs := readline.NewCancelableStdin(os.Stdin) cs := readline.NewCancelableStdin(afmt.Stdin)
go func() { go func() {
<-ctx.Done() <-ctx.Done()
cs.Close() // nolint:errcheck cs.Close() // nolint:errcheck
@ -537,7 +539,7 @@ uiLoop:
switch state { switch state {
case "import": case "import":
fmt.Print("Data CID (from " + color.YellowString("lotus client import") + "): ") afmt.Print("Data CID (from " + color.YellowString("lotus client import") + "): ")
_cidStr, _, err := rl.ReadLine() _cidStr, _, err := rl.ReadLine()
cidStr := string(_cidStr) cidStr := string(_cidStr)
@ -560,7 +562,7 @@ uiLoop:
state = "duration" state = "duration"
case "duration": case "duration":
fmt.Print("Deal duration (days): ") afmt.Print("Deal duration (days): ")
_daystr, _, err := rl.ReadLine() _daystr, _, err := rl.ReadLine()
daystr := string(_daystr) daystr := string(_daystr)
@ -605,7 +607,7 @@ uiLoop:
continue continue
} }
fmt.Print("\nMake this a verified deal? (yes/no): ") afmt.Print("\nMake this a verified deal? (yes/no): ")
_yn, _, err := rl.ReadLine() _yn, _, err := rl.ReadLine()
yn := string(_yn) yn := string(_yn)
@ -619,13 +621,13 @@ uiLoop:
case "no": case "no":
verified = false verified = false
default: default:
fmt.Println("Type in full 'yes' or 'no'") afmt.Println("Type in full 'yes' or 'no'")
continue continue
} }
state = "miner" state = "miner"
case "miner": case "miner":
fmt.Print("Miner Addresses (f0.. f0..), none to find: ") afmt.Print("Miner Addresses (f0.. f0..), none to find: ")
_maddrsStr, _, err := rl.ReadLine() _maddrsStr, _, err := rl.ReadLine()
maddrsStr := string(_maddrsStr) maddrsStr := string(_maddrsStr)
@ -664,11 +666,11 @@ uiLoop:
candidateAsks = append(candidateAsks, ask) candidateAsks = append(candidateAsks, ask)
} }
fmt.Printf("Found %d candidate asks\n", len(candidateAsks)) afmt.Printf("Found %d candidate asks\n", len(candidateAsks))
state = "find-budget" state = "find-budget"
case "find-budget": case "find-budget":
fmt.Printf("Proposing from %s, Current Balance: %s\n", a, types.FIL(fromBal)) afmt.Printf("Proposing from %s, Current Balance: %s\n", a, types.FIL(fromBal))
fmt.Print("Maximum budget (FIL): ") // TODO: Propose some default somehow? afmt.Print("Maximum budget (FIL): ") // TODO: Propose some default somehow?
_budgetStr, _, err := rl.ReadLine() _budgetStr, _, err := rl.ReadLine()
budgetStr := string(_budgetStr) budgetStr := string(_budgetStr)
@ -698,10 +700,10 @@ uiLoop:
} }
} }
candidateAsks = goodAsks candidateAsks = goodAsks
fmt.Printf("%d asks within budget\n", len(candidateAsks)) afmt.Printf("%d asks within budget\n", len(candidateAsks))
state = "find-count" state = "find-count"
case "find-count": case "find-count":
fmt.Print("Deals to make (1): ") afmt.Print("Deals to make (1): ")
dealcStr, _, err := rl.ReadLine() dealcStr, _, err := rl.ReadLine()
if err != nil { if err != nil {
printErr(xerrors.Errorf("reading deal count: %w", err)) printErr(xerrors.Errorf("reading deal count: %w", err))
@ -780,12 +782,12 @@ uiLoop:
case "confirm": case "confirm":
// TODO: do some more or epochs math (round to miner PP, deal start buffer) // TODO: do some more or epochs math (round to miner PP, deal start buffer)
fmt.Printf("-----\n") afmt.Printf("-----\n")
fmt.Printf("Proposing from %s\n", a) afmt.Printf("Proposing from %s\n", a)
fmt.Printf("\tBalance: %s\n", types.FIL(fromBal)) afmt.Printf("\tBalance: %s\n", types.FIL(fromBal))
fmt.Printf("\n") afmt.Printf("\n")
fmt.Printf("Piece size: %s (Payload size: %s)\n", units.BytesSize(float64(ds.PieceSize)), units.BytesSize(float64(ds.PayloadSize))) afmt.Printf("Piece size: %s (Payload size: %s)\n", units.BytesSize(float64(ds.PieceSize)), units.BytesSize(float64(ds.PayloadSize)))
fmt.Printf("Duration: %s\n", dur) afmt.Printf("Duration: %s\n", dur)
pricePerGib := big.Zero() pricePerGib := big.Zero()
for _, a := range ask { for _, a := range ask {
@ -804,7 +806,7 @@ uiLoop:
if len(ask) > 1 { if len(ask) > 1 {
totalPrice := types.BigMul(epochPrice, types.NewInt(uint64(epochs))) totalPrice := types.BigMul(epochPrice, types.NewInt(uint64(epochs)))
fmt.Printf("Miner %s (Power:%s) price: ~%s (%s per epoch)\n", color.YellowString(a.Miner.String()), color.GreenString(types.SizeStr(mpow.MinerPower.QualityAdjPower)), color.BlueString(types.FIL(totalPrice).String()), types.FIL(epochPrice)) afmt.Printf("Miner %s (Power:%s) price: ~%s (%s per epoch)\n", color.YellowString(a.Miner.String()), color.GreenString(types.SizeStr(mpow.MinerPower.QualityAdjPower)), color.BlueString(types.FIL(totalPrice).String()), types.FIL(epochPrice))
} }
} }
@ -812,12 +814,12 @@ uiLoop:
epochPrice := types.BigDiv(types.BigMul(pricePerGib, types.NewInt(uint64(ds.PieceSize))), gib) epochPrice := types.BigDiv(types.BigMul(pricePerGib, types.NewInt(uint64(ds.PieceSize))), gib)
totalPrice := types.BigMul(epochPrice, types.NewInt(uint64(epochs))) totalPrice := types.BigMul(epochPrice, types.NewInt(uint64(epochs)))
fmt.Printf("Total price: ~%s (%s per epoch)\n", color.CyanString(types.FIL(totalPrice).String()), types.FIL(epochPrice)) afmt.Printf("Total price: ~%s (%s per epoch)\n", color.CyanString(types.FIL(totalPrice).String()), types.FIL(epochPrice))
fmt.Printf("Verified: %v\n", verified) afmt.Printf("Verified: %v\n", verified)
state = "accept" state = "accept"
case "accept": case "accept":
fmt.Print("\nAccept (yes/no): ") afmt.Print("\nAccept (yes/no): ")
_yn, _, err := rl.ReadLine() _yn, _, err := rl.ReadLine()
yn := string(_yn) yn := string(_yn)
@ -830,7 +832,7 @@ uiLoop:
} }
if yn != "yes" { if yn != "yes" {
fmt.Println("Type in full 'yes' or 'no'") afmt.Println("Type in full 'yes' or 'no'")
continue continue
} }
@ -861,7 +863,7 @@ uiLoop:
return err return err
} }
fmt.Printf("Deal (%s) CID: %s\n", maddr, color.GreenString(encoder.Encode(*proposal))) afmt.Printf("Deal (%s) CID: %s\n", maddr, color.GreenString(encoder.Encode(*proposal)))
} }
return nil return nil
@ -975,6 +977,7 @@ var clientRetrieveCmd = &cli.Command{
} }
defer closer() defer closer()
ctx := ReqContext(cctx) ctx := ReqContext(cctx)
afmt := NewAppFmt(cctx.App)
var payer address.Address var payer address.Address
if cctx.String("from") != "" { if cctx.String("from") != "" {
@ -1083,14 +1086,14 @@ var clientRetrieveCmd = &cli.Command{
select { select {
case evt, ok := <-updates: case evt, ok := <-updates:
if ok { if ok {
fmt.Printf("> Recv: %s, Paid %s, %s (%s)\n", afmt.Printf("> Recv: %s, Paid %s, %s (%s)\n",
types.SizeStr(types.NewInt(evt.BytesReceived)), types.SizeStr(types.NewInt(evt.BytesReceived)),
types.FIL(evt.FundsSpent), types.FIL(evt.FundsSpent),
retrievalmarket.ClientEvents[evt.Event], retrievalmarket.ClientEvents[evt.Event],
retrievalmarket.DealStatuses[evt.Status], retrievalmarket.DealStatuses[evt.Status],
) )
} else { } else {
fmt.Println("Success") afmt.Println("Success")
return nil return nil
} }
@ -1269,8 +1272,9 @@ var clientQueryAskCmd = &cli.Command{
}, },
}, },
Action: func(cctx *cli.Context) error { Action: func(cctx *cli.Context) error {
afmt := NewAppFmt(cctx.App)
if cctx.NArg() != 1 { if cctx.NArg() != 1 {
fmt.Println("Usage: query-ask [minerAddress]") afmt.Println("Usage: query-ask [minerAddress]")
return nil return nil
} }
@ -1311,23 +1315,23 @@ var clientQueryAskCmd = &cli.Command{
return err return err
} }
fmt.Printf("Ask: %s\n", maddr) afmt.Printf("Ask: %s\n", maddr)
fmt.Printf("Price per GiB: %s\n", types.FIL(ask.Price)) afmt.Printf("Price per GiB: %s\n", types.FIL(ask.Price))
fmt.Printf("Verified Price per GiB: %s\n", types.FIL(ask.VerifiedPrice)) afmt.Printf("Verified Price per GiB: %s\n", types.FIL(ask.VerifiedPrice))
fmt.Printf("Max Piece size: %s\n", types.SizeStr(types.NewInt(uint64(ask.MaxPieceSize)))) afmt.Printf("Max Piece size: %s\n", types.SizeStr(types.NewInt(uint64(ask.MaxPieceSize))))
size := cctx.Int64("size") size := cctx.Int64("size")
if size == 0 { if size == 0 {
return nil return nil
} }
perEpoch := types.BigDiv(types.BigMul(ask.Price, types.NewInt(uint64(size))), types.NewInt(1<<30)) perEpoch := types.BigDiv(types.BigMul(ask.Price, types.NewInt(uint64(size))), types.NewInt(1<<30))
fmt.Printf("Price per Block: %s\n", types.FIL(perEpoch)) afmt.Printf("Price per Block: %s\n", types.FIL(perEpoch))
duration := cctx.Int64("duration") duration := cctx.Int64("duration")
if duration == 0 { if duration == 0 {
return nil return nil
} }
fmt.Printf("Total Price: %s\n", types.FIL(types.BigMul(perEpoch, types.NewInt(uint64(duration))))) afmt.Printf("Total Price: %s\n", types.FIL(types.BigMul(perEpoch, types.NewInt(uint64(duration)))))
return nil return nil
}, },
@ -1410,7 +1414,7 @@ var clientListDeals = &cli.Command{
} }
} }
return outputStorageDeals(ctx, os.Stdout, api, localDeals, cctx.Bool("verbose"), cctx.Bool("color"), showFailed) return outputStorageDeals(ctx, cctx.App.Writer, api, localDeals, cctx.Bool("verbose"), cctx.Bool("color"), showFailed)
}, },
} }

22
cli/client_test.go Normal file
View File

@ -0,0 +1,22 @@
package cli
import (
"context"
"os"
"testing"
"time"
clitest "github.com/filecoin-project/lotus/cli/test"
)
// TestClient does a basic test to exercise the client CLI
// commands
func TestClient(t *testing.T) {
_ = os.Setenv("BELLMAN_NO_GPU", "1")
clitest.QuietMiningLogs()
blocktime := 5 * time.Millisecond
ctx := context.Background()
clientNode, _ := clitest.StartOneNodeOneMiner(ctx, t, blocktime)
clitest.RunClientTest(t, Commands, clientNode)
}

View File

@ -2,15 +2,16 @@ package cli
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"github.com/urfave/cli/v2" ufcli "github.com/urfave/cli/v2"
"golang.org/x/xerrors" "golang.org/x/xerrors"
) )
type PrintHelpErr struct { type PrintHelpErr struct {
Err error Err error
Ctx *cli.Context Ctx *ufcli.Context
} }
func (e *PrintHelpErr) Error() string { func (e *PrintHelpErr) Error() string {
@ -26,11 +27,11 @@ func (e *PrintHelpErr) Is(o error) bool {
return ok return ok
} }
func ShowHelp(cctx *cli.Context, err error) error { func ShowHelp(cctx *ufcli.Context, err error) error {
return &PrintHelpErr{Err: err, Ctx: cctx} return &PrintHelpErr{Err: err, Ctx: cctx}
} }
func RunApp(app *cli.App) { func RunApp(app *ufcli.App) {
if err := app.Run(os.Args); err != nil { if err := app.Run(os.Args); err != nil {
if os.Getenv("LOTUS_DEV") != "" { if os.Getenv("LOTUS_DEV") != "" {
log.Warnf("%+v", err) log.Warnf("%+v", err)
@ -39,8 +40,40 @@ func RunApp(app *cli.App) {
} }
var phe *PrintHelpErr var phe *PrintHelpErr
if xerrors.As(err, &phe) { if xerrors.As(err, &phe) {
_ = cli.ShowCommandHelp(phe.Ctx, phe.Ctx.Command.Name) _ = ufcli.ShowCommandHelp(phe.Ctx, phe.Ctx.Command.Name)
} }
os.Exit(1) os.Exit(1)
} }
} }
type AppFmt struct {
app *ufcli.App
Stdin io.Reader
}
func NewAppFmt(a *ufcli.App) *AppFmt {
var stdin io.Reader
istdin, ok := a.Metadata["stdin"]
if ok {
stdin = istdin.(io.Reader)
} else {
stdin = os.Stdin
}
return &AppFmt{app: a, Stdin: stdin}
}
func (a *AppFmt) Print(args ...interface{}) {
fmt.Fprint(a.app.Writer, args...)
}
func (a *AppFmt) Println(args ...interface{}) {
fmt.Fprintln(a.app.Writer, args...)
}
func (a *AppFmt) Printf(fmtstr string, args ...interface{}) {
fmt.Fprintf(a.app.Writer, fmtstr, args...)
}
func (a *AppFmt) Scan(args ...interface{}) (int, error) {
return fmt.Fscan(a.Stdin, args...)
}

View File

@ -6,50 +6,17 @@ import (
"testing" "testing"
"time" "time"
"github.com/filecoin-project/go-address"
"github.com/filecoin-project/lotus/api/test"
clitest "github.com/filecoin-project/lotus/cli/test" clitest "github.com/filecoin-project/lotus/cli/test"
builder "github.com/filecoin-project/lotus/node/test"
) )
// TestMultisig does a basic test to exercise the multisig CLI // TestMultisig does a basic test to exercise the multisig CLI
// commands // commands
func TestMultisig(t *testing.T) { func TestMultisig(t *testing.T) {
_ = os.Setenv("BELLMAN_NO_GPU", "1") _ = os.Setenv("BELLMAN_NO_GPU", "1")
clitest.QuietMiningLogs()
blocktime := 5 * time.Millisecond blocktime := 5 * time.Millisecond
ctx := context.Background() ctx := context.Background()
nodes, _ := startNodes(ctx, t, blocktime) clientNode, _ := clitest.StartOneNodeOneMiner(ctx, t, blocktime)
clientNode := nodes[0]
clitest.RunMultisigTest(t, Commands, clientNode) clitest.RunMultisigTest(t, Commands, clientNode)
} }
func startNodes(ctx context.Context, t *testing.T, blocktime time.Duration) ([]test.TestNode, []address.Address) {
n, sn := builder.RPCMockSbBuilder(t, test.OneFull, test.OneMiner)
full := n[0]
miner := sn[0]
// Get everyone connected
addrs, err := full.NetAddrsListen(ctx)
if err != nil {
t.Fatal(err)
}
if err := miner.NetConnect(ctx, addrs); err != nil {
t.Fatal(err)
}
// Start mining blocks
bm := test.NewBlockMiner(ctx, t, miner, blocktime)
bm.MineBlocks()
// Get the creator's address
creatorAddr, err := full.WalletDefaultAddress(ctx)
if err != nil {
t.Fatal(err)
}
// Create mock CLI
return n, []address.Address{creatorAddr}
}

115
cli/test/client.go Normal file
View File

@ -0,0 +1,115 @@
package test
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
"github.com/filecoin-project/lotus/api/test"
"github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/specs-actors/v2/actors/builtin"
"github.com/stretchr/testify/require"
lcli "github.com/urfave/cli/v2"
)
// RunClientTest exercises some of the client CLI commands
func RunClientTest(t *testing.T, cmds []*lcli.Command, clientNode test.TestNode) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
// Create mock CLI
mockCLI := newMockCLI(t, cmds)
clientCLI := mockCLI.client(clientNode.ListenAddr)
// Get the miner address
addrs, err := clientNode.StateListMiners(ctx, types.EmptyTSK)
require.NoError(t, err)
require.Len(t, addrs, 1)
minerAddr := addrs[0]
fmt.Println("Miner:", minerAddr)
// client query-ask <miner addr>
cmd := []string{
"client", "query-ask", minerAddr.String(),
}
out := clientCLI.runCmd(cmd)
require.Regexp(t, regexp.MustCompile("Ask:"), out)
// Create a deal (non-interactive)
// client deal <cid> <miner addr> 1000000attofil <duration>
res, _, err := test.CreateClientFile(ctx, clientNode, 1)
require.NoError(t, err)
dataCid := res.Root
price := "1000000attofil"
duration := fmt.Sprintf("%d", build.MinDealDuration)
cmd = []string{
"client", "deal", dataCid.String(), minerAddr.String(), price, duration,
}
out = clientCLI.runCmd(cmd)
fmt.Println("client deal", out)
// Create a deal (interactive)
// client deal
// <cid>
// <duration> (in days)
// <miner addr>
// "no" (verified client)
// "yes" (confirm deal)
res, _, err = test.CreateClientFile(ctx, clientNode, 2)
require.NoError(t, err)
dataCid2 := res.Root
duration = fmt.Sprintf("%d", build.MinDealDuration/builtin.EpochsInDay)
cmd = []string{
"client", "deal",
}
interactiveCmds := []string{
dataCid2.String(),
duration,
minerAddr.String(),
"no",
"yes",
}
out = clientCLI.runInteractiveCmd(cmd, interactiveCmds)
fmt.Println("client deal:\n", out)
// Wait for provider to start sealing deal
dealStatus := ""
for dealStatus != "StorageDealSealing" {
// client list-deals
cmd = []string{"client", "list-deals"}
out = clientCLI.runCmd(cmd)
fmt.Println("list-deals:\n", out)
lines := strings.Split(out, "\n")
require.Len(t, lines, 2)
re := regexp.MustCompile(`\s+`)
parts := re.Split(lines[1], -1)
if len(parts) < 4 {
require.Fail(t, "bad list-deals output format")
}
dealStatus = parts[3]
fmt.Println(" Deal status:", dealStatus)
time.Sleep(time.Second)
}
// Retrieve the first file from the miner
// client retrieve <cid> <file path>
tmpdir, err := ioutil.TempDir(os.TempDir(), "test-cli-client")
require.NoError(t, err)
path := filepath.Join(tmpdir, "outfile.dat")
cmd = []string{
"client", "retrieve", dataCid.String(), path,
}
out = clientCLI.runCmd(cmd)
fmt.Println("retrieve:\n", out)
require.Regexp(t, regexp.MustCompile("Success"), out)
}

View File

@ -122,3 +122,12 @@ func (c *mockCLIClient) flagSet(cmd *lcli.Command) *flag.FlagSet {
} }
return fs return fs
} }
func (c *mockCLIClient) runInteractiveCmd(cmd []string, interactive []string) string {
c.toStdin(strings.Join(interactive, "\n") + "\n")
return c.runCmd(cmd)
}
func (c *mockCLIClient) toStdin(s string) {
c.cctx.App.Metadata["stdin"] = bytes.NewBufferString(s)
}

View File

@ -10,19 +10,10 @@ import (
"github.com/filecoin-project/go-address" "github.com/filecoin-project/go-address"
"github.com/filecoin-project/lotus/api/test" "github.com/filecoin-project/lotus/api/test"
"github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types"
logging "github.com/ipfs/go-log/v2"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
lcli "github.com/urfave/cli/v2" lcli "github.com/urfave/cli/v2"
) )
func QuietMiningLogs() {
logging.SetLogLevel("miner", "ERROR")
logging.SetLogLevel("chainstore", "ERROR")
logging.SetLogLevel("chain", "ERROR")
logging.SetLogLevel("sub", "ERROR")
logging.SetLogLevel("storageminer", "ERROR")
}
func RunMultisigTest(t *testing.T, cmds []*lcli.Command, clientNode test.TestNode) { func RunMultisigTest(t *testing.T, cmds []*lcli.Command, clientNode test.TestNode) {
ctx := context.Background() ctx := context.Background()

41
cli/test/net.go Normal file
View File

@ -0,0 +1,41 @@
package test
import (
"context"
"testing"
"time"
"github.com/filecoin-project/go-address"
"github.com/filecoin-project/lotus/api/test"
test2 "github.com/filecoin-project/lotus/node/test"
)
func StartOneNodeOneMiner(ctx context.Context, t *testing.T, blocktime time.Duration) (test.TestNode, address.Address) {
n, sn := test2.RPCMockSbBuilder(t, test.OneFull, test.OneMiner)
full := n[0]
miner := sn[0]
// Get everyone connected
addrs, err := full.NetAddrsListen(ctx)
if err != nil {
t.Fatal(err)
}
if err := miner.NetConnect(ctx, addrs); err != nil {
t.Fatal(err)
}
// Start mining blocks
bm := test.NewBlockMiner(ctx, t, miner, blocktime)
bm.MineBlocks()
// Get the full node's wallet address
fullAddr, err := full.WalletDefaultAddress(ctx)
if err != nil {
t.Fatal(err)
}
// Create mock CLI
return full, fullAddr
}

12
cli/test/util.go Normal file
View File

@ -0,0 +1,12 @@
package test
import "github.com/ipfs/go-log/v2"
func QuietMiningLogs() {
_ = log.SetLogLevel("miner", "ERROR")
_ = log.SetLogLevel("chainstore", "ERROR")
_ = log.SetLogLevel("chain", "ERROR")
_ = log.SetLogLevel("sub", "ERROR")
_ = log.SetLogLevel("storageminer", "ERROR")
_ = log.SetLogLevel("pubsub", "ERROR")
}

View File

@ -67,6 +67,7 @@ type gatewayDepsAPI interface {
StateMinerAvailableBalance(context.Context, address.Address, types.TipSetKey) (types.BigInt, error) StateMinerAvailableBalance(context.Context, address.Address, types.TipSetKey) (types.BigInt, error)
StateMinerProvingDeadline(context.Context, address.Address, types.TipSetKey) (*dline.Info, error) StateMinerProvingDeadline(context.Context, address.Address, types.TipSetKey) (*dline.Info, error)
StateCirculatingSupply(context.Context, types.TipSetKey) (abi.TokenAmount, error) StateCirculatingSupply(context.Context, types.TipSetKey) (abi.TokenAmount, error)
StateVerifiedClientStatus(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*abi.StoragePower, error)
StateVMCirculatingSupplyInternal(context.Context, types.TipSetKey) (api.CirculatingSupply, error) StateVMCirculatingSupplyInternal(context.Context, types.TipSetKey) (api.CirculatingSupply, error)
} }
@ -364,6 +365,13 @@ func (a *GatewayAPI) StateCirculatingSupply(ctx context.Context, tsk types.TipSe
} }
func (a *GatewayAPI) StateVerifiedClientStatus(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*abi.StoragePower, error) {
if err := a.checkTipsetKey(ctx, tsk); err != nil {
return nil, err
}
return a.api.StateVerifiedClientStatus(ctx, addr, tsk)
}
func (a *GatewayAPI) StateVMCirculatingSupplyInternal(ctx context.Context, tsk types.TipSetKey) (api.CirculatingSupply, error) { func (a *GatewayAPI) StateVMCirculatingSupplyInternal(ctx context.Context, tsk types.TipSetKey) (api.CirculatingSupply, error) {
if err := a.checkTipsetKey(ctx, tsk); err != nil { if err := a.checkTipsetKey(ctx, tsk); err != nil {
return api.CirculatingSupply{}, err return api.CirculatingSupply{}, err

View File

@ -11,7 +11,6 @@ import (
"github.com/filecoin-project/lotus/cli" "github.com/filecoin-project/lotus/cli"
clitest "github.com/filecoin-project/lotus/cli/test" clitest "github.com/filecoin-project/lotus/cli/test"
logging "github.com/ipfs/go-log/v2"
init2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/init" init2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/init"
multisig2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/multisig" multisig2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/multisig"
@ -45,6 +44,7 @@ func init() {
// node that is connected through a gateway to a full API node // node that is connected through a gateway to a full API node
func TestWalletMsig(t *testing.T) { func TestWalletMsig(t *testing.T) {
_ = os.Setenv("BELLMAN_NO_GPU", "1") _ = os.Setenv("BELLMAN_NO_GPU", "1")
clitest.QuietMiningLogs()
blocktime := 5 * time.Millisecond blocktime := 5 * time.Millisecond
ctx := context.Background() ctx := context.Background()
@ -155,80 +155,35 @@ func TestMsigCLI(t *testing.T) {
blocktime := 5 * time.Millisecond blocktime := 5 * time.Millisecond
ctx := context.Background() ctx := context.Background()
nodes := startNodes(ctx, t, blocktime, maxLookbackCap, maxStateWaitLookbackLimit) nodes := startNodesWithFunds(ctx, t, blocktime, maxLookbackCap, maxStateWaitLookbackLimit)
defer nodes.closer() defer nodes.closer()
lite := nodes.lite lite := nodes.lite
full := nodes.full
// The full node starts with a wallet
fullWalletAddr, err := full.WalletDefaultAddress(ctx)
require.NoError(t, err)
// Create a wallet on the lite node
liteWalletAddr, err := lite.WalletNew(ctx, types.KTSecp256k1)
require.NoError(t, err)
// Send some funds from the full node to the lite node
err = sendFunds(ctx, full, fullWalletAddr, liteWalletAddr, types.NewInt(1e18))
require.NoError(t, err)
clitest.RunMultisigTest(t, cli.Commands, lite) clitest.RunMultisigTest(t, cli.Commands, lite)
} }
func sendFunds(ctx context.Context, fromNode test.TestNode, fromAddr address.Address, toAddr address.Address, amt types.BigInt) error {
msg := &types.Message{
From: fromAddr,
To: toAddr,
Value: amt,
}
sm, err := fromNode.MpoolPushMessage(ctx, msg, nil)
if err != nil {
return err
}
res, err := fromNode.StateWaitMsg(ctx, sm.Cid(), 1)
if err != nil {
return err
}
if res.Receipt.ExitCode != 0 {
return xerrors.Errorf("send funds failed with exit code %d", res.Receipt.ExitCode)
}
return nil
}
func TestDealFlow(t *testing.T) { func TestDealFlow(t *testing.T) {
_ = os.Setenv("BELLMAN_NO_GPU", "1") _ = os.Setenv("BELLMAN_NO_GPU", "1")
clitest.QuietMiningLogs()
logging.SetLogLevel("miner", "ERROR")
logging.SetLogLevel("chainstore", "ERROR")
logging.SetLogLevel("chain", "ERROR")
logging.SetLogLevel("sub", "ERROR")
logging.SetLogLevel("storageminer", "ERROR")
blocktime := 5 * time.Millisecond blocktime := 5 * time.Millisecond
ctx := context.Background() ctx := context.Background()
nodes := startNodes(ctx, t, blocktime, maxLookbackCap, maxStateWaitLookbackLimit) nodes := startNodesWithFunds(ctx, t, blocktime, maxLookbackCap, maxStateWaitLookbackLimit)
defer nodes.closer() defer nodes.closer()
full := nodes.full test.MakeDeal(t, ctx, 6, nodes.lite, nodes.miner, false, false)
lite := nodes.lite }
// The full node starts with a wallet func TestCLIDealFlow(t *testing.T) {
fullWalletAddr, err := full.WalletDefaultAddress(ctx) _ = os.Setenv("BELLMAN_NO_GPU", "1")
require.NoError(t, err) clitest.QuietMiningLogs()
// Create a wallet on the lite node blocktime := 5 * time.Millisecond
liteWalletAddr, err := lite.WalletNew(ctx, types.KTSecp256k1) ctx := context.Background()
require.NoError(t, err) nodes := startNodesWithFunds(ctx, t, blocktime, maxLookbackCap, maxStateWaitLookbackLimit)
defer nodes.closer()
// Send some funds from the full node to the lite node clitest.RunClientTest(t, cli.Commands, nodes.lite)
err = sendFunds(ctx, full, fullWalletAddr, liteWalletAddr, types.NewInt(1e18))
require.NoError(t, err)
test.MakeDeal(t, ctx, 6, lite, nodes.miner, false, false)
} }
type testNodes struct { type testNodes struct {
@ -238,6 +193,30 @@ type testNodes struct {
closer jsonrpc.ClientCloser closer jsonrpc.ClientCloser
} }
func startNodesWithFunds(
ctx context.Context,
t *testing.T,
blocktime time.Duration,
lookbackCap time.Duration,
stateWaitLookbackLimit abi.ChainEpoch,
) *testNodes {
nodes := startNodes(ctx, t, blocktime, lookbackCap, stateWaitLookbackLimit)
// The full node starts with a wallet
fullWalletAddr, err := nodes.full.WalletDefaultAddress(ctx)
require.NoError(t, err)
// Create a wallet on the lite node
liteWalletAddr, err := nodes.lite.WalletNew(ctx, types.KTSecp256k1)
require.NoError(t, err)
// Send some funds from the full node to the lite node
err = sendFunds(ctx, nodes.full, fullWalletAddr, liteWalletAddr, types.NewInt(1e18))
require.NoError(t, err)
return nodes
}
func startNodes( func startNodes(
ctx context.Context, ctx context.Context,
t *testing.T, t *testing.T,
@ -302,3 +281,26 @@ func startNodes(
return &testNodes{lite: lite, full: full, miner: miner, closer: closer} return &testNodes{lite: lite, full: full, miner: miner, closer: closer}
} }
func sendFunds(ctx context.Context, fromNode test.TestNode, fromAddr address.Address, toAddr address.Address, amt types.BigInt) error {
msg := &types.Message{
From: fromAddr,
To: toAddr,
Value: amt,
}
sm, err := fromNode.MpoolPushMessage(ctx, msg, nil)
if err != nil {
return err
}
res, err := fromNode.StateWaitMsg(ctx, sm.Cid(), 1)
if err != nil {
return err
}
if res.Receipt.ExitCode != 0 {
return xerrors.Errorf("send funds failed with exit code %d", res.Receipt.ExitCode)
}
return nil
}

View File

@ -52,6 +52,7 @@ type StateModuleAPI interface {
StateMarketStorageDeal(ctx context.Context, dealId abi.DealID, tsk types.TipSetKey) (*api.MarketDeal, error) StateMarketStorageDeal(ctx context.Context, dealId abi.DealID, tsk types.TipSetKey) (*api.MarketDeal, error)
StateMinerInfo(ctx context.Context, actor address.Address, tsk types.TipSetKey) (miner.MinerInfo, error) StateMinerInfo(ctx context.Context, actor address.Address, tsk types.TipSetKey) (miner.MinerInfo, error)
StateNetworkVersion(ctx context.Context, key types.TipSetKey) (network.Version, error) StateNetworkVersion(ctx context.Context, key types.TipSetKey) (network.Version, error)
StateVerifiedClientStatus(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*abi.StoragePower, error)
StateWaitMsg(ctx context.Context, msg cid.Cid, confidence uint64) (*api.MsgLookup, error) StateWaitMsg(ctx context.Context, msg cid.Cid, confidence uint64) (*api.MsgLookup, error)
} }
@ -1165,19 +1166,19 @@ func (a *StateAPI) StateVerifierStatus(ctx context.Context, addr address.Address
// StateVerifiedClientStatus returns the data cap for the given address. // StateVerifiedClientStatus returns the data cap for the given address.
// Returns zero if there is no entry in the data cap table for the // Returns zero if there is no entry in the data cap table for the
// address. // address.
func (a *StateAPI) StateVerifiedClientStatus(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*abi.StoragePower, error) { func (m *StateModule) StateVerifiedClientStatus(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*abi.StoragePower, error) {
act, err := a.StateGetActor(ctx, verifreg.Address, tsk) act, err := m.StateGetActor(ctx, verifreg.Address, tsk)
if err != nil { if err != nil {
return nil, err return nil, err
} }
aid, err := a.StateLookupID(ctx, addr, tsk) aid, err := m.StateLookupID(ctx, addr, tsk)
if err != nil { if err != nil {
log.Warnf("lookup failure %v", err) log.Warnf("lookup failure %v", err)
return nil, err return nil, err
} }
vrs, err := verifreg.Load(a.StateManager.ChainStore().Store(ctx), act) vrs, err := verifreg.Load(m.StateManager.ChainStore().Store(ctx), act)
if err != nil { if err != nil {
return nil, xerrors.Errorf("failed to load verified registry state: %w", err) return nil, xerrors.Errorf("failed to load verified registry state: %w", err)
} }