Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 86 additions & 104 deletions cmd/testutil.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
//nolint:forcetypeassert
package cmd

import (
Expand All @@ -10,25 +9,69 @@ import (
"os"
"path/filepath"
"regexp"
"strconv"
"slices"
"strings"
"sync"
"testing"
"time"

"slices"

"github.com/cockroachdb/errors"
"github.com/data-preservation-programs/singularity/handler/wallet"
"github.com/data-preservation-programs/singularity/model"
"github.com/data-preservation-programs/singularity/pack"
"github.com/fatih/color"
commp "github.com/filecoin-project/go-fil-commp-hashhash"
"github.com/mattn/go-shellwords"
"github.com/parnurzeal/gorequest"
"github.com/rjNemo/underscore"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)

// CompareDirectories compares the contents of two directories recursively.
func CompareDirectories(t *testing.T, dir1, dir2 string) {
t.Helper()
filesInDir2 := make(map[string]struct{})

err := filepath.Walk(dir1, func(path1 string, info1 os.FileInfo, err error) error {
require.NoError(t, err)
relPath := strings.TrimPrefix(path1, dir1)
path2 := filepath.Join(dir2, relPath)
info2, err := os.Stat(path2)
if os.IsNotExist(err) {
require.Failf(t, "Missing file or directory in dir2", "File: %s", relPath)
return nil
}
require.NoError(t, err)
if !info1.IsDir() {
filesInDir2[relPath] = struct{}{}
}
if info1.IsDir() && info2.IsDir() {
return nil
}
require.Equal(t, info1.Size(), info2.Size(), "Size mismatch for %s", relPath)
content1, err := os.ReadFile(filepath.Clean(path1))
require.NoError(t, err)
content2, err := os.ReadFile(filepath.Clean(path2))
require.NoError(t, err)
require.True(t, bytes.Equal(content1, content2), "Content mismatch for %s", relPath)
return nil
})
require.NoError(t, err)

err = filepath.Walk(dir2, func(path2 string, info2 os.FileInfo, err error) error {
require.NoError(t, err)
relPath := strings.TrimPrefix(path2, dir2)
if _, ok := filesInDir2[relPath]; ok || info2.IsDir() {
return nil
}
require.Failf(t, "Extra file or directory in dir2", "File: %s", relPath)
return nil
})
require.NoError(t, err)
}

type RunnerMode string

const (
Expand All @@ -42,7 +85,33 @@ type Runner struct {
mode RunnerMode
}

var colorMutex = sync.Mutex{}
var (
removeANSI = regexp.MustCompile(`\x1B\[[0-?]*[ -/]*[@-~]`)
timeRegex = regexp.MustCompile(`\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}`)
colorMutex = sync.Mutex{}
)

// Save writes the captured output to testdata files for inspection.
func (r *Runner) Save(t *testing.T, tempDirs ...string) {
t.Helper()
ansi := r.sb.String()
if ansi == "" {
return
}
for i, tempDir := range tempDirs {
ansi = strings.ReplaceAll(ansi, tempDir, "/tempDir/"+fmt.Sprint(i))
}
ansi = timeRegex.ReplaceAllString(ansi, "2023-04-05 06:07:08")
ansiPath := "testdata/" + t.Name() + ".ansi"
err := os.MkdirAll("testdata", 0700)
require.NoError(t, err)
err = os.WriteFile(ansiPath, []byte(ansi), 0600)
require.NoError(t, err)
plain := removeANSI.ReplaceAllString(ansi, "")
plainPath := "testdata/" + t.Name() + ".txt"
err = os.WriteFile(plainPath, []byte(plain), 0600)
require.NoError(t, err)
}

// NewRunner creates a new Runner to capture CLI args
//
Expand All @@ -55,14 +124,21 @@ func NewRunner() *Runner {
if color.NoColor {
color.NoColor = false
}
// Always swap in a mock wallet handler for all CLI tests
// This ensures all wallet DB operations use the mock, not the real DB
mockHandler := new(wallet.MockWallet)
// Set up default no-op mocks for all handler methods to avoid nil panics
mockHandler.On("CreateHandler", mock.Anything, mock.Anything, mock.Anything).Return(&model.Wallet{}, nil)
mockHandler.On("ImportHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Wallet{}, nil)
mockHandler.On("InitHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Wallet{}, nil)
mockHandler.On("ListHandler", mock.Anything, mock.Anything).Return([]model.Wallet{}, nil)
mockHandler.On("RemoveHandler", mock.Anything, mock.Anything, mock.Anything).Return(nil)
mockHandler.On("UpdateHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Wallet{}, nil)
wallet.Default = mockHandler
return &Runner{}
}

func (r *Runner) WithMode(mode RunnerMode) *Runner {
r.mode = mode
return r
}

// Run executes the CLI command with the given arguments and captures output.
func (r *Runner) Run(ctx context.Context, args string) (string, string, error) {
if strings.HasPrefix(args, "singularity ") {
switch r.mode {
Expand All @@ -82,35 +158,6 @@ func (r *Runner) Run(ctx context.Context, args string) (string, string, error) {
return out, stderr, err
}

var removeANSI = regexp.MustCompile(`\x1B\[[0-?]*[ -/]*[@-~]`)
var timeRegex = regexp.MustCompile(`\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}`)

func (r *Runner) Save(t *testing.T, tempDirs ...string) {
t.Helper()
t.Helper()
ansi := r.sb.String()
if ansi == "" {
return
}

for i, tempDir := range tempDirs {
ansi = strings.ReplaceAll(ansi, tempDir, "/tempDir/"+strconv.Itoa(i))
}

ansi = timeRegex.ReplaceAllString(ansi, "2023-04-05 06:07:08")

ansiPath := filepath.Join("testdata", t.Name()+".ansi")
err := os.MkdirAll(filepath.Dir(ansiPath), 0700)
require.NoError(t, err)
err = os.WriteFile(ansiPath, []byte(ansi), 0600)
require.NoError(t, err)

plain := removeANSI.ReplaceAllString(ansi, "")
plainPath := filepath.Join("testdata", t.Name()+".txt")
err = os.WriteFile(plainPath, []byte(plain), 0600)
require.NoError(t, err)
}

var pieceCIDRegex = regexp.MustCompile("baga6ea[0-9a-z]+")

func GetAllPieceCIDs(content string) []string {
Expand Down Expand Up @@ -261,71 +308,6 @@ func Download(ctx context.Context, url string, nThreads int) ([]byte, error) {
return result.Bytes(), nil
}

func CompareDirectories(t *testing.T, dir1, dir2 string) {
t.Helper()
filesInDir2 := make(map[string]struct{})

err := filepath.Walk(dir1, func(path1 string, info1 os.FileInfo, err error) error {
// Propagate any error
require.NoError(t, err)

// Construct the path to the corresponding file or directory in dir2
relPath := strings.TrimPrefix(path1, dir1)
path2 := filepath.Join(dir2, relPath)

// Get info about the file or directory in dir2
info2, err := os.Stat(path2)
if os.IsNotExist(err) {
require.Failf(t, "Missing file or directory in dir2", "File: %s", relPath)
return nil
}
require.NoError(t, err)

if !info1.IsDir() {
filesInDir2[relPath] = struct{}{}
}

// If both are directories, no need to compare content
if info1.IsDir() && info2.IsDir() {
return nil
}

// Compare file sizes
require.Equal(t, info1.Size(), info2.Size(), "Size mismatch for %s", relPath)

// Compare file content
content1, err := os.ReadFile(filepath.Clean(path1))
require.NoError(t, err)

content2, err := os.ReadFile(filepath.Clean(path2))
require.NoError(t, err)

require.True(t, bytes.Equal(content1, content2), "Content mismatch for %s", relPath)

return nil
})

require.NoError(t, err)

err = filepath.Walk(dir2, func(path2 string, info2 os.FileInfo, err error) error {
// Propagate any error
require.NoError(t, err)

relPath := strings.TrimPrefix(path2, dir2)

// If we've already checked this file (because it exists in dir1), then skip it
if _, ok := filesInDir2[relPath]; ok || info2.IsDir() {
return nil
}

// If we get here, it means this file/dir exists in dir2 but not in dir1
require.Failf(t, "Extra file or directory in dir2", "File: %s", relPath)
return nil
})

require.NoError(t, err)
}

func runWithCapture(ctx context.Context, args string) (string, string, error) {
// Create a clone of the app so that we can runWithCapture from different tests concurrently
app := *App
Expand Down
6 changes: 3 additions & 3 deletions cmd/wallet/balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ var BalanceCmd = &cli.Command{
Name: "balance",
Usage: "Get wallet balance information",
ArgsUsage: "<wallet_address>",
Description: `Get FIL balance and FIL+ datacap balance for a specific wallet address.
Description: `Get FIL balance and FIL+ datacap balance for a specific wallet address.
This command queries the Lotus network to retrieve current balance information.

Examples:
singularity wallet balance f12syf7zd3lfsv43aj2kb454ymaqw7debhumjnbqa
singularity wallet balance --json f1abc123...def456
singularity wallet balance f1abcde7zd3lfsv43aj2kb454ymaqw7debhumjxyz
singularity wallet balance --json f1abcde7zd3lfsv43aj2kb454ymaqw7debhumjxyz

The command returns:
- FIL balance in human-readable format (e.g., "1.000000 FIL")
Expand Down
89 changes: 85 additions & 4 deletions cmd/wallet/list.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,109 @@
package wallet

import (
"fmt"

"github.com/cockroachdb/errors"
"github.com/data-preservation-programs/singularity/cmd/cliutil"
"github.com/data-preservation-programs/singularity/database"
"github.com/data-preservation-programs/singularity/handler/wallet"
"github.com/data-preservation-programs/singularity/model"
"github.com/data-preservation-programs/singularity/util"
"github.com/urfave/cli/v2"
)

var ListCmd = &cli.Command{
Name: "list",
Usage: "List all imported wallets",
Name: "list",
Usage: "List all imported wallets or a specific wallet if address is provided",
ArgsUsage: "[wallet_address]",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "with-balance",
Usage: "Fetch and display live wallet balances from Lotus",
Value: false,
},
&cli.StringFlag{
Name: "lotus-api",
Usage: "Lotus JSON-RPC API endpoint for fetching live balances",
EnvVars: []string{"LOTUS_API"},
},
&cli.StringFlag{
Name: "lotus-token",
Usage: "Lotus API authorization token",
EnvVars: []string{"LOTUS_TOKEN"},
},
},
Action: func(c *cli.Context) error {
db, closer, err := database.OpenFromCLI(c)
if err != nil {
return errors.WithStack(err)
}
defer func() { _ = closer.Close() }()

var address string
if c.NArg() > 0 {
address = c.Args().Get(0)
}

if c.Bool("with-balance") {
// If no Lotus API is provided, use the default from app config
lotusAPI := c.String("lotus-api")
if lotusAPI == "" {
lotusAPI = "https://api.node.glif.io/rpc/v1"
}
lotusClient := util.NewLotusClient(lotusAPI, c.String("lotus-token"))

if address != "" {
// For a single address, call GetBalanceHandler directly like balance command
resp, err := wallet.Default.GetBalanceHandler(c.Context, db, lotusClient, address)
if err != nil {
return errors.WithStack(err)
}
cliutil.Print(c, resp)
return nil
}

// For all wallets, convert WalletWithBalance to BalanceResponse for consistent output
walletsWithBalance, err := wallet.ListWithBalanceHandler(c.Context, db, lotusClient)
if err != nil {
return errors.WithStack(err)
}
responses := make([]*wallet.BalanceResponse, len(walletsWithBalance))
for i, w := range walletsWithBalance {
responses[i] = &wallet.BalanceResponse{
Address: w.Address,
Balance: w.Balance,
BalanceAttoFIL: w.BalanceAttoFIL,
DataCap: w.DataCap,
DataCapBytes: w.DataCapBytes,
Error: w.Error,
}
}
cliutil.Print(c, responses)
return nil
}

wallets, err := wallet.Default.ListHandler(c.Context, db)
if err != nil {
return errors.WithStack(err)
}

cliutil.Print(c, wallets)
if address != "" {
found := false
for _, w := range wallets {
if w.Address == address {
cliutil.Print(c, []model.Wallet{w})
found = true
break
}
}
if !found {
fmt.Fprintln(c.App.Writer, "No wallet found with the specified address.")
}
} else {
cliutil.Print(c, wallets)
}
return nil
},
}


Loading
Loading