github.com/ydb-platform/ydb-go-sdk/v3@v3.57.0/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 21 "github.com/ydb-platform/ydb-go-sdk/v3" 22 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xsync" 23 "github.com/ydb-platform/ydb-go-sdk/v3/log" 24 "github.com/ydb-platform/ydb-go-sdk/v3/sugar" 25 "github.com/ydb-platform/ydb-go-sdk/v3/table" 26 "github.com/ydb-platform/ydb-go-sdk/v3/table/options" 27 "github.com/ydb-platform/ydb-go-sdk/v3/topic/topicoptions" 28 "github.com/ydb-platform/ydb-go-sdk/v3/topic/topicreader" 29 "github.com/ydb-platform/ydb-go-sdk/v3/topic/topictypes" 30 "github.com/ydb-platform/ydb-go-sdk/v3/topic/topicwriter" 31 "github.com/ydb-platform/ydb-go-sdk/v3/trace" 32 ) 33 34 type scopeT struct { 35 Ctx context.Context 36 fixenv.Env 37 Require *require.Assertions 38 t testing.TB 39 } 40 41 func newScope(t *testing.T) *scopeT { 42 at := require.New(t) 43 fEnv := fixenv.NewEnv(t) 44 ctx, ctxCancel := context.WithCancel(context.Background()) 45 t.Cleanup(func() { 46 ctxCancel() 47 }) 48 res := &scopeT{ 49 Ctx: ctx, 50 Env: fEnv, 51 Require: at, 52 t: t, 53 } 54 return res 55 } 56 57 func (scope *scopeT) T() testing.TB { 58 return scope.t 59 } 60 61 func (scope *scopeT) Logf(format string, args ...interface{}) { 62 scope.t.Helper() 63 scope.t.Logf(format, args...) 64 } 65 66 func (scope *scopeT) Failed() bool { 67 return scope.t.Failed() 68 } 69 70 func (scope *scopeT) ConnectionString() string { 71 if envString := os.Getenv("YDB_CONNECTION_STRING"); envString != "" { 72 return envString 73 } 74 return "grpc://localhost:2136/local" 75 } 76 77 func (scope *scopeT) AuthToken() string { 78 return os.Getenv("YDB_ACCESS_TOKEN_CREDENTIALS") 79 } 80 81 func (scope *scopeT) Driver(opts ...ydb.Option) *ydb.Driver { 82 f := func() (*fixenv.GenericResult[*ydb.Driver], error) { 83 connectionString := scope.ConnectionString() 84 scope.Logf("Connect with connection string: %v", connectionString) 85 86 token := scope.AuthToken() 87 if token == "" { 88 scope.Logf("With empty auth token") 89 } else { 90 scope.Logf("With auth token") 91 } 92 93 connectionContext, cancel := context.WithTimeout(scope.Ctx, time.Second*10) 94 defer cancel() 95 96 driver, err := ydb.Open(connectionContext, connectionString, 97 append(opts, 98 ydb.WithAccessTokenCredentials(token), 99 ydb.WithLogger( 100 scope.LoggerMinLevel(log.WARN), 101 trace.DetailsAll, 102 ), 103 )..., 104 ) 105 clean := func() { 106 scope.Require.NoError(driver.Close(scope.Ctx)) 107 } 108 109 return fixenv.NewGenericResultWithCleanup(driver, clean), err 110 } 111 112 return fixenv.CacheResult(scope.Env, f) 113 } 114 115 func (scope *scopeT) SQLDriver(opts ...ydb.ConnectorOption) *sql.DB { 116 return scope.Cache(nil, nil, func() (res interface{}, err error) { 117 driver := scope.Driver() 118 scope.Logf("Create sql db connector") 119 connector, err := ydb.Connector(driver, opts...) 120 if err != nil { 121 return nil, err 122 } 123 124 db := sql.OpenDB(connector) 125 126 scope.Logf("Ping db") 127 err = db.PingContext(scope.Ctx) 128 if err != nil { 129 return nil, err 130 } 131 return db, nil 132 }).(*sql.DB) 133 } 134 135 func (scope *scopeT) SQLDriverWithFolder(opts ...ydb.ConnectorOption) *sql.DB { 136 return scope.SQLDriver( 137 append([]ydb.ConnectorOption{ydb.WithTablePathPrefix(scope.Folder())}, opts...)..., 138 ) 139 } 140 141 func (scope *scopeT) Folder() string { 142 f := func() (*fixenv.GenericResult[string], error) { 143 driver := scope.Driver() 144 folderPath := path.Join(driver.Name(), scope.T().Name()) 145 scope.Require.NoError(sugar.RemoveRecursive(scope.Ctx, driver, folderPath)) 146 147 scope.Logf("Create folder: %v", folderPath) 148 scope.Require.NoError(driver.Scheme().MakeDirectory(scope.Ctx, folderPath)) 149 clean := func() { 150 if !scope.Failed() { 151 scope.Require.NoError(sugar.RemoveRecursive(scope.Ctx, driver, folderPath)) 152 } 153 } 154 return fixenv.NewGenericResultWithCleanup(folderPath, clean), nil 155 } 156 return fixenv.CacheResult(scope.Env, f) 157 } 158 159 func (scope *scopeT) Logger() *testLogger { 160 return scope.Cache(nil, nil, func() (res interface{}, err error) { 161 return newLogger(scope.t), nil 162 }).(*testLogger) 163 } 164 165 func (scope *scopeT) LoggerMinLevel(level log.Level) *testLogger { 166 return scope.Cache(level, nil, func() (res interface{}, err error) { 167 return newLoggerWithMinLevel(scope.t, level), nil 168 }).(*testLogger) 169 } 170 171 func (scope *scopeT) TopicConsumerName() string { 172 return "test-consumer" 173 } 174 175 func (scope *scopeT) TopicPath() string { 176 f := func() (*fixenv.GenericResult[string], error) { 177 topicName := strings.Replace(scope.T().Name(), "/", "__", -1) 178 topicPath := path.Join(scope.Folder(), topicName) 179 client := scope.Driver().Topic() 180 181 cleanup := func() { 182 if !scope.Failed() { 183 _ = client.Drop(scope.Ctx, topicPath) 184 } 185 } 186 cleanup() 187 188 err := client.Create(scope.Ctx, topicPath, topicoptions.CreateWithConsumer( 189 topictypes.Consumer{ 190 Name: scope.TopicConsumerName(), 191 }, 192 )) 193 194 return fixenv.NewGenericResultWithCleanup(topicPath, cleanup), err 195 } 196 return fixenv.CacheResult(scope.Env, f) 197 } 198 199 func (scope *scopeT) TopicReader() *topicreader.Reader { 200 f := func() (*fixenv.GenericResult[*topicreader.Reader], error) { 201 reader, err := scope.Driver().Topic().StartReader( 202 scope.TopicConsumerName(), 203 topicoptions.ReadTopic(scope.TopicPath()), 204 ) 205 cleanup := func() { 206 if reader != nil { 207 _ = reader.Close(scope.Ctx) 208 } 209 } 210 return fixenv.NewGenericResultWithCleanup(reader, cleanup), err 211 } 212 213 return fixenv.CacheResult(scope.Env, f) 214 } 215 216 func (scope *scopeT) TopicWriter() *topicwriter.Writer { 217 f := func() (*fixenv.GenericResult[*topicwriter.Writer], error) { 218 writer, err := scope.Driver().Topic().StartWriter( 219 scope.TopicPath(), 220 topicoptions.WithWriterProducerID(scope.TopicWriterProducerID()), 221 topicoptions.WithWriterWaitServerAck(true), 222 ) 223 cleanup := func() { 224 if writer != nil { 225 _ = writer.Close(scope.Ctx) 226 } 227 } 228 return fixenv.NewGenericResultWithCleanup(writer, cleanup), err 229 } 230 231 return fixenv.CacheResult(scope.Env, f) 232 } 233 234 func (scope *scopeT) TopicWriterProducerID() string { 235 return "test-producer-id" 236 } 237 238 type tableNameParams struct { 239 tableName string 240 createTableQueryTemplate string 241 createTableOptions []options.CreateTableOption 242 } 243 244 func withTableName(tableName string) func(t *tableNameParams) { 245 return func(t *tableNameParams) { 246 t.tableName = tableName 247 } 248 } 249 250 func withCreateTableOptions(opts ...options.CreateTableOption) func(t *tableNameParams) { 251 return func(t *tableNameParams) { 252 t.createTableOptions = opts 253 } 254 } 255 256 func withCreateTableQueryTemplate(createTableQueryTemplate string) func(t *tableNameParams) { 257 return func(t *tableNameParams) { 258 t.createTableQueryTemplate = createTableQueryTemplate 259 } 260 } 261 262 // TableName return name (without path) to example table with struct: 263 // id Int64 NOT NULL, 264 // val Text 265 func (scope *scopeT) TableName(opts ...func(t *tableNameParams)) string { 266 params := tableNameParams{ 267 tableName: "table", 268 createTableQueryTemplate: ` 269 PRAGMA TablePathPrefix("{{.TablePathPrefix}}"); 270 CREATE TABLE {{.TableName}} ( 271 id Int64 NOT NULL, 272 val Text, 273 PRIMARY KEY (id) 274 ) 275 `, 276 } 277 for _, opt := range opts { 278 opt(¶ms) 279 } 280 return scope.Cache(params.tableName, nil, func() (res interface{}, err error) { 281 err = scope.Driver().Table().Do(scope.Ctx, func(ctx context.Context, s table.Session) (err error) { 282 if len(params.createTableOptions) == 0 { 283 tmpl, err := template.New("").Parse(params.createTableQueryTemplate) 284 if err != nil { 285 return err 286 } 287 var query bytes.Buffer 288 err = tmpl.Execute(&query, struct { 289 TablePathPrefix string 290 TableName string 291 }{ 292 TablePathPrefix: scope.Folder(), 293 TableName: params.tableName, 294 }) 295 if err != nil { 296 return err 297 } 298 if err != nil { 299 panic(err) 300 } 301 return s.ExecuteSchemeQuery(ctx, query.String()) 302 } 303 return s.CreateTable(ctx, path.Join(scope.Folder(), params.tableName), params.createTableOptions...) 304 }) 305 return params.tableName, err 306 }).(string) 307 } 308 309 // TablePath return path to example table with struct: 310 // id Int64 NOT NULL, 311 // val Text 312 func (scope *scopeT) TablePath(opts ...func(t *tableNameParams)) string { 313 return path.Join(scope.Folder(), scope.TableName(opts...)) 314 } 315 316 // logger for tests 317 type testLogger struct { 318 test testing.TB 319 testName string 320 minLevel log.Level 321 322 m xsync.Mutex 323 closed bool 324 messages []string 325 } 326 327 func newLogger(t testing.TB) *testLogger { 328 return newLoggerWithMinLevel(t, 0) 329 } 330 331 func newLoggerWithMinLevel(t testing.TB, level log.Level) *testLogger { 332 logger := &testLogger{ 333 test: t, 334 testName: t.Name(), 335 minLevel: level, 336 } 337 t.Cleanup(logger.flush) 338 return logger 339 } 340 341 func (t *testLogger) Log(ctx context.Context, msg string, fields ...log.Field) { 342 t.test.Helper() 343 lvl := log.LevelFromContext(ctx) 344 if lvl < t.minLevel { 345 return 346 } 347 348 names := log.NamesFromContext(ctx) 349 350 loggerName := strings.Join(names, ".") 351 values := make(map[string]string) 352 for _, field := range fields { 353 values[field.Key()] = field.String() 354 } 355 timeString := time.Now().UTC().Format("15:04:05.999999999") // RFC3339Nano without date and timezone 356 message := fmt.Sprintf("%s: %s [%s] %s: %v (%v)", t.testName, timeString, lvl, loggerName, msg, values) 357 t.m.WithLock(func() { 358 if t.closed { 359 _, _ = fmt.Fprintf(os.Stderr, "\nFINISHED TEST %q:\n%s\n\n", t.testName, message) 360 } else { 361 t.messages = append(t.messages, message) 362 } 363 }) 364 } 365 366 func (t *testLogger) flush() { 367 t.m.WithLock(func() { 368 t.closed = true 369 message := "\n" + strings.Join(t.messages, "\n") 370 t.test.Log(message) 371 }) 372 }