Projet

Général

Profil

0001-workflows-only-log-condition-errors-as-functional-er.patch

Thomas Noël, 03 juillet 2018 15:21

Télécharger (15,8 ko)

Voir les différences:

Subject: [PATCH] workflows: only log condition errors as functional errors
 (#24472)

 tests/test_backoffice_pages.py | 18 +++++++----
 tests/test_form_pages.py       |  3 +-
 tests/test_workflows.py        | 58 +++++++++++++++++++++++++++++++++-
 wcs/admin/logged_errors.py     | 30 ++++++++++++++++--
 wcs/conditions.py              | 10 ++++--
 wcs/logged_errors.py           | 30 +++++++++++++++---
 wcs/wf/jump.py                 |  3 +-
 wcs/workflows.py               |  3 +-
 8 files changed, 136 insertions(+), 19 deletions(-)
tests/test_backoffice_pages.py
3907 3907

  
3908 3908
    app = login(get_app(pub))
3909 3909
    resp = app.get('/backoffice/forms/%s/' % formdef.id)
3910
    assert 'ZeroDivisionError' in resp.body
3910
    assert 'Failed to evaluate condition' in resp.body
3911
    assert 'error ZeroDivisionError' in resp.body
3911 3912
    resp = resp.click('1 error')
3912 3913

  
3913 3914
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
3914 3915
    resp2 = resp.click('1 error')
3915
    assert 'ZeroDivisionError' in resp2.body
3916
    resp = resp2.click('ZeroDivisionError')
3917
    assert 'integer division or modulo by zero' in resp.body
3916
    assert 'Failed to evaluate condition' in resp2.body
3917
    assert 'error ZeroDivisionError' in resp2.body
3918
    resp = resp2.click('Failed to evaluate condition')
3919
    assert 'ZeroDivisionError: integer division or modulo by zero' in resp.body
3920
    assert 'Python Expression: <pre>1/0</pre>' in resp.body
3918 3921
    assert not 'Acked' in resp.body
3919 3922
    resp = resp.click('Ack').follow()
3920 3923
    assert 'Acked' in resp.body
......
3933 3936

  
3934 3937
    app = login(get_app(pub))
3935 3938
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
3936
    assert 'ZeroDivisionError' in resp.body
3939
    assert 'Failed to evaluate condition' in resp2.body
3940
    assert 'error ZeroDivisionError' in resp2.body
3937 3941
    resp2 = resp.click('1 error')
3938
    resp = resp2.click('ZeroDivisionError')
3942
    resp = resp2.click('Failed to evaluate condition')
3939 3943
    assert 'href="http://example.net/backoffice/management/test/' in resp.body
3940 3944

  
3941 3945
    # very long error string (check it creates a viable tech_id)
......
3950 3954

  
3951 3955
    # remove formdef
3952 3956
    FormDef.wipe()
3953
    resp = resp2.click('ZeroDivisionError')
3957
    resp = resp2.click('Failed to evaluate condition')
3954 3958
    assert not 'href="http://example.net/backoffice/management/test/' in resp.body
3955 3959

  
3956 3960
def test_backoffice_private_status_and_history(pub):
tests/test_form_pages.py
4492 4492
    assert LoggedError.count() == 1
4493 4493

  
4494 4494
    error = LoggedError.get_on_index(
4495
            '34-12-None-None-zerodivisionerror-integer-division-or-modulo-by-zero', 'tech_id')
4495
        '34-12-just_submitted-_jump-failed-to-evaluate-condition-ZeroDivisionError-integer-division-or-modulo-by-zero',
4496
        'tech_id')
4496 4497
    assert error.occurences_count == 2
4497 4498

  
4498 4499
    assert len(LoggedError.get_ids_with_indexed_value('formdef_id', '34')) == 1
tests/test_workflows.py
19 19
from wcs import sessions
20 20
from wcs.fields import (StringField, DateField, MapField, FileField, ItemField,
21 21
        ItemsField, CommentField)
22
from wcs.logged_errors import LoggedError
22 23
from wcs.roles import Role
23 24
from wcs.workflows import (Workflow, WorkflowStatusItem,
24 25
        SendmailWorkflowStatusItem, SendSMSWorkflowStatusItem,
......
258 259
            'value': 'utils.time_delta(utils.time.localtime(), "2015-01-04").days > 0'}
259 260
    assert item.must_jump(formdata) is True
260 261

  
261

  
262 262
def test_jump_count_condition(pub):
263 263
    FormDef.wipe()
264 264
    formdef = FormDef()
......
278 278
    item.condition = {'type': 'python', 'value': 'form_objects.count < 2'}
279 279
    assert item.must_jump(formdata) is False
280 280

  
281
def test_jump_bad_python_condition(pub):
282
    FormDef.wipe()
283
    formdef = FormDef()
284
    formdef.name = 'foobar'
285
    formdef.store()
286
    pub.substitutions.feed(formdef)
287
    formdef.data_class().wipe()
288
    formdata = formdef.data_class()()
289
    item = JumpWorkflowStatusItem()
290

  
291
    LoggedError.wipe()
292
    item.condition = {'type': 'python', 'value': 'form_var_foobar == 0'}
293
    assert item.must_jump(formdata) is False
294
    assert LoggedError.count() == 1
295
    logged_error = LoggedError.select()[0]
296
    assert logged_error.summary == 'Failed to evaluate condition'
297
    assert logged_error.exception_class == 'NameError'
298
    assert logged_error.exception_message == "name 'form_var_foobar' is not defined"
299
    assert logged_error.expression == 'form_var_foobar == 0'
300
    assert logged_error.expression_type == 'python'
301

  
302
    LoggedError.wipe()
303
    item.condition = {'type': 'python', 'value': '~ invalid ~'}
304
    assert item.must_jump(formdata) is False
305
    assert LoggedError.count() == 1
306
    logged_error = LoggedError.select()[0]
307
    assert logged_error.summary == 'Failed to evaluate condition'
308
    assert logged_error.exception_class == 'SyntaxError'
309
    assert logged_error.exception_message == 'unexpected EOF while parsing (<string>, line 1)'
310
    assert logged_error.expression == '~ invalid ~'
311
    assert logged_error.expression_type == 'python'
312

  
281 313
def test_jump_django_conditions(pub):
282 314
    FormDef.wipe()
283 315
    formdef = FormDef()
......
289 321
    pub.substitutions.feed(formdata)
290 322
    item = JumpWorkflowStatusItem()
291 323

  
324
    LoggedError.wipe()
292 325
    item.condition = {'type': 'django', 'value': '1 < 2'}
293 326
    assert item.must_jump(formdata) is True
294 327

  
......
301 334
    item.condition = {'type': 'django', 'value': 'form_var_foo|first|upper == "X"'}
302 335
    assert item.must_jump(formdata) is False
303 336

  
337
    assert LoggedError.count() == 0
338

  
304 339
    item.condition = {'type': 'django', 'value': '~ invalid ~'}
305 340
    assert item.must_jump(formdata) is False
341
    assert LoggedError.count() == 1
342
    logged_error = LoggedError.select()[0]
343
    assert logged_error.summary == 'Failed to evaluate condition'
344
    assert logged_error.exception_class == 'TemplateSyntaxError'
345
    assert logged_error.exception_message == "Could not parse the remainder: '~' from '~'"
346
    assert logged_error.expression == '~ invalid ~'
347
    assert logged_error.expression_type == 'django'
306 348

  
307 349
def test_check_auth(pub):
308 350
    user = pub.user_class(name='foo')
......
3504 3546
    choice.condition = {'type': 'python', 'value': 'form_name == "baz"'}
3505 3547
    workflow.store()
3506 3548
    assert len(FormDef.get(formdef.id).data_class().get_actionable_ids([role.id])) == 2
3549

  
3550
    # bad condition
3551
    LoggedError.wipe()
3552
    choice.condition = {'type': 'python', 'value': 'foobar == barfoo'}
3553
    workflow.store()
3554
    assert len(FormDef.get(formdef.id).data_class().get_actionable_ids([role.id])) == 0
3555
    assert LoggedError.count() == 1
3556
    logged_error = LoggedError.select()[0]
3557
    assert logged_error.occurences_count > 1  # should be 2... == 12 with pickle, 4 with sql
3558
    assert logged_error.summary == 'Failed to evaluate condition'
3559
    assert logged_error.exception_class == 'NameError'
3560
    assert logged_error.exception_message == "name 'foobar' is not defined"
3561
    assert logged_error.expression == 'foobar == barfoo'
3562
    assert logged_error.expression_type == 'python'
wcs/admin/logged_errors.py
70 70
                            _(status_item.description))
71 71
                r += htmltext('</ul>')
72 72

  
73

  
74
        if self.error.expression or self.error.expression_type:
75
            expression_title = {
76
                'python': N_('Python Expression'),
77
                'django': N_('Django Expression'),
78
                'template': N_('Template'),
79
                'text': N_('Text'),
80
            }.get(self.error.expression_type, N_('Unknown'))
81
            r += htmltext('  <li>%s <code>%s</code></li>') % (
82
                    _('%s:') % expression_title, self.error.expression)
83

  
84
        if self.error.exception_class or self.error.exception_message:
85
            r += htmltext('  <li>%s <code>%s: %s</code></li>') % (_('Error message:'),
86
                                                                self.error.exception_class,
87
                                                                self.error.exception_message)
88

  
73 89
        if formdef:
74 90
            formdata = self.error.get_formdata()
75 91
            if formdata:
......
159 175
                '%(count)d error', '%(count)d errors', len(errors)) % {'count': len(errors)}
160 176
        r += htmltext('<ul>')
161 177
        for error in errors[:3]:
162
            r += htmltext('<li><a href="logged-errors/%s/">%s</a></li>') % (error.id, error.summary)
178
            r += htmltext('<li><a href="logged-errors/%s/">%s</a> ') % (error.id, error.summary)
179
            if error.exception_class or error.exception_message:
180
                r += htmltext(_('error %(class)s (%(message)s)')) % {
181
                    'class': error.exception_class,
182
                    'message': error.exception_message,
183
                }
163 184
            r += htmltext('</li>')
164 185
        if len(errors) > 3:
165 186
            r += htmltext('<li>...</li>')
......
180 201
        r += htmltext('<h2>%s</h2>') % _('Logged Errors')
181 202
        r += htmltext('<ul class="biglist">')
182 203
        for error in self.get_errors(formdef_id=self.formdef_id, workflow_id=self.workflow_id):
183
            r += htmltext('<li><strong><a href="%s/">%s</a></strong>') % (error.id, error.summary)
204
            r += htmltext('<li><strong><a href="%s/">%s</a></strong> ') % (error.id, error.summary)
205
            if error.exception_class or error.exception_message:
206
                r += htmltext(_('error %(class)s (%(message)s)')) % {
207
                    'class': error.exception_class,
208
                    'message': error.exception_message,
209
                }
184 210
            r += htmltext('<p class="details badge">%s</p>') % error.occurences_count
185 211
            r += htmltext('</li>')
186 212

  
wcs/conditions.py
30 30
    record_errors = True
31 31
    log_errors = False
32 32

  
33
    def __init__(self, condition, context=None):
33
    def __init__(self, condition, context={}):
34 34
        if not condition:
35 35
            condition = {}
36 36
        self.type = condition.get('type')
......
53 53
            if self.log_errors:
54 54
                get_logger().warn('failed to evaluate %r (%r)', self, e)
55 55
            if self.record_errors:
56
                get_publisher().notify_of_exception(sys.exc_info())
56
                from wcs.logged_errors import LoggedError
57
                summary = _('Failed to evaluate condition')
58
                LoggedError.record(summary,
59
                                   formdata=self.context.get('formdata'),
60
                                   status_item=self.context.get('status_item'),
61
                                   expression=self.value, expression_type=self.type,
62
                                   exception=e)
57 63
            raise RuntimeError()
58 64

  
59 65
    def evaluate_python(self, local_variables):
wcs/logged_errors.py
33 33
    workflow_id = None
34 34
    status_id = None
35 35
    status_item_id = None
36
    expression = None
37
    expression_type = None
36 38
    traceback = None
39
    exception_class = None
40
    exception_message = None
37 41
    occurences_count = 0
38 42
    first_occurence_timestamp = None
39 43
    latest_occurence_timestamp = None
......
42 46
    # declarations for serialization
43 47
    XML_NODES = [
44 48
            ('summary', 'str'), ('traceback', 'str'),
49
            ('exception_class', 'str'), ('exception_message', 'str'),
50
            ('expression', 'str'), ('expression_type', 'str'),
45 51
            ('formdata_id', 'str'), ('formdef_id', 'str'), ('workflow_id', 'str'),
46 52
            ('status_id', 'str'), ('status_item_id', 'str'),
47 53
            ('occurences_count', 'int'),
......
51 57

  
52 58
    @classmethod
53 59
    def record(cls, error_summary, plain_error_msg=None, formdata=None,
54
               formdef=None, workflow=None, status=None, status_item=None):
60
               formdef=None, workflow=None, status=None, status_item=None,
61
               expression=None, expression_type=None, exception=None):
55 62
        error = cls()
56 63
        error.summary = error_summary
57 64
        error.traceback = plain_error_msg
65
        error.expression = expression
66
        error.expression_type = expression_type
67

  
68
        if exception:
69
            error.exception_class = exception.__class__.__name__
70
            error.exception_message = str(exception)
58 71

  
59 72
        if formdata:
60 73
            error.formdata_id = str(formdata.id)
......
74 87

  
75 88
        if status_item:
76 89
            error.status_item_id = status_item.id
77
            if status_item.parent:
90
            if getattr(status_item, 'parent', None):
78 91
                error.status_id = status_item.parent.id
79 92
        if status:
80 93
            error.status_id = status.id
......
110 123

  
111 124
    @property
112 125
    def tech_id(self):
113
        return ('%s-%s-%s-%s-%s' % (self.formdef_id, self.workflow_id, self.status_id,
114
                                    self.status_item_id, simplify(self.summary)))[:200]
126
        tech_id = '%s-%s-' % (self.formdef_id, self.workflow_id)
127
        if self.status_id:
128
            tech_id += '%s-' % self.status_id
129
        if self.status_item_id:
130
            tech_id += '%s-' % self.status_item_id
131
        tech_id += '%s' % simplify(self.summary)
132
        if self.exception_class:
133
            tech_id += '-%s' % self.exception_class
134
        if self.exception_message:
135
            tech_id += '-%s' % simplify(self.exception_message)
136
        return tech_id[:200]
115 137

  
116 138
    def get_formdef(self):
117 139
        return FormDef.get(self.formdef_id, ignore_errors=True)
wcs/wf/jump.py
218 218
        must_jump = True
219 219

  
220 220
        if self.condition:
221
            context = {'formdata': formdata, 'status_item': self}
221 222
            try:
222
                must_jump = Condition(self.condition).evaluate()
223
                must_jump = Condition(self.condition, context).evaluate()
223 224
            except RuntimeError:
224 225
                must_jump = False
225 226

  
wcs/workflows.py
1583 1583
        return False
1584 1584

  
1585 1585
    def check_condition(self, formdata):
1586
        context = {'formdata': formdata, 'status_item': self}
1586 1587
        try:
1587
            return Condition(self.condition).evaluate()
1588
            return Condition(self.condition, context).evaluate()
1588 1589
        except RuntimeError:
1589 1590
            return False
1590 1591

  
1591
-