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 }