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

     1  /*
     2  Copyright 2022 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 vdiff
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"regexp"
    25  	"strings"
    26  	"sync"
    27  	"testing"
    28  
    29  	"github.com/stretchr/testify/assert"
    30  	"github.com/stretchr/testify/require"
    31  
    32  	"vitess.io/vitess/go/mysql"
    33  	"vitess.io/vitess/go/sqltypes"
    34  	"vitess.io/vitess/go/vt/binlog/binlogplayer"
    35  	"vitess.io/vitess/go/vt/grpcclient"
    36  	"vitess.io/vitess/go/vt/sqlparser"
    37  	"vitess.io/vitess/go/vt/topo"
    38  	"vitess.io/vitess/go/vt/vttablet/queryservice"
    39  	"vitess.io/vitess/go/vt/vttablet/tabletconn"
    40  	"vitess.io/vitess/go/vt/vttablet/tabletconntest"
    41  	"vitess.io/vitess/go/vt/vttablet/tabletmanager/vreplication"
    42  	"vitess.io/vitess/go/vt/vttablet/tabletserver/schema"
    43  	"vitess.io/vitess/go/vt/vttablet/tabletserver/vstreamer"
    44  	"vitess.io/vitess/go/vt/vttablet/tabletserver/vstreamer/testenv"
    45  	"vitess.io/vitess/go/vt/vttablet/tmclient"
    46  	"vitess.io/vitess/go/vt/vttablet/tmclienttest"
    47  
    48  	binlogdatapb "vitess.io/vitess/go/vt/proto/binlogdata"
    49  	querypb "vitess.io/vitess/go/vt/proto/query"
    50  	tabletmanagerdatapb "vitess.io/vitess/go/vt/proto/tabletmanagerdata"
    51  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    52  )
    53  
    54  const (
    55  	optionsJS         = `{"core_options": {"auto_retry": true}}`
    56  	vdiffTestCols     = "id|vdiff_uuid|workflow|keyspace|shard|db_name|state|options|last_error"
    57  	vdiffTestColTypes = "int64|varchar|varbinary|varbinary|varchar|varbinary|varbinary|json|varbinary"
    58  	// This is also the keyspace name used
    59  	vdiffDBName = "vttest"
    60  
    61  	// vdiffSourceGtid should be the position reported by the source side VStreamResults.
    62  	vdiffSourceGtid            = "MySQL56/f69ed286-6909-11ed-8342-0a50724f3211:1-110"
    63  	vdiffTargetPrimaryPosition = vdiffSourceGtid
    64  )
    65  
    66  var (
    67  	vreplSource       = fmt.Sprintf(`keyspace:"%s" shard:"0" filter:{rules:{match:"t1" filter:"select * from t1"}}`, vdiffDBName)
    68  	singleRowAffected = &sqltypes.Result{RowsAffected: 1}
    69  	noResults         = &sqltypes.Result{}
    70  	testSchema        = &tabletmanagerdatapb.SchemaDefinition{
    71  		TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
    72  			{
    73  				Name:              "t1",
    74  				Columns:           []string{"c1", "c2"},
    75  				PrimaryKeyColumns: []string{"c1"},
    76  				Fields:            sqltypes.MakeTestFields("c1|c2", "int64|int64"),
    77  			}, {
    78  				Name:              "nonpktext",
    79  				Columns:           []string{"c1", "textcol"},
    80  				PrimaryKeyColumns: []string{"c1"},
    81  				Fields:            sqltypes.MakeTestFields("c1|textcol", "int64|varchar"),
    82  			}, {
    83  				Name:              "pktext",
    84  				Columns:           []string{"textcol", "c2"},
    85  				PrimaryKeyColumns: []string{"textcol"},
    86  				Fields:            sqltypes.MakeTestFields("textcol|c2", "varchar|int64"),
    87  			}, {
    88  				Name:              "multipk",
    89  				Columns:           []string{"c1", "c2"},
    90  				PrimaryKeyColumns: []string{"c1", "c2"},
    91  				Fields:            sqltypes.MakeTestFields("c1|c2", "int64|int64"),
    92  			}, {
    93  				Name:              "aggr",
    94  				Columns:           []string{"c1", "c2", "c3", "c4"},
    95  				PrimaryKeyColumns: []string{"c1"},
    96  				Fields:            sqltypes.MakeTestFields("c1|c2|c3|c4", "int64|int64|int64|int64"),
    97  			}, {
    98  				Name:              "datze",
    99  				Columns:           []string{"id", "dt"},
   100  				PrimaryKeyColumns: []string{"id"},
   101  				Fields:            sqltypes.MakeTestFields("id|dt", "int64|datetime"),
   102  			},
   103  		},
   104  	}
   105  	tableDefMap = map[string]int{
   106  		"t1":        0,
   107  		"nonpktext": 1,
   108  		"pktext":    2,
   109  		"multipk":   3,
   110  		"aggr":      4,
   111  		"datze":     5,
   112  	}
   113  )
   114  
   115  type testVDiffEnv struct {
   116  	workflow        string
   117  	se              *schema.Engine
   118  	vse             *vstreamer.Engine
   119  	vre             *vreplication.Engine
   120  	vde             *Engine
   121  	tmc             *fakeTMClient
   122  	opts            *tabletmanagerdatapb.VDiffOptions
   123  	dbClient        *binlogplayer.MockDBClient
   124  	dbClientFactory func() binlogplayer.DBClient
   125  	tmClientFactory func() tmclient.TabletManagerClient
   126  
   127  	mu      sync.Mutex
   128  	tablets map[int]*fakeTabletConn
   129  }
   130  
   131  var (
   132  	// vdiffenv has to be a global for RegisterDialer to work.
   133  	vdiffenv          *testVDiffEnv
   134  	tstenv            *testenv.Env
   135  	globalFBC         = &fakeBinlogClient{}
   136  	globalDBQueries   = make(chan string, 1000)
   137  	doNotLogDBQueries = false
   138  	once              sync.Once
   139  )
   140  
   141  type LogExpectation struct {
   142  	Type   string
   143  	Detail string
   144  }
   145  
   146  func init() {
   147  	tabletconn.RegisterDialer("test", func(tablet *topodatapb.Tablet, failFast grpcclient.FailFast) (queryservice.QueryService, error) {
   148  		vdiffenv.mu.Lock()
   149  		defer vdiffenv.mu.Unlock()
   150  		if qs, ok := vdiffenv.tablets[int(tablet.Alias.Uid)]; ok {
   151  			return qs, nil
   152  		}
   153  		return nil, fmt.Errorf("tablet %d not found", tablet.Alias.Uid)
   154  	})
   155  	// TableDiffer does a default grpc dial just to be sure it can talk to the tablet.
   156  	tabletconn.RegisterDialer("grpc", func(tablet *topodatapb.Tablet, failFast grpcclient.FailFast) (queryservice.QueryService, error) {
   157  		vdiffenv.mu.Lock()
   158  		defer vdiffenv.mu.Unlock()
   159  		if qs, ok := vdiffenv.tablets[int(tablet.Alias.Uid)]; ok {
   160  			return qs, nil
   161  		}
   162  		return nil, fmt.Errorf("tablet %d not found", tablet.Alias.Uid)
   163  	})
   164  	tabletconntest.SetProtocol("go.vt.vttablet.tabletmanager.vdiff.framework_test", "test")
   165  
   166  	binlogplayer.RegisterClientFactory("test", func() binlogplayer.Client { return globalFBC })
   167  	binlogplayer.SetProtocol("vdiff_test_framework", "test")
   168  
   169  	tmclient.RegisterTabletManagerClientFactory("test", func() tmclient.TabletManagerClient { return vdiffenv.tmc })
   170  	tmclienttest.SetProtocol("go.vt.vttablet.tabletmanager.vdiff.framework_test", "test")
   171  }
   172  
   173  func TestMain(m *testing.M) {
   174  	exitCode := func() int {
   175  		var err error
   176  		tstenv, err = testenv.Init()
   177  		if err != nil {
   178  			fmt.Fprintf(os.Stderr, "%v", err)
   179  			return 1
   180  		}
   181  		defer tstenv.Close()
   182  
   183  		return m.Run()
   184  	}()
   185  	os.Exit(exitCode)
   186  }
   187  
   188  func resetBinlogClient() {
   189  	globalFBC = &fakeBinlogClient{}
   190  }
   191  
   192  // shortCircuitTestAfterQuery is used to short circuit a test after a specific query is executed.
   193  // This can be used to end a vdiff, by returning an error from the specified query, once the test
   194  // has verified the necessary behavior.
   195  func shortCircuitTestAfterQuery(query string, dbClient *binlogplayer.MockDBClient) {
   196  	dbClient.ExpectRequest(query, singleRowAffected, fmt.Errorf("Short circuiting test"))
   197  	dbClient.ExpectRequest("update _vt.vdiff set state = 'error', last_error = 'Short circuiting test'  where id = 1", singleRowAffected, nil)
   198  	dbClient.ExpectRequest("insert into _vt.vdiff_log(vdiff_id, message) values (1, 'State changed to: error')", singleRowAffected, nil)
   199  	dbClient.ExpectRequest("insert into _vt.vdiff_log(vdiff_id, message) values (1, 'Error: Short circuiting test')", singleRowAffected, nil)
   200  }
   201  
   202  //--------------------------------------
   203  // Topos and tablets
   204  
   205  // fakeTabletConn implement TabletConn interface. We only care about the
   206  // health check part. The state reported by the tablet will depend
   207  // on the Tag values "serving" and "healthy".
   208  type fakeTabletConn struct {
   209  	queryservice.QueryService
   210  	tablet *topodatapb.Tablet
   211  }
   212  
   213  // StreamHealth is part of queryservice.QueryService.
   214  func (ftc *fakeTabletConn) StreamHealth(ctx context.Context, callback func(*querypb.StreamHealthResponse) error) error {
   215  	return callback(&querypb.StreamHealthResponse{
   216  		Serving: true,
   217  		Target: &querypb.Target{
   218  			Keyspace:   ftc.tablet.Keyspace,
   219  			Shard:      ftc.tablet.Shard,
   220  			TabletType: ftc.tablet.Type,
   221  		},
   222  		RealtimeStats: &querypb.RealtimeStats{},
   223  	})
   224  }
   225  
   226  // vstreamHook allows you to do work just before calling VStream.
   227  var vstreamHook func(ctx context.Context)
   228  
   229  // VStream directly calls into the pre-initialized engine.
   230  func (ftc *fakeTabletConn) VStream(ctx context.Context, request *binlogdatapb.VStreamRequest, send func([]*binlogdatapb.VEvent) error) error {
   231  	if request.Target.Keyspace != vdiffDBName {
   232  		<-ctx.Done()
   233  		return io.EOF
   234  	}
   235  	if vstreamHook != nil {
   236  		vstreamHook(ctx)
   237  	}
   238  	return vdiffenv.vse.Stream(ctx, request.Position, request.TableLastPKs, request.Filter, send)
   239  }
   240  
   241  // vstreamRowsHook allows you to do work just before calling VStreamRows.
   242  var vstreamRowsHook func(ctx context.Context)
   243  
   244  // vstreamRowsSendHook allows you to do work just before VStreamRows calls send.
   245  var vstreamRowsSendHook func(ctx context.Context)
   246  
   247  // VStreamRows directly calls into the pre-initialized engine.
   248  func (ftc *fakeTabletConn) VStreamRows(ctx context.Context, request *binlogdatapb.VStreamRowsRequest, send func(*binlogdatapb.VStreamRowsResponse) error) error {
   249  	if vstreamRowsHook != nil {
   250  		vstreamRowsHook(ctx)
   251  	}
   252  	var row []sqltypes.Value
   253  	if request.Lastpk != nil {
   254  		r := sqltypes.Proto3ToResult(request.Lastpk)
   255  		if len(r.Rows) != 1 {
   256  			return fmt.Errorf("unexpected lastpk input: %v", request.Lastpk)
   257  		}
   258  		row = r.Rows[0]
   259  	}
   260  	return vdiffenv.vse.StreamRows(ctx, request.Query, row, func(rows *binlogdatapb.VStreamRowsResponse) error {
   261  		if vstreamRowsSendHook != nil {
   262  			vstreamRowsSendHook(ctx)
   263  		}
   264  		return send(rows)
   265  	})
   266  }
   267  
   268  func (ftc *fakeTabletConn) Close(ctx context.Context) error {
   269  	return nil
   270  }
   271  
   272  //--------------------------------------
   273  // Binlog Client to TabletManager
   274  
   275  // fakeBinlogClient satisfies binlogplayer.Client.
   276  // Not to be used concurrently.
   277  type fakeBinlogClient struct {
   278  	lastTablet   *topodatapb.Tablet
   279  	lastPos      string
   280  	lastTables   []string
   281  	lastKeyRange *topodatapb.KeyRange
   282  	lastCharset  *binlogdatapb.Charset
   283  }
   284  
   285  func (fbc *fakeBinlogClient) Dial(tablet *topodatapb.Tablet) error {
   286  	fbc.lastTablet = tablet
   287  	return nil
   288  }
   289  
   290  func (fbc *fakeBinlogClient) Close() {
   291  }
   292  
   293  func (fbc *fakeBinlogClient) StreamTables(ctx context.Context, position string, tables []string, charset *binlogdatapb.Charset) (binlogplayer.BinlogTransactionStream, error) {
   294  	fbc.lastPos = position
   295  	fbc.lastTables = tables
   296  	fbc.lastCharset = charset
   297  	return &btStream{ctx: ctx}, nil
   298  }
   299  
   300  func (fbc *fakeBinlogClient) StreamKeyRange(ctx context.Context, position string, keyRange *topodatapb.KeyRange, charset *binlogdatapb.Charset) (binlogplayer.BinlogTransactionStream, error) {
   301  	fbc.lastPos = position
   302  	fbc.lastKeyRange = keyRange
   303  	fbc.lastCharset = charset
   304  	return &btStream{ctx: ctx}, nil
   305  }
   306  
   307  // btStream satisfies binlogplayer.BinlogTransactionStream
   308  type btStream struct {
   309  	ctx  context.Context
   310  	sent bool
   311  }
   312  
   313  func (bts *btStream) Recv() (*binlogdatapb.BinlogTransaction, error) {
   314  	if !bts.sent {
   315  		bts.sent = true
   316  		return &binlogdatapb.BinlogTransaction{
   317  			Statements: []*binlogdatapb.BinlogTransaction_Statement{
   318  				{
   319  					Category: binlogdatapb.BinlogTransaction_Statement_BL_INSERT,
   320  					Sql:      []byte("insert into t values(1)"),
   321  				},
   322  			},
   323  			EventToken: &querypb.EventToken{
   324  				Timestamp: 72,
   325  				Position:  "MariaDB/0-1-1235",
   326  			},
   327  		}, nil
   328  	}
   329  	<-bts.ctx.Done()
   330  	return nil, bts.ctx.Err()
   331  }
   332  
   333  //--------------------------------------
   334  // DBCLient wrapper
   335  
   336  func realDBClientFactory() binlogplayer.DBClient {
   337  	return &realDBClient{}
   338  }
   339  
   340  type realDBClient struct {
   341  	conn  *mysql.Conn
   342  	nolog bool
   343  }
   344  
   345  func (dbc *realDBClient) DBName() string {
   346  	return vdiffDBName
   347  }
   348  
   349  func (dbc *realDBClient) Connect() error {
   350  	app, err := tstenv.Dbcfgs.AppWithDB().MysqlParams()
   351  	if err != nil {
   352  		return err
   353  	}
   354  	app.DbName = vdiffDBName
   355  	conn, err := mysql.Connect(context.Background(), app)
   356  	if err != nil {
   357  		return err
   358  	}
   359  	dbc.conn = conn
   360  	return nil
   361  }
   362  
   363  func (dbc *realDBClient) Begin() error {
   364  	_, err := dbc.ExecuteFetch("begin", 10000)
   365  	return err
   366  }
   367  
   368  func (dbc *realDBClient) Commit() error {
   369  	_, err := dbc.ExecuteFetch("commit", 10000)
   370  	return err
   371  }
   372  
   373  func (dbc *realDBClient) Rollback() error {
   374  	_, err := dbc.ExecuteFetch("rollback", 10000)
   375  	return err
   376  }
   377  
   378  func (dbc *realDBClient) Close() {
   379  	dbc.conn.Close()
   380  }
   381  
   382  func (dbc *realDBClient) ExecuteFetch(query string, maxrows int) (*sqltypes.Result, error) {
   383  	// Use Clone() because the contents of memory region referenced by
   384  	// string can change when clients (e.g. vcopier) use unsafe string methods.
   385  	query = strings.Clone(query)
   386  	qr, err := dbc.conn.ExecuteFetch(query, 10000, true)
   387  	if doNotLogDBQueries {
   388  		return qr, err
   389  	}
   390  	if !strings.HasPrefix(query, "select") && !strings.HasPrefix(query, "set") && !dbc.nolog {
   391  		globalDBQueries <- query
   392  	}
   393  	return qr, err
   394  }
   395  
   396  //----------------------------------------------
   397  // fakeTMClient
   398  
   399  type fakeTMClient struct {
   400  	tmclient.TabletManagerClient
   401  	schema    *tabletmanagerdatapb.SchemaDefinition
   402  	vrQueries map[int]map[string]*querypb.QueryResult
   403  	waitpos   map[int]string
   404  	vrpos     map[int]string
   405  	pos       map[int]string
   406  }
   407  
   408  func newFakeTMClient() *fakeTMClient {
   409  	return &fakeTMClient{
   410  		vrQueries: make(map[int]map[string]*querypb.QueryResult),
   411  		waitpos:   make(map[int]string),
   412  		vrpos:     make(map[int]string),
   413  		pos:       make(map[int]string),
   414  	}
   415  }
   416  
   417  func (tmc *fakeTMClient) GetSchema(ctx context.Context, tablet *topodatapb.Tablet, request *tabletmanagerdatapb.GetSchemaRequest) (*tabletmanagerdatapb.SchemaDefinition, error) {
   418  	return tmc.schema, nil
   419  }
   420  
   421  // setVRResults allows you to specify VReplicationExec queries and their results. You can specify
   422  // exact strings or strings prefixed with a '/', in which case they will be treated as a valid
   423  // regexp.
   424  func (tmc *fakeTMClient) setVRResults(tablet *topodatapb.Tablet, query string, result *sqltypes.Result) {
   425  	queries, ok := tmc.vrQueries[int(tablet.Alias.Uid)]
   426  	if !ok {
   427  		queries = make(map[string]*querypb.QueryResult)
   428  		tmc.vrQueries[int(tablet.Alias.Uid)] = queries
   429  	}
   430  	queries[query] = sqltypes.ResultToProto3(result)
   431  }
   432  
   433  func (tmc *fakeTMClient) VReplicationExec(ctx context.Context, tablet *topodatapb.Tablet, query string) (*querypb.QueryResult, error) {
   434  	if result, ok := tmc.vrQueries[int(tablet.Alias.Uid)][query]; ok {
   435  		return result, nil
   436  	}
   437  	for qry, res := range tmc.vrQueries[int(tablet.Alias.Uid)] {
   438  		if strings.HasPrefix(qry, "/") {
   439  			re := regexp.MustCompile(qry)
   440  			if re.MatchString(qry) {
   441  				return res, nil
   442  			}
   443  		}
   444  	}
   445  	return nil, fmt.Errorf("query %q not found for tablet %d", query, tablet.Alias.Uid)
   446  }
   447  
   448  func (tmc *fakeTMClient) WaitForPosition(ctx context.Context, tablet *topodatapb.Tablet, pos string) error {
   449  	select {
   450  	case <-ctx.Done():
   451  		return ctx.Err()
   452  	default:
   453  	}
   454  	if pos != tmc.waitpos[int(tablet.Alias.Uid)] {
   455  		return fmt.Errorf("waitpos %s not reached for tablet %d", pos, tablet.Alias.Uid)
   456  	}
   457  	return nil
   458  }
   459  
   460  func (tmc *fakeTMClient) VReplicationWaitForPos(ctx context.Context, tablet *topodatapb.Tablet, id int, pos string) error {
   461  	select {
   462  	case <-ctx.Done():
   463  		return ctx.Err()
   464  	default:
   465  	}
   466  	if pos != tmc.vrpos[int(tablet.Alias.Uid)] {
   467  		return fmt.Errorf("vrpos %s not reached for tablet %d", pos, tablet.Alias.Uid)
   468  	}
   469  	return nil
   470  }
   471  
   472  func (tmc *fakeTMClient) PrimaryPosition(ctx context.Context, tablet *topodatapb.Tablet) (string, error) {
   473  	pos, ok := tmc.pos[int(tablet.Alias.Uid)]
   474  	if !ok {
   475  		return "", fmt.Errorf("no primary position for %d", tablet.Alias.Uid)
   476  	}
   477  	return pos, nil
   478  }
   479  
   480  // ----------------------------------------------
   481  // testVDiffEnv
   482  
   483  func newTestVDiffEnv(t *testing.T) *testVDiffEnv {
   484  	vdiffenv = &testVDiffEnv{
   485  		workflow: "testwf",
   486  		tablets:  make(map[int]*fakeTabletConn),
   487  		tmc:      newFakeTMClient(),
   488  		se:       schema.NewEngineForTests(),
   489  		dbClient: binlogplayer.NewMockDBClient(t),
   490  	}
   491  	vdiffenv.dbClientFactory = func() binlogplayer.DBClient { return vdiffenv.dbClient }
   492  	vdiffenv.tmClientFactory = func() tmclient.TabletManagerClient { return vdiffenv.tmc }
   493  
   494  	tstenv.KeyspaceName = vdiffDBName
   495  
   496  	vdiffenv.vse = vstreamer.NewEngine(tstenv.TabletEnv, tstenv.SrvTopo, vdiffenv.se, nil, tstenv.Cells[0])
   497  	vdiffenv.vse.InitDBConfig(tstenv.KeyspaceName, tstenv.ShardName)
   498  	vdiffenv.vse.Open()
   499  
   500  	once.Do(func() {
   501  		var ddls []string
   502  
   503  		// This is needed for the vstreamer engine and the snapshotConn which
   504  		// use the real DB started by vttestserver
   505  		ddls = append(ddls, fmt.Sprintf("create database if not exists %s", vdiffDBName))
   506  		ddls = append(ddls, fmt.Sprintf("create table if not exists %s.t1 (c1 bigint primary key, c2 bigint)", vdiffDBName))
   507  
   508  		for _, ddl := range ddls {
   509  			if err := tstenv.Mysqld.ExecuteSuperQuery(context.Background(), ddl); err != nil {
   510  				fmt.Fprintf(os.Stderr, "%v", err)
   511  			}
   512  		}
   513  	})
   514  
   515  	vdiffenv.vre = vreplication.NewSimpleTestEngine(tstenv.TopoServ, tstenv.Cells[0], tstenv.Mysqld, realDBClientFactory, realDBClientFactory, vdiffDBName, nil)
   516  	vdiffenv.vre.Open(context.Background())
   517  
   518  	vdiffenv.tmc.schema = testSchema
   519  	// We need to add t1, which we use for a full VDiff in TestVDiff, to
   520  	// the schema engine with the PK val.
   521  	st := &schema.Table{
   522  		Name:      sqlparser.NewIdentifierCS(testSchema.TableDefinitions[tableDefMap["t1"]].Name),
   523  		Fields:    testSchema.TableDefinitions[tableDefMap["t1"]].Fields,
   524  		PKColumns: []int{0},
   525  	}
   526  	vdiffenv.se.SetTableForTests(st)
   527  
   528  	tabletID := 100
   529  	primary := vdiffenv.addTablet(tabletID, tstenv.KeyspaceName, tstenv.ShardName, topodatapb.TabletType_PRIMARY)
   530  
   531  	vdiffenv.vde = NewTestEngine(tstenv.TopoServ, primary.tablet, vdiffDBName, vdiffenv.dbClientFactory, vdiffenv.tmClientFactory)
   532  	require.False(t, vdiffenv.vde.IsOpen())
   533  
   534  	vdiffenv.opts = &tabletmanagerdatapb.VDiffOptions{
   535  		CoreOptions: &tabletmanagerdatapb.VDiffCoreOptions{},
   536  		ReportOptions: &tabletmanagerdatapb.VDiffReportOptions{
   537  			Format:     "json",
   538  			OnlyPks:    true,
   539  			DebugQuery: true,
   540  		},
   541  	}
   542  
   543  	// vdiff.syncTargets. This actually happens after stopTargets.
   544  	// But this is one statement per stream.
   545  	vdiffenv.tmc.setVRResults(
   546  		primary.tablet,
   547  		"/update _vt.vreplication set state='Running', stop_pos='MySQL56/.*', message='synchronizing for vdiff' where id=1",
   548  		noResults,
   549  	)
   550  
   551  	// vdiff.stopTargets
   552  	vdiffenv.tmc.setVRResults(primary.tablet, fmt.Sprintf("update _vt.vreplication set state='Stopped', message='for vdiff' where workflow = '%s' and db_name = '%s'", vdiffenv.workflow, vdiffDBName), singleRowAffected)
   553  
   554  	// vdiff.syncTargets (continued)
   555  	vdiffenv.tmc.vrpos[tabletID] = vdiffSourceGtid
   556  	vdiffenv.tmc.pos[tabletID] = vdiffTargetPrimaryPosition
   557  
   558  	// vdiff.startQueryStreams
   559  	vdiffenv.tmc.waitpos[tabletID] = vdiffTargetPrimaryPosition
   560  
   561  	// vdiff.restartTargets
   562  	vdiffenv.tmc.setVRResults(primary.tablet, fmt.Sprintf("update _vt.vreplication set state='Running', message='', stop_pos='' where db_name='%s' and workflow='%s'", vdiffDBName, vdiffenv.workflow), singleRowAffected)
   563  
   564  	vdiffenv.dbClient.ExpectRequest("select * from _vt.vdiff where state in ('started','pending')", noResults, nil)
   565  	vdiffenv.vde.Open(context.Background(), vdiffenv.vre)
   566  	assert.True(t, vdiffenv.vde.IsOpen())
   567  	assert.Equal(t, 0, len(vdiffenv.vde.controllers))
   568  
   569  	resetBinlogClient()
   570  
   571  	return vdiffenv
   572  }
   573  
   574  func (tvde *testVDiffEnv) close() {
   575  	tvde.mu.Lock()
   576  	defer tvde.mu.Unlock()
   577  	for _, t := range tvde.tablets {
   578  		tstenv.TopoServ.DeleteTablet(context.Background(), t.tablet.Alias)
   579  		topo.DeleteTabletReplicationData(context.Background(), tstenv.TopoServ, t.tablet)
   580  		tstenv.SchemaEngine.Reload(context.Background())
   581  	}
   582  	tvde.tablets = nil
   583  	vdiffenv.vse.Close()
   584  	vdiffenv.vre.Close()
   585  	vdiffenv.vde.Close()
   586  	vdiffenv.dbClient.Close()
   587  }
   588  
   589  func (tvde *testVDiffEnv) addTablet(id int, keyspace, shard string, tabletType topodatapb.TabletType) *fakeTabletConn {
   590  	tvde.mu.Lock()
   591  	defer tvde.mu.Unlock()
   592  	tablet := &topodatapb.Tablet{
   593  		Alias: &topodatapb.TabletAlias{
   594  			Cell: tstenv.Cells[0],
   595  			Uid:  uint32(id),
   596  		},
   597  		Keyspace: keyspace,
   598  		Shard:    shard,
   599  		KeyRange: &topodatapb.KeyRange{},
   600  		Type:     tabletType,
   601  		PortMap: map[string]int32{
   602  			"test": int32(id),
   603  		},
   604  	}
   605  	tvde.tablets[id] = &fakeTabletConn{tablet: tablet}
   606  	if err := tstenv.TopoServ.InitTablet(context.Background(), tablet, false /* allowPrimaryOverride */, true /* createShardAndKeyspace */, false /* allowUpdate */); err != nil {
   607  		panic(err)
   608  	}
   609  	if tabletType == topodatapb.TabletType_PRIMARY {
   610  		_, err := tstenv.TopoServ.UpdateShardFields(context.Background(), keyspace, shard, func(si *topo.ShardInfo) error {
   611  			si.PrimaryAlias = tablet.Alias
   612  			return nil
   613  		})
   614  		if err != nil {
   615  			panic(err)
   616  		}
   617  	}
   618  	tstenv.SchemaEngine.Reload(context.Background())
   619  	return tvde.tablets[id]
   620  }