vitess.io/vitess@v0.16.2/go/test/endtoend/utils/utils.go (about)

     1  /*
     2  Copyright 2021 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 utils
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"vitess.io/vitess/go/test/endtoend/cluster"
    26  
    27  	"vitess.io/vitess/go/test/utils"
    28  
    29  	"github.com/google/go-cmp/cmp"
    30  	"github.com/stretchr/testify/assert"
    31  	"github.com/stretchr/testify/require"
    32  
    33  	"vitess.io/vitess/go/mysql"
    34  	"vitess.io/vitess/go/sqltypes"
    35  )
    36  
    37  // AssertContains ensures the given query result contains the expected results.
    38  func AssertContains(t testing.TB, conn *mysql.Conn, query, expected string) {
    39  	t.Helper()
    40  	qr := Exec(t, conn, query)
    41  	got := fmt.Sprintf("%v", qr.Rows)
    42  	assert.Contains(t, got, expected, "Query: %s", query)
    43  }
    44  
    45  // AssertMatches ensures the given query produces the expected results.
    46  func AssertMatches(t testing.TB, conn *mysql.Conn, query, expected string) {
    47  	t.Helper()
    48  	qr := Exec(t, conn, query)
    49  	got := fmt.Sprintf("%v", qr.Rows)
    50  	diff := cmp.Diff(expected, got)
    51  	if diff != "" {
    52  		t.Errorf("Query: %s (-want +got):\n%s\nGot:%s", query, diff, got)
    53  	}
    54  }
    55  
    56  // AssertMatchesContains ensures the given query produces the given substring.
    57  func AssertMatchesContains(t testing.TB, conn *mysql.Conn, query string, substrings ...string) {
    58  	t.Helper()
    59  	qr := Exec(t, conn, query)
    60  	got := fmt.Sprintf("%v", qr.Rows)
    61  	for _, substring := range substrings {
    62  		if !strings.Contains(got, substring) {
    63  			t.Errorf("Query: %s Got:\n%s\nLooking for substring:%s", query, got, substring)
    64  		}
    65  	}
    66  }
    67  
    68  // AssertMatchesNotContains ensures the given query's output doesn't have the given substring.
    69  func AssertMatchesNotContains(t testing.TB, conn *mysql.Conn, query string, substrings ...string) {
    70  	t.Helper()
    71  	qr := Exec(t, conn, query)
    72  	got := fmt.Sprintf("%v", qr.Rows)
    73  	for _, substring := range substrings {
    74  		if strings.Contains(got, substring) {
    75  			t.Errorf("Query: %s Got:\n%s\nFound substring:%s", query, got, substring)
    76  		}
    77  	}
    78  }
    79  
    80  // AssertMatchesAny ensures the given query produces any one of the expected results.
    81  func AssertMatchesAny(t testing.TB, conn *mysql.Conn, query string, expected ...string) {
    82  	t.Helper()
    83  	qr := Exec(t, conn, query)
    84  	got := fmt.Sprintf("%v", qr.Rows)
    85  	for _, e := range expected {
    86  		diff := cmp.Diff(e, got)
    87  		if diff == "" {
    88  			return
    89  		}
    90  	}
    91  	t.Errorf("Query: %s (-want +got):\n%v\nGot:%s", query, expected, got)
    92  }
    93  
    94  // AssertMatchesCompareMySQL executes the given query on both Vitess and MySQL and make sure
    95  // they have the same result set. The result set of Vitess is then matched with the given expectation.
    96  func AssertMatchesCompareMySQL(t *testing.T, vtConn, mysqlConn *mysql.Conn, query, expected string) {
    97  	t.Helper()
    98  	qr := ExecCompareMySQL(t, vtConn, mysqlConn, query)
    99  	got := fmt.Sprintf("%v", qr.Rows)
   100  	diff := cmp.Diff(expected, got)
   101  	if diff != "" {
   102  		t.Errorf("Query: %s (-want +got):\n%s\nGot:%s", query, diff, got)
   103  	}
   104  }
   105  
   106  // AssertContainsError ensures that the given query returns a certain error.
   107  func AssertContainsError(t *testing.T, conn *mysql.Conn, query, expected string) {
   108  	t.Helper()
   109  	_, err := ExecAllowError(t, conn, query)
   110  	require.Error(t, err)
   111  	assert.Contains(t, err.Error(), expected, "actual error: %s", err.Error())
   112  }
   113  
   114  // AssertMatchesNoOrder executes the given query and makes sure it matches the given `expected` string.
   115  // The order applied to the results or expectation is ignored. They are both re-sorted.
   116  func AssertMatchesNoOrder(t *testing.T, conn *mysql.Conn, query, expected string) {
   117  	t.Helper()
   118  	qr := Exec(t, conn, query)
   119  	actual := fmt.Sprintf("%v", qr.Rows)
   120  	assert.Equal(t, utils.SortString(expected), utils.SortString(actual), "for query: [%s] expected \n%s \nbut actual \n%s", query, expected, actual)
   121  }
   122  
   123  // AssertIsEmpty ensures that the given query returns 0 row.
   124  func AssertIsEmpty(t *testing.T, conn *mysql.Conn, query string) {
   125  	t.Helper()
   126  	qr := Exec(t, conn, query)
   127  	assert.Empty(t, qr.Rows, "for query: "+query)
   128  }
   129  
   130  func AssertSingleRowIsReturned(t *testing.T, conn *mysql.Conn, predicate string, expectedKs string) {
   131  	t.Run(predicate, func(t *testing.T) {
   132  		qr, err := conn.ExecuteFetch("SELECT distinct table_schema FROM information_schema.tables WHERE "+predicate, 1000, true)
   133  		require.NoError(t, err)
   134  		assert.Equal(t, 1, len(qr.Rows), "did not get enough rows back")
   135  		assert.Equal(t, expectedKs, qr.Rows[0][0].ToString())
   136  	})
   137  }
   138  
   139  func AssertResultIsEmpty(t *testing.T, conn *mysql.Conn, pre string) {
   140  	t.Run(pre, func(t *testing.T) {
   141  		qr, err := conn.ExecuteFetch("SELECT distinct table_schema FROM information_schema.tables WHERE "+pre, 1000, true)
   142  		require.NoError(t, err)
   143  		assert.Empty(t, qr.Rows)
   144  	})
   145  }
   146  
   147  // Exec executes the given query using the given connection. The results are returned.
   148  // The test fails if the query produces an error.
   149  func Exec(t testing.TB, conn *mysql.Conn, query string) *sqltypes.Result {
   150  	t.Helper()
   151  	qr, err := conn.ExecuteFetch(query, 1000, true)
   152  	require.NoError(t, err, "for query: "+query)
   153  	return qr
   154  }
   155  
   156  // ExecCompareMySQL executes the given query against both Vitess and MySQL and compares
   157  // the two result set. If there is a mismatch, the difference will be printed and the
   158  // test will fail. If the query produces an error in either Vitess or MySQL, the test
   159  // will be marked as failed.
   160  // The result set of Vitess is returned to the caller.
   161  func ExecCompareMySQL(t *testing.T, vtConn, mysqlConn *mysql.Conn, query string) *sqltypes.Result {
   162  	t.Helper()
   163  	vtQr, err := vtConn.ExecuteFetch(query, 1000, true)
   164  	require.NoError(t, err, "[Vitess Error] for query: "+query)
   165  
   166  	mysqlQr, err := mysqlConn.ExecuteFetch(query, 1000, true)
   167  	require.NoError(t, err, "[MySQL Error] for query: "+query)
   168  	compareVitessAndMySQLResults(t, query, vtQr, mysqlQr, false)
   169  	return vtQr
   170  }
   171  
   172  // ExecAllowError executes the given query without failing the test if it produces
   173  // an error. The error is returned to the client, along with the result set.
   174  func ExecAllowError(t *testing.T, conn *mysql.Conn, query string) (*sqltypes.Result, error) {
   175  	t.Helper()
   176  	return conn.ExecuteFetch(query, 1000, true)
   177  }
   178  
   179  // SkipIfBinaryIsBelowVersion skips the given test if the binary's major version is below majorVersion.
   180  func SkipIfBinaryIsBelowVersion(t *testing.T, majorVersion int, binary string) {
   181  	version, err := cluster.GetMajorVersion(binary)
   182  	if err != nil {
   183  		return
   184  	}
   185  	if version < majorVersion {
   186  		t.Skip("Current version of ", binary, ": v", version, ", expected version >= v", majorVersion)
   187  	}
   188  }
   189  
   190  // AssertMatchesWithTimeout asserts that the given query produces the expected result.
   191  // The query will be executed every 'r' duration until it matches the expected result.
   192  // If after 'd' duration we still did not find the expected result, the test will be marked as failed.
   193  func AssertMatchesWithTimeout(t *testing.T, conn *mysql.Conn, query, expected string, r time.Duration, d time.Duration, failureMsg string) {
   194  	t.Helper()
   195  	timeout := time.After(d)
   196  	diff := "actual and expectation does not match"
   197  	for len(diff) > 0 {
   198  		select {
   199  		case <-timeout:
   200  			require.Fail(t, failureMsg, diff)
   201  		case <-time.After(r):
   202  			qr, err := ExecAllowError(t, conn, query)
   203  			if err != nil {
   204  				diff = err.Error()
   205  				break
   206  			}
   207  			diff = cmp.Diff(expected,
   208  				fmt.Sprintf("%v", qr.Rows))
   209  		}
   210  
   211  	}
   212  }
   213  
   214  // WaitForAuthoritative waits for a table to become authoritative
   215  func WaitForAuthoritative(t *testing.T, vtgateProcess cluster.VtgateProcess, ks, tbl string) error {
   216  	timeout := time.After(10 * time.Second)
   217  	for {
   218  		select {
   219  		case <-timeout:
   220  			return fmt.Errorf("schema tracking didn't mark table t2 as authoritative until timeout")
   221  		default:
   222  			time.Sleep(1 * time.Second)
   223  			res, err := vtgateProcess.ReadVSchema()
   224  			require.NoError(t, err, res)
   225  			t2Map := getTableT2Map(res, ks, tbl)
   226  			authoritative, fieldPresent := t2Map["column_list_authoritative"]
   227  			if !fieldPresent {
   228  				continue
   229  			}
   230  			authoritativeBool, isBool := authoritative.(bool)
   231  			if !isBool || !authoritativeBool {
   232  				continue
   233  			}
   234  			return nil
   235  		}
   236  	}
   237  }
   238  
   239  // WaitForColumn waits for a table's column to be present
   240  func WaitForColumn(t *testing.T, vtgateProcess cluster.VtgateProcess, ks, tbl, col string) error {
   241  	timeout := time.After(10 * time.Second)
   242  	for {
   243  		select {
   244  		case <-timeout:
   245  			return fmt.Errorf("schema tracking did not find column '%s' in table '%s'", col, tbl)
   246  		default:
   247  			time.Sleep(1 * time.Second)
   248  			res, err := vtgateProcess.ReadVSchema()
   249  			require.NoError(t, err, res)
   250  			t2Map := getTableT2Map(res, ks, tbl)
   251  			authoritative, fieldPresent := t2Map["column_list_authoritative"]
   252  			if !fieldPresent {
   253  				break
   254  			}
   255  			authoritativeBool, isBool := authoritative.(bool)
   256  			if !isBool || !authoritativeBool {
   257  				break
   258  			}
   259  			colMap, exists := t2Map["columns"]
   260  			if !exists {
   261  				break
   262  			}
   263  			colList, isSlice := colMap.([]interface{})
   264  			if !isSlice {
   265  				break
   266  			}
   267  			for _, c := range colList {
   268  				colDef, isMap := c.(map[string]interface{})
   269  				if !isMap {
   270  					break
   271  				}
   272  				if colName, exists := colDef["name"]; exists && colName == col {
   273  					return nil
   274  				}
   275  			}
   276  		}
   277  	}
   278  }
   279  
   280  func getTableT2Map(res *interface{}, ks, tbl string) map[string]interface{} {
   281  	step1 := convertToMap(*res)["keyspaces"]
   282  	step2 := convertToMap(step1)[ks]
   283  	step3 := convertToMap(step2)["tables"]
   284  	tblMap := convertToMap(step3)[tbl]
   285  	return convertToMap(tblMap)
   286  }
   287  
   288  func convertToMap(input interface{}) map[string]interface{} {
   289  	output := input.(map[string]interface{})
   290  	return output
   291  }
   292  
   293  // TimeoutAction performs the action within the given timeout limit.
   294  // If the timeout is reached, the test is failed with errMsg.
   295  // If action returns false, the timeout loop continues, if it returns true, the function succeeds.
   296  func TimeoutAction(t *testing.T, timeout time.Duration, errMsg string, action func() bool) {
   297  	deadline := time.After(timeout)
   298  	ok := false
   299  	for !ok {
   300  		select {
   301  		case <-deadline:
   302  			t.Error(errMsg)
   303  		case <-time.After(1 * time.Second):
   304  			ok = action()
   305  		}
   306  	}
   307  }