vitess.io/vitess@v0.16.2/go/vt/vttablet/tabletmanager/vreplication/framework_test.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package vreplication
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"reflect"
    25  	"regexp"
    26  	"strings"
    27  	"testing"
    28  	"time"
    29  
    30  	"github.com/spf13/pflag"
    31  	"github.com/stretchr/testify/require"
    32  	"google.golang.org/protobuf/proto"
    33  
    34  	_flag "vitess.io/vitess/go/internal/flag"
    35  	"vitess.io/vitess/go/mysql"
    36  	"vitess.io/vitess/go/sqltypes"
    37  	"vitess.io/vitess/go/vt/binlog/binlogplayer"
    38  	"vitess.io/vitess/go/vt/dbconfigs"
    39  	"vitess.io/vitess/go/vt/grpcclient"
    40  	"vitess.io/vitess/go/vt/log"
    41  	"vitess.io/vitess/go/vt/servenv"
    42  	"vitess.io/vitess/go/vt/sidecardb"
    43  	"vitess.io/vitess/go/vt/topo"
    44  	"vitess.io/vitess/go/vt/vttablet/queryservice"
    45  	"vitess.io/vitess/go/vt/vttablet/queryservice/fakes"
    46  	"vitess.io/vitess/go/vt/vttablet/tabletconn"
    47  	"vitess.io/vitess/go/vt/vttablet/tabletconntest"
    48  	qh "vitess.io/vitess/go/vt/vttablet/tabletmanager/vreplication/queryhistory"
    49  	"vitess.io/vitess/go/vt/vttablet/tabletserver/vstreamer"
    50  	"vitess.io/vitess/go/vt/vttablet/tabletserver/vstreamer/testenv"
    51  
    52  	binlogdatapb "vitess.io/vitess/go/vt/proto/binlogdata"
    53  	querypb "vitess.io/vitess/go/vt/proto/query"
    54  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    55  )
    56  
    57  var (
    58  	playerEngine          *Engine
    59  	streamerEngine        *vstreamer.Engine
    60  	env                   *testenv.Env
    61  	globalFBC             = &fakeBinlogClient{}
    62  	vrepldb               = "vrepl"
    63  	globalDBQueries       = make(chan string, 1000)
    64  	testForeignKeyQueries = false
    65  	doNotLogDBQueries     = false
    66  )
    67  
    68  type LogExpectation struct {
    69  	Type   string
    70  	Detail string
    71  }
    72  
    73  var heartbeatRe *regexp.Regexp
    74  
    75  // setFlag() sets a flag for a test in a non-racy way:
    76  //   - it registers the flag using a different flagset scope
    77  //   - clears other flags by passing a dummy os.Args() while parsing this flagset
    78  //   - sets the specific flag, if it has not already been defined
    79  //   - resets the os.Args() so that the remaining flagsets can be parsed correctly
    80  func setFlag(flagName, flagValue string) {
    81  	flagSetName := "vreplication-unit-test"
    82  	var tmp []string
    83  	tmp, os.Args = os.Args[:], []string{flagSetName}
    84  	defer func() { os.Args = tmp }()
    85  
    86  	servenv.OnParseFor(flagSetName, func(fs *pflag.FlagSet) {
    87  		if fs.Lookup(flagName) != nil {
    88  			fmt.Printf("found %s: %+v", flagName, fs.Lookup(flagName).Value)
    89  			return
    90  		}
    91  	})
    92  	servenv.ParseFlags(flagSetName)
    93  
    94  	if err := pflag.Set(flagName, flagValue); err != nil {
    95  		msg := "failed to set flag %q to %q: %v"
    96  		log.Errorf(msg, flagName, flagValue, err)
    97  	}
    98  }
    99  
   100  func init() {
   101  	tabletconn.RegisterDialer("test", func(tablet *topodatapb.Tablet, failFast grpcclient.FailFast) (queryservice.QueryService, error) {
   102  		return &fakeTabletConn{
   103  			QueryService: fakes.ErrorQueryService,
   104  			tablet:       tablet,
   105  		}, nil
   106  	})
   107  	tabletconntest.SetProtocol("go.vt.vttablet.tabletmanager.vreplication.framework_test", "test")
   108  
   109  	binlogplayer.RegisterClientFactory("test", func() binlogplayer.Client { return globalFBC })
   110  	heartbeatRe = regexp.MustCompile(`update _vt.vreplication set time_updated=\d+ where id=\d+`)
   111  }
   112  
   113  func TestMain(m *testing.M) {
   114  	binlogplayer.SetProtocol("vreplication_test_framework", "test")
   115  	_flag.ParseFlagsForTest()
   116  	exitCode := func() int {
   117  		var err error
   118  		env, err = testenv.Init()
   119  		if err != nil {
   120  			fmt.Fprintf(os.Stderr, "%v", err)
   121  			return 1
   122  		}
   123  		defer env.Close()
   124  
   125  		vreplicationExperimentalFlags = 0
   126  
   127  		// engines cannot be initialized in testenv because it introduces
   128  		// circular dependencies.
   129  		streamerEngine = vstreamer.NewEngine(env.TabletEnv, env.SrvTopo, env.SchemaEngine, nil, env.Cells[0])
   130  		streamerEngine.InitDBConfig(env.KeyspaceName, env.ShardName)
   131  		streamerEngine.Open()
   132  		defer streamerEngine.Close()
   133  
   134  		if err := env.Mysqld.ExecuteSuperQuery(context.Background(), fmt.Sprintf("create database %s", vrepldb)); err != nil {
   135  			fmt.Fprintf(os.Stderr, "%v", err)
   136  			return 1
   137  		}
   138  
   139  		if err := env.Mysqld.ExecuteSuperQuery(context.Background(), "set @@global.innodb_lock_wait_timeout=1"); err != nil {
   140  			fmt.Fprintf(os.Stderr, "%v", err)
   141  			return 1
   142  		}
   143  
   144  		externalConfig := map[string]*dbconfigs.DBConfigs{
   145  			"exta": env.Dbcfgs,
   146  			"extb": env.Dbcfgs,
   147  		}
   148  		playerEngine = NewTestEngine(env.TopoServ, env.Cells[0], env.Mysqld, realDBClientFactory, realDBClientFactory, vrepldb, externalConfig)
   149  		playerEngine.Open(context.Background())
   150  		defer playerEngine.Close()
   151  
   152  		return m.Run()
   153  	}()
   154  	os.Exit(exitCode)
   155  }
   156  
   157  func resetBinlogClient() {
   158  	globalFBC = &fakeBinlogClient{}
   159  }
   160  
   161  func primaryPosition(t *testing.T) string {
   162  	t.Helper()
   163  	pos, err := env.Mysqld.PrimaryPosition()
   164  	if err != nil {
   165  		t.Fatal(err)
   166  	}
   167  	return mysql.EncodePosition(pos)
   168  }
   169  
   170  func execStatements(t *testing.T, queries []string) {
   171  	t.Helper()
   172  	if err := env.Mysqld.ExecuteSuperQueryList(context.Background(), queries); err != nil {
   173  		log.Errorf("Error executing query: %s", err.Error())
   174  		t.Error(err)
   175  	}
   176  }
   177  
   178  //--------------------------------------
   179  // Topos and tablets
   180  
   181  func addTablet(id int) *topodatapb.Tablet {
   182  	tablet := &topodatapb.Tablet{
   183  		Alias: &topodatapb.TabletAlias{
   184  			Cell: env.Cells[0],
   185  			Uid:  uint32(id),
   186  		},
   187  		Keyspace: env.KeyspaceName,
   188  		Shard:    env.ShardName,
   189  		KeyRange: &topodatapb.KeyRange{},
   190  		Type:     topodatapb.TabletType_REPLICA,
   191  		PortMap: map[string]int32{
   192  			"test": int32(id),
   193  		},
   194  	}
   195  	if err := env.TopoServ.CreateTablet(context.Background(), tablet); err != nil {
   196  		panic(err)
   197  	}
   198  	env.SchemaEngine.Reload(context.Background())
   199  	return tablet
   200  }
   201  
   202  func addOtherTablet(id int, keyspace, shard string) *topodatapb.Tablet {
   203  	tablet := &topodatapb.Tablet{
   204  		Alias: &topodatapb.TabletAlias{
   205  			Cell: env.Cells[0],
   206  			Uid:  uint32(id),
   207  		},
   208  		Keyspace: keyspace,
   209  		Shard:    shard,
   210  		KeyRange: &topodatapb.KeyRange{},
   211  		Type:     topodatapb.TabletType_REPLICA,
   212  		PortMap: map[string]int32{
   213  			"test": int32(id),
   214  		},
   215  	}
   216  	if err := env.TopoServ.CreateTablet(context.Background(), tablet); err != nil {
   217  		panic(err)
   218  	}
   219  	env.SchemaEngine.Reload(context.Background())
   220  	return tablet
   221  }
   222  
   223  func deleteTablet(tablet *topodatapb.Tablet) {
   224  	env.TopoServ.DeleteTablet(context.Background(), tablet.Alias)
   225  	// This is not automatically removed from shard replication, which results in log spam.
   226  	topo.DeleteTabletReplicationData(context.Background(), env.TopoServ, tablet)
   227  	env.SchemaEngine.Reload(context.Background())
   228  }
   229  
   230  // fakeTabletConn implement TabletConn interface. We only care about the
   231  // health check part. The state reported by the tablet will depend
   232  // on the Tag values "serving" and "healthy".
   233  type fakeTabletConn struct {
   234  	queryservice.QueryService
   235  	tablet *topodatapb.Tablet
   236  }
   237  
   238  // StreamHealth is part of queryservice.QueryService.
   239  func (ftc *fakeTabletConn) StreamHealth(ctx context.Context, callback func(*querypb.StreamHealthResponse) error) error {
   240  	return callback(&querypb.StreamHealthResponse{
   241  		Serving: true,
   242  		Target: &querypb.Target{
   243  			Keyspace:   ftc.tablet.Keyspace,
   244  			Shard:      ftc.tablet.Shard,
   245  			TabletType: ftc.tablet.Type,
   246  		},
   247  		RealtimeStats: &querypb.RealtimeStats{},
   248  	})
   249  }
   250  
   251  // vstreamHook allows you to do work just before calling VStream.
   252  var vstreamHook func(ctx context.Context)
   253  
   254  // VStream directly calls into the pre-initialized engine.
   255  func (ftc *fakeTabletConn) VStream(ctx context.Context, request *binlogdatapb.VStreamRequest, send func([]*binlogdatapb.VEvent) error) error {
   256  	if request.Target.Keyspace != "vttest" {
   257  		<-ctx.Done()
   258  		return io.EOF
   259  	}
   260  	if vstreamHook != nil {
   261  		vstreamHook(ctx)
   262  	}
   263  	return streamerEngine.Stream(ctx, request.Position, request.TableLastPKs, request.Filter, send)
   264  }
   265  
   266  // vstreamRowsHook allows you to do work just before calling VStreamRows.
   267  var vstreamRowsHook func(ctx context.Context)
   268  
   269  // vstreamRowsSendHook allows you to do work just before VStreamRows calls send.
   270  var vstreamRowsSendHook func(ctx context.Context)
   271  
   272  // VStreamRows directly calls into the pre-initialized engine.
   273  func (ftc *fakeTabletConn) VStreamRows(ctx context.Context, request *binlogdatapb.VStreamRowsRequest, send func(*binlogdatapb.VStreamRowsResponse) error) error {
   274  	if vstreamRowsHook != nil {
   275  		vstreamRowsHook(ctx)
   276  	}
   277  	var row []sqltypes.Value
   278  	if request.Lastpk != nil {
   279  		r := sqltypes.Proto3ToResult(request.Lastpk)
   280  		if len(r.Rows) != 1 {
   281  			return fmt.Errorf("unexpected lastpk input: %v", request.Lastpk)
   282  		}
   283  		row = r.Rows[0]
   284  	}
   285  	return streamerEngine.StreamRows(ctx, request.Query, row, func(rows *binlogdatapb.VStreamRowsResponse) error {
   286  		if vstreamRowsSendHook != nil {
   287  			vstreamRowsSendHook(ctx)
   288  		}
   289  		return send(rows)
   290  	})
   291  }
   292  
   293  //--------------------------------------
   294  // Binlog Client to TabletManager
   295  
   296  // fakeBinlogClient satisfies binlogplayer.Client.
   297  // Not to be used concurrently.
   298  type fakeBinlogClient struct {
   299  	lastTablet   *topodatapb.Tablet
   300  	lastPos      string
   301  	lastTables   []string
   302  	lastKeyRange *topodatapb.KeyRange
   303  	lastCharset  *binlogdatapb.Charset
   304  }
   305  
   306  func (fbc *fakeBinlogClient) Dial(tablet *topodatapb.Tablet) error {
   307  	fbc.lastTablet = tablet
   308  	return nil
   309  }
   310  
   311  func (fbc *fakeBinlogClient) Close() {
   312  }
   313  
   314  func (fbc *fakeBinlogClient) StreamTables(ctx context.Context, position string, tables []string, charset *binlogdatapb.Charset) (binlogplayer.BinlogTransactionStream, error) {
   315  	fbc.lastPos = position
   316  	fbc.lastTables = tables
   317  	fbc.lastCharset = charset
   318  	return &btStream{ctx: ctx}, nil
   319  }
   320  
   321  func (fbc *fakeBinlogClient) StreamKeyRange(ctx context.Context, position string, keyRange *topodatapb.KeyRange, charset *binlogdatapb.Charset) (binlogplayer.BinlogTransactionStream, error) {
   322  	fbc.lastPos = position
   323  	fbc.lastKeyRange = keyRange
   324  	fbc.lastCharset = charset
   325  	return &btStream{ctx: ctx}, nil
   326  }
   327  
   328  // btStream satisfies binlogplayer.BinlogTransactionStream
   329  type btStream struct {
   330  	ctx  context.Context
   331  	sent bool
   332  }
   333  
   334  func (bts *btStream) Recv() (*binlogdatapb.BinlogTransaction, error) {
   335  	if !bts.sent {
   336  		bts.sent = true
   337  		return &binlogdatapb.BinlogTransaction{
   338  			Statements: []*binlogdatapb.BinlogTransaction_Statement{
   339  				{
   340  					Category: binlogdatapb.BinlogTransaction_Statement_BL_INSERT,
   341  					Sql:      []byte("insert into t values(1)"),
   342  				},
   343  			},
   344  			EventToken: &querypb.EventToken{
   345  				Timestamp: 72,
   346  				Position:  "MariaDB/0-1-1235",
   347  			},
   348  		}, nil
   349  	}
   350  	<-bts.ctx.Done()
   351  	return nil, bts.ctx.Err()
   352  }
   353  
   354  func expectFBCRequest(t *testing.T, tablet *topodatapb.Tablet, pos string, tables []string, kr *topodatapb.KeyRange) {
   355  	t.Helper()
   356  	if !proto.Equal(tablet, globalFBC.lastTablet) {
   357  		t.Errorf("Request tablet: %v, want %v", globalFBC.lastTablet, tablet)
   358  	}
   359  	if pos != globalFBC.lastPos {
   360  		t.Errorf("Request pos: %v, want %v", globalFBC.lastPos, pos)
   361  	}
   362  	if !reflect.DeepEqual(tables, globalFBC.lastTables) {
   363  		t.Errorf("Request tables: %v, want %v", globalFBC.lastTables, tables)
   364  	}
   365  	if !proto.Equal(kr, globalFBC.lastKeyRange) {
   366  		t.Errorf("Request KeyRange: %v, want %v", globalFBC.lastKeyRange, kr)
   367  	}
   368  }
   369  
   370  //--------------------------------------
   371  // DBCLient wrapper
   372  
   373  func realDBClientFactory() binlogplayer.DBClient {
   374  	return &realDBClient{}
   375  }
   376  
   377  type realDBClient struct {
   378  	conn  *mysql.Conn
   379  	nolog bool
   380  }
   381  
   382  func (dbc *realDBClient) DBName() string {
   383  	return vrepldb
   384  }
   385  
   386  func (dbc *realDBClient) Connect() error {
   387  	app, err := env.Dbcfgs.AppWithDB().MysqlParams()
   388  	if err != nil {
   389  		return err
   390  	}
   391  	app.DbName = vrepldb
   392  	conn, err := mysql.Connect(context.Background(), app)
   393  	if err != nil {
   394  		return err
   395  	}
   396  	dbc.conn = conn
   397  	return nil
   398  }
   399  
   400  func (dbc *realDBClient) Begin() error {
   401  	_, err := dbc.ExecuteFetch("begin", 10000)
   402  	return err
   403  }
   404  
   405  func (dbc *realDBClient) Commit() error {
   406  	_, err := dbc.ExecuteFetch("commit", 10000)
   407  	return err
   408  }
   409  
   410  func (dbc *realDBClient) Rollback() error {
   411  	_, err := dbc.ExecuteFetch("rollback", 10000)
   412  	return err
   413  }
   414  
   415  func (dbc *realDBClient) Close() {
   416  	dbc.conn.Close()
   417  }
   418  
   419  func (dbc *realDBClient) ExecuteFetch(query string, maxrows int) (*sqltypes.Result, error) {
   420  	// Use Clone() because the contents of memory region referenced by
   421  	// string can change when clients (e.g. vcopier) use unsafe string methods.
   422  	query = strings.Clone(query)
   423  	qr, err := dbc.conn.ExecuteFetch(query, 10000, true)
   424  	if doNotLogDBQueries {
   425  		return qr, err
   426  	}
   427  	if !strings.HasPrefix(query, "select") && !strings.HasPrefix(query, "set") && !dbc.nolog {
   428  		globalDBQueries <- query
   429  	} else if testForeignKeyQueries && strings.Contains(query, "foreign_key_checks") { //allow select/set for foreign_key_checks
   430  		globalDBQueries <- query
   431  	}
   432  	return qr, err
   433  }
   434  
   435  func expectDeleteQueries(t *testing.T) {
   436  	t.Helper()
   437  	expectNontxQueries(t, qh.Expect(
   438  		"/delete from _vt.vreplication",
   439  		"/delete from _vt.copy_state",
   440  		"/delete from _vt.post_copy_action",
   441  	))
   442  }
   443  
   444  func expectLogsAndUnsubscribe(t *testing.T, logs []LogExpectation, logCh chan any) {
   445  	t.Helper()
   446  	defer vrLogStatsLogger.Unsubscribe(logCh)
   447  	failed := false
   448  	for i, log := range logs {
   449  		if failed {
   450  			t.Errorf("no logs received")
   451  			continue
   452  		}
   453  		select {
   454  		case data := <-logCh:
   455  			got, ok := data.(*VrLogStats)
   456  			if !ok {
   457  				t.Errorf("got not ok casting to VrLogStats: %v", data)
   458  			}
   459  			var match bool
   460  			match = (log.Type == got.Type)
   461  			if match {
   462  				if log.Detail[0] == '/' {
   463  					result, err := regexp.MatchString(log.Detail[1:], got.Detail)
   464  					if err != nil {
   465  						panic(err)
   466  					}
   467  					match = result
   468  				} else {
   469  					match = (got.Detail == log.Detail)
   470  				}
   471  			}
   472  
   473  			if !match {
   474  				t.Errorf("log:\n%q, does not match log %d:\n%q", got, i, log)
   475  			}
   476  		case <-time.After(5 * time.Second):
   477  			t.Errorf("no logs received, expecting %s", log)
   478  			failed = true
   479  		}
   480  	}
   481  }
   482  
   483  func shouldIgnoreQuery(query string) bool {
   484  	queriesToIgnore := []string{
   485  		"_vt.vreplication_log",   // ignore all selects, updates and inserts into this table
   486  		"@@session.sql_mode",     // ignore all selects, and sets of this variable
   487  		", time_heartbeat=",      // update of last heartbeat time, can happen out-of-band, so can't test for it
   488  		", time_throttled=",      // update of last throttle time, can happen out-of-band, so can't test for it
   489  		", component_throttled=", // update of last throttle time, can happen out-of-band, so can't test for it
   490  		"context cancel",
   491  	}
   492  	if sidecardb.MatchesInitQuery(query) {
   493  		return true
   494  	}
   495  	for _, q := range queriesToIgnore {
   496  		if strings.Contains(query, q) {
   497  			return true
   498  		}
   499  	}
   500  	return heartbeatRe.MatchString(query)
   501  }
   502  
   503  func expectDBClientQueries(t *testing.T, expectations qh.ExpectationSequence, skippableOnce ...string) {
   504  	t.Helper()
   505  	failed := false
   506  	skippedOnce := false
   507  	validator := qh.NewVerifier(expectations)
   508  
   509  	for len(validator.Pending()) > 0 {
   510  		if failed {
   511  			t.Errorf("no query received")
   512  			continue
   513  		}
   514  		var got string
   515  	retry:
   516  		select {
   517  		case got = <-globalDBQueries:
   518  			// We rule out heartbeat time update queries because otherwise our query list
   519  			// is indeterminable and varies with each test execution.
   520  			if shouldIgnoreQuery(got) {
   521  				goto retry
   522  			}
   523  
   524  			result := validator.AcceptQuery(got)
   525  
   526  			if !result.Accepted {
   527  				if !skippedOnce {
   528  					// let's see if "got" is a skippable query
   529  					for _, skippable := range skippableOnce {
   530  						if ok, _ := qh.MatchQueries(skippable, got); ok {
   531  							skippedOnce = true
   532  							goto retry
   533  						}
   534  					}
   535  				}
   536  				require.True(t, result.Accepted, fmt.Sprintf(
   537  					"query:%q\nmessage:%s\nexpectation:%s\nmatched:%t\nerror:%v\nhistory:%s",
   538  					got, result.Message, result.Expectation, result.Matched, result.Error, validator.History(),
   539  				))
   540  			}
   541  		case <-time.After(5 * time.Second):
   542  			t.Fatalf("no query received")
   543  			failed = true
   544  		}
   545  	}
   546  	for {
   547  		select {
   548  		case got := <-globalDBQueries:
   549  			if shouldIgnoreQuery(got) {
   550  				continue
   551  			}
   552  			t.Errorf("unexpected query: %s", got)
   553  		default:
   554  			// Assert there are no pending expectations.
   555  			require.Len(t, validator.Pending(), 0)
   556  			return
   557  		}
   558  	}
   559  }
   560  
   561  // expectNontxQueries disregards transactional statements like begin and commit.
   562  // It also disregards updates to _vt.vreplication.
   563  func expectNontxQueries(t *testing.T, expectations qh.ExpectationSequence) {
   564  	t.Helper()
   565  
   566  	failed := false
   567  
   568  	validator := qh.NewVerifier(expectations)
   569  
   570  	for len(validator.Pending()) > 0 {
   571  		if failed {
   572  			t.Errorf("no query received")
   573  			continue
   574  		}
   575  		var got string
   576  	retry:
   577  		select {
   578  		case got = <-globalDBQueries:
   579  			if got == "begin" || got == "commit" || got == "rollback" || strings.Contains(got, "update _vt.vreplication set pos") || shouldIgnoreQuery(got) {
   580  				goto retry
   581  			}
   582  
   583  			result := validator.AcceptQuery(got)
   584  
   585  			require.True(t, result.Accepted, fmt.Sprintf(
   586  				"query:%q\nmessage:%s\nexpectation:%s\nmatched:%t\nerror:%v\nhistory:%s",
   587  				got, result.Message, result.Expectation, result.Matched, result.Error, validator.History(),
   588  			))
   589  		case <-time.After(5 * time.Second):
   590  			t.Fatalf("no query received")
   591  			failed = true
   592  		}
   593  	}
   594  	for {
   595  		select {
   596  		case got := <-globalDBQueries:
   597  			if got == "begin" || got == "commit" || got == "rollback" || strings.Contains(got, "_vt.vreplication") {
   598  				continue
   599  			}
   600  			if shouldIgnoreQuery(got) {
   601  				continue
   602  			}
   603  			t.Errorf("unexpected query: %s", got)
   604  		default:
   605  			// Assert there are no pending expectations.
   606  			require.Len(t, validator.Pending(), 0)
   607  			return
   608  		}
   609  	}
   610  }
   611  
   612  func expectData(t *testing.T, table string, values [][]string) {
   613  	t.Helper()
   614  	customExpectData(t, table, values, env.Mysqld.FetchSuperQuery)
   615  }
   616  
   617  func expectQueryResult(t *testing.T, query string, values [][]string) {
   618  	t.Helper()
   619  	err := compareQueryResults(t, query, values, env.Mysqld.FetchSuperQuery)
   620  	if err != nil {
   621  		require.FailNow(t, "data mismatch", err)
   622  	}
   623  }
   624  
   625  func customExpectData(t *testing.T, table string, values [][]string, exec func(ctx context.Context, query string) (*sqltypes.Result, error)) {
   626  	t.Helper()
   627  
   628  	var query string
   629  	if len(strings.Split(table, ".")) == 1 {
   630  		query = fmt.Sprintf("select * from %s.%s", vrepldb, table)
   631  	} else {
   632  		query = fmt.Sprintf("select * from %s", table)
   633  	}
   634  	err := compareQueryResults(t, query, values, exec)
   635  	if err != nil {
   636  		require.FailNow(t, "data mismatch", err)
   637  	}
   638  }
   639  
   640  func compareQueryResults(t *testing.T, query string, values [][]string,
   641  	exec func(ctx context.Context, query string) (*sqltypes.Result, error)) error {
   642  
   643  	t.Helper()
   644  	qr, err := exec(context.Background(), query)
   645  	if err != nil {
   646  		return err
   647  	}
   648  	if len(values) != len(qr.Rows) {
   649  		return fmt.Errorf("row counts don't match: %v, want %v", qr.Rows, values)
   650  	}
   651  	for i, row := range values {
   652  		if len(row) != len(qr.Rows[i]) {
   653  			return fmt.Errorf("Too few columns, \nrow: %d, \nresult: %d:%v, \nwant: %d:%v", i, len(qr.Rows[i]), qr.Rows[i], len(row), row)
   654  		}
   655  		for j, val := range row {
   656  			if got := qr.Rows[i][j].ToString(); got != val {
   657  				return fmt.Errorf("Mismatch at (%d, %d): %v, want %s", i, j, qr.Rows[i][j], val)
   658  			}
   659  		}
   660  	}
   661  
   662  	return nil
   663  }
   664  
   665  func validateQueryCountStat(t *testing.T, phase string, want int64) {
   666  	var count int64
   667  	for _, ct := range globalStats.status().Controllers {
   668  		for ph, cnt := range ct.QueryCounts {
   669  			if ph == phase {
   670  				count += cnt
   671  			}
   672  		}
   673  	}
   674  	require.Equal(t, want, count, "QueryCount stat is incorrect")
   675  }
   676  
   677  func validateCopyRowCountStat(t *testing.T, want int64) {
   678  	var count int64
   679  	for _, ct := range globalStats.status().Controllers {
   680  		count += ct.CopyRowCount
   681  	}
   682  	require.Equal(t, want, count, "CopyRowCount stat is incorrect")
   683  }