github.com/stellar/stellar-etl@v1.0.1-0.20240312145900-4874b6bf2b89/cmd/export_ledgers_test.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"flag"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"log"
     9  	"os"
    10  	"os/exec"
    11  	"path"
    12  	"path/filepath"
    13  	"sort"
    14  	"strings"
    15  	"testing"
    16  
    17  	"github.com/stellar/stellar-etl/internal/utils"
    18  	"github.com/stretchr/testify/assert"
    19  )
    20  
    21  var executableName = "stellar-etl"
    22  var archiveURL = "http://history.stellar.org/prd/core-live/core_live_001"
    23  var archiveURLs = []string{archiveURL}
    24  var latestLedger = getLastSeqNum(archiveURLs)
    25  var update = flag.Bool("update", false, "update the golden files of this test")
    26  var gotFolder = "testdata/got/"
    27  
    28  type cliTest struct {
    29  	name              string
    30  	args              []string
    31  	golden            string
    32  	wantErr           error
    33  	sortForComparison bool
    34  }
    35  
    36  func TestMain(m *testing.M) {
    37  	if err := os.Chdir(".."); err != nil {
    38  		cmdLogger.Error("could not change directory", err)
    39  		os.Exit(1)
    40  	}
    41  
    42  	// This does the setup for further tests. It generates an executeable that can be run on the command line by other tests
    43  	buildCmd := exec.Command("go", "build", "-o", executableName)
    44  	if err := buildCmd.Run(); err != nil {
    45  		cmdLogger.Error("could not build executable", err)
    46  		os.Exit(1)
    47  	}
    48  
    49  	flag.Parse()
    50  	exitCode := m.Run()
    51  	os.Exit(exitCode)
    52  }
    53  
    54  func gotTestDir(t *testing.T, filename string) string {
    55  	return filepath.Join(gotFolder, t.Name(), filename)
    56  }
    57  
    58  func TestExportLedger(t *testing.T) {
    59  	tests := []cliTest{
    60  		{
    61  			name:    "end before start",
    62  			args:    []string{"export_ledgers", "-s", "100", "-e", "50"},
    63  			golden:  "",
    64  			wantErr: fmt.Errorf("could not read ledgers: End sequence number is less than start (50 < 100)"),
    65  		},
    66  		{
    67  			name:    "start too large",
    68  			args:    []string{"export_ledgers", "-s", "4294967295", "-e", "4294967295"},
    69  			golden:  "",
    70  			wantErr: fmt.Errorf("could not read ledgers: Latest sequence number is less than start sequence number (%d < 4294967295)", latestLedger),
    71  		},
    72  		{
    73  			name:    "end too large",
    74  			args:    []string{"export_ledgers", "-e", "4294967295", "-l", "4294967295"},
    75  			golden:  "",
    76  			wantErr: fmt.Errorf("could not read ledgers: Latest sequence number is less than end sequence number (%d < 4294967295)", latestLedger),
    77  		},
    78  		{
    79  			name:    "start is 0",
    80  			args:    []string{"export_ledgers", "-s", "0", "-e", "4294967295", "-l", "4294967295"},
    81  			golden:  "",
    82  			wantErr: fmt.Errorf("could not read ledgers: Start sequence number equal to 0. There is no ledger 0 (genesis ledger is ledger 1)"),
    83  		},
    84  		{
    85  			name:    "end is 0",
    86  			args:    []string{"export_ledgers", "-e", "0", "-l", "4294967295"},
    87  			golden:  "",
    88  			wantErr: fmt.Errorf("could not read ledgers: End sequence number equal to 0. There is no ledger 0 (genesis ledger is ledger 1)"),
    89  		},
    90  		{
    91  			name:    "single ledger",
    92  			args:    []string{"export_ledgers", "-s", "30822015", "-e", "30822015", "-o", gotTestDir(t, "single_ledger.txt")},
    93  			golden:  "single_ledger.golden",
    94  			wantErr: nil,
    95  		},
    96  		{
    97  			name:    "10 ledgers",
    98  			args:    []string{"export_ledgers", "-s", "30822015", "-e", "30822025", "-o", gotTestDir(t, "10_ledgers.txt")},
    99  			golden:  "10_ledgers.golden",
   100  			wantErr: nil,
   101  		},
   102  		{
   103  			name:    "range too large",
   104  			args:    []string{"export_ledgers", "-s", "30822015", "-e", "30822025", "-l", "5", "-o", gotTestDir(t, "large_range_ledgers.txt")},
   105  			golden:  "large_range_ledgers.golden",
   106  			wantErr: nil,
   107  		},
   108  	}
   109  
   110  	for _, test := range tests {
   111  		runCLITest(t, test, "testdata/ledgers/")
   112  	}
   113  }
   114  
   115  func indexOf(l []string, s string) int {
   116  	for idx, e := range l {
   117  		if e == s {
   118  			return idx
   119  		}
   120  	}
   121  	return -1
   122  }
   123  
   124  func sortByName(files []os.FileInfo) {
   125  	sort.Slice(files, func(i, j int) bool {
   126  		return files[i].Name() < files[j].Name()
   127  	})
   128  }
   129  
   130  func runCLITest(t *testing.T, test cliTest, goldenFolder string) {
   131  	t.Run(test.name, func(t *testing.T) {
   132  		dir, err := os.Getwd()
   133  		assert.NoError(t, err)
   134  
   135  		cmd := exec.Command(path.Join(dir, executableName), test.args...)
   136  		errOut, actualError := cmd.CombinedOutput()
   137  
   138  		idxOfOutputArg := indexOf(test.args, "-o")
   139  		testOutput := []byte{}
   140  		if idxOfOutputArg > -1 {
   141  			outLocation := test.args[idxOfOutputArg+1]
   142  			stat, err := os.Stat(outLocation)
   143  			assert.NoError(t, err)
   144  
   145  			// If the output arg specified is a directory, concat the contents for comparison.
   146  			if stat.IsDir() {
   147  				files, err := ioutil.ReadDir(outLocation)
   148  				if err != nil {
   149  					log.Fatal(err)
   150  				}
   151  				var buf bytes.Buffer
   152  				sortByName(files)
   153  				for _, f := range files {
   154  					b, err := ioutil.ReadFile(filepath.Join(outLocation, f.Name()))
   155  					if err != nil {
   156  						log.Fatal(err)
   157  					}
   158  					buf.Write(b)
   159  				}
   160  				testOutput = buf.Bytes()
   161  			} else {
   162  				// If the output is written to a file, read the contents of the file for comparison.
   163  				testOutput, err = ioutil.ReadFile(outLocation)
   164  				if err != nil {
   165  					log.Fatal(err)
   166  				}
   167  			}
   168  		}
   169  		// Since the CLI uses a logger to report errors, the final error message isn't the same as the errors thrown in code.
   170  		// Instead, it's wrapped in other os/system errors
   171  		// By reading the error text from the logger, we can extract the lower level error that the user would see
   172  		if test.golden == "" {
   173  			errorMsg := fmt.Errorf(extractErrorMsg(string(errOut)))
   174  			assert.Equal(t, test.wantErr, errorMsg)
   175  			return
   176  		}
   177  
   178  		assert.Equal(t, test.wantErr, actualError)
   179  		actualString := string(testOutput)
   180  		if test.sortForComparison {
   181  			trimmed := strings.Trim(actualString, "\n")
   182  			lines := strings.Split(trimmed, "\n")
   183  			sort.Strings(lines)
   184  			actualString = strings.Join(lines, "\n")
   185  			actualString = fmt.Sprintf("%s\n", actualString)
   186  		}
   187  
   188  		wantString, err := getGolden(t, goldenFolder+test.golden, actualString, *update)
   189  		assert.NoError(t, err)
   190  		assert.Equal(t, wantString, actualString)
   191  	})
   192  }
   193  
   194  func extractErrorMsg(loggerOutput string) string {
   195  	errIndex := strings.Index(loggerOutput, "msg=") + 5
   196  	endIndex := strings.Index(loggerOutput[errIndex:], "\"")
   197  	return loggerOutput[errIndex : errIndex+endIndex]
   198  }
   199  
   200  func removeCoreLogging(loggerOutput string) string {
   201  	endIndex := strings.Index(loggerOutput, "{\"")
   202  	// if there is no bracket, then nothing was exported except logs
   203  	if endIndex == -1 {
   204  		return ""
   205  	}
   206  
   207  	return loggerOutput[endIndex:]
   208  }
   209  
   210  func getLastSeqNum(archiveURLs []string) uint32 {
   211  	num, err := utils.GetLatestLedgerSequence(archiveURLs)
   212  	if err != nil {
   213  		panic(err)
   214  	}
   215  	return num
   216  }
   217  
   218  func getGolden(t *testing.T, goldenFile string, actual string, update bool) (string, error) {
   219  	t.Helper()
   220  	f, err := os.OpenFile(goldenFile, os.O_RDWR, 0644)
   221  	defer f.Close()
   222  	if err != nil {
   223  		return "", err
   224  	}
   225  
   226  	// If the update flag is true, clear the current contents of the golden file and write the actual output
   227  	// This is useful for when new tests or added or functionality changes that breaks current tests
   228  	if update {
   229  		err := os.Truncate(goldenFile, 0)
   230  		if err != nil {
   231  			return "", err
   232  		}
   233  
   234  		_, err = f.WriteString(actual)
   235  		if err != nil {
   236  			return "", err
   237  		}
   238  
   239  		return actual, nil
   240  	}
   241  
   242  	wantOutput, err := ioutil.ReadAll(f)
   243  	if err != nil {
   244  		return "", err
   245  	}
   246  
   247  	return string(wantOutput), nil
   248  }