github.com/netdata/go.d.plugin@v0.58.1/modules/pgbouncer/pgbouncer_test.go (about)

     1  // SPDX-License-Identifier: GPL-3.0-or-later
     2  
     3  package pgbouncer
     4  
     5  import (
     6  	"bufio"
     7  	"bytes"
     8  	"database/sql/driver"
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/DATA-DOG/go-sqlmock"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  )
    19  
    20  var (
    21  	dataV170Version, _    = os.ReadFile("testdata/v1.7.0/version.txt")
    22  	dataV1170Version, _   = os.ReadFile("testdata/v1.17.0/version.txt")
    23  	dataV1170Config, _    = os.ReadFile("testdata/v1.17.0/config.txt")
    24  	dataV1170Databases, _ = os.ReadFile("testdata/v1.17.0/databases.txt")
    25  	dataV1170Pools, _     = os.ReadFile("testdata/v1.17.0/pools.txt")
    26  	dataV1170Stats, _     = os.ReadFile("testdata/v1.17.0/stats.txt")
    27  )
    28  
    29  func Test_testDataIsValid(t *testing.T) {
    30  	for name, data := range map[string][]byte{
    31  		"dataV170Version":    dataV170Version,
    32  		"dataV1170Version":   dataV1170Version,
    33  		"dataV1170Config":    dataV1170Config,
    34  		"dataV1170Databases": dataV1170Databases,
    35  		"dataV1170Pools":     dataV1170Pools,
    36  		"dataV1170Stats":     dataV1170Stats,
    37  	} {
    38  		require.NotNilf(t, data, name)
    39  	}
    40  }
    41  
    42  func TestPgBouncer_Init(t *testing.T) {
    43  	tests := map[string]struct {
    44  		wantFail bool
    45  		config   Config
    46  	}{
    47  		"Success with default": {
    48  			wantFail: false,
    49  			config:   New().Config,
    50  		},
    51  		"Fail when DSN not set": {
    52  			wantFail: true,
    53  			config:   Config{DSN: ""},
    54  		},
    55  	}
    56  
    57  	for name, test := range tests {
    58  		t.Run(name, func(t *testing.T) {
    59  			p := New()
    60  			p.Config = test.config
    61  
    62  			if test.wantFail {
    63  				assert.False(t, p.Init())
    64  			} else {
    65  				assert.True(t, p.Init())
    66  			}
    67  		})
    68  	}
    69  }
    70  
    71  func TestPgBouncer_Charts(t *testing.T) {
    72  	assert.NotNil(t, New().Charts())
    73  }
    74  
    75  func TestPgBouncer_Check(t *testing.T) {
    76  	tests := map[string]struct {
    77  		prepareMock func(t *testing.T, m sqlmock.Sqlmock)
    78  		wantFail    bool
    79  	}{
    80  		"Success when all queries are successful (v1.17.0)": {
    81  			wantFail: false,
    82  			prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
    83  				mockExpect(t, m, queryShowVersion, dataV1170Version)
    84  				mockExpect(t, m, queryShowConfig, dataV1170Config)
    85  				mockExpect(t, m, queryShowDatabases, dataV1170Databases)
    86  				mockExpect(t, m, queryShowStats, dataV1170Stats)
    87  				mockExpect(t, m, queryShowPools, dataV1170Pools)
    88  			},
    89  		},
    90  		"Fail when querying version returns an error": {
    91  			wantFail: true,
    92  			prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
    93  				mockExpectErr(m, queryShowVersion)
    94  			},
    95  		},
    96  		"Fail when querying version returns unsupported version": {
    97  			wantFail: true,
    98  			prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
    99  				mockExpect(t, m, queryShowVersion, dataV170Version)
   100  			},
   101  		},
   102  		"Fail when querying config returns an error": {
   103  			wantFail: true,
   104  			prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
   105  				mockExpect(t, m, queryShowVersion, dataV1170Version)
   106  				mockExpectErr(m, queryShowConfig)
   107  			},
   108  		},
   109  	}
   110  
   111  	for name, test := range tests {
   112  		t.Run(name, func(t *testing.T) {
   113  			db, mock, err := sqlmock.New(
   114  				sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual),
   115  			)
   116  			require.NoError(t, err)
   117  			p := New()
   118  			p.db = db
   119  			defer func() { _ = db.Close() }()
   120  
   121  			require.True(t, p.Init())
   122  
   123  			test.prepareMock(t, mock)
   124  
   125  			if test.wantFail {
   126  				assert.False(t, p.Check())
   127  			} else {
   128  				assert.True(t, p.Check())
   129  			}
   130  			assert.NoError(t, mock.ExpectationsWereMet())
   131  		})
   132  	}
   133  }
   134  
   135  func TestPgBouncer_Collect(t *testing.T) {
   136  	type testCaseStep struct {
   137  		prepareMock func(t *testing.T, m sqlmock.Sqlmock)
   138  		check       func(t *testing.T, p *PgBouncer)
   139  	}
   140  	tests := map[string][]testCaseStep{
   141  		"Success on all queries (v1.17.0)": {
   142  			{
   143  				prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
   144  					mockExpect(t, m, queryShowVersion, dataV1170Version)
   145  					mockExpect(t, m, queryShowConfig, dataV1170Config)
   146  					mockExpect(t, m, queryShowDatabases, dataV1170Databases)
   147  					mockExpect(t, m, queryShowStats, dataV1170Stats)
   148  					mockExpect(t, m, queryShowPools, dataV1170Pools)
   149  				},
   150  				check: func(t *testing.T, p *PgBouncer) {
   151  					mx := p.Collect()
   152  
   153  					expected := map[string]int64{
   154  						"cl_conns_utilization":              47,
   155  						"db_myprod1_avg_query_time":         575,
   156  						"db_myprod1_avg_xact_time":          575,
   157  						"db_myprod1_cl_active":              15,
   158  						"db_myprod1_cl_cancel_req":          0,
   159  						"db_myprod1_cl_waiting":             0,
   160  						"db_myprod1_maxwait":                0,
   161  						"db_myprod1_sv_active":              15,
   162  						"db_myprod1_sv_conns_utilization":   0,
   163  						"db_myprod1_sv_idle":                5,
   164  						"db_myprod1_sv_login":               0,
   165  						"db_myprod1_sv_tested":              0,
   166  						"db_myprod1_sv_used":                0,
   167  						"db_myprod1_total_query_count":      12683170,
   168  						"db_myprod1_total_query_time":       7223566620,
   169  						"db_myprod1_total_received":         809093651,
   170  						"db_myprod1_total_sent":             1990971542,
   171  						"db_myprod1_total_wait_time":        1029555,
   172  						"db_myprod1_total_xact_count":       12683170,
   173  						"db_myprod1_total_xact_time":        7223566620,
   174  						"db_myprod2_avg_query_time":         581,
   175  						"db_myprod2_avg_xact_time":          581,
   176  						"db_myprod2_cl_active":              12,
   177  						"db_myprod2_cl_cancel_req":          0,
   178  						"db_myprod2_cl_waiting":             0,
   179  						"db_myprod2_maxwait":                0,
   180  						"db_myprod2_sv_active":              11,
   181  						"db_myprod2_sv_conns_utilization":   0,
   182  						"db_myprod2_sv_idle":                9,
   183  						"db_myprod2_sv_login":               0,
   184  						"db_myprod2_sv_tested":              0,
   185  						"db_myprod2_sv_used":                0,
   186  						"db_myprod2_total_query_count":      12538544,
   187  						"db_myprod2_total_query_time":       7144226450,
   188  						"db_myprod2_total_received":         799867464,
   189  						"db_myprod2_total_sent":             1968267687,
   190  						"db_myprod2_total_wait_time":        993313,
   191  						"db_myprod2_total_xact_count":       12538544,
   192  						"db_myprod2_total_xact_time":        7144226450,
   193  						"db_pgbouncer_avg_query_time":       0,
   194  						"db_pgbouncer_avg_xact_time":        0,
   195  						"db_pgbouncer_cl_active":            2,
   196  						"db_pgbouncer_cl_cancel_req":        0,
   197  						"db_pgbouncer_cl_waiting":           0,
   198  						"db_pgbouncer_maxwait":              0,
   199  						"db_pgbouncer_sv_active":            0,
   200  						"db_pgbouncer_sv_conns_utilization": 0,
   201  						"db_pgbouncer_sv_idle":              0,
   202  						"db_pgbouncer_sv_login":             0,
   203  						"db_pgbouncer_sv_tested":            0,
   204  						"db_pgbouncer_sv_used":              0,
   205  						"db_pgbouncer_total_query_count":    45,
   206  						"db_pgbouncer_total_query_time":     0,
   207  						"db_pgbouncer_total_received":       0,
   208  						"db_pgbouncer_total_sent":           0,
   209  						"db_pgbouncer_total_wait_time":      0,
   210  						"db_pgbouncer_total_xact_count":     45,
   211  						"db_pgbouncer_total_xact_time":      0,
   212  						"db_postgres_avg_query_time":        2790,
   213  						"db_postgres_avg_xact_time":         2790,
   214  						"db_postgres_cl_active":             18,
   215  						"db_postgres_cl_cancel_req":         0,
   216  						"db_postgres_cl_waiting":            0,
   217  						"db_postgres_maxwait":               0,
   218  						"db_postgres_sv_active":             18,
   219  						"db_postgres_sv_conns_utilization":  0,
   220  						"db_postgres_sv_idle":               2,
   221  						"db_postgres_sv_login":              0,
   222  						"db_postgres_sv_tested":             0,
   223  						"db_postgres_sv_used":               0,
   224  						"db_postgres_total_query_count":     25328823,
   225  						"db_postgres_total_query_time":      72471882827,
   226  						"db_postgres_total_received":        1615791619,
   227  						"db_postgres_total_sent":            3976053858,
   228  						"db_postgres_total_wait_time":       50439622253,
   229  						"db_postgres_total_xact_count":      25328823,
   230  						"db_postgres_total_xact_time":       72471882827,
   231  					}
   232  
   233  					assert.Equal(t, expected, mx)
   234  				},
   235  			},
   236  		},
   237  		"Fail when querying version returns an error": {
   238  			{
   239  				prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
   240  					mockExpectErr(m, queryShowVersion)
   241  				},
   242  				check: func(t *testing.T, p *PgBouncer) {
   243  					mx := p.Collect()
   244  					var expected map[string]int64
   245  					assert.Equal(t, expected, mx)
   246  				},
   247  			},
   248  		},
   249  		"Fail when querying version returns unsupported version": {
   250  			{
   251  				prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
   252  					mockExpect(t, m, queryShowVersion, dataV170Version)
   253  				},
   254  				check: func(t *testing.T, p *PgBouncer) {
   255  					mx := p.Collect()
   256  					var expected map[string]int64
   257  					assert.Equal(t, expected, mx)
   258  				},
   259  			},
   260  		},
   261  		"Fail when querying config returns an error": {
   262  			{
   263  				prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
   264  					mockExpect(t, m, queryShowVersion, dataV1170Version)
   265  					mockExpectErr(m, queryShowConfig)
   266  				},
   267  				check: func(t *testing.T, p *PgBouncer) {
   268  					mx := p.Collect()
   269  					var expected map[string]int64
   270  					assert.Equal(t, expected, mx)
   271  				},
   272  			},
   273  		},
   274  	}
   275  
   276  	for name, test := range tests {
   277  		t.Run(name, func(t *testing.T) {
   278  			db, mock, err := sqlmock.New(
   279  				sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual),
   280  			)
   281  			require.NoError(t, err)
   282  			p := New()
   283  			p.db = db
   284  			defer func() { _ = db.Close() }()
   285  
   286  			require.True(t, p.Init())
   287  
   288  			for i, step := range test {
   289  				t.Run(fmt.Sprintf("step[%d]", i), func(t *testing.T) {
   290  					step.prepareMock(t, mock)
   291  					step.check(t, p)
   292  				})
   293  			}
   294  			assert.NoError(t, mock.ExpectationsWereMet())
   295  		})
   296  	}
   297  }
   298  
   299  func mockExpect(t *testing.T, mock sqlmock.Sqlmock, query string, rows []byte) {
   300  	mock.ExpectQuery(query).WillReturnRows(mustMockRows(t, rows)).RowsWillBeClosed()
   301  }
   302  
   303  func mockExpectErr(mock sqlmock.Sqlmock, query string) {
   304  	mock.ExpectQuery(query).WillReturnError(fmt.Errorf("mock error (%s)", query))
   305  }
   306  
   307  func mustMockRows(t *testing.T, data []byte) *sqlmock.Rows {
   308  	rows, err := prepareMockRows(data)
   309  	require.NoError(t, err)
   310  	return rows
   311  }
   312  
   313  func prepareMockRows(data []byte) (*sqlmock.Rows, error) {
   314  	r := bytes.NewReader(data)
   315  	sc := bufio.NewScanner(r)
   316  
   317  	var numColumns int
   318  	var rows *sqlmock.Rows
   319  
   320  	for sc.Scan() {
   321  		s := strings.TrimSpace(sc.Text())
   322  		if s == "" || strings.HasPrefix(s, "---") {
   323  			continue
   324  		}
   325  
   326  		parts := strings.Split(s, "|")
   327  		for i, v := range parts {
   328  			parts[i] = strings.TrimSpace(v)
   329  		}
   330  
   331  		if rows == nil {
   332  			numColumns = len(parts)
   333  			rows = sqlmock.NewRows(parts)
   334  			continue
   335  		}
   336  
   337  		if len(parts) != numColumns {
   338  			return nil, fmt.Errorf("prepareMockRows(): columns != values (%d/%d)", numColumns, len(parts))
   339  		}
   340  
   341  		values := make([]driver.Value, len(parts))
   342  		for i, v := range parts {
   343  			values[i] = v
   344  		}
   345  		rows.AddRow(values...)
   346  	}
   347  
   348  	if rows == nil {
   349  		return nil, errors.New("prepareMockRows(): nil rows result")
   350  	}
   351  
   352  	return rows, nil
   353  }