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  }