pgmon/test_pgmon.py

617 lines
17 KiB
Python
Raw Normal View History

2025-01-03 07:16:40 +00:00
import unittest
2025-01-08 07:39:20 +00:00
from datetime import datetime, timedelta
2025-01-08 22:01:11 +00:00
import tempfile
import logging
2025-01-08 07:39:20 +00:00
2025-01-03 07:16:40 +00:00
import pgmon
2025-01-08 22:01:11 +00:00
# Silence most logging output
logging.disable(logging.CRITICAL)
2025-01-03 07:16:40 +00:00
class TestPgmonMethods(unittest.TestCase):
##
# update_deep
##
def test_update_deep__empty_cases(self):
# Test empty dict cases
d1 = {}
d2 = {}
pgmon.update_deep(d1, d2)
self.assertEqual(d1, {})
self.assertEqual(d2, {})
d1 = {'a': 1}
d2 = {}
pgmon.update_deep(d1, d2)
self.assertEqual(d1, { 'a': 1 })
self.assertEqual(d2, {})
d1 = {}
d2 = {'a': 1}
pgmon.update_deep(d1, d2)
self.assertEqual(d1, { 'a': 1 })
self.assertEqual(d2, d1)
def test_update_deep__scalars(self):
# Test adding/updating scalar values
d1 = {'foo': 1, 'bar': "text", 'hello': "world"}
d2 = {'foo': 2, 'baz': "blah"}
pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'foo': 2, 'bar': "text", 'baz': "blah", 'hello': "world"})
self.assertEqual(d2, {'foo': 2, 'baz': "blah"})
def test_update_deep__lists(self):
# Test adding to lists
d1 = {'lst1': []}
d2 = {'lst1': [1, 2]}
pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'lst1': [1, 2]})
self.assertEqual(d2, d1)
d1 = {'lst1': [1, 2]}
d2 = {'lst1': []}
pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'lst1': [1, 2]})
self.assertEqual(d2, {'lst1': []})
d1 = {'lst1': [1, 2, 3]}
d2 = {'lst1': [3, 4]}
pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'lst1': [1, 2, 3, 3, 4]})
self.assertEqual(d2, {'lst1': [3, 4]})
# Lists of objects
d1 = {'lst1': [{'id': 1}, {'id': 2}, {'id': 3}]}
d2 = {'lst1': [{'id': 3}, {'id': 4}]}
pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'lst1': [{'id': 1}, {'id': 2}, {'id': 3}, {'id': 3}, {'id': 4}]})
self.assertEqual(d2, {'lst1': [{'id': 3}, {'id': 4}]})
# Nested lists
d1 = {'obj1': {'l1': [1, 2]}}
d2 = {'obj1': {'l1': [3, 4]}}
pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'obj1': {'l1': [1, 2, 3, 4]}})
self.assertEqual(d2, {'obj1': {'l1': [3, 4]}})
def test_update_deep__dicts(self):
# Test adding to lists
d1 = {'obj1': {}}
d2 = {'obj1': {'a': 1, 'b': 2}}
pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'obj1': {'a': 1, 'b': 2}})
self.assertEqual(d2, d1)
d1 = {'obj1': {'a': 1, 'b': 2}}
d2 = {'obj1': {}}
pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'obj1': {'a': 1, 'b': 2}})
self.assertEqual(d2, {'obj1': {}})
d1 = {'obj1': {'a': 1, 'b': 2}}
d2 = {'obj1': {'a': 5, 'c': 12}}
pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'obj1': {'a': 5, 'b': 2, 'c': 12}})
self.assertEqual(d2, {'obj1': {'a': 5, 'c': 12}})
# Nested dicts
d1 = {'obj1': {'d1': {'a': 1, 'b': 2}}}
d2 = {'obj1': {'d1': {'a': 5, 'c': 12}}}
pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'obj1': {'d1': {'a': 5, 'b': 2, 'c': 12}}})
self.assertEqual(d2, {'obj1': {'d1': {'a': 5, 'c': 12}}})
def test_update_deep__types(self):
# Test mismatched types
d1 = {'foo': 5}
d2 = None
self.assertRaises(TypeError, pgmon.update_deep, d1, d2)
d1 = None
d2 = {'foo': 5}
self.assertRaises(TypeError, pgmon.update_deep, d1, d2)
# Nested mismatched types
d1 = {'foo': [1, 2]}
d2 = {'foo': {'a': 7}}
self.assertRaises(TypeError, pgmon.update_deep, d1, d2)
2025-01-08 07:39:20 +00:00
##
# get_pool
##
def test_get_pool__simple(self):
# Just get a pool in a normal case
pgmon.config.update(pgmon.default_config)
pool = pgmon.get_pool('postgres')
self.assertIsNotNone(pool)
def test_get_pool__unhappy(self):
# Test getting an unhappy database pool
pgmon.config.update(pgmon.default_config)
pgmon.unhappy_cooldown['postgres'] = datetime.now() + timedelta(60)
self.assertRaises(pgmon.UnhappyDBError, pgmon.get_pool, 'postgres')
# Test getting a different database when there's an unhappy one
pool = pgmon.get_pool('template0')
self.assertIsNotNone(pool)
##
# handle_connect_failure
##
def test_handle_connect_failure__simple(self):
# Test adding to an empty unhappy list
pgmon.config.update(pgmon.default_config)
pgmon.unhappy_cooldown = {}
pool = pgmon.get_pool('postgres')
pgmon.handle_connect_failure(pool)
self.assertGreater(pgmon.unhappy_cooldown['postgres'], datetime.now())
# Test adding another database
pool = pgmon.get_pool('template0')
pgmon.handle_connect_failure(pool)
self.assertGreater(pgmon.unhappy_cooldown['postgres'], datetime.now())
self.assertGreater(pgmon.unhappy_cooldown['template0'], datetime.now())
self.assertEqual(len(pgmon.unhappy_cooldown), 2)
##
# get_query
##
def test_get_query__basic(self):
# Test getting a query with one version
metric = {
'type': 'value',
'query': {
0: 'DEFAULT'
}
}
self.assertEqual(pgmon.get_query(metric, 100000), 'DEFAULT')
def test_get_query__versions(self):
metric = {
'type': 'value',
'query': {
0: 'DEFAULT',
110000: 'NEW'
}
}
# Test getting the default version of a query with no lower bound and a newer version
self.assertEqual(pgmon.get_query(metric, 100000), 'DEFAULT')
# Test getting the newer version of a query with no lower bound and a newer version for the newer version
self.assertEqual(pgmon.get_query(metric, 110000), 'NEW')
# Test getting the newer version of a query with no lower bound and a newer version for an even newer version
self.assertEqual(pgmon.get_query(metric, 160000), 'NEW')
# Test getting a version in bwtween two other versions
metric = {
'type': 'value',
'query': {
0: 'DEFAULT',
96000: 'OLD',
110000: 'NEW'
}
}
self.assertEqual(pgmon.get_query(metric, 100000), 'OLD')
def test_get_query__missing_version(self):
2025-01-08 22:01:11 +00:00
metric = {
'type': 'value',
'query': {
96000: 'OLD',
110000: 'NEW',
150000: ''
}
}
2025-01-08 07:39:20 +00:00
# Test getting a metric that only exists for newer versions
2025-01-08 22:01:11 +00:00
self.assertRaises(pgmon.MetricVersionError, pgmon.get_query, metric, 80000)
2025-01-08 07:39:20 +00:00
# Test getting a metric that only exists for older versions
2025-01-08 22:01:11 +00:00
self.assertRaises(pgmon.MetricVersionError, pgmon.get_query, metric, 160000)
##
# read_config
##
def test_read_config__simple(self):
pgmon.config = {}
# Test reading just a metric and using the defaults for everything else
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
# This is a comment!
metrics:
test1:
type: value
query:
0: TEST1
""")
pgmon.read_config(f"{tmpdirname}/config.yml")
self.assertEqual(pgmon.config['max_pool_size'], pgmon.default_config['max_pool_size'])
self.assertEqual(pgmon.config['dbuser'], pgmon.default_config['dbuser'])
pgmon.config = {}
# Test reading a basic config
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
# This is a comment!
min_pool_size: 1
max_pool_size: 2
max_idle_time: 10
log_level: debug
dbuser: someone
dbhost: localhost
dbport: 5555
dbname: template0
pool_slot_timeout: 1
connect_timeout: 1
reconnect_cooldown: 15
version_check_period: 3600
metrics:
test1:
type: value
query:
0: TEST1
test2:
type: set
query:
0: TEST2
test3:
type: row
query:
0: TEST3
test4:
type: column
query:
0: TEST4
""")
pgmon.read_config(f"{tmpdirname}/config.yml")
self.assertEqual(pgmon.config['dbuser'], 'someone')
self.assertEqual(pgmon.config['metrics']['test1']['type'], 'value')
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')
self.assertEqual(pgmon.config['metrics']['test2']['query'][0], 'TEST2')
def test_read_config__include(self):
pgmon.config = {}
# Test reading a config that includes other files (absolute and relative paths, multiple levels)
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write(f"""---
# This is a comment!
min_pool_size: 1
max_pool_size: 2
max_idle_time: 10
log_level: debug
pool_slot_timeout: 1
connect_timeout: 1
reconnect_cooldown: 15
version_check_period: 3600
include:
- dbsettings.yml
- {tmpdirname}/metrics.yml
""")
with open(f"{tmpdirname}/dbsettings.yml", 'w') as f:
f.write(f"""---
dbuser: someone
dbhost: localhost
dbport: 5555
dbname: template0
""")
with open(f"{tmpdirname}/metrics.yml", 'w') as f:
f.write(f"""---
metrics:
test1:
type: value
query:
0: TEST1
test2:
type: value
query:
0: TEST2
include:
- more_metrics.yml
""")
with open(f"{tmpdirname}/more_metrics.yml", 'w') as f:
f.write(f"""---
metrics:
test3:
type: value
query:
0: TEST3
""")
pgmon.read_config(f"{tmpdirname}/config.yml")
self.assertEqual(pgmon.config['max_idle_time'], 10)
self.assertEqual(pgmon.config['dbuser'], 'someone')
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')
self.assertEqual(pgmon.config['metrics']['test2']['query'][0], 'TEST2')
self.assertEqual(pgmon.config['metrics']['test3']['query'][0], 'TEST3')
def test_read_config__reload(self):
pgmon.config = {}
# Test rereading a config to update an existing config
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
# This is a comment!
min_pool_size: 1
max_pool_size: 2
max_idle_time: 10
log_level: debug
dbuser: someone
dbhost: localhost
dbport: 5555
dbname: template0
pool_slot_timeout: 1
connect_timeout: 1
reconnect_cooldown: 15
version_check_period: 3600
metrics:
test1:
type: value
query:
0: TEST1
test2:
type: value
query:
0: TEST2
""")
pgmon.read_config(f"{tmpdirname}/config.yml")
# Just make sure the first config was read
self.assertEqual(len(pgmon.config['metrics']), 2)
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
# This is a comment!
min_pool_size: 7
metrics:
test1:
type: value
query:
0: NEW1
""")
pgmon.read_config(f"{tmpdirname}/config.yml")
self.assertEqual(pgmon.config['min_pool_size'], 7)
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'NEW1')
self.assertEqual(len(pgmon.config['metrics']), 1)
def test_read_config__query_file(self):
pgmon.config = {}
# Read a config file that reads a query from a file
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
metrics:
test1:
type: value
query:
0: file:some_query.sql
""")
with open(f"{tmpdirname}/some_query.sql", 'w') as f:
f.write("This is a query")
pgmon.read_config(f"{tmpdirname}/config.yml")
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'This is a query')
def test_read_config__invalid(self):
pgmon.config = {}
# For all of these tests, we start with a valid config and also ensure that
# it is not modified when a new config read fails
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
metrics:
test1:
type: value
query:
0: TEST1
""")
pgmon.read_config(f"{tmpdirname}/config.yml")
# Just make sure the config was read
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')
# Test reading a nonexistant config file
with tempfile.TemporaryDirectory() as tmpdirname:
self.assertRaises(FileNotFoundError, pgmon.read_config, f'{tmpdirname}/missing.yml')
# Test reading an invalid config file
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""[default]
This looks a lot like an ini file to me
Or maybe a TOML?
""")
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml')
# Test reading a config that includes an invalid file
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
dbuser: evil
metrics:
test1:
type: value
query:
0: EVIL1
include:
- missing_file.yml
""")
self.assertRaises(FileNotFoundError, pgmon.read_config, f'{tmpdirname}/config.yml')
self.assertEqual(pgmon.config['dbuser'], 'postgres')
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')
# Test invalid log level
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
log_level: noisy
dbuser: evil
metrics:
test1:
type: value
query:
0: EVIL1
""")
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml')
self.assertEqual(pgmon.config['dbuser'], 'postgres')
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')
# Test invalid query return type
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
dbuser: evil
metrics:
test1:
type: lots_of_data
query:
0: EVIL1
""")
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml')
self.assertEqual(pgmon.config['dbuser'], 'postgres')
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')
# Test invalid query dict type
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
dbuser: evil
metrics:
test1:
type: lots_of_data
query: EVIL1
""")
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml')
self.assertEqual(pgmon.config['dbuser'], 'postgres')
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')
# Test incomplete metric: missing type
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
dbuser: evil
metrics:
test1:
query:
0: EVIL1
""")
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml')
self.assertEqual(pgmon.config['dbuser'], 'postgres')
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')
# Test incomplete metric: missing queries
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
dbuser: evil
metrics:
test1:
type: value
""")
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml')
self.assertEqual(pgmon.config['dbuser'], 'postgres')
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')
# Test incomplete metric: empty queries
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
dbuser: evil
metrics:
test1:
type: value
query: {}
""")
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml')
self.assertEqual(pgmon.config['dbuser'], 'postgres')
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')
# Test incomplete metric: query dict is None
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
dbuser: evil
metrics:
test1:
type: value
query:
""")
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml')
self.assertEqual(pgmon.config['dbuser'], 'postgres')
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')
# Test reading a config with no metrics
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
dbuser: evil
""")
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml')
self.assertEqual(pgmon.config['dbuser'], 'postgres')
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')
# Test reading a query defined in a file but the file is missing
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
dbuser: evil
metrics:
test1:
type: value
query:
0: file:missing.sql
""")
self.assertRaises(FileNotFoundError, pgmon.read_config, f'{tmpdirname}/config.yml')
self.assertEqual(pgmon.config['dbuser'], 'postgres')
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')
# Test invalid query versions
with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f:
f.write("""---
dbuser: evil
metrics:
test1:
type: value
query:
default: EVIL1
""")
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml')
self.assertEqual(pgmon.config['dbuser'], 'postgres')
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1')