// stm: #unit package cli import ( "bytes" "context" "encoding/json" "fmt" "regexp" "strings" "testing" "github.com/golang/mock/gomock" "github.com/ipfs/go-cid" "github.com/stretchr/testify/assert" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types/mock" "github.com/filecoin-project/specs-actors/v7/actors/builtin" ) func TestChainHead(t *testing.T) { app, mockApi, buf, done := NewMockAppWithFullAPI(t, WithCategory("chain", ChainHeadCmd)) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() ts := mock.TipSet(mock.MkBlock(nil, 0, 0)) gomock.InOrder( mockApi.EXPECT().ChainHead(ctx).Return(ts, nil), ) //stm: @CLI_CHAIN_HEAD_001 err := app.Run([]string{"chain", "head"}) assert.NoError(t, err) assert.Regexp(t, regexp.MustCompile(ts.Cids()[0].String()), buf.String()) } func TestGetBlock(t *testing.T) { app, mockApi, buf, done := NewMockAppWithFullAPI(t, WithCategory("chain", ChainGetBlock)) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() block := mock.MkBlock(nil, 0, 0) blockMsgs := api.BlockMessages{} gomock.InOrder( mockApi.EXPECT().ChainGetBlock(ctx, block.Cid()).Return(block, nil), mockApi.EXPECT().ChainGetBlockMessages(ctx, block.Cid()).Return(&blockMsgs, nil), mockApi.EXPECT().ChainGetParentMessages(ctx, block.Cid()).Return([]api.Message{}, nil), mockApi.EXPECT().ChainGetParentReceipts(ctx, block.Cid()).Return([]*types.MessageReceipt{}, nil), ) //stm: @CLI_CHAIN_GET_BLOCK_001 err := app.Run([]string{"chain", "getblock", block.Cid().String()}) assert.NoError(t, err) // expected output format out := struct { types.BlockHeader BlsMessages []*types.Message SecpkMessages []*types.SignedMessage ParentReceipts []*types.MessageReceipt ParentMessages []cid.Cid }{} err = json.Unmarshal(buf.Bytes(), &out) assert.NoError(t, err) assert.True(t, block.Cid().Equals(out.Cid())) } func TestReadOjb(t *testing.T) { app, mockApi, buf, done := NewMockAppWithFullAPI(t, WithCategory("chain", ChainReadObjCmd)) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() block := mock.MkBlock(nil, 0, 0) obj := new(bytes.Buffer) err := block.MarshalCBOR(obj) assert.NoError(t, err) gomock.InOrder( mockApi.EXPECT().ChainReadObj(ctx, block.Cid()).Return(obj.Bytes(), nil), ) //stm: @CLI_CHAIN_READ_OBJECT_001 err = app.Run([]string{"chain", "read-obj", block.Cid().String()}) assert.NoError(t, err) assert.Equal(t, buf.String(), fmt.Sprintf("%x\n", obj.Bytes())) } func TestChainDeleteObj(t *testing.T) { cmd := WithCategory("chain", ChainDeleteObjCmd) block := mock.MkBlock(nil, 0, 0) // given no force flag, it should return an error and no API calls should be made t.Run("no-really-do-it", func(t *testing.T) { app, _, _, done := NewMockAppWithFullAPI(t, cmd) defer done() //stm: @CLI_CHAIN_DELETE_OBJECT_002 err := app.Run([]string{"chain", "delete-obj", block.Cid().String()}) assert.Error(t, err) }) // given a force flag, it calls API delete t.Run("really-do-it", func(t *testing.T) { app, mockApi, buf, done := NewMockAppWithFullAPI(t, cmd) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() gomock.InOrder( mockApi.EXPECT().ChainDeleteObj(ctx, block.Cid()).Return(nil), ) //stm: @CLI_CHAIN_DELETE_OBJECT_001 err := app.Run([]string{"chain", "delete-obj", "--really-do-it=true", block.Cid().String()}) assert.NoError(t, err) assert.Contains(t, buf.String(), block.Cid().String()) }) } func TestChainStatObj(t *testing.T) { cmd := WithCategory("chain", ChainStatObjCmd) block := mock.MkBlock(nil, 0, 0) stat := api.ObjStat{Size: 123, Links: 321} checkOutput := func(buf *bytes.Buffer) { out := buf.String() outSplit := strings.Split(out, "\n") assert.Contains(t, outSplit[0], fmt.Sprintf("%d", stat.Links)) assert.Contains(t, outSplit[1], fmt.Sprintf("%d", stat.Size)) } // given no --base flag, it calls ChainStatObj with base=cid.Undef t.Run("no-base", func(t *testing.T) { app, mockApi, buf, done := NewMockAppWithFullAPI(t, cmd) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() gomock.InOrder( mockApi.EXPECT().ChainStatObj(ctx, block.Cid(), cid.Undef).Return(stat, nil), ) //stm: @CLI_CHAIN_STAT_OBJECT_001 err := app.Run([]string{"chain", "stat-obj", block.Cid().String()}) assert.NoError(t, err) checkOutput(buf) }) // given a --base flag, it calls ChainStatObj with that base t.Run("base", func(t *testing.T) { app, mockApi, buf, done := NewMockAppWithFullAPI(t, cmd) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() gomock.InOrder( mockApi.EXPECT().ChainStatObj(ctx, block.Cid(), block.Cid()).Return(stat, nil), ) //stm: @CLI_CHAIN_STAT_OBJECT_002 err := app.Run([]string{"chain", "stat-obj", fmt.Sprintf("-base=%s", block.Cid().String()), block.Cid().String()}) assert.NoError(t, err) checkOutput(buf) }) } func TestChainGetMsg(t *testing.T) { app, mockApi, buf, done := NewMockAppWithFullAPI(t, WithCategory("chain", ChainGetMsgCmd)) defer done() addrs, err := mock.RandomActorAddresses(12345, 2) assert.NoError(t, err) from := addrs[0] to := addrs[1] msg := mock.UnsignedMessage(*from, *to, 0) obj := new(bytes.Buffer) err = msg.MarshalCBOR(obj) assert.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) defer cancel() gomock.InOrder( mockApi.EXPECT().ChainReadObj(ctx, msg.Cid()).Return(obj.Bytes(), nil), ) //stm: @CLI_CHAIN_GET_MESSAGE_001 err = app.Run([]string{"chain", "getmessage", msg.Cid().String()}) assert.NoError(t, err) var out types.Message err = json.Unmarshal(buf.Bytes(), &out) assert.NoError(t, err) assert.Equal(t, *msg, out) } func TestSetHead(t *testing.T) { cmd := WithCategory("chain", ChainSetHeadCmd) genesis := mock.TipSet(mock.MkBlock(nil, 0, 0)) ts := mock.TipSet(mock.MkBlock(genesis, 1, 0)) epoch := abi.ChainEpoch(uint64(0)) // given the -genesis flag, resets head to genesis ignoring the provided ts positional argument t.Run("genesis", func(t *testing.T) { app, mockApi, _, done := NewMockAppWithFullAPI(t, cmd) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() gomock.InOrder( mockApi.EXPECT().ChainGetGenesis(ctx).Return(genesis, nil), mockApi.EXPECT().ChainSetHead(ctx, genesis.Key()).Return(nil), ) //stm: @CLI_CHAIN_SET_HEAD_003 err := app.Run([]string{"chain", "sethead", "-genesis=true", ts.Key().String()}) assert.NoError(t, err) }) // given the -epoch flag, resets head to given epoch, ignoring the provided ts positional argument t.Run("epoch", func(t *testing.T) { app, mockApi, _, done := NewMockAppWithFullAPI(t, cmd) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() gomock.InOrder( mockApi.EXPECT().ChainGetTipSetByHeight(ctx, epoch, types.EmptyTSK).Return(genesis, nil), mockApi.EXPECT().ChainSetHead(ctx, genesis.Key()).Return(nil), ) //stm: @CLI_CHAIN_SET_HEAD_002 err := app.Run([]string{"chain", "sethead", fmt.Sprintf("-epoch=%s", epoch), ts.Key().String()}) assert.NoError(t, err) }) // given no flag, resets the head to given tipset key t.Run("default", func(t *testing.T) { app, mockApi, _, done := NewMockAppWithFullAPI(t, cmd) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() gomock.InOrder( mockApi.EXPECT().ChainGetBlock(ctx, ts.Key().Cids()[0]).Return(ts.Blocks()[0], nil), mockApi.EXPECT().ChainSetHead(ctx, ts.Key()).Return(nil), ) //stm: @CLI_CHAIN_SET_HEAD_001 err := app.Run([]string{"chain", "sethead", ts.Key().Cids()[0].String()}) assert.NoError(t, err) }) } func TestInspectUsage(t *testing.T) { cmd := WithCategory("chain", ChainInspectUsage) ts := mock.TipSet(mock.MkBlock(nil, 0, 0)) addrs, err := mock.RandomActorAddresses(12345, 2) assert.NoError(t, err) from := addrs[0] to := addrs[1] msg := mock.UnsignedMessage(*from, *to, 0) msgs := []api.Message{{Cid: msg.Cid(), Message: msg}} actor := &types.Actor{ Code: builtin.StorageMarketActorCodeID, Nonce: 0, Balance: big.NewInt(1000000000), } t.Run("default", func(t *testing.T) { app, mockApi, buf, done := NewMockAppWithFullAPI(t, cmd) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() gomock.InOrder( mockApi.EXPECT().ChainHead(ctx).Return(ts, nil), mockApi.EXPECT().ChainGetParentMessages(ctx, ts.Blocks()[0].Cid()).Return(msgs, nil), mockApi.EXPECT().ChainGetTipSet(ctx, ts.Parents()).Return(nil, nil), mockApi.EXPECT().StateGetActor(ctx, *to, ts.Key()).Return(actor, nil), ) //stm: @CLI_CHAIN_INSPECT_USAGE_001 err := app.Run([]string{"chain", "inspect-usage"}) assert.NoError(t, err) out := buf.String() // output is plaintext, had to do string matching assert.Contains(t, out, from.String()) assert.Contains(t, out, to.String()) // check for gas by sender assert.Contains(t, out, "By Sender") // check for gas by method assert.Contains(t, out, "By Method:\nSend") }) } func TestChainList(t *testing.T) { cmd := WithCategory("chain", ChainListCmd) genesis := mock.TipSet(mock.MkBlock(nil, 0, 0)) blk := mock.MkBlock(genesis, 0, 0) blk.Height = 1 head := mock.TipSet(blk) addrs, err := mock.RandomActorAddresses(12345, 2) assert.NoError(t, err) from := addrs[0] to := addrs[1] msg := mock.UnsignedMessage(*from, *to, 0) msgs := []api.Message{{Cid: msg.Cid(), Message: msg}} blockMsgs := &api.BlockMessages{} receipts := []*types.MessageReceipt{} t.Run("default", func(t *testing.T) { app, mockApi, buf, done := NewMockAppWithFullAPI(t, cmd) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() // same method gets called mocked multiple times bcs it's called in a for loop for all tipsets (2 in this case) gomock.InOrder( mockApi.EXPECT().ChainHead(ctx).Return(head, nil), mockApi.EXPECT().ChainGetTipSet(ctx, head.Parents()).Return(genesis, nil), mockApi.EXPECT().ChainGetBlockMessages(ctx, genesis.Blocks()[0].Cid()).Return(blockMsgs, nil), mockApi.EXPECT().ChainGetParentMessages(ctx, head.Blocks()[0].Cid()).Return(msgs, nil), mockApi.EXPECT().ChainGetParentReceipts(ctx, head.Blocks()[0].Cid()).Return(receipts, nil), mockApi.EXPECT().ChainGetBlockMessages(ctx, head.Blocks()[0].Cid()).Return(blockMsgs, nil), ) //stm: CLI_CHAIN_LIST_001 err := app.Run([]string{"chain", "love", "--gas-stats=true"}) // chain is love ❤️ assert.NoError(t, err) out := buf.String() // should print out 2 blocks, indexed with 0: and 1: assert.Contains(t, out, "0:") assert.Contains(t, out, "1:") }) } func TestChainGet(t *testing.T) { blk := mock.MkBlock(nil, 0, 0) ts := mock.TipSet(blk) cmd := WithCategory("chain", ChainGetCmd) // given no -as-type flag & ipfs prefix, should print object as JSON if it's marshalable t.Run("ipfs", func(t *testing.T) { path := fmt.Sprintf("/ipfs/%s", blk.Cid().String()) app, mockApi, buf, done := NewMockAppWithFullAPI(t, cmd) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() gomock.InOrder( mockApi.EXPECT().ChainGetNode(ctx, path).Return(&api.IpldObject{Cid: blk.Cid(), Obj: blk}, nil), ) //stm: @CLI_CHAIN_GET_001 err := app.Run([]string{"chain", "get", path}) assert.NoError(t, err) var out types.BlockHeader err = json.Unmarshal(buf.Bytes(), &out) assert.NoError(t, err) assert.Equal(t, *blk, out) }) // given no -as-type flag & ipfs prefix, should traverse from head.ParentStateRoot and print JSON if it's marshalable t.Run("pstate", func(t *testing.T) { p1 := "/pstate" p2 := fmt.Sprintf("/ipfs/%s", ts.ParentState().String()) app, mockApi, buf, done := NewMockAppWithFullAPI(t, cmd) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() gomock.InOrder( mockApi.EXPECT().ChainHead(ctx).Return(ts, nil), mockApi.EXPECT().ChainGetNode(ctx, p2).Return(&api.IpldObject{Cid: blk.Cid(), Obj: blk}, nil), ) //stm: @CLI_CHAIN_GET_002 err := app.Run([]string{"chain", "get", p1}) assert.NoError(t, err) var out types.BlockHeader err = json.Unmarshal(buf.Bytes(), &out) assert.NoError(t, err) assert.Equal(t, *blk, out) }) // given an unknown -as-type value, return an error t.Run("unknown-type", func(t *testing.T) { app, mockApi, _, done := NewMockAppWithFullAPI(t, cmd) defer done() path := fmt.Sprintf("/ipfs/%s", blk.Cid().String()) ctx, cancel := context.WithCancel(context.Background()) defer cancel() gomock.InOrder( mockApi.EXPECT().ChainGetNode(ctx, path).Return(&api.IpldObject{Cid: blk.Cid(), Obj: blk}, nil), ) //stm: @CLI_CHAIN_GET_004 err := app.Run([]string{"chain", "get", "-as-type=foo", path}) assert.Error(t, err) }) } func TestChainBisect(t *testing.T) { blk1 := mock.MkBlock(nil, 0, 0) blk1.Height = 0 ts1 := mock.TipSet(blk1) blk2 := mock.MkBlock(ts1, 0, 0) blk2.Height = 1 ts2 := mock.TipSet(blk2) subpath := "whatever/its/mocked" minHeight := uint64(0) maxHeight := uint64(1) shell := "echo" path := fmt.Sprintf("/ipld/%s/%s", ts2.ParentState(), subpath) cmd := WithCategory("chain", ChainBisectCmd) app, mockApi, buf, done := NewMockAppWithFullAPI(t, cmd) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() gomock.InOrder( mockApi.EXPECT().ChainGetTipSetByHeight(ctx, abi.ChainEpoch(maxHeight), types.EmptyTSK).Return(ts2, nil), mockApi.EXPECT().ChainGetTipSetByHeight(ctx, abi.ChainEpoch(maxHeight), ts2.Key()).Return(ts2, nil), mockApi.EXPECT().ChainGetNode(ctx, path).Return(&api.IpldObject{Cid: blk2.Cid(), Obj: blk2}, nil), ) //stm: @CLI_CHAIN_BISECT_001 err := app.Run([]string{"chain", "bisect", fmt.Sprintf("%d", minHeight), fmt.Sprintf("%d", maxHeight), subpath, shell}) assert.NoError(t, err) out := buf.String() assert.Contains(t, out, path) } func TestChainExport(t *testing.T) { app, mockApi, _, done := NewMockAppWithFullAPI(t, WithCategory("chain", ChainExportCmd)) defer done() // export writes to a file, I mocked it so there are no side-effects mockFile := mockExportFile{new(bytes.Buffer)} app.Metadata["export-file"] = mockFile blk := mock.MkBlock(nil, 0, 0) ts := mock.TipSet(blk) ctx, cancel := context.WithCancel(context.Background()) defer cancel() export := make(chan []byte, 2) expBytes := []byte("whatever") export <- expBytes export <- []byte{} // empty slice means export is complete close(export) gomock.InOrder( mockApi.EXPECT().ChainHead(ctx).Return(ts, nil), mockApi.EXPECT().ChainExport(ctx, abi.ChainEpoch(0), false, ts.Key()).Return(export, nil), ) //stm: @CLI_CHAIN_EXPORT_001 err := app.Run([]string{"chain", "export", "whatever.car"}) assert.NoError(t, err) assert.Equal(t, expBytes, mockFile.Bytes()) } func TestChainGasPrice(t *testing.T) { app, mockApi, buf, done := NewMockAppWithFullAPI(t, WithCategory("chain", ChainGasPriceCmd)) defer done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() // estimate gas is called with various num blocks in implementation, // so we mock and count how many times it's called, and we expect that many results printed calls := 0 mockApi. EXPECT(). GasEstimateGasPremium(ctx, gomock.Any(), builtin.SystemActorAddr, int64(10000), types.EmptyTSK). Return(big.NewInt(0), nil). AnyTimes(). Do(func(a, b, c, d, e interface{}) { // looks funny, but we don't care about args here, just counting calls++ }) //stm: @CLI_CHAIN_GAS_PRICE_001 err := app.Run([]string{"chain", "gas-price"}) assert.NoError(t, err) lines := strings.Split(strings.Trim(buf.String(), "\n"), "\n") assert.Equal(t, calls, len(lines)) } type mockExportFile struct { *bytes.Buffer } func (mef mockExportFile) Close() error { return nil }