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