Skip to content

Commit 742bc1f

Browse files
authored
Merge pull request #15 from suutari-ai/coverage
Improve test coverage etc.
2 parents 99695a4 + c1231fa commit 742bc1f

23 files changed

+262
-66
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ cache: pip
44
python:
55
- "2.7"
66
- "3.4"
7+
- "3.5"
8+
- "3.6"
79
install:
810
- pip install -r requirements-test.txt
911
script:
1012
- py.test -vvv --cov database_sanitizer --cov-report=term-missing
1113
after_success:
1214
- curl -s https://codecov.io/bash | bash
13-

database_sanitizer/__main__.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@
1010
from .dump import run
1111

1212

13-
def main(args=None):
14-
if args is None:
15-
args = sys.argv[1:]
16-
13+
def main(argv=sys.argv):
1714
parser = argparse.ArgumentParser(
15+
prog=(argv[0] if len(argv) else "database-sanitizer"),
1816
description="Sanitizes contents of databases.",
1917
)
2018
parser.add_argument(
@@ -30,16 +28,16 @@ def main(args=None):
3028
type=str,
3129
dest="output",
3230
help=(
33-
"Path to the file where the sanitized database will be written into. "
34-
"If omitted, standard output will be used instead."
31+
"Path to the file where the sanitized database will be written "
32+
"into. If omitted, standard output will be used instead."
3533
),
3634
)
3735
parser.add_argument(
3836
"url",
3937
help="Database URL to which to connect into and sanitize contents.",
4038
)
4139

42-
args = parser.parse_args(args=args)
40+
args = parser.parse_args(args=argv[1:])
4341
output = sys.stdout
4442
config = None
4543

database_sanitizer/config.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import unicode_literals
44

55
import importlib
6+
67
import six
78
import yaml
89

@@ -22,7 +23,7 @@ class Configuration(object):
2223
"""
2324
def __init__(self):
2425
self.sanitizers = {}
25-
self.addon_packages = None
26+
self.addon_packages = []
2627

2728
@classmethod
2829
def from_file(cls, filename):
@@ -83,10 +84,8 @@ def load_addon_packages(self, config_data):
8384
),
8485
)
8586

86-
section_addons = section_config.get("addons")
87+
section_addons = section_config.get("addons", [])
8788
if not isinstance(section_addons, list):
88-
if section_addons is None:
89-
return
9089
raise ConfigurationError(
9190
"'config.addons' is %s instead of list" % (
9291
type(section_addons),
@@ -102,7 +101,7 @@ def load_addon_packages(self, config_data):
102101
),
103102
)
104103

105-
self.addon_packages = tuple(section_addons)
104+
self.addon_packages = list(section_addons)
106105

107106
def load_sanitizers(self, config_data):
108107
"""
@@ -203,18 +202,17 @@ def find_sanitizer(self, name):
203202

204203
# Phase 2: Look for the sanitizer under "addon" packages, if any of
205204
# such have been defined.
206-
if self.addon_packages:
207-
for addon_package_name in self.addon_packages:
208-
module_name = "%s.%s" % (
209-
addon_package_name,
210-
module_name_suffix,
211-
)
212-
callback = self.find_sanitizer_from_module(
213-
module_name=module_name,
214-
function_name=function_name,
215-
)
216-
if callback:
217-
return callback
205+
for addon_package_name in self.addon_packages:
206+
module_name = "%s.%s" % (
207+
addon_package_name,
208+
module_name_suffix,
209+
)
210+
callback = self.find_sanitizer_from_module(
211+
module_name=module_name,
212+
function_name=function_name,
213+
)
214+
if callback:
215+
return callback
218216

219217
# Phase 3: Look from builtin sanitizers.
220218
module_name = "database_sanitizer.sanitizers.%s" % (module_name_suffix,)

database_sanitizer/dump/mysql.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
get_mysqldump_args_and_env_from_url,
1313
)
1414

15-
1615
#: Regular expression which matches `INSERT INTO` statements produced by the
1716
#: `mysqldump` utility, even when extended inserts have been enabled.
1817
INSERT_INTO_PATTERN = re.compile(

database_sanitizer/dump/postgres.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
from ..utils.postgres import decode_copy_value, encode_copy_value
1010

11-
1211
COPY_LINE_PATTERN = re.compile(
1312
r"^COPY \"(?P<schema>[^\"]*)\".\"(?P<table>[^\"]*)\" "
1413
r"\((?P<columns>.*)\) "

database_sanitizer/tests/_compat.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

database_sanitizer/tests/test_config.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,25 @@
22

33
from __future__ import unicode_literals
44

5-
import pytest
6-
75
from collections import namedtuple
86

7+
import mock
8+
import pytest
9+
10+
from .. import config
911
from ..config import Configuration, ConfigurationError
1012

11-
from ._compat import mock
13+
14+
@mock.patch.object(config, 'open')
15+
@mock.patch('yaml.load')
16+
def test_from_file(mocked_yaml_load, mocked_open):
17+
mocked_yaml_load.return_value = {}
18+
19+
Configuration.from_file('filename.yml')
20+
21+
assert mocked_open.call_args == (('filename.yml', 'rb'), {})
22+
opened_file = mocked_open.return_value.__enter__.return_value
23+
assert mocked_yaml_load.call_args == ((opened_file,), {})
1224

1325

1426
def test_load_config_data_must_be_dict():
@@ -22,13 +34,13 @@ def test_load_addon_packages():
2234
config = Configuration()
2335

2436
config.load_addon_packages({})
25-
assert config.addon_packages is None
37+
assert config.addon_packages == []
2638

2739
with pytest.raises(ConfigurationError):
2840
config.load_addon_packages({"config": "test"})
2941

3042
config.load_addon_packages({"config": {}})
31-
assert config.addon_packages is None
43+
assert config.addon_packages == []
3244

3345
with pytest.raises(ConfigurationError):
3446
config.load_addon_packages({"config": {"addons": "test"}})
@@ -43,7 +55,7 @@ def test_load_addon_packages():
4355
"test3",
4456
],
4557
}})
46-
assert config.addon_packages == ("test1", "test2", "test3")
58+
assert config.addon_packages == ["test1", "test2", "test3"]
4759

4860

4961
def test_load_sanitizers():

database_sanitizer/tests/test_dump.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import subprocess
2+
from io import BytesIO, StringIO
3+
4+
import mock
5+
import pytest
6+
7+
from database_sanitizer import dump
8+
9+
EXPECTED_POPEN_KWARGS = {
10+
'mysql://User:Pass@HostName/Db': {
11+
'args': (
12+
'mysqldump --complete-insert --extended-insert'
13+
' --net_buffer_length=10240 -h hostname -u User Db').split(),
14+
'env': {'MYSQL_PWD': 'Pass'},
15+
'stdout': subprocess.PIPE,
16+
},
17+
'postgres:///Db': {
18+
'args': tuple((
19+
'pg_dump --encoding=utf-8 --quote-all-identifiers'
20+
' --dbname postgres:///Db').split()),
21+
'stdout': subprocess.PIPE,
22+
},
23+
}
24+
25+
for url in ['postgresql:///Db', 'postgis:///Db']:
26+
EXPECTED_POPEN_KWARGS[url] = EXPECTED_POPEN_KWARGS['postgres:///Db'].copy()
27+
EXPECTED_POPEN_KWARGS[url]['args'] = tuple(
28+
' '.join(EXPECTED_POPEN_KWARGS[url]['args'])
29+
.replace('postgres', 'postgresql').split())
30+
31+
32+
@pytest.mark.parametrize('url', list(EXPECTED_POPEN_KWARGS))
33+
@mock.patch('subprocess.Popen')
34+
def test_run(mocked_popen, url):
35+
mocked_popen.return_value.stdout = BytesIO(b'INPUT DUMP')
36+
output = StringIO()
37+
config = None
38+
dump.run(url, output, config)
39+
40+
expected_popen_kwargs = EXPECTED_POPEN_KWARGS[url]
41+
(popen_args, popen_kwargs) = mocked_popen.call_args
42+
expected_popen_args = (
43+
(expected_popen_kwargs.pop('args'),) if popen_args else ())
44+
assert popen_args == expected_popen_args
45+
assert popen_kwargs == expected_popen_kwargs
46+
47+
48+
@mock.patch('subprocess.Popen')
49+
def test_run_unknown_scheme(mocked_popen):
50+
with pytest.raises(ValueError) as excinfo:
51+
dump.run('unknown:///db', None, None)
52+
assert str(excinfo.value) == "Unsupported database scheme: 'unknown'"
53+
mocked_popen.assert_not_called()

database_sanitizer/tests/test_dump_mysql.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from __future__ import unicode_literals
44

55
import io
6-
import pytest
76

7+
import pytest
88
from six.moves.urllib import parse as urlparse
99

1010
from ..config import Configuration
@@ -15,7 +15,6 @@
1515
sanitize_from_stream,
1616
)
1717

18-
1918
MOCK_MYSQLDUMP_OUTPUT = b"""
2019
--- Fake MySQL database dump
2120
@@ -89,7 +88,43 @@ def test_parse_column_names(text, expected_column_names):
8988
("('test'),('test')", (("test",), ("test",))),
9089
("(1,2),(3,4),", ((1, 2), (3, 4))),
9190
("(TRUE),(FALSE),(NULL)", ((True,), (False,), (None,))),
91+
("(x')", ()), # Invalid data
9292
),
9393
)
9494
def test_parse_values(text, expected_values):
9595
assert tuple(parse_values(text)) == expected_values
96+
97+
98+
@pytest.mark.parametrize('config_type', [
99+
'no-config', 'empty-config', 'single-column-config'])
100+
@pytest.mark.parametrize('data_label', ['ok', 'invalid'])
101+
def test_optimizations(config_type, data_label):
102+
if config_type == 'no-config':
103+
config = None
104+
decoder_call_count = 0
105+
else:
106+
config = Configuration()
107+
if config_type == 'empty-config':
108+
decoder_call_count = 0
109+
else:
110+
assert config_type == 'single-column-config'
111+
config.sanitizers["test.notes"] = (lambda x: x)
112+
decoder_call_count = 3 # Number of rows in test table
113+
114+
data = {
115+
'ok': MOCK_MYSQLDUMP_OUTPUT,
116+
'invalid': INVALID_MOCK_MYSQLDUMP_OUTPUT,
117+
}[data_label]
118+
119+
should_raise = (
120+
config_type == 'single-column-config'
121+
and data_label == 'invalid')
122+
123+
dump_stream = io.BytesIO(data)
124+
if should_raise:
125+
with pytest.raises(ValueError):
126+
list(sanitize_from_stream(dump_stream, config))
127+
else:
128+
expected_output = data.decode('utf-8').splitlines()
129+
result = list(sanitize_from_stream(dump_stream, config))
130+
assert result == expected_output

database_sanitizer/tests/test_dump_postgres.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,17 @@
33
from __future__ import unicode_literals
44

55
import io
6-
import pytest
7-
86
from collections import namedtuple
7+
8+
import mock
9+
import pytest
910
from six.moves.urllib import parse as urlparse
1011

1112
from ..config import Configuration
12-
from ..dump.postgres import parse_column_names, parse_values, sanitize
1313
from ..dump import postgres as dump_postgres
14+
from ..dump.postgres import parse_column_names, parse_values, sanitize
1415
from ..utils.postgres import decode_copy_value
1516

16-
from ._compat import mock
17-
18-
1917
MOCK_PG_DUMP_OUTPUT = b"""
2018
--- Fake PostgreSQL database dump
2119

0 commit comments

Comments
 (0)