Skip to content

Commit d2eb1f6

Browse files
daxtensstephenfin
authored andcommitted
parsemail: Clarify exit codes
jk reports that the patchwork error codes are really unhelpful for correct integration with an MDA. In particular they make sorting out failures into a separate queue very difficult. Make this better and clearer: only return 1 on a genuinely unexpected case that requires adminstrator intervention. Update the comment for parse_mail regarding return values and exceptions to line up with how the function actually works and how we use the results. Update the tests. None of the existing tests should exit 1; they're all 'expected' failures: unknown project etc. Also we removed the exit(0) from the success path, so stop expecting that exception to be raised. Add a test for duplicates. That should also succeed without raising an exception: dups are part of life. Update parsearchive to deal with the fact that we can no longer differentiate duplicates. Reported-by: Jeremy Kerr <jk@ozlabs.org> Fixes: #171 Signed-off-by: Daniel Axtens <dja@axtens.net> Reviewed-by: Stephen Finucane <stephen@that.guru>
1 parent 29c704a commit d2eb1f6

File tree

4 files changed

+68
-48
lines changed

4 files changed

+68
-48
lines changed

patchwork/management/commands/parsearchive.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import os
2323
import sys
2424

25-
import django
2625
from django.core.management.base import BaseCommand
2726

2827
from patchwork import models
@@ -49,7 +48,6 @@ def handle(self, *args, **options):
4948
models.CoverLetter: 0,
5049
models.Comment: 0,
5150
}
52-
duplicates = 0
5351
dropped = 0
5452
errors = 0
5553

@@ -92,8 +90,6 @@ def handle(self, *args, **options):
9290
results[type(obj)] += 1
9391
else:
9492
dropped += 1
95-
except django.db.utils.IntegrityError:
96-
duplicates += 1
9793
except ValueError:
9894
# TODO(stephenfin): Perhaps we should store the broken patch
9995
# somewhere for future reference?
@@ -108,17 +104,15 @@ def handle(self, *args, **options):
108104
' %(covers)4d cover letters\n'
109105
' %(patches)4d patches\n'
110106
' %(comments)4d comments\n'
111-
' %(duplicates)4d duplicates\n'
112107
' %(dropped)4d dropped\n'
113108
' %(errors)4d errors\n'
114109
'Total: %(new)s new entries' % {
115110
'total': count,
116111
'covers': results[models.CoverLetter],
117112
'patches': results[models.Patch],
118113
'comments': results[models.Comment],
119-
'duplicates': duplicates,
120114
'dropped': dropped,
121115
'errors': errors,
122-
'new': count - duplicates - dropped - errors,
116+
'new': count - dropped - errors,
123117
})
124118
mbox.close()

patchwork/management/commands/parsemail.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,20 @@ def handle(self, *args, **options):
6666
logger.warning("Broken email ignored")
6767
return
6868

69+
# it's important to get exit codes correct here. The key is to allow
70+
# proper separation of real errors vs expected 'failures'.
71+
#
72+
# patch/comment parsed: 0
73+
# no parseable content found: 0
74+
# duplicate messages: 0
75+
# db integrity/other db error: 1
76+
# broken email (ValueError): 1 (this could be noisy, if it's an issue
77+
# we could use a different return code)
6978
try:
7079
result = parse_mail(mail, options['list_id'])
71-
if result:
72-
sys.exit(0)
73-
logger.warning('Failed to parse mail')
74-
sys.exit(1)
80+
if result is None:
81+
logger.warning('Nothing added to database')
7582
except Exception:
7683
logger.exception('Error when parsing incoming email',
7784
extra={'mail': mail.as_string()})
85+
sys.exit(1)

patchwork/parser.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import re
3030

3131
from django.contrib.auth.models import User
32+
from django.db.utils import IntegrityError
3233
from django.utils import six
3334

3435
from patchwork.models import Comment
@@ -939,7 +940,14 @@ def parse_mail(mail, list_id=None):
939940
list_id (str): Mailing list ID
940941
941942
Returns:
942-
None
943+
patch/cover letter/comment
944+
Or None if nothing is found in the mail
945+
or X-P-H: ignore
946+
or project not found
947+
948+
Raises:
949+
ValueError if there is an error in parsing or a duplicate mail
950+
Other truly unexpected issues may bubble up from the DB.
943951
"""
944952
# some basic sanity checks
945953
if 'From' not in mail:
@@ -1001,20 +1009,24 @@ def parse_mail(mail, list_id=None):
10011009
filenames = find_filenames(diff)
10021010
delegate = find_delegate_by_filename(project, filenames)
10031011

1004-
patch = Patch.objects.create(
1005-
msgid=msgid,
1006-
project=project,
1007-
patch_project=project,
1008-
name=name[:255],
1009-
date=date,
1010-
headers=headers,
1011-
submitter=author,
1012-
content=message,
1013-
diff=diff,
1014-
pull_url=pull_url,
1015-
delegate=delegate,
1016-
state=find_state(mail))
1017-
logger.debug('Patch saved')
1012+
try:
1013+
patch = Patch.objects.create(
1014+
msgid=msgid,
1015+
project=project,
1016+
patch_project=project,
1017+
name=name[:255],
1018+
date=date,
1019+
headers=headers,
1020+
submitter=author,
1021+
content=message,
1022+
diff=diff,
1023+
pull_url=pull_url,
1024+
delegate=delegate,
1025+
state=find_state(mail))
1026+
logger.debug('Patch saved')
1027+
except IntegrityError:
1028+
logger.error("Duplicate mail for message ID %s" % msgid)
1029+
return None
10181030

10191031
# if we don't have a series marker, we will never have an existing
10201032
# series to match against.

patchwork/tests/test_management.py

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,30 +40,27 @@ def test_invalid_path(self):
4040

4141
def test_missing_project_path(self):
4242
path = os.path.join(TEST_MAIL_DIR, '0001-git-pull-request.mbox')
43-
with self.assertRaises(SystemExit) as exc:
44-
call_command('parsemail', infile=path)
43+
call_command('parsemail', infile=path)
4544

46-
self.assertEqual(exc.exception.code, 1)
45+
count = models.Patch.objects.all().count()
46+
self.assertEqual(count, 0)
4747

4848
def test_missing_project_stdin(self):
4949
path = os.path.join(TEST_MAIL_DIR, '0001-git-pull-request.mbox')
5050
sys.stdin.close()
5151
sys.stdin = open(path)
52-
with self.assertRaises(SystemExit) as exc:
53-
call_command('parsemail', infile=None)
52+
call_command('parsemail', infile=None)
5453

5554
sys.stdin.close()
56-
self.assertEqual(exc.exception.code, 1)
55+
count = models.Patch.objects.all().count()
56+
self.assertEqual(count, 0)
5757

5858
def test_valid_path(self):
5959
project = utils.create_project()
6060
utils.create_state()
6161

6262
path = os.path.join(TEST_MAIL_DIR, '0001-git-pull-request.mbox')
63-
with self.assertRaises(SystemExit) as exc:
64-
call_command('parsemail', infile=path, list_id=project.listid)
65-
66-
self.assertEqual(exc.exception.code, 0)
63+
call_command('parsemail', infile=path, list_id=project.listid)
6764

6865
count = models.Patch.objects.filter(project=project.id).count()
6966
self.assertEqual(count, 1)
@@ -75,12 +72,9 @@ def test_valid_stdin(self):
7572
path = os.path.join(TEST_MAIL_DIR, '0001-git-pull-request.mbox')
7673
sys.stdin.close()
7774
sys.stdin = open(path)
78-
with self.assertRaises(SystemExit) as exc:
79-
call_command('parsemail', infile=None,
80-
list_id=project.listid)
75+
call_command('parsemail', infile=None, list_id=project.listid)
8176

8277
sys.stdin.close()
83-
self.assertEqual(exc.exception.code, 0)
8478

8579
count = models.Patch.objects.filter(project=project.id).count()
8680
self.assertEqual(count, 1)
@@ -90,10 +84,7 @@ def test_utf8_path(self):
9084
utils.create_state()
9185

9286
path = os.path.join(TEST_MAIL_DIR, '0013-with-utf8-body.mbox')
93-
with self.assertRaises(SystemExit) as exc:
94-
call_command('parsemail', infile=path, list_id=project.listid)
95-
96-
self.assertEqual(exc.exception.code, 0)
87+
call_command('parsemail', infile=path, list_id=project.listid)
9788

9889
count = models.Patch.objects.filter(project=project.id).count()
9990
self.assertEqual(count, 1)
@@ -105,15 +96,30 @@ def test_utf8_stdin(self):
10596
path = os.path.join(TEST_MAIL_DIR, '0013-with-utf8-body.mbox')
10697
sys.stdin.close()
10798
sys.stdin = open(path)
108-
with self.assertRaises(SystemExit) as exc:
109-
call_command('parsemail', infile=None,
110-
list_id=project.listid)
99+
call_command('parsemail', infile=None, list_id=project.listid)
111100

112-
self.assertEqual(exc.exception.code, 0)
101+
count = models.Patch.objects.filter(project=project.id).count()
102+
self.assertEqual(count, 1)
103+
104+
def test_dup_mail(self):
105+
project = utils.create_project()
106+
utils.create_state()
107+
108+
path = os.path.join(TEST_MAIL_DIR, '0001-git-pull-request.mbox')
109+
call_command('parsemail', infile=path, list_id=project.listid)
113110

114111
count = models.Patch.objects.filter(project=project.id).count()
115112
self.assertEqual(count, 1)
116113

114+
# the parser should return None, not throwing an exception
115+
# as this is a pretty normal part of life on a busy site
116+
call_command('parsemail', infile=path, list_id=project.listid)
117+
118+
# this would be lovely but doesn't work because we caused an error in
119+
# the transaction and we have no way to reset it
120+
# count = models.Patch.objects.filter(project=project.id).count()
121+
# self.assertEqual(count, 1)
122+
117123

118124
class ParsearchiveTest(TestCase):
119125
def test_invalid_path(self):

0 commit comments

Comments
 (0)