github.com/ydb-platform/ydb-go-sdk/v3@v3.89.2/tests/integration/helpers_test.go (about) 1 //go:build integration 2 // +build integration 3 4 package integration 5 6 import ( 7 "bytes" 8 "context" 9 "database/sql" 10 "fmt" 11 "os" 12 "path" 13 "strings" 14 "testing" 15 "text/template" 16 "time" 17 18 "github.com/rekby/fixenv" 19 "github.com/stretchr/testify/require" 20 "google.golang.org/grpc" 21 22 "github.com/ydb-platform/ydb-go-sdk/v3" 23 "github.com/ydb-platform/ydb-go-sdk/v3/config" 24 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xsync" 25 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xtest" 26 "github.com/ydb-platform/ydb-go-sdk/v3/log" 27 "github.com/ydb-platform/ydb-go-sdk/v3/sugar" 28 "github.com/ydb-platform/ydb-go-sdk/v3/table" 29 "github.com/ydb-platform/ydb-go-sdk/v3/table/options" 30 "github.com/ydb-platform/ydb-go-sdk/v3/topic/topicoptions" 31 "github.com/ydb-platform/ydb-go-sdk/v3/topic/topicreader" 32 "github.com/ydb-platform/ydb-go-sdk/v3/topic/topictypes" 33 "github.com/ydb-platform/ydb-go-sdk/v3/topic/topicwriter" 34 "github.com/ydb-platform/ydb-go-sdk/v3/trace" 35 ) 36 37 type scopeT struct { 38 Ctx context.Context 39 fixenv.Env 40 Require *require.Assertions 41 t *xtest.SyncedTest 42 } 43 44 func newScope(t *testing.T) *scopeT { 45 st := xtest.MakeSyncedTest(t) 46 at := require.New(st) 47 fEnv := fixenv.New(st) 48 ctx, ctxCancel := context.WithCancel(context.Background()) 49 st.Cleanup(func() { 50 ctxCancel() 51 }) 52 res := &scopeT{ 53 Ctx: ctx, 54 Env: fEnv, 55 Require: at, 56 t: st, 57 } 58 return res 59 } 60 61 func (scope *scopeT) T() testing.TB { 62 return scope.t 63 } 64 65 func (scope *scopeT) Logf(format string, args ...interface{}) { 66 scope.t.Helper() 67 scope.t.Logf(format, args...) 68 } 69 70 func (scope *scopeT) Failed() bool { 71 return scope.t.Failed() 72 } 73 74 func (scope *scopeT) ConnectionString() string { 75 if envString := os.Getenv("YDB_CONNECTION_STRING"); envString != "" { 76 return envString 77 } 78 return "grpc://localhost:2136/local" 79 } 80 81 func (scope *scopeT) AuthToken() string { 82 return os.Getenv("YDB_ACCESS_TOKEN_CREDENTIALS") 83 } 84 85 func (scope *scopeT) Driver(opts ...ydb.Option) *ydb.Driver { 86 return scope.driverNamed("default", opts...) 87 } 88 89 func (scope *scopeT) DriverWithLogs(opts ...ydb.Option) *ydb.Driver { 90 return scope.driverNamed("logged", 91 append(opts, ydb.WithTraceQuery( 92 log.Query( 93 log.Default(os.Stdout, 94 log.WithLogQuery(), 95 log.WithMinLevel(log.INFO), 96 ), 97 trace.QueryEvents, 98 ), 99 ))..., 100 ) 101 } 102 103 func (scope *scopeT) DriverWithGRPCLogging() *ydb.Driver { 104 return scope.driverNamed("grpc-logged", ydb.With(config.WithGrpcOptions( 105 grpc.WithChainUnaryInterceptor(xtest.NewGrpcLogger(scope.t).UnaryClientInterceptor), 106 grpc.WithChainStreamInterceptor(xtest.NewGrpcLogger(scope.t).StreamClientInterceptor), 107 )), 108 ) 109 } 110 111 func (scope *scopeT) driverNamed(name string, opts ...ydb.Option) *ydb.Driver { 112 f := func() (*fixenv.GenericResult[*ydb.Driver], error) { 113 connectionString := scope.ConnectionString() 114 scope.Logf("Connect with connection string, driver name %q: %v", name, connectionString) 115 116 token := scope.AuthToken() 117 if token == "" { 118 scope.Logf("With empty auth token") 119 } else { 120 scope.Logf("With auth token") 121 } 122 123 connectionContext, cancel := context.WithTimeout(scope.Ctx, time.Second*10) 124 defer cancel() 125 126 driver, err := ydb.Open(connectionContext, connectionString, 127 append(opts, 128 ydb.WithAccessTokenCredentials(token), 129 )..., 130 ) 131 clean := func() { 132 if driver != nil { 133 scope.Require.NoError(driver.Close(scope.Ctx)) 134 } 135 } 136 137 return fixenv.NewGenericResultWithCleanup(driver, clean), err 138 } 139 140 return fixenv.CacheResult(scope.Env, f, fixenv.CacheOptions{CacheKey: name}) 141 } 142 143 func (scope *scopeT) SQLDriver(opts ...ydb.ConnectorOption) *sql.DB { 144 f := func() (*fixenv.GenericResult[*sql.DB], error) { 145 driver := scope.Driver() 146 scope.Logf("Create sql db connector") 147 connector, err := ydb.Connector(driver, opts...) 148 if err != nil { 149 return nil, err 150 } 151 152 db := sql.OpenDB(connector) 153 154 scope.Logf("Ping db") 155 err = db.PingContext(scope.Ctx) 156 if err != nil { 157 return nil, err 158 } 159 return fixenv.NewGenericResult(db), nil 160 } 161 return fixenv.CacheResult(scope.Env, f) 162 } 163 164 func (scope *scopeT) SQLDriverWithFolder(opts ...ydb.ConnectorOption) *sql.DB { 165 return scope.SQLDriver( 166 append([]ydb.ConnectorOption{ydb.WithTablePathPrefix(scope.Folder())}, opts...)..., 167 ) 168 } 169 170 func (scope *scopeT) Folder() string { 171 f := func() (*fixenv.GenericResult[string], error) { 172 driver := scope.Driver() 173 folderPath := path.Join(driver.Name(), scope.T().Name()) 174 scope.Require.NoError(sugar.RemoveRecursive(scope.Ctx, driver, folderPath)) 175 176 scope.Logf("Creating folder: %v", folderPath) 177 scope.Require.NoError(driver.Scheme().MakeDirectory(scope.Ctx, folderPath)) 178 clean := func() { 179 if !scope.Failed() { 180 scope.Require.NoError(sugar.RemoveRecursive(scope.Ctx, driver, folderPath)) 181 } 182 } 183 scope.Logf("Creating folder done: %v", folderPath) 184 return fixenv.NewGenericResultWithCleanup(folderPath, clean), nil 185 } 186 return fixenv.CacheResult(scope.Env, f) 187 } 188 189 func (scope *scopeT) Logger() *testLogger { 190 return scope.CacheResult(func() (*fixenv.Result, error) { 191 return fixenv.NewResult(newLogger(scope.t)), nil 192 }).(*testLogger) 193 } 194 195 func (scope *scopeT) LoggerMinLevel(level log.Level) *testLogger { 196 return scope.CacheResult(func() (res *fixenv.Result, err error) { 197 return fixenv.NewResult(newLoggerWithMinLevel(scope.t, level)), nil 198 }, fixenv.CacheOptions{CacheKey: level}).(*testLogger) 199 } 200 201 func (scope *scopeT) TopicConsumerName() string { 202 return "test-consumer" 203 } 204 205 func (scope *scopeT) TopicPath() string { 206 f := func() (*fixenv.GenericResult[string], error) { 207 topicName := strings.Replace(scope.T().Name(), "/", "__", -1) 208 topicPath := path.Join(scope.Folder(), topicName) 209 client := scope.Driver().Topic() 210 211 cleanup := func() { 212 if !scope.Failed() { 213 _ = client.Drop(scope.Ctx, topicPath) 214 } 215 } 216 cleanup() 217 218 scope.Logf("Drop topic if exists: %q", topicPath) 219 if err := client.Drop(scope.Ctx, topicPath); err != nil && !ydb.IsOperationErrorSchemeError(err) { 220 scope.t.Logf("failed drop previous topic %q: %v", topicPath, err) 221 } 222 223 scope.Logf("Creating topic %q", topicPath) 224 err := client.Create(scope.Ctx, topicPath, topicoptions.CreateWithConsumer( 225 topictypes.Consumer{ 226 Name: scope.TopicConsumerName(), 227 }, 228 )) 229 230 scope.Logf("Topic created: %q", topicPath) 231 232 return fixenv.NewGenericResultWithCleanup(topicPath, cleanup), err 233 } 234 return fixenv.CacheResult(scope.Env, f) 235 } 236 237 func (scope *scopeT) TopicReader() *topicreader.Reader { 238 return scope.TopicReaderNamed("default-reader") 239 } 240 241 func (scope *scopeT) TopicReaderNamed(name string) *topicreader.Reader { 242 f := func() (*fixenv.GenericResult[*topicreader.Reader], error) { 243 reader, err := scope.Driver().Topic().StartReader( 244 scope.TopicConsumerName(), 245 topicoptions.ReadTopic(scope.TopicPath()), 246 ) 247 cleanup := func() { 248 if reader != nil { 249 _ = reader.Close(scope.Ctx) 250 } 251 } 252 return fixenv.NewGenericResultWithCleanup(reader, cleanup), err 253 } 254 255 return fixenv.CacheResult(scope.Env, f, fixenv.CacheOptions{CacheKey: name}) 256 } 257 258 func (scope *scopeT) TopicWriter() *topicwriter.Writer { 259 f := func() (*fixenv.GenericResult[*topicwriter.Writer], error) { 260 writer, err := scope.Driver().Topic().StartWriter( 261 scope.TopicPath(), 262 topicoptions.WithWriterProducerID(scope.TopicWriterProducerID()), 263 topicoptions.WithWriterWaitServerAck(true), 264 ) 265 cleanup := func() { 266 if writer != nil { 267 _ = writer.Close(scope.Ctx) 268 } 269 } 270 return fixenv.NewGenericResultWithCleanup(writer, cleanup), err 271 } 272 273 return fixenv.CacheResult(scope.Env, f) 274 } 275 276 func (scope *scopeT) TopicWriterProducerID() string { 277 return "test-producer-id" 278 } 279 280 type tableNameParams struct { 281 tableName string 282 createTableQueryTemplate string 283 createTableOptions []options.CreateTableOption 284 } 285 286 func withTableName(tableName string) func(t *tableNameParams) { 287 return func(t *tableNameParams) { 288 t.tableName = tableName 289 } 290 } 291 292 func withCreateTableOptions(opts ...options.CreateTableOption) func(t *tableNameParams) { 293 return func(t *tableNameParams) { 294 t.createTableOptions = opts 295 } 296 } 297 298 func withCreateTableQueryTemplate(createTableQueryTemplate string) func(t *tableNameParams) { 299 return func(t *tableNameParams) { 300 t.createTableQueryTemplate = createTableQueryTemplate 301 } 302 } 303 304 // TableName return name (without path) to example table with struct: 305 // id Int64 NOT NULL, 306 // val Text 307 func (scope *scopeT) TableName(opts ...func(t *tableNameParams)) string { 308 params := tableNameParams{ 309 tableName: "table", 310 createTableQueryTemplate: ` 311 PRAGMA TablePathPrefix("{{.TablePathPrefix}}"); 312 CREATE TABLE {{.TableName}} ( 313 id Int64 NOT NULL, 314 val Text, 315 PRIMARY KEY (id) 316 ) 317 `, 318 } 319 for _, opt := range opts { 320 if opt != nil { 321 opt(¶ms) 322 } 323 } 324 325 f := func() (*fixenv.GenericResult[string], error) { 326 tablePath := path.Join(scope.Folder(), params.tableName) 327 328 // drop previous table if exists 329 err := scope.Driver().Table().Do(scope.Ctx, func(ctx context.Context, s table.Session) error { 330 return s.DropTable(ctx, tablePath) 331 }) 332 if err != nil && !ydb.IsOperationErrorSchemeError(err) { 333 return nil, fmt.Errorf("failed to drop previous table: %w", err) 334 } 335 336 createTableErr := scope.Driver().Table().Do(scope.Ctx, func(ctx context.Context, s table.Session) (err error) { 337 if len(params.createTableOptions) == 0 { 338 tmpl, err := template.New("").Parse(params.createTableQueryTemplate) 339 if err != nil { 340 return err 341 } 342 var query bytes.Buffer 343 err = tmpl.Execute(&query, struct { 344 TablePathPrefix string 345 TableName string 346 }{ 347 TablePathPrefix: scope.Folder(), 348 TableName: params.tableName, 349 }) 350 if err != nil { 351 return err 352 } 353 if err != nil { 354 panic(err) 355 } 356 return s.ExecuteSchemeQuery(ctx, query.String()) 357 } 358 359 return s.CreateTable(ctx, tablePath, params.createTableOptions...) 360 }) 361 362 if createTableErr != nil { 363 return nil, err 364 } 365 366 cleanup := func() { 367 // doesn't drop table after fail test - for debugging 368 if !scope.t.Failed() { 369 _ = scope.Driver().Table().Do(scope.Ctx, func(ctx context.Context, s table.Session) error { 370 return s.DropTable(ctx, tablePath) 371 }) 372 } 373 } 374 375 return fixenv.NewGenericResultWithCleanup(params.tableName, cleanup), nil 376 } 377 378 return fixenv.CacheResult(scope.Env, f, fixenv.CacheOptions{CacheKey: params.tableName}) 379 } 380 381 // TablePath return path to example table with struct: 382 // id Int64 NOT NULL, 383 // val Text 384 func (scope *scopeT) TablePath(opts ...func(t *tableNameParams)) string { 385 return path.Join(scope.Folder(), scope.TableName(opts...)) 386 } 387 388 // logger for tests 389 type testLogger struct { 390 test *xtest.SyncedTest 391 testName string 392 minLevel log.Level 393 394 m xsync.Mutex 395 closed bool 396 messages []string 397 } 398 399 func newLogger(t *xtest.SyncedTest) *testLogger { 400 return newLoggerWithMinLevel(t, 0) 401 } 402 403 func newLoggerWithMinLevel(t *xtest.SyncedTest, level log.Level) *testLogger { 404 logger := &testLogger{ 405 test: t, 406 testName: t.Name(), 407 minLevel: level, 408 } 409 t.Cleanup(logger.flush) 410 return logger 411 } 412 413 func (t *testLogger) Log(ctx context.Context, msg string, fields ...log.Field) { 414 t.test.Helper() 415 lvl := log.LevelFromContext(ctx) 416 if lvl < t.minLevel { 417 return 418 } 419 420 names := log.NamesFromContext(ctx) 421 422 loggerName := strings.Join(names, ".") 423 values := make(map[string]string) 424 for _, field := range fields { 425 values[field.Key()] = field.String() 426 } 427 timeString := time.Now().UTC().Format("15:04:05.999999999") // RFC3339Nano without date and timezone 428 message := fmt.Sprintf("%s: %s [%s] %s: %v (%v)", t.testName, timeString, lvl, loggerName, msg, values) 429 t.m.WithLock(func() { 430 if t.closed { 431 _, _ = fmt.Fprintf(os.Stderr, "\nFINISHED TEST %q:\n%s\n\n", t.testName, message) 432 } else { 433 t.messages = append(t.messages, message) 434 } 435 }) 436 } 437 438 func (t *testLogger) flush() { 439 t.m.WithLock(func() { 440 t.test.Helper() 441 t.closed = true 442 message := "\n" + strings.Join(t.messages, "\n") 443 t.test.Log(message) 444 }) 445 }