Add unit tests with 68% coverage, refactor a bit
This commit is contained in:
parent
7967b1d024
commit
2b9435dddd
9 changed files with 213 additions and 39 deletions
|
@ -15,9 +15,6 @@ import sys
|
||||||
import sphinx_typlog_theme
|
import sphinx_typlog_theme
|
||||||
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.abspath('.'),
|
|
||||||
"..", "..", "src"))
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'chweb'
|
project = 'chweb'
|
||||||
|
|
0
src/chweb/__init__.py
Normal file
0
src/chweb/__init__.py
Normal file
|
@ -6,7 +6,7 @@ import asyncio
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
from typing import Tuple
|
from typing import Any, Dict, Tuple
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
@ -27,6 +27,15 @@ def configure(name) -> Tuple[Config, Logger]:
|
||||||
'Defaults to /etc/checker.yaml'))
|
'Defaults to /etc/checker.yaml'))
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
with open(args.config, 'r') as conf_file:
|
||||||
|
config = yaml.load(conf_file, Loader=yaml.FullLoader)
|
||||||
|
logging.config.dictConfig(config['logging'])
|
||||||
|
|
||||||
|
logger = logging.getLogger("chweb.{}".format(name))
|
||||||
|
return (config, logger)
|
||||||
|
|
||||||
|
|
||||||
|
def create_config(conf: Dict[str, Any]):
|
||||||
kafka_servers_env = os.getenv('KAFKA_SERVERS')
|
kafka_servers_env = os.getenv('KAFKA_SERVERS')
|
||||||
if kafka_servers_env is not None:
|
if kafka_servers_env is not None:
|
||||||
kafka_servers = kafka_servers_env.split(',')
|
kafka_servers = kafka_servers_env.split(',')
|
||||||
|
@ -39,23 +48,18 @@ def configure(name) -> Tuple[Config, Logger]:
|
||||||
pg_user = os.getenv('POSTGRES_USER')
|
pg_user = os.getenv('POSTGRES_USER')
|
||||||
pg_pass = os.getenv('POSTGRES_PASS')
|
pg_pass = os.getenv('POSTGRES_PASS')
|
||||||
|
|
||||||
with open(args.config, 'r') as conf_file:
|
config = Config(**conf)
|
||||||
config = yaml.load(conf_file, Loader=yaml.FullLoader)
|
config.kafka.servers = (kafka_servers if kafka_servers_env
|
||||||
logging.config.dictConfig(config['logging'])
|
else config.kafka.servers)
|
||||||
|
config.kafka.topic = kafka_topic or config.kafka.topic
|
||||||
|
config.postgres.dbhost = pg_host or config.postgres.dbhost
|
||||||
|
config.postgres.dbname = pg_db or config.postgres.dbname
|
||||||
|
config.postgres.dbport = (int(pg_port) if pg_port is not None
|
||||||
|
else config.postgres.dbport)
|
||||||
|
config.postgres.dbuser = pg_user or config.postgres.dbuser
|
||||||
|
config.postgres.dbpass = pg_pass or config.postgres.dbpass
|
||||||
|
|
||||||
config = Config(**config)
|
return config
|
||||||
config.kafka.servers = (kafka_servers if kafka_servers_env
|
|
||||||
else config.kafka.servers)
|
|
||||||
config.kafka.topic = kafka_topic or config.kafka.topic
|
|
||||||
config.postgres.dbhost = pg_host or config.postgres.dbhost
|
|
||||||
config.postgres.dbname = pg_db or config.postgres.dbname
|
|
||||||
config.postgres.dbport = pg_port or config.postgres.dbport
|
|
||||||
config.postgres.dbuser = pg_user or config.postgres.dbuser
|
|
||||||
config.postgres.dbpass = pg_pass or config.postgres.dbpass
|
|
||||||
|
|
||||||
logger = logging.getLogger("chweb.{}".format(name))
|
|
||||||
print(config)
|
|
||||||
return (config, logger)
|
|
||||||
|
|
||||||
|
|
||||||
def collect():
|
def collect():
|
||||||
|
|
|
@ -2,16 +2,17 @@
|
||||||
Checks status of web servers and sends them to a configured Kafka topic.
|
Checks status of web servers and sends them to a configured Kafka topic.
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import aiokafka # type: ignore
|
import aiokafka # type: ignore
|
||||||
import requests
|
import requests
|
||||||
from requests import ConnectionError
|
from requests import exceptions as rqexc
|
||||||
|
|
||||||
from chweb.base import Service
|
from chweb.base import Service
|
||||||
from chweb.models import Check, SiteConfig
|
from chweb.models import Check, Config, SiteConfig
|
||||||
|
|
||||||
|
|
||||||
class Collector(Service):
|
class Collector(Service):
|
||||||
|
@ -30,8 +31,8 @@ class Collector(Service):
|
||||||
``chweb.collector.Collector.check_forever``.
|
``chweb.collector.Collector.check_forever``.
|
||||||
"""
|
"""
|
||||||
res = await self.loop.run_in_executor(None, requests.get, url)
|
res = await self.loop.run_in_executor(None, requests.get, url)
|
||||||
matches = None # The matches value should be None since the regex can
|
matches = None # The matches value should be None since the regex can
|
||||||
# be ommited from the config.
|
# be ommited from the config.
|
||||||
if regex is not None:
|
if regex is not None:
|
||||||
matches = re.search(regex, res.text) is not None
|
matches = re.search(regex, res.text) is not None
|
||||||
return Check(
|
return Check(
|
||||||
|
@ -57,10 +58,10 @@ class Collector(Service):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
data = await self.check(site.url, site.regex)
|
data = await self.check(site.url, site.regex)
|
||||||
except ConnectionError as exc:
|
except rqexc.ConnectionError as exc:
|
||||||
errmsg = "{}; {}".format(site.url, exc)
|
errmsg = "{}; {}".format(site.url, exc)
|
||||||
self.logger.error(errmsg)
|
self.logger.error(errmsg)
|
||||||
break # Break the loop and destroy the Task.
|
break # Break the loop and destroy the Task.
|
||||||
self.queue.put_nowait(data)
|
self.queue.put_nowait(data)
|
||||||
await asyncio.sleep(site.check_interval)
|
await asyncio.sleep(site.check_interval)
|
||||||
|
|
||||||
|
@ -75,28 +76,36 @@ class Producer(Service):
|
||||||
"""
|
"""
|
||||||
Kafka producer.
|
Kafka producer.
|
||||||
|
|
||||||
Reads from the queue that :class:`chweb.collector.Collector` writes in and
|
Reads checks from the queue written by :class:`chweb.collector.Collector`
|
||||||
sends all messages in a kafka topic.
|
and sends all messages in a kafka topic.
|
||||||
"""
|
"""
|
||||||
async def produce(self):
|
|
||||||
"""
|
def __init__(self, config: Config,
|
||||||
Creates and starts an ``aiokafka.AIOKafkaProducer`` and runs a loop
|
logger: logging.Logger,
|
||||||
that reads from the queue and sends the messages to the topic from the
|
event_loop: asyncio.AbstractEventLoop,
|
||||||
config.
|
queue: asyncio.Queue):
|
||||||
"""
|
super().__init__(config, logger, event_loop, queue)
|
||||||
producer = aiokafka.AIOKafkaProducer(
|
self.producer = aiokafka.AIOKafkaProducer(
|
||||||
loop=self.loop,
|
loop=self.loop,
|
||||||
bootstrap_servers=self.config.kafka.servers)
|
bootstrap_servers=self.config.kafka.servers)
|
||||||
|
|
||||||
await producer.start()
|
async def produce(self):
|
||||||
|
"""
|
||||||
|
Creates and starts an ``aiokafka.AIOKafkaProducer`` and runs a loop
|
||||||
|
that reads from the queue and sends the messages to the topic defined
|
||||||
|
in the config.
|
||||||
|
"""
|
||||||
|
await self.producer.start()
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
check = await self.queue.get()
|
check = await self.queue.get()
|
||||||
msg = bytes(check.json().encode("utf-8"))
|
msg = bytes(check.json().encode("utf-8"))
|
||||||
await producer.send_and_wait(self.config.kafka.topic, msg)
|
await self.producer.send_and_wait(self.config.kafka.topic, msg)
|
||||||
|
except Exception as exc:
|
||||||
|
self.logger.error(exc)
|
||||||
finally:
|
finally:
|
||||||
self.logger.warning("Kafka producer destroyed!")
|
self.logger.warning("Kafka producer destroyed!")
|
||||||
await producer.stop()
|
await self.producer.stop()
|
||||||
|
|
||||||
def __call__(self) -> asyncio.Future:
|
def __call__(self) -> asyncio.Future:
|
||||||
return self.produce()
|
return self.produce()
|
||||||
|
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
49
tests/conftest.py
Normal file
49
tests/conftest.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import asyncio
|
||||||
|
import pytest
|
||||||
|
from chweb.cmd import create_config
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def config():
|
||||||
|
config_dict = {
|
||||||
|
'kafka': {
|
||||||
|
'servers': ["localhost:9992"],
|
||||||
|
'topic': "sample",
|
||||||
|
},
|
||||||
|
'postgres': {
|
||||||
|
'dbhost': "localhost",
|
||||||
|
'dbport': 5432,
|
||||||
|
'dbname': "chweb",
|
||||||
|
'dbuser': "vladan",
|
||||||
|
'dbpass': "",
|
||||||
|
},
|
||||||
|
'sites': [{
|
||||||
|
'url': "https://example.com",
|
||||||
|
'regex': "aaaaaaaaaaaaa",
|
||||||
|
'check_interval': 8,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return create_config(config_dict)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config_invalid():
|
||||||
|
config_dict = {
|
||||||
|
'kafka': {
|
||||||
|
'servers': ["localhost:9992"],
|
||||||
|
'topic': "sample",
|
||||||
|
},
|
||||||
|
'postgres': {
|
||||||
|
'dbhost': "localhost",
|
||||||
|
'dbport': 5432,
|
||||||
|
'dbname': "chweb",
|
||||||
|
'dbuser': "vladan",
|
||||||
|
'dbpass': "",
|
||||||
|
},
|
||||||
|
'sites': [{
|
||||||
|
'url': "https://dsadakjhkjsahkjh.com",
|
||||||
|
'regex': "domain",
|
||||||
|
'check_interval': 5,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return create_config(config_dict)
|
67
tests/test_checker.py
Normal file
67
tests/test_checker.py
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
"""
|
||||||
|
All tests fot the ``chweb.checker`` module.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from chweb.collector import Collector
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_valid_site_200(config, event_loop):
|
||||||
|
queue = asyncio.Queue()
|
||||||
|
coll = Collector(config, Mock(), event_loop, queue)
|
||||||
|
check = await coll.check('https://example.com', None)
|
||||||
|
assert check.domain == 'example.com'
|
||||||
|
assert check.regex_matches is None
|
||||||
|
assert check.status == 200
|
||||||
|
assert check.response_time > 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_valid_site_404(config, event_loop):
|
||||||
|
queue = asyncio.Queue()
|
||||||
|
coll = Collector(config, Mock(), event_loop, queue)
|
||||||
|
check = await coll.check('https://example.com/404', None)
|
||||||
|
assert check.domain == 'example.com'
|
||||||
|
assert check.regex_matches is None
|
||||||
|
assert check.status == 404
|
||||||
|
assert check.response_time > 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_invalid_site(config, event_loop):
|
||||||
|
queue = asyncio.Queue()
|
||||||
|
coll = Collector(config, Mock(), event_loop, queue)
|
||||||
|
with pytest.raises(requests.exceptions.ConnectionError):
|
||||||
|
_ = await coll.check('https://non.existant.domain.noooo', None)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_forever_valid(config, event_loop):
|
||||||
|
"""
|
||||||
|
The :meth:`chweb.collector.Collector.check_forever` method runs an infinite
|
||||||
|
loop, so we'll test if it's running for 2s and assume it's ok.
|
||||||
|
"""
|
||||||
|
queue = asyncio.Queue()
|
||||||
|
coll = Collector(config, Mock(), event_loop, queue)
|
||||||
|
task = event_loop.create_task(coll.check_forever(config.sites[0]))
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
assert not task.done()
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_forever_invalid(config_invalid, event_loop):
|
||||||
|
"""
|
||||||
|
The :meth:`chweb.collector.Collector.check_forever` method cancels the Task
|
||||||
|
on error, so if we get an invalid site, the task should be done.
|
||||||
|
"""
|
||||||
|
queue = asyncio.Queue()
|
||||||
|
coll = Collector(config_invalid, Mock(), event_loop, queue)
|
||||||
|
task = event_loop.create_task(coll.check_forever(config_invalid.sites[0]))
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
assert task.done()
|
47
tests/test_producer.py
Normal file
47
tests/test_producer.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import aiokafka
|
||||||
|
from mock import Mock
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from chweb.collector import Producer
|
||||||
|
from chweb.models import Check
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_producer_called(config, event_loop):
|
||||||
|
queue = asyncio.Queue()
|
||||||
|
producer = Producer(config, Mock(), event_loop, queue)
|
||||||
|
check = Check()
|
||||||
|
await queue.put(check)
|
||||||
|
|
||||||
|
async def async_patch():
|
||||||
|
pass
|
||||||
|
Mock.__await__ = lambda x: async_patch().__await__()
|
||||||
|
|
||||||
|
producer.producer = Mock()
|
||||||
|
|
||||||
|
task = event_loop.create_task(producer.produce())
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
producer.producer.send_and_wait.assert_called_with(
|
||||||
|
config.kafka.topic, bytes(check.json().encode('utf-8')))
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_producer_called_invalid(config, event_loop):
|
||||||
|
queue = asyncio.Queue()
|
||||||
|
producer = Producer(config, Mock(), event_loop, queue)
|
||||||
|
check = Check()
|
||||||
|
await queue.put('')
|
||||||
|
|
||||||
|
async def async_patch():
|
||||||
|
pass
|
||||||
|
Mock.__await__ = lambda x: async_patch().__await__()
|
||||||
|
|
||||||
|
producer.producer = Mock()
|
||||||
|
|
||||||
|
task = event_loop.create_task(producer.produce())
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
producer.logger.error.assert_called()
|
||||||
|
assert task.done()
|
1
tox.ini
1
tox.ini
|
@ -5,6 +5,7 @@ envlist = clean,lint,py3,report
|
||||||
deps =
|
deps =
|
||||||
mock
|
mock
|
||||||
pytest
|
pytest
|
||||||
|
pytest-asyncio
|
||||||
pytest-cov
|
pytest-cov
|
||||||
pytest-mock
|
pytest-mock
|
||||||
commands =
|
commands =
|
||||||
|
|
Loading…
Reference in a new issue