Launchpad automatic translations update.
[odoo/odoo.git] / bin / tools / test_reports.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2010 OpenERP s.a. (<http://openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 """ Helper functions for reports testing.
23
24     Please /do not/ import this file by default, but only explicitly call it
25     through the code of yaml tests.
26 """
27
28 import netsvc
29 import tools
30 import logging
31 import pooler
32 from tools.safe_eval import safe_eval
33 from subprocess import Popen, PIPE
34 import os
35 import tempfile
36
37 def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None):
38     """ Try to render a report <rname> with contents of ids
39     
40         This function should also check for common pitfalls of reports.
41     """
42     if our_module:
43         log = logging.getLogger('tests.%s' % our_module)
44     else:
45         log = logging.getLogger('tools.test_reports')
46     if data is None:
47         data = {}
48     if context is None:
49         context = {}
50     if rname.startswith('report.'):
51         rname_s = rname[7:]
52     else:
53         rname_s = rname
54     log.log(netsvc.logging.TEST, "  - Trying %s.create(%r)", rname, ids)
55     res = netsvc.LocalService(rname).create(cr, uid, ids, data, context)
56     if not isinstance(res, tuple):
57         raise RuntimeError("Result of %s.create() should be a (data,format) tuple, now it is a %s" % \
58                                 (rname, type(res)))
59     (res_data, res_format) = res
60
61     if not res_data:
62         raise ValueError("Report %s produced an empty result!" % rname)
63
64     if tools.config['test_report_directory']:
65         file(os.path.join(tools.config['test_report_directory'], rname+ '.'+res_format), 'wb+').write(res_data)
66
67     log.debug("Have a %s report for %s, will examine it", res_format, rname)
68     if res_format == 'pdf':
69         if res_data[:5] != '%PDF-':
70             raise ValueError("Report %s produced a non-pdf header, %r" % (rname, res_data[:10]))
71
72         res_text = False
73         try:
74             fd, rfname = tempfile.mkstemp(suffix=res_format)
75             os.write(fd, res_data)
76             os.close(fd)
77
78             fp = Popen(['pdftotext', '-enc', 'UTF-8', '-nopgbrk', rfname, '-'], shell=False, stdout=PIPE).stdout
79             res_text = tools.ustr(fp.read())
80             os.unlink(rfname)
81         except Exception:
82             log.debug("Unable to parse PDF report: install pdftotext to perform automated tests.")
83
84         if res_text is not False:
85             for line in res_text.split('\n'):
86                 if ('[[' in line) or ('[ [' in line):
87                     log.error("Report %s may have bad expression near: \"%s\".", rname, line[80:])
88             # TODO more checks, what else can be a sign of a faulty report?
89     elif res_format == 'foobar':
90         # TODO
91         pass
92     else:
93         log.warning("Report %s produced a \"%s\" chunk, cannot examine it", rname, res_format)
94         return False
95
96     log.log(netsvc.logging.TEST, "  + Report %s produced correctly.", rname)
97     return True
98
99 def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
100                 wiz_data=None, wiz_buttons=None,
101                 context=None, our_module=None):
102     """Take an ir.action.act_window and follow it until a report is produced
103
104         :param action_id: the integer id of an action, or a reference to xml id
105                 of the act_window (can search [our_module.]+xml_id
106         :param active_model, active_ids: call the action as if it had been launched
107                 from that model+ids (tree/form view action)
108         :param wiz_data: a dictionary of values to use in the wizard, if needed.
109                 They will override (or complete) the default values of the
110                 wizard form.
111         :param wiz_buttons: a list of button names, or button icon strings, which
112                 should be preferred to press during the wizard.
113                 Eg. 'OK' or 'gtk-print'
114         :param our_module: the name of the calling module (string), like 'account'
115     """
116
117     if not our_module and isinstance(action_id, basestring):
118         if '.' in action_id:
119             our_module = action_id.split('.', 1)[0]
120
121     if context is None:
122         context = {}
123     else:
124         context = context.copy() # keep it local
125     # TODO context fill-up
126
127     pool = pooler.get_pool(cr.dbname)
128     if our_module:
129         log = logging.getLogger('tests.%s' % our_module)
130     else:
131         log = logging.getLogger('tools.test_reports')
132
133     def log_test(msg, *args):
134         log.log(netsvc.logging.TEST, "  - " + msg, *args)
135
136     datas = {}
137     if active_model:
138         datas['model'] = active_model
139     if active_ids:
140         datas['ids'] = active_ids
141
142     if not wiz_buttons:
143         wiz_buttons = []
144
145     if isinstance(action_id, basestring):
146         if '.' in action_id:
147             act_module, act_xmlid = action_id.split('.', 1)
148         else:
149             if not our_module:
150                 raise ValueError('You cannot only specify action_id "%s" without a module name' % action_id)
151             act_module = our_module
152             act_xmlid = action_id
153         act_model, act_id = pool.get('ir.model.data').get_object_reference(cr, uid, act_module, act_xmlid)
154     else:
155         assert isinstance(action_id, (long, int))
156         act_model = 'ir.action.act_window'     # assume that
157         act_id = action_id
158         act_xmlid = '<%s>' % act_id
159
160     def _exec_action(action, datas, context):
161         # taken from client/modules/action/main.py:84 _exec_action()
162         if isinstance(action, bool) or 'type' not in action:
163             return
164         # Updating the context : Adding the context of action in order to use it on Views called from buttons
165         if datas.get('id',False):
166             context.update( {'active_id': datas.get('id',False), 'active_ids': datas.get('ids',[]), 'active_model': datas.get('model',False)})
167         context.update(safe_eval(action.get('context','{}'), context.copy()))
168         if action['type'] in ['ir.actions.act_window', 'ir.actions.submenu']:
169             for key in ('res_id', 'res_model', 'view_type', 'view_mode',
170                     'limit', 'auto_refresh', 'search_view', 'auto_search', 'search_view_id'):
171                 datas[key] = action.get(key, datas.get(key, None))
172
173             view_id = False
174             if action.get('views', []):
175                 if isinstance(action['views'],list):
176                     view_id = action['views'][0][0]
177                     datas['view_mode']= action['views'][0][1]
178                 else:
179                     if action.get('view_id', False):
180                         view_id = action['view_id'][0]
181             elif action.get('view_id', False):
182                 view_id = action['view_id'][0]
183
184             assert datas['res_model'], "Cannot use the view without a model"
185             # Here, we have a view that we need to emulate
186             log_test("will emulate a %s view: %s#%s",
187                         action['view_type'], datas['res_model'], view_id or '?')
188
189             view_res = pool.get(datas['res_model']).fields_view_get(cr, uid, view_id, action['view_type'], context)
190             assert view_res and view_res.get('arch'), "Did not return any arch for the view"
191             view_data = {}
192             if view_res.get('fields',{}).keys():
193                 view_data = pool.get(datas['res_model']).default_get(cr, uid, view_res['fields'].keys(), context)
194             if datas.get('form'):
195                 view_data.update(datas.get('form'))
196             if wiz_data:
197                 view_data.update(wiz_data)
198             log.debug("View data is: %r", view_data)
199
200             for fk, field in view_res.get('fields',{}).items():
201                 # Default fields returns list of int, while at create()
202                 # we need to send a [(6,0,[int,..])]
203                 if field['type'] in ('one2many', 'many2many') \
204                         and view_data.get(fk, False) \
205                         and isinstance(view_data[fk], list) \
206                         and not isinstance(view_data[fk][0], tuple) :
207                     view_data[fk] = [(6, 0, view_data[fk])]
208
209             action_name = action.get('name')
210             try:
211                 from xml.dom import minidom
212                 cancel_found = False
213                 buttons = []
214                 dom_doc = minidom.parseString(view_res['arch'])
215                 if not action_name:
216                     action_name = dom_doc.documentElement.getAttribute('name')
217
218                 for button in dom_doc.getElementsByTagName('button'):
219                     button_weight = 0
220                     if button.getAttribute('special') == 'cancel':
221                         cancel_found = True
222                         continue
223                     if button.getAttribute('icon') == 'gtk-cancel':
224                         cancel_found = True
225                         continue
226                     if button.getAttribute('default_focus') == '1':
227                         button_weight += 20
228                     if button.getAttribute('string') in wiz_buttons:
229                         button_weight += 30
230                     elif button.getAttribute('icon') in wiz_buttons:
231                         button_weight += 10
232                     string = button.getAttribute('string') or '?%s' % len(buttons)
233
234                     buttons.append( { 'name': button.getAttribute('name'),
235                                 'string': string,
236                                 'type': button.getAttribute('type'),
237                                 'weight': button_weight,
238                                 })
239             except Exception, e:
240                 log.warning("Cannot resolve the view arch and locate the buttons!", exc_info=True)
241                 raise AssertionError(e.args[0])
242
243             if not datas['res_id']:
244                 # it is probably an orm_memory object, we need to create
245                 # an instance
246                 datas['res_id'] = pool.get(datas['res_model']).create(cr, uid, view_data, context)
247
248             if not buttons:
249                 raise AssertionError("view form doesn't have any buttons to press!")
250
251             buttons.sort(key=lambda b: b['weight'])
252             log.debug('Buttons are: %s', ', '.join([ '%s: %d' % (b['string'], b['weight']) for b in buttons]))
253
254             res = None
255             while buttons and not res:
256                 b = buttons.pop()
257                 log_test("in the \"%s\" form, I will press the \"%s\" button.", action_name, b['string'])
258                 if not b['type']:
259                     log_test("the \"%s\" button has no type, cannot use it", b['string'])
260                     continue
261                 if b['type'] == 'object':
262                     #there we are! press the button!
263                     fn =  getattr(pool.get(datas['res_model']), b['name'])
264                     if not fn:
265                         log.error("The %s model doesn't have a %s attribute!", datas['res_model'], b['name'])
266                         continue
267                     res = fn(cr, uid, [datas['res_id'],], context)
268                     break
269                 else:
270                     log.warning("in the \"%s\" form, the \"%s\" button has unknown type %s",
271                         action_name, b['string'], b['type'])
272             return res
273
274         elif action['type']=='ir.actions.report.xml':
275             if 'window' in datas:
276                 del datas['window']
277             if not datas:
278                 datas = action.get('datas',{})
279             datas = datas.copy()
280             ids = datas.get('ids')
281             if 'ids' in datas:
282                 del datas['ids']
283             res = try_report(cr, uid, 'report.'+action['report_name'], ids, datas, context, our_module=our_module)
284             return res
285         else:
286             raise Exception("Cannot handle action of type %s" % act_model)
287
288     log_test("will be using %s action %s #%d", act_model, act_xmlid, act_id)
289     action = pool.get(act_model).read(cr, uid, act_id, context=context)
290     assert action, "Could not read action %s[%s]" %(act_model, act_id)
291     loop = 0
292     while action:
293         loop += 1
294         # This part tries to emulate the loop of the Gtk client
295         if loop > 100:
296             log.error("Passed %d loops, giving up", loop)
297             raise Exception("Too many loops at action")
298         log_test("it is an %s action at loop #%d", action.get('type', 'unknown'), loop)
299         result = _exec_action(action, datas, context)
300         if not isinstance(result, dict):
301             break
302         datas = result.get('datas', {})
303         if datas:
304             del result['datas']
305         action = result
306
307     return True
308
309 #eof