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(&params)
   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  }