code.vegaprotocol.io/vega@v0.79.0/datanode/integration/integration_test.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package integration_test 17 18 import ( 19 "context" 20 "encoding/json" 21 "flag" 22 "fmt" 23 "io/fs" 24 "log" 25 "os" 26 "os/signal" 27 "path/filepath" 28 "strconv" 29 "strings" 30 "sync" 31 "syscall" 32 "testing" 33 "time" 34 35 "code.vegaprotocol.io/vega/cmd/data-node/commands/start" 36 "code.vegaprotocol.io/vega/datanode/config" 37 "code.vegaprotocol.io/vega/datanode/config/encoding" 38 "code.vegaprotocol.io/vega/datanode/sqlstore" 39 "code.vegaprotocol.io/vega/datanode/utils" 40 "code.vegaprotocol.io/vega/datanode/utils/databasetest" 41 vgfs "code.vegaprotocol.io/vega/libs/fs" 42 "code.vegaprotocol.io/vega/logging" 43 "code.vegaprotocol.io/vega/paths" 44 45 "github.com/machinebox/graphql" 46 "github.com/stretchr/testify/assert" 47 "github.com/stretchr/testify/require" 48 ) 49 50 const ( 51 lastEpoch = 110 52 playbackTimeout = 5 * time.Minute 53 chainID = "testnet-001" 54 compressedTestdata = "testdata/system_tests.evt.gz" 55 eventsDir = "testdata/events" 56 decompressedTestdata = "testdata/events/system_tests.evt" 57 ) 58 59 var ( 60 client *graphql.Client 61 blockWhenDone = flag.Bool("block", false, "leave services running after tests are complete NOTE: EMBEDDED POSGRESQL WILL NOT SHUT DOWN PROPERLY") 62 writeGolden = flag.Bool("golden", false, "write query results to 'golden' files for comparison") 63 goldenDir string 64 ) 65 66 func TestMain(m *testing.M) { 67 flag.Parse() 68 ctx, cfunc := context.WithCancel(context.Background()) 69 defer cfunc() 70 71 if testing.Short() { 72 log.Print("Skipping datanode integration tests, go test run with -short") 73 return 74 } 75 76 vegaHome, postgresRuntimePath, err := setupDirs() 77 if err != nil { 78 log.Fatalf("couldn't setup directories: %s", err) 79 } 80 defer func() { _ = os.RemoveAll(postgresRuntimePath) }() 81 82 testDBSocketDir := filepath.Join(postgresRuntimePath) 83 cfg, err := newTestConfig(testDBSocketDir) 84 if err != nil { 85 log.Fatal("couldn't set up config: ", err) 86 } 87 88 err = os.MkdirAll(eventsDir, os.ModePerm) 89 if err != nil { 90 log.Fatal("failed to make events dir: ", err) 91 } 92 93 cwd, err := os.Getwd() 94 if err != nil { 95 log.Fatal("failed to get working dir: ", err) 96 } 97 98 decompressedTestDataPath := filepath.Join(cwd, decompressedTestdata) 99 if err = utils.DecompressFile(filepath.Join(cwd, compressedTestdata), decompressedTestDataPath); err != nil { 100 log.Fatal("couldn't decompress event file ", err) 101 } 102 103 defer func() { 104 if err := os.RemoveAll(decompressedTestDataPath); err != nil { 105 log.Printf("failed to remove event file: %s", err) 106 } 107 }() 108 109 wg := sync.WaitGroup{} 110 wg.Add(1) 111 112 go func() { 113 defer wg.Done() 114 if err := runTestNode(ctx, cfg, vegaHome); err != nil { 115 cfunc() 116 log.Fatal("running test node: ", err) 117 } 118 }() 119 120 client = graphql.NewClient(fmt.Sprintf("http://localhost:%v/graphql", cfg.Gateway.Port)) 121 if err = waitForEpoch(client, lastEpoch, playbackTimeout); err != nil { 122 cfunc() 123 log.Fatal("problem piping event stream: ", err) 124 } 125 // normal run - services should be terminated properly 126 if blockWhenDone == nil || !*blockWhenDone { 127 go handleSignal(ctx, cfunc) 128 } 129 130 // Cheesy sleep to give everything chance to percolate 131 time.Sleep(5 * time.Second) 132 133 select { 134 case <-ctx.Done(): 135 return 136 default: 137 m.Run() 138 } 139 140 log.Printf("Integration tests completed") 141 142 // When you're debugging tests, it's helpful to stop here so you can go in and poke around 143 // sending queries via the graphql playground etc.. 144 if blockWhenDone != nil && *blockWhenDone { 145 log.Print("Blocking now to allow debugging") 146 c := make(chan os.Signal) 147 signal.Notify(c, os.Interrupt, syscall.SIGTERM) // nolint 148 <-c 149 os.Exit(0) 150 } 151 152 cfunc() 153 wg.Wait() 154 } 155 156 func handleSignal(ctx context.Context, cfunc func()) { 157 c := make(chan os.Signal, 1) 158 signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) 159 for { 160 select { 161 case sig := <-c: 162 log.Printf("Received %+v signal", sig) 163 cfunc() // cancel context 164 return 165 case <-ctx.Done(): 166 // context was cancelled for some reason, close stopper channel 167 log.Printf("Context cancelled") 168 return 169 } 170 } 171 } 172 173 func setupDirs() (string, string, error) { 174 cwd, err := os.Getwd() 175 if err != nil { 176 return "", "", fmt.Errorf("couldn't get working directory: %w", err) 177 } 178 179 goldenDir = filepath.Join(cwd, "testdata", "golden") 180 if err = vgfs.EnsureDir(goldenDir); err != nil { 181 return "", "", fmt.Errorf("couldn't create golden dir: %w", err) 182 } 183 184 vegaHome, err := os.MkdirTemp("", "datanode_test") 185 if err != nil { 186 return "", "", fmt.Errorf("couldn't create temp dir: %w", err) 187 } 188 189 postgresRuntimePath := filepath.Join(vegaHome, "pgdb") 190 191 if err = os.Mkdir(postgresRuntimePath, fs.ModePerm); err != nil { 192 return "", "", fmt.Errorf("couldn't create postgres runtime dir: %w", err) 193 } 194 195 return vegaHome, postgresRuntimePath, nil 196 } 197 198 type queryDetails struct { 199 TestName string 200 Query string 201 Result json.RawMessage 202 Duration time.Duration 203 } 204 205 func assertGraphQLQueriesReturnSame(t *testing.T, query string) { 206 t.Helper() 207 208 req := graphql.NewRequest(query) 209 var resp map[string]interface{} 210 s := time.Now() 211 err := client.Run(context.Background(), req, &resp) 212 require.NoError(t, err, "failed to run query: '%s'; %s", query, err) 213 elapsed := time.Since(s) 214 215 var respJsn json.RawMessage 216 respJsn, err = json.MarshalIndent(resp, "", "\t") 217 require.NoError(t, err) 218 219 niceName := strings.ReplaceAll(t.Name(), "/", "_") 220 goldenFile := filepath.Join(goldenDir, niceName) 221 222 if *writeGolden { 223 details := queryDetails{ 224 TestName: niceName, 225 Query: query, 226 Result: respJsn, 227 Duration: elapsed, 228 } 229 jsonBytes, err := json.MarshalIndent(details, "", "\t") 230 require.NoError(t, err) 231 require.NoError(t, os.WriteFile(goldenFile, jsonBytes, 0o644)) 232 } else { 233 jsonBytes, err := os.ReadFile(goldenFile) 234 require.NoError(t, err, "No golden file for this test, generate one by running 'go test' with the -golden flag") 235 details := queryDetails{} 236 require.NoError(t, json.Unmarshal(jsonBytes, &details), "Unable to unmarshal golden file") 237 assert.Equal(t, details.Query, query, "GraphQL query string differs from recorded in the golden file, regenerate by running 'go test' with the -golden flag") 238 assert.JSONEq(t, string(respJsn), string(details.Result)) 239 } 240 } 241 242 func newTestConfig(postgresRuntimePath string) (*config.Config, error) { 243 cwd, err := os.Getwd() 244 if err != nil { 245 return nil, fmt.Errorf("couldn't get working directory: %w", err) 246 } 247 248 cfg := config.NewDefaultConfig() 249 // cfg.API.RateLimit.TrustedProxies = []string{} 250 cfg.Broker.UseEventFile = true 251 cfg.Broker.PanicOnError = true 252 cfg.Broker.FileEventSourceConfig.Directory = filepath.Join(cwd, eventsDir) 253 cfg.Broker.FileEventSourceConfig.TimeBetweenBlocks = encoding.Duration{Duration: 0} 254 cfg.API.WebUIEnabled = true 255 cfg.API.Reflection = true 256 cfg.ChainID = chainID 257 cfg.SQLStore = databasetest.NewTestConfig(5432, "", postgresRuntimePath) 258 cfg.NetworkHistory.Enabled = false 259 cfg.SQLStore.RetentionPeriod = sqlstore.RetentionPeriodArchive 260 261 return &cfg, nil 262 } 263 264 func runTestNode(ctx context.Context, cfg *config.Config, vegaHome string) error { 265 vegaPaths := paths.New(vegaHome) 266 267 loader, err := config.InitialiseLoader(vegaPaths) 268 if err != nil { 269 return fmt.Errorf("couldn't create config loader: %w", err) 270 } 271 272 if err = loader.Save(cfg); err != nil { 273 return fmt.Errorf("couldn't save config: %w", err) 274 } 275 276 logger := logging.NewLoggerFromConfig(logging.NewDefaultConfig()) 277 configWatcher, err := config.NewWatcher(context.Background(), logger, vegaPaths) 278 if err != nil { 279 return fmt.Errorf("couldn't create config watcher: %w", err) 280 } 281 282 cmd := start.NodeCommand{ 283 Log: logger, 284 Version: "test", 285 VersionHash: "", 286 } 287 288 if err = cmd.Run(ctx, configWatcher, vegaPaths, []string{}); err != nil { 289 return fmt.Errorf("couldn't run node: %w", err) 290 } 291 return nil 292 } 293 294 func waitForEpoch(client *graphql.Client, epoch int, timeout time.Duration) error { 295 giveUpAt := time.Now().Add(timeout) 296 for { 297 currentEpoch, err := getCurrentEpoch(client) 298 if err == nil && currentEpoch >= epoch { 299 return nil 300 } 301 302 log.Printf("Current epoch is %d, waiting for %d", currentEpoch, epoch) 303 304 if time.Now().After(giveUpAt) { 305 return fmt.Errorf("didn't reach epoch %v within %v", epoch, timeout) 306 } 307 time.Sleep(time.Second) 308 } 309 } 310 311 func getCurrentEpoch(client *graphql.Client) (int, error) { 312 req := graphql.NewRequest("{ epoch{id} }") 313 resp := struct{ Epoch struct{ ID string } }{} 314 315 if err := client.Run(context.Background(), req, &resp); err != nil { 316 return 0, err 317 } 318 if resp.Epoch.ID == "" { 319 return 0, fmt.Errorf("empty epoch id") 320 } 321 322 return strconv.Atoi(resp.Epoch.ID) 323 }