test_reports: first draft of a full action/wizard/report tester
[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.warning("Cannot extract report's text:", exc_info=True)
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, res_model=None, res_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 res_model, res_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 res_model:
138         datas['model'] = res_model
139     if res_ids:
140         datas['ids'] = res_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             action_name = action.get('name')
201             try:
202                 from xml.dom import minidom
203                 cancel_found = False
204                 buttons = []
205                 dom_doc = minidom.parseString(view_res['arch'])
206                 if not action_name:
207                     action_name = dom_doc.documentElement.getAttribute('name')
208                 
209                 for button in dom_doc.getElementsByTagName('button'):
210                     button_weight = 0
211                     if button.getAttribute('special') == 'cancel':
212                         cancel_found = True
213                         continue
214                     if button.getAttribute('icon') == 'gtk-cancel':
215                         cancel_found = True
216                         continue
217                     if button.getAttribute('default_focus') == '1':
218                         button_weight += 20
219                     if button.getAttribute('string') in wiz_buttons:
220                         button_weight += 30
221                     elif button.getAttribute('icon') in wiz_buttons:
222                         button_weight += 10
223                     string = button.getAttribute('string') or '?%s' % len(buttons)
224                     
225                     buttons.append( { 'name': button.getAttribute('name'),
226                                 'string': string,
227                                 'type': button.getAttribute('type'),
228                                 'weight': button_weight,
229                                 })
230             except Exception, e:
231                 log.warning("Cannot resolve the view arch and locate the buttons!", exc_info=True)
232                 raise AssertionError(e.args[0])
233             
234             if not datas['res_id']:
235                 # it is probably an orm_memory object, we need to create
236                 # an instance
237                 datas['res_id'] = pool.get(datas['res_model']).create(cr, uid, view_data, context)
238         
239             if not buttons:
240                 raise AssertionError("view form doesn't have any buttons to press!")
241             
242             buttons.sort(key=lambda b: b['weight'])
243             log.debug('Buttons are: %s', ', '.join([ '%s: %d' % (b['string'], b['weight']) for b in buttons]))
244             
245             res = None
246             while buttons and not res:
247                 b = buttons.pop()
248                 log_test("in the %s form, I will press the %s button.", action_name, b['string'])
249                 if not b['type']:
250                     log_test("the %s button has no type, cannot use it", b['string'])
251                     continue
252                 if b['type'] == 'object':
253                     #there we are! press the button!
254                     fn =  getattr(pool.get(datas['res_model']), b['name'])
255                     if not fn:
256                         log.error("The %s model doesn't have a %s attribute!", datas['res_model'], b['name'])
257                         continue
258                     res = fn(cr, uid, [datas['res_id'],], context)
259                     break
260                 else:
261                     log.warning("in the %s form, the %s button has unknown type %s", 
262                         action_name, b['string'], b['type'])
263             return res
264         #elif action['type']=='ir.actions.server':
265             #res = rpc.session.rpc_exec_auth('/object', 'execute', 'ir.actions.server', 'run', [action['id']], context)
266             #if res:
267                 #if not isinstance(res, list):
268                     #res = [res]
269                 #for r in res:
270                     #self._exec_action(r, datas, context)
271
272         #elif action['type']=='ir.actions.wizard':
273             #win=None
274             #if 'window' in datas:
275                 #win=datas['window']
276                 #del datas['window']
277             #wizard.execute(action['wiz_name'], datas, parent=win, context=context)
278
279         #elif action['type']=='ir.actions.report.custom':
280             #if 'window' in datas:
281                 #win=datas['window']
282                 #del datas['window']
283             #datas['report_id'] = action['report_id']
284             #self.exec_report('custom', datas, context)
285
286         #elif action['type']=='ir.actions.report.int':
287             #if 'window' in datas:
288                 #win=datas['window']
289                 #del datas['window']
290             #self.exec_report(action['report_name'], datas)
291
292         elif action['type']=='ir.actions.report.xml':
293             if 'window' in datas:
294                 del datas['window']
295             if not datas:
296                 datas = action.get('datas',{})
297             datas = datas.copy()
298             ids = datas.get('ids')
299             if 'ids' in datas:
300                 del datas['ids']
301             res = try_report(cr, uid, 'report.'+action['report_name'], ids, datas, context, our_module=our_module)
302             return res
303         else:
304             raise Exception("Cannot handle action of type %s" % act_model)
305
306     log_test("will be using %s action %s #%d", act_model, act_xmlid, act_id)
307     action = pool.get(act_model).read(cr, uid, act_id, context=context)
308     assert action, "Could not read action %s[%s]" %(act_model, act_id)
309     loop = 0
310     while action:
311         loop += 1
312         # This part tries to emulate the loop of the Gtk client
313         if loop > 100:
314             log.error("Passed %d loops, giving up", loop)
315             raise Exception("Too many loops at action")
316         log_test("it is an %s action at loop #%d", action.get('type', 'unknown'), loop)
317         result = _exec_action(action, datas, context)
318         if not isinstance(result, dict):
319             break
320         datas = result.get('datas', {})
321         if datas:
322             del result['datas']
323         action = result
324         
325     return True
326
327
328 #eof