Skip to content

Commit 2e19035

Browse files
committed
Added context manager dbdiff.exact
1 parent 39971e8 commit 2e19035

File tree

6 files changed

+141
-3
lines changed

6 files changed

+141
-3
lines changed

README.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ Example::
8080
If any difference is found between the database and the test fixture, then
8181
``diff()`` will return the diff as outputed by GNU diff.
8282

83+
A context manager that will raise an exception if a diff is found is also
84+
provided, it's able to delete models and reset the PK sequences for them::
85+
86+
with dbdiff.exact('your_app/fixture.json'):
87+
# do stuff
88+
8389
More public API tests can be found in dbidff/tests/test_dbdiff.py.
8490

8591
Django model observer

dbdiff/dbdiff.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,95 @@
11
"""Public API for dbdiff."""
22

33
from django.apps import apps
4+
from django.core.management.color import no_style
5+
from django.db import connections
46

7+
from .exceptions import DiffFound
58
from .fixture_diff import FixtureDiff
69

710
__all__ = ('diff',)
811

912
DEBUG = apps.get_app_config('dbdiff').debug
1013

1114

15+
class exact(object): # noqa
16+
"""
17+
Context manager interface for dbdiff.
18+
19+
To clean up the database from any model that's provided by the fixture on
20+
context enter, and raise a :py:class:`~dbdiff.exceptions.DiffFound` on
21+
context enter if any diff was found::
22+
23+
with dbdiff.exact('your_app/tests/your_test_expected.json'):
24+
# do stuff
25+
26+
.. py:attribute:: diff
27+
28+
:py:class:`~dbdiff.fixture_diff.FixtureDiff` instance for fixture.
29+
30+
.. py:attribute:: database
31+
32+
Database to use, 'default' by default.
33+
34+
.. py:attribute:: reset_models
35+
36+
If False, do not delete models and reset PK sequences on context enter.
37+
"""
38+
39+
def __init__(self, fixture, reset_models=True, database=None):
40+
"""Instanciate with a fixture."""
41+
self.diff = FixtureDiff(fixture)
42+
self.database = database if database else 'default'
43+
self.reset_models = reset_models
44+
45+
def __enter__(self):
46+
if not self.reset_models:
47+
return self
48+
49+
tables = set()
50+
51+
for model_name in self.diff.fixture_models:
52+
model = apps.get_model(model_name)
53+
tables.add(model._meta.db_table)
54+
tables.update(f.m2m_db_table() for f in
55+
model._meta.local_many_to_many)
56+
57+
connection = connections[self.database]
58+
59+
statements = connection.ops.sql_flush(
60+
no_style(),
61+
list(tables),
62+
connection.introspection.sequence_list(),
63+
True
64+
)
65+
if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3':
66+
# Initially, we were just doing a model.objects.all().delete() in
67+
# this method, and it worked only in SQLite:
68+
# https://travis-ci.org/yourlabs/django-dbdiff/builds/100059628
69+
# That's why this method now uses sqlflush method, but then PKs are
70+
# not reset anymore in SQLite:
71+
# http://stackoverflow.com/questions/24098733/why-doesnt-django-reset-sequences-in-sqlite3 # noqa
72+
statements += [
73+
"UPDATE SQLITE_SEQUENCE SET SEQ=0 WHERE NAME='%s';" % t
74+
for t in tables
75+
]
76+
cursor = connection.cursor()
77+
78+
for statement in statements:
79+
cursor.execute(statement)
80+
81+
return self
82+
83+
def __exit__(self, exception_type, exception_value, traceback):
84+
out = self.diff.get_diff()
85+
86+
if not out:
87+
self.diff.clean()
88+
else:
89+
print(self.diff.cmd)
90+
raise DiffFound(out)
91+
92+
1293
def diff(fixture):
1394
"""
1495
Return the diff between the database and a fixture file as a string.
@@ -28,6 +109,8 @@ def diff(fixture):
28109
except:
29110
if not DEBUG:
30111
diff.clean()
112+
else:
113+
print(diff.cmd)
31114
raise
32115

33116
if not DEBUG:

dbdiff/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,11 @@ class EmptyFixtures(DbDiffException):
1111
def __init__(self, path):
1212
"""Exception for when path does not contain any fixture data."""
1313
super(EmptyFixtures, self).__init__('%s is empty' % path)
14+
15+
16+
class DiffFound(DbDiffException):
17+
"""Raised when a diff is found by the context manager."""
18+
19+
def __init__(self, out):
20+
"""Exception for when a diff command had output."""
21+
super(DiffFound, self).__init__(out)

dbdiff/fixture_diff.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ def get_diff(self):
7070
traceback=True, indent=self.fixture_indent,
7171
stdout=f)
7272

73-
cmd = 'diff -u1 %s %s | sed "1,2 d"' % (
73+
self.cmd = 'diff -u1 %s %s | sed "1,2 d"' % (
7474
self.fixture_path, self.dump_path)
7575

7676
if apps.get_app_config('dbdiff').debug: # pragma: no cover
77-
print(cmd)
77+
print(self.cmd)
7878

79-
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
79+
proc = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, shell=True)
8080
out, err = proc.communicate()
8181

8282
return out
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"fields": {
4+
"name": "initial_name",
5+
"permissions": []
6+
},
7+
"model": "auth.group",
8+
"pk": 1
9+
}
10+
]

dbdiff/tests/test_dbdiff.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django import test
88
from django.apps import apps
99
from django.conf import settings
10+
from django.contrib.auth.models import Group
1011

1112
import mock
1213

@@ -15,6 +16,7 @@
1516
from . import base
1617
from .project.decimal_test.models import TestModel as DecimalTestModel
1718
from .. import dbdiff
19+
from ..exceptions import DiffFound
1820

1921

2022
@mock.patch('dbdiff.dbdiff.FixtureDiff')
@@ -77,6 +79,35 @@ def test_data_diff_has_changed(self):
7779
''').lstrip()
7880

7981

82+
class ResetModelsTest(test.TestCase):
83+
def test_pk_reset(self):
84+
fixture = 'dbdiff/fixtures/dbdiff_test_group.json'
85+
Group.objects.create(name='noise')
86+
87+
with dbdiff.exact(fixture):
88+
Group.objects.create(name='initial_name')
89+
90+
with dbdiff.exact(fixture, reset_models=False):
91+
pass
92+
93+
with self.assertRaises(DiffFound) as e:
94+
with dbdiff.exact(fixture):
95+
Group.objects.create(name='different_name')
96+
97+
expected = six.b('''
98+
@@ -3,3 +3,3 @@
99+
"fields": {
100+
- "name": "initial_name",
101+
+ "name": "different_name",
102+
"permissions": []
103+
''').lstrip()
104+
105+
if six.PY2:
106+
assert e.exception.message == expected
107+
else:
108+
assert e.exception.args[0] == expected
109+
110+
80111
class DecimalDiffTest(test.TestCase):
81112
fixtures = ['decimal_test_fixture']
82113
expected = os.path.join(

0 commit comments

Comments
 (0)