github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/cmd/util/helper_test.go (about)

     1  // Copyright 2021 PingCAP, 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  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package util
    15  
    16  import (
    17  	"bytes"
    18  	"fmt"
    19  	"net/url"
    20  	"os"
    21  	"path/filepath"
    22  	"syscall"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/pingcap/tiflow/pkg/config"
    27  	"github.com/pingcap/tiflow/pkg/util"
    28  	"github.com/spf13/cobra"
    29  	"github.com/stretchr/testify/require"
    30  )
    31  
    32  func TestProxyFields(t *testing.T) {
    33  	revIndex := map[string]int{
    34  		"http_proxy":  0,
    35  		"https_proxy": 1,
    36  		"no_proxy":    2,
    37  	}
    38  	envs := []string{"http_proxy", "https_proxy", "no_proxy"}
    39  	envPreset := []string{"http://127.0.0.1:8080", "https://127.0.0.1:8443", "localhost,127.0.0.1"}
    40  
    41  	// Exhaust all combinations of those environment variables' selection.
    42  	// Each bit of the mask decided whether this index of `envs` would be set.
    43  	for mask := 0; mask <= 0b111; mask++ {
    44  		for _, env := range envs {
    45  			require.Nil(t, os.Unsetenv(env))
    46  		}
    47  
    48  		for i := 0; i < 3; i++ {
    49  			if (1<<i)&mask != 0 {
    50  				require.Nil(t, os.Setenv(envs[i], envPreset[i]))
    51  			}
    52  		}
    53  
    54  		for _, field := range findProxyFields() {
    55  			idx, ok := revIndex[field.Key]
    56  			require.True(t, ok)
    57  			require.NotEqual(t, 0, (1<<idx)&mask)
    58  			require.Equal(t, field.String, envPreset[idx])
    59  		}
    60  	}
    61  }
    62  
    63  func TestVerifyPdEndpoint(t *testing.T) {
    64  	// empty URL.
    65  	url := ""
    66  	require.Regexp(t, ".*PD endpoint should be a valid http or https URL.*",
    67  		VerifyPdEndpoint(url, false))
    68  
    69  	// invalid URL.
    70  	url = "\n hi"
    71  	require.Regexp(t, ".*invalid control character in URL.*",
    72  		VerifyPdEndpoint(url, false))
    73  
    74  	// http URL without host.
    75  	url = "http://"
    76  	require.Regexp(t, ".*PD endpoint should be a valid http or https URL.*",
    77  		VerifyPdEndpoint(url, false))
    78  
    79  	// https URL without host.
    80  	url = "https://"
    81  	require.Regexp(t, ".*PD endpoint should be a valid http or https URL.*",
    82  		VerifyPdEndpoint(url, false))
    83  
    84  	// postgres scheme.
    85  	url = "postgres://postgres@localhost/cargo_registry"
    86  	require.Regexp(t, ".*PD endpoint should be a valid http or https URL.*",
    87  		VerifyPdEndpoint(url, false))
    88  
    89  	// https scheme without TLS.
    90  	url = "https://aa"
    91  	require.Regexp(t, ".*PD endpoint scheme is https, please provide certificate.*",
    92  		VerifyPdEndpoint(url, false))
    93  
    94  	// http scheme with TLS.
    95  	url = "http://aa"
    96  	require.Regexp(t, ".*PD endpoint scheme should be https.*", VerifyPdEndpoint(url, true))
    97  
    98  	// valid http URL.
    99  	require.Nil(t, VerifyPdEndpoint("http://aa", false))
   100  
   101  	// valid https URL with TLS.
   102  	require.Nil(t, VerifyPdEndpoint("https://aa", true))
   103  }
   104  
   105  func TestStrictDecodeValidFile(t *testing.T) {
   106  	dataDir := t.TempDir()
   107  	tmpDir := t.TempDir()
   108  
   109  	configPath := filepath.Join(tmpDir, "ticdc.toml")
   110  	configContent := fmt.Sprintf(`
   111  addr = "128.0.0.1:1234"
   112  advertise-addr = "127.0.0.1:1111"
   113  
   114  log-file = "/root/cdc1.log"
   115  log-level = "warn"
   116  
   117  data-dir = "%+v"
   118  gc-ttl = 500
   119  tz = "US"
   120  capture-session-ttl = 10
   121  
   122  owner-flush-interval = "600ms"
   123  processor-flush-interval = "600ms"
   124  
   125  [log.file]
   126  max-size = 200
   127  max-days = 1
   128  max-backups = 1
   129  
   130  [sorter]
   131  sort-dir = "/tmp/just_a_test"
   132  
   133  [security]
   134  ca-path = "aa"
   135  cert-path = "bb"
   136  key-path = "cc"
   137  cert-allowed-cn = ["dd","ee"]
   138  `, dataDir)
   139  	err := os.WriteFile(configPath, []byte(configContent), 0o644)
   140  	require.Nil(t, err)
   141  
   142  	conf := config.GetDefaultServerConfig()
   143  	err = StrictDecodeFile(configPath, "test", conf)
   144  	require.Nil(t, err)
   145  }
   146  
   147  func TestStrictDecodeInvalidFile(t *testing.T) {
   148  	dataDir := t.TempDir()
   149  	tmpDir := t.TempDir()
   150  
   151  	configPath := filepath.Join(tmpDir, "ticdc.toml")
   152  	configContent := fmt.Sprintf(`
   153  	unknown = "128.0.0.1:1234"
   154  	data-dir = "%+v"
   155  
   156  	[log.unkown]
   157  	max-size = 200
   158  	max-days = 1
   159  	max-backups = 1
   160  	`, dataDir)
   161  	err := os.WriteFile(configPath, []byte(configContent), 0o644)
   162  	require.Nil(t, err)
   163  
   164  	conf := config.GetDefaultServerConfig()
   165  	err = StrictDecodeFile(configPath, "test", conf)
   166  	require.Contains(t, err.Error(), "contained unknown configuration options")
   167  }
   168  
   169  func TestAndWriteExampleReplicaTOML(t *testing.T) {
   170  	cfg := config.GetDefaultReplicaConfig()
   171  	err := StrictDecodeFile("changefeed.toml", "cdc", &cfg)
   172  	require.Nil(t, err)
   173  
   174  	require.True(t, cfg.CaseSensitive)
   175  	require.Equal(t, &config.FilterConfig{
   176  		IgnoreTxnStartTs: []uint64{1, 2},
   177  		Rules:            []string{"*.*", "!test.*"},
   178  	}, cfg.Filter)
   179  	require.Equal(t, &config.MounterConfig{
   180  		WorkerNum: 16,
   181  	}, cfg.Mounter)
   182  
   183  	sinkURL, err := url.Parse("kafka://127.0.0.1:9092")
   184  	require.NoError(t, err)
   185  
   186  	err = cfg.ValidateAndAdjust(sinkURL)
   187  	require.NoError(t, err)
   188  	require.Equal(t, &config.SinkConfig{
   189  		EncoderConcurrency: util.AddressOf(config.DefaultEncoderGroupConcurrency),
   190  		DispatchRules: []*config.DispatchRule{
   191  			{PartitionRule: "ts", TopicRule: "hello_{schema}", Matcher: []string{"test1.*", "test2.*"}},
   192  			{PartitionRule: "rowid", TopicRule: "{schema}_world", Matcher: []string{"test3.*", "test4.*"}},
   193  		},
   194  		ColumnSelectors: []*config.ColumnSelector{
   195  			{Matcher: []string{"test1.*", "test2.*"}, Columns: []string{"column1", "column2"}},
   196  			{Matcher: []string{"test3.*", "test4.*"}, Columns: []string{"!a", "column3"}},
   197  		},
   198  		CSVConfig: &config.CSVConfig{
   199  			Quote:                string(config.DoubleQuoteChar),
   200  			Delimiter:            string(config.Comma),
   201  			NullString:           config.NULL,
   202  			BinaryEncodingMethod: config.BinaryEncodingBase64,
   203  		},
   204  		Terminator:                       util.AddressOf("\r\n"),
   205  		DateSeparator:                    util.AddressOf(config.DateSeparatorDay.String()),
   206  		EnablePartitionSeparator:         util.AddressOf(true),
   207  		EnableKafkaSinkV2:                util.AddressOf(false),
   208  		OnlyOutputUpdatedColumns:         util.AddressOf(false),
   209  		DeleteOnlyOutputHandleKeyColumns: util.AddressOf(false),
   210  		ContentCompatible:                util.AddressOf(false),
   211  		Protocol:                         util.AddressOf("open-protocol"),
   212  		AdvanceTimeoutInSec:              util.AddressOf(uint(150)),
   213  		SendBootstrapIntervalInSec:       util.AddressOf(int64(120)),
   214  		SendBootstrapInMsgCount:          util.AddressOf(int32(10000)),
   215  		SendBootstrapToAllPartition:      util.AddressOf(true),
   216  		DebeziumDisableSchema:            util.AddressOf(false),
   217  		OpenProtocol:                     &config.OpenProtocolConfig{OutputOldValue: true},
   218  		Debezium:                         &config.DebeziumConfig{OutputOldValue: true},
   219  	}, cfg.Sink)
   220  }
   221  
   222  func TestAndWriteStorageSinkTOML(t *testing.T) {
   223  	cfg := config.GetDefaultReplicaConfig()
   224  	err := StrictDecodeFile("changefeed_storage_sink.toml", "cdc", &cfg)
   225  	require.NoError(t, err)
   226  
   227  	sinkURL, err := url.Parse("s3://127.0.0.1:9092")
   228  	require.NoError(t, err)
   229  
   230  	cfg.Sink.Protocol = util.AddressOf(config.ProtocolCanalJSON.String())
   231  	err = cfg.ValidateAndAdjust(sinkURL)
   232  	require.NoError(t, err)
   233  	require.Equal(t, &config.SinkConfig{
   234  		Protocol:                 util.AddressOf(config.ProtocolCanalJSON.String()),
   235  		EncoderConcurrency:       util.AddressOf(config.DefaultEncoderGroupConcurrency),
   236  		Terminator:               util.AddressOf(config.CRLF),
   237  		TxnAtomicity:             util.AddressOf(config.AtomicityLevel("")),
   238  		DateSeparator:            util.AddressOf("day"),
   239  		EnablePartitionSeparator: util.AddressOf(true),
   240  		FileIndexWidth:           util.AddressOf(config.DefaultFileIndexWidth),
   241  		EnableKafkaSinkV2:        util.AddressOf(false),
   242  		CSVConfig: &config.CSVConfig{
   243  			Delimiter:            ",",
   244  			Quote:                "\"",
   245  			NullString:           "\\N",
   246  			IncludeCommitTs:      false,
   247  			BinaryEncodingMethod: config.BinaryEncodingBase64,
   248  		},
   249  		OnlyOutputUpdatedColumns:         util.AddressOf(false),
   250  		DeleteOnlyOutputHandleKeyColumns: util.AddressOf(false),
   251  		ContentCompatible:                util.AddressOf(false),
   252  		AdvanceTimeoutInSec:              util.AddressOf(uint(150)),
   253  		SendBootstrapIntervalInSec:       util.AddressOf(int64(120)),
   254  		SendBootstrapInMsgCount:          util.AddressOf(int32(10000)),
   255  		SendBootstrapToAllPartition:      util.AddressOf(true),
   256  		DebeziumDisableSchema:            util.AddressOf(false),
   257  		OpenProtocol:                     &config.OpenProtocolConfig{OutputOldValue: true},
   258  		Debezium:                         &config.DebeziumConfig{OutputOldValue: true},
   259  	}, cfg.Sink)
   260  }
   261  
   262  func TestAndWriteExampleServerTOML(t *testing.T) {
   263  	cfg := config.GetDefaultServerConfig()
   264  	err := StrictDecodeFile("ticdc.toml", "cdc", &cfg)
   265  	require.Nil(t, err)
   266  	defcfg := config.GetDefaultServerConfig()
   267  	defcfg.AdvertiseAddr = "127.0.0.1:8300"
   268  	defcfg.LogFile = "/tmp/ticdc/ticdc.log"
   269  	require.Equal(t, defcfg, cfg)
   270  }
   271  
   272  func TestJSONPrint(t *testing.T) {
   273  	cmd := new(cobra.Command)
   274  	type testStruct struct {
   275  		A string `json:"a"`
   276  	}
   277  
   278  	data := testStruct{
   279  		A: "string",
   280  	}
   281  
   282  	var b bytes.Buffer
   283  	cmd.SetOut(&b)
   284  
   285  	err := JSONPrint(cmd, &data)
   286  	require.Nil(t, err)
   287  
   288  	output := `{
   289    "a": "string"
   290  }
   291  `
   292  	require.Equal(t, output, b.String())
   293  }
   294  
   295  func TestIgnoreStrictCheckItem(t *testing.T) {
   296  	dataDir := t.TempDir()
   297  	tmpDir := t.TempDir()
   298  
   299  	configPath := filepath.Join(tmpDir, "ticdc.toml")
   300  	configContent := fmt.Sprintf(`
   301  data-dir = "%+v"
   302  [unknown]
   303  max-size = 200
   304  max-days = 1
   305  max-backups = 1
   306  `, dataDir)
   307  	err := os.WriteFile(configPath, []byte(configContent), 0o644)
   308  	require.Nil(t, err)
   309  
   310  	conf := config.GetDefaultServerConfig()
   311  	err = StrictDecodeFile(configPath, "test", conf, "unknown")
   312  	require.Nil(t, err)
   313  
   314  	configContent = fmt.Sprintf(`
   315  data-dir = "%+v"
   316  [unknown]
   317  max-size = 200
   318  max-days = 1
   319  max-backups = 1
   320  [unknown2]
   321  max-size = 200
   322  max-days = 1
   323  max-backups = 1
   324  `, dataDir)
   325  	err = os.WriteFile(configPath, []byte(configContent), 0o644)
   326  	require.Nil(t, err)
   327  
   328  	err = StrictDecodeFile(configPath, "test", conf, "unknown")
   329  	require.Contains(t, err.Error(), "contained unknown configuration options: unknown2")
   330  
   331  	configContent = fmt.Sprintf(`
   332  data-dir = "%+v"
   333  [debug]
   334  unknown = 1
   335  `, dataDir)
   336  	err = os.WriteFile(configPath, []byte(configContent), 0o644)
   337  	require.Nil(t, err)
   338  
   339  	err = StrictDecodeFile(configPath, "test", conf, "debug")
   340  	require.Nil(t, err)
   341  }
   342  
   343  func TestInitSignalHandlingGracefulShutdown(t *testing.T) {
   344  	shutdownCh := make(chan struct{}, 1)
   345  	shutdown := func() <-chan struct{} { return shutdownCh }
   346  	cancelCh := make(chan struct{}, 1)
   347  	cancel := func() { cancelCh <- struct{}{} }
   348  	InitSignalHandling(shutdown, cancel)
   349  	self, err := os.FindProcess(os.Getpid())
   350  	require.Nil(t, err)
   351  
   352  	// First signal for preparing shutdown.
   353  	err = self.Signal(syscall.SIGTERM)
   354  	require.Nil(t, err)
   355  	select {
   356  	case <-shutdownCh:
   357  		require.Fail(t, "unexpected")
   358  	case <-cancelCh:
   359  		require.Fail(t, "unexpected")
   360  	case <-time.After(100 * time.Millisecond):
   361  	}
   362  
   363  	// Graceful shutdown complete.
   364  	shutdownCh <- struct{}{}
   365  	select {
   366  	case <-cancelCh:
   367  	case <-time.After(100 * time.Millisecond):
   368  		require.Fail(t, "timeout")
   369  	}
   370  }
   371  
   372  func TestInitSignalHandlingForceShutdown(t *testing.T) {
   373  	shutdownCh := make(chan struct{}, 1)
   374  	shutdown := func() <-chan struct{} { return shutdownCh }
   375  	cancelCh := make(chan struct{}, 1)
   376  	cancel := func() { cancelCh <- struct{}{} }
   377  	InitSignalHandling(shutdown, cancel)
   378  	self, err := os.FindProcess(os.Getpid())
   379  	require.Nil(t, err)
   380  	err = self.Signal(syscall.SIGTERM)
   381  	require.Nil(t, err)
   382  	// Second signal for force shutdown.
   383  	// We use another signal, to avoid lost signal, because sending a signal
   384  	// is setting a bit in Unix.
   385  	err = self.Signal(syscall.SIGQUIT)
   386  	require.Nil(t, err)
   387  	select {
   388  	case <-cancelCh:
   389  	case <-time.After(1 * time.Second):
   390  		require.Fail(t, "timeout")
   391  	}
   392  }