github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/dtestutils/sql_server_driver/server.go (about)

     1  // Copyright 2022 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package sql_server_driver
    16  
    17  import (
    18  	"io"
    19  	"os"
    20  	"path/filepath"
    21  	"strings"
    22  
    23  	"github.com/creasty/defaults"
    24  	"gopkg.in/yaml.v3"
    25  )
    26  
    27  // |Connection| represents a single connection to a sql-server instance defined
    28  // in the test. The connection will be established and every |Query| in
    29  // |Queries| will be run against it. At the end, the connection will be torn down.
    30  // If |RestartServer| is non-nil, the server which the connection targets will
    31  // be restarted after the connection is terminated.
    32  type Connection struct {
    33  	On            string       `yaml:"on"`
    34  	Queries       []Query      `yaml:"queries"`
    35  	RestartServer *RestartArgs `yaml:"restart_server"`
    36  
    37  	// Rarely needed, allows the entire connection assertion to be retried
    38  	// on an assertion failure. Use this is only for idempotent connection
    39  	// interactions and only if the sql-server is prone to tear down the
    40  	// connection based on things that are happening, such as cluster role
    41  	// transitions.
    42  	RetryAttempts int `yaml:"retry_attempts"`
    43  
    44  	// The user to connect as.
    45  	User string `default:"root" yaml:"user"`
    46  	// The password to connect with.
    47  	Pass     string `yaml:"password"`
    48  	PassFile string `yaml:"password_file"`
    49  	// Any driver params to pass in the DSN.
    50  	DriverParams map[string]string `yaml:"driver_params"`
    51  }
    52  
    53  func (c *Connection) UnmarshalYAML(unmarshal func(interface{}) error) error {
    54  	defaults.Set(c)
    55  	type plain Connection
    56  	if err := unmarshal((*plain)(c)); err != nil {
    57  		return err
    58  	}
    59  	return nil
    60  }
    61  
    62  func (c Connection) Password() (string, error) {
    63  	if c.PassFile != "" {
    64  		passFile := c.PassFile
    65  		if v := os.Getenv("TESTGENDIR"); v != "" {
    66  			passFile = strings.ReplaceAll(passFile, "$TESTGENDIR", v)
    67  		}
    68  		bs, err := os.ReadFile(passFile)
    69  		if err != nil {
    70  			return "", err
    71  		}
    72  		return strings.TrimSpace(string(bs)), nil
    73  	}
    74  	return c.Pass, nil
    75  }
    76  
    77  // |RestartArgs| are possible arguments, to change the arguments which are
    78  // provided to the sql-server process when it is restarted. This is used, for
    79  // example, to change server config on a restart.
    80  type RestartArgs struct {
    81  	Args *[]string `yaml:"args"`
    82  	Envs *[]string `yaml:"envs"`
    83  }
    84  
    85  // |TestRepo| represents an init'd dolt repository that is available to a
    86  // server instance. It can be created with some files and with remotes defined.
    87  // |Name| can include path components separated by `/`, which will create the
    88  // repository in a subdirectory.
    89  type TestRepo struct {
    90  	Name        string       `yaml:"name"`
    91  	WithFiles   []WithFile   `yaml:"with_files"`
    92  	WithRemotes []WithRemote `yaml:"with_remotes"`
    93  
    94  	// Only valid on Test.Repos, not in Test.MultiRepos.Repos. If set, a
    95  	// sql-server process will be run against this TestRepo. It will be
    96  	// available as TestRepo.Name.
    97  	Server         *Server         `yaml:"server"`
    98  	ExternalServer *ExternalServer `yaml:"external-server"`
    99  }
   100  
   101  // |MultiRepo| is a subdirectory where many |TestRepo|s can be defined. You can
   102  // start a sql-server on a |MultiRepo|, in which case there will be no default
   103  // database to connect to.
   104  type MultiRepo struct {
   105  	Name      string     `yaml:"name"`
   106  	Repos     []TestRepo `yaml:"repos"`
   107  	WithFiles []WithFile `yaml:"with_files"`
   108  
   109  	// If set, a sql-server process will be run against this TestRepo. It
   110  	// will be available as MultiRepo.Name.
   111  	Server *Server `yaml:"server"`
   112  }
   113  
   114  // |WithRemote| defines remotes which should be defined on the repository
   115  // before the sql-server is started.
   116  type WithRemote struct {
   117  	Name string `yaml:"name"`
   118  	URL  string `yaml:"url"`
   119  }
   120  
   121  // |WithFile| defines a file and its contents to be created in a |Repo| or
   122  // |MultiRepo| before the servers are started.
   123  type WithFile struct {
   124  	Name string `yaml:"name"`
   125  
   126  	// The contents of the file, provided inline in the YAML.
   127  	Contents string `yaml:"contents"`
   128  
   129  	// A source file path to copy to |Name|. Mutually exclusive with
   130  	// Contents.
   131  	SourcePath string `yaml:"source_path"`
   132  }
   133  
   134  func (f WithFile) WriteAtDir(dir string) error {
   135  	path := filepath.Join(dir, f.Name)
   136  	d := filepath.Dir(path)
   137  	err := os.MkdirAll(d, 0750)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	if f.SourcePath != "" {
   142  		sourcePath := f.SourcePath
   143  		if v := os.Getenv("TESTGENDIR"); v != "" {
   144  			sourcePath = strings.ReplaceAll(sourcePath, "$TESTGENDIR", v)
   145  		}
   146  		source, err := os.Open(sourcePath)
   147  		if err != nil {
   148  			return err
   149  		}
   150  		defer source.Close()
   151  		dest, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0550)
   152  		if err != nil {
   153  			return err
   154  		}
   155  		_, err = io.Copy(dest, source)
   156  		return err
   157  	} else {
   158  		return os.WriteFile(path, []byte(f.Contents), 0550)
   159  	}
   160  }
   161  
   162  // |Server| defines a sql-server process to start. |Name| must match the
   163  // top-level |Name| of a |TestRepo| or |MultiRepo|.
   164  type Server struct {
   165  	Name string   `yaml:"name"`
   166  	Args []string `yaml:"args"`
   167  	Envs []string `yaml:"envs"`
   168  
   169  	// The |Port| which the server will be running on. For now, it is up to
   170  	// the |Args| to make sure this is true. Defaults to 3308.
   171  	Port int `yaml:"port"`
   172  
   173  	// DebugPort if set to a non-zero value will cause this server to be started with |dlv| listening for a debugger
   174  	// connection on the port given.
   175  	DebugPort int `yaml:"debug_port"`
   176  
   177  	// Assertions to be run against the log output of the server process
   178  	// after the server process successfully terminates.
   179  	LogMatches []string `yaml:"log_matches"`
   180  
   181  	// Assertions to be run against the log output of the server process
   182  	// after the server process exits with an error. If |ErrorMatches| is
   183  	// defined, then the server process must exit with a non-0 exit code
   184  	// after it is launched. This will be asserted before any |Connections|
   185  	// interactions are performed.
   186  	ErrorMatches []string `yaml:"error_matches"`
   187  }
   188  
   189  type ExternalServer struct {
   190  	Name     string `yaml:"name"`
   191  	Host     string `yaml:"host"`
   192  	User     string `yaml:"user"`
   193  	Password string `yaml:"password"`
   194  	// The |Port| which the server will be running on. For now, it is up to
   195  	// the |Args| to make sure this is true. Defaults to 3308.
   196  	Port int `yaml:"port"`
   197  }
   198  
   199  // The primary interaction of a |Connection|. Either |Query| or |Exec| should
   200  // be set, not both.
   201  type Query struct {
   202  	// Run a query against the connection.
   203  	Query string `yaml:"query"`
   204  
   205  	// Run a command against the connection.
   206  	Exec string `yaml:"exec"`
   207  
   208  	// Args to be passed as query parameters to either Query or Exec.
   209  	Args []string `yaml:"args"`
   210  
   211  	// This can only be non-empty for a |Query|. Asserts the results of the
   212  	// |Query|.
   213  	Result QueryResult `yaml:"result"`
   214  
   215  	// If this is non-empty, asserts the |Query| or the |Exec|
   216  	// generates an error that matches this string.
   217  	ErrorMatch string `yaml:"error_match"`
   218  
   219  	// If this is non-zero, it represents the number of times to try the
   220  	// |Query| or the |Exec| and to check its assertions before we fail the
   221  	// test as a result of failed assertions. When interacting with queries
   222  	// that introspect things like replication state, this can be used to
   223  	// wait for quiescence in an inherently racey process. Interactions
   224  	// will be delayed slightly between each failure.
   225  	RetryAttempts int `yaml:"retry_attempts"`
   226  }
   227  
   228  // |QueryResult| specifies assertions on the results of a |Query|. Columns must
   229  // be specified for a |Query| and the query results must fully match. If Rows
   230  // are omitted, anything is allowed as long as all rows are read successfully.
   231  // All assertions here are string equality.
   232  type QueryResult struct {
   233  	Columns []string   `yaml:"columns"`
   234  	Rows    ResultRows `yaml:"rows"`
   235  }
   236  
   237  type ResultRows struct {
   238  	Or *[][][]string
   239  }
   240  
   241  func (r *ResultRows) UnmarshalYAML(value *yaml.Node) error {
   242  	if value.Kind == yaml.SequenceNode {
   243  		res := make([][][]string, 1)
   244  		r.Or = &res
   245  		return value.Decode(&(*r.Or)[0])
   246  	}
   247  	var or struct {
   248  		Or *[][][]string `yaml:"or"`
   249  	}
   250  	err := value.Decode(&or)
   251  	if err != nil {
   252  		return err
   253  	}
   254  	r.Or = or.Or
   255  	return nil
   256  }