vitess.io/vitess@v0.16.2/go/vt/vttablet/endtoend/framework/testcase.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 framework 18 19 import ( 20 "errors" 21 "fmt" 22 "reflect" 23 "strings" 24 25 "vitess.io/vitess/go/vt/vterrors" 26 27 "vitess.io/vitess/go/sqltypes" 28 querypb "vitess.io/vitess/go/vt/proto/query" 29 ) 30 31 // Testable restricts the types that can be added to 32 // a test case. 33 type Testable interface { 34 Test(name string, client *QueryClient) error 35 Benchmark(client *QueryClient) error 36 } 37 38 var ( 39 _ Testable = TestQuery("") 40 _ Testable = &TestCase{} 41 _ Testable = &MultiCase{} 42 ) 43 44 // TestQuery represents a plain query. It will be 45 // executed without a bind variable. The framework 46 // will check for errors, but nothing beyond. Statements 47 // like 'begin', etc. will be converted to the corresponding 48 // transaction commands. 49 type TestQuery string 50 51 // Test executes the query and returns an error if it failed. 52 func (tq TestQuery) Test(name string, client *QueryClient) error { 53 _, err := exec(client, string(tq), nil) 54 if err != nil { 55 if name == "" { 56 return err 57 } 58 return vterrors.Wrapf(err, "%s: Execute failed", name) 59 } 60 return nil 61 } 62 63 // Benchmark executes the query and discards the results 64 func (tq TestQuery) Benchmark(client *QueryClient) error { 65 _, err := exec(client, string(tq), nil) 66 return err 67 } 68 69 // TestCase represents one test case. It will execute the 70 // query and verify its results and effects against what 71 // must be expected. Expected fields are optional. 72 type TestCase struct { 73 // Name gives a name to the test case. It will be used 74 // for reporting failures. 75 Name string 76 77 // Query and BindVars are the input. 78 Query string 79 BindVars map[string]*querypb.BindVariable 80 81 // Result is the list of rows that must be returned. 82 // It's represented as 2-d strings. They byte values 83 // will be compared against The bytes returned by the 84 // query. The check is skipped if Result is nil. 85 Result [][]string 86 87 // RowsAffected affected can be nil or an int. 88 RowsAffected any 89 90 // RowsReturned affected can be nil or an int. 91 RowsReturned any 92 93 // Rewritten specifies how the query should have be rewritten. 94 Rewritten []string 95 96 // Plan specifies the plan type that was used. It will be matched 97 // against tabletserver.PlanType(val).String(). 98 Plan string 99 100 // If Table is specified, then the framework will validate the 101 // cache stats for that table. If the stat values are nil, then 102 // the check is skipped. 103 Table string 104 Hits any 105 Misses any 106 Absent any 107 Invalidations any 108 } 109 110 // Benchmark executes the test case and discards the results without verifying them 111 func (tc *TestCase) Benchmark(client *QueryClient) error { 112 _, err := exec(client, tc.Query, tc.BindVars) 113 return err 114 } 115 116 // Test executes the test case and returns an error if it failed. 117 // The name parameter is used if the test case doesn't have a name. 118 func (tc *TestCase) Test(name string, client *QueryClient) error { 119 var errs []string 120 if tc.Name != "" { 121 name = tc.Name 122 } 123 124 // wait for all previous test cases to have been settled in cache 125 client.server.QueryPlanCacheWait() 126 127 catcher := NewQueryCatcher() 128 defer catcher.Close() 129 130 qr, err := exec(client, tc.Query, tc.BindVars) 131 if err != nil { 132 return vterrors.Wrapf(err, "%s: Execute failed", name) 133 } 134 135 if tc.Result != nil { 136 result := RowsToStrings(qr) 137 if !reflect.DeepEqual(result, tc.Result) { 138 errs = append(errs, fmt.Sprintf("Result mismatch:\n'%+v' does not match\n'%+v'", result, tc.Result)) 139 } 140 } 141 142 if tc.RowsAffected != nil { 143 want := tc.RowsAffected.(int) 144 if int(qr.RowsAffected) != want { 145 errs = append(errs, fmt.Sprintf("RowsAffected mismatch: %d, want %d", int(qr.RowsAffected), want)) 146 } 147 } 148 149 if tc.RowsReturned != nil { 150 want := tc.RowsReturned.(int) 151 if len(qr.Rows) != want { 152 errs = append(errs, fmt.Sprintf("RowsReturned mismatch: %d, want %d", len(qr.Rows), want)) 153 } 154 } 155 156 queryInfo, err := catcher.Next() 157 if err != nil { 158 errs = append(errs, fmt.Sprintf("Query catcher failed: %v", err)) 159 } 160 if tc.Rewritten != nil { 161 // Work-around for a quirk. The stream comments also contain 162 // "; ". So, we have to get rid of the additional artifacts 163 // to make it match expected results. 164 unstripped := strings.Split(queryInfo.RewrittenSQL(), "; ") 165 got := make([]string, 0, len(unstripped)) 166 for _, str := range unstripped { 167 if str == "" || str == "*/" { 168 continue 169 } 170 got = append(got, str) 171 } 172 if !reflect.DeepEqual(got, tc.Rewritten) { 173 errs = append(errs, fmt.Sprintf("Rewritten mismatch:\n'%q' does not match\n'%q'", got, tc.Rewritten)) 174 } 175 } 176 if tc.Plan != "" { 177 if queryInfo.PlanType != tc.Plan { 178 errs = append(errs, fmt.Sprintf("Plan mismatch: %s, want %s", queryInfo.PlanType, tc.Plan)) 179 } 180 } 181 if len(errs) != 0 { 182 if name == "" { 183 return errors.New(strings.Join(errs, "\n")) 184 } 185 return errors.New(fmt.Sprintf("%s failed:\n", name) + strings.Join(errs, "\n")) 186 } 187 return nil 188 } 189 190 func exec(client *QueryClient, query string, bv map[string]*querypb.BindVariable) (*sqltypes.Result, error) { 191 switch query { 192 case "begin": 193 return nil, client.Begin(false) 194 case "commit": 195 return nil, client.Commit() 196 case "rollback": 197 return nil, client.Rollback() 198 } 199 return client.Execute(query, bv) 200 } 201 202 // RowsToStrings converts qr.Rows to [][]string. 203 func RowsToStrings(qr *sqltypes.Result) [][]string { 204 var result [][]string 205 for _, row := range qr.Rows { 206 var srow []string 207 for _, cell := range row { 208 srow = append(srow, cell.ToString()) 209 } 210 result = append(result, srow) 211 } 212 return result 213 } 214 215 // MultiCase groups a number of test cases under a name. 216 // A MultiCase is also Testable. So, it can be recursive. 217 type MultiCase struct { 218 Name string 219 Cases []Testable 220 } 221 222 // Test executes the test cases in MultiCase. The test is 223 // interrupted if there is a failure. The name parameter is 224 // used if MultiCase doesn't have a Name. 225 func (mc *MultiCase) Test(name string, client *QueryClient) error { 226 if mc.Name != "" { 227 name = mc.Name 228 } 229 for _, tcase := range mc.Cases { 230 if err := tcase.Test(name, client); err != nil { 231 client.Rollback() 232 return err 233 } 234 } 235 return nil 236 } 237 238 // Benchmark executes the test cases in MultiCase and discards the 239 // results without validating them. 240 func (mc *MultiCase) Benchmark(client *QueryClient) error { 241 for _, tcase := range mc.Cases { 242 if err := tcase.Benchmark(client); err != nil { 243 client.Rollback() 244 return err 245 } 246 } 247 return nil 248 }