1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2010 OpenERP s.a. (<http://openerp.com>).
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.
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.
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/>.
20 ##############################################################################
22 """ Helper functions for reports testing.
24 Please /do not/ import this file by default, but only explicitly call it
25 through the code of yaml tests.
28 import openerp.netsvc as netsvc
29 import openerp.tools as tools
31 import openerp.pooler as pooler
32 from openerp.tools.safe_eval import safe_eval
33 from subprocess import Popen, PIPE
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
40 This function should also check for common pitfalls of reports.
43 log = logging.getLogger('tests.%s' % our_module)
45 log = logging.getLogger('tools.test_reports')
50 if rname.startswith('report.'):
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" % \
59 (res_data, res_format) = res
62 raise ValueError("Report %s produced an empty result!" % rname)
64 if tools.config['test_report_directory']:
65 file(os.path.join(tools.config['test_report_directory'], rname+ '.'+res_format), 'wb+').write(res_data)
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]))
74 fd, rfname = tempfile.mkstemp(suffix=res_format)
75 os.write(fd, res_data)
78 fp = Popen(['pdftotext', '-enc', 'UTF-8', '-nopgbrk', rfname, '-'], shell=False, stdout=PIPE).stdout
79 res_text = tools.ustr(fp.read())
82 log.debug("Unable to parse PDF report: install pdftotext to perform automated tests.")
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':
93 log.warning("Report %s produced a \"%s\" chunk, cannot examine it", rname, res_format)
96 log.log(netsvc.logging.TEST, " + Report %s produced correctly.", rname)
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
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
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'
117 if not our_module and isinstance(action_id, basestring):
119 our_module = action_id.split('.', 1)[0]
124 context = context.copy() # keep it local
125 # TODO context fill-up
127 pool = pooler.get_pool(cr.dbname)
129 log = logging.getLogger('tests.%s' % our_module)
131 log = logging.getLogger('tools.test_reports')
133 def log_test(msg, *args):
134 log.log(netsvc.logging.TEST, " - " + msg, *args)
138 datas['model'] = active_model
140 datas['ids'] = active_ids
145 if isinstance(action_id, basestring):
147 act_module, act_xmlid = action_id.split('.', 1)
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)
155 assert isinstance(action_id, (long, int))
156 act_model = 'ir.action.act_window' # assume that
158 act_xmlid = '<%s>' % act_id
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:
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))
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]
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]
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 '?')
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"
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'))
197 view_data.update(wiz_data)
198 log.debug("View data is: %r", view_data)
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])]
209 action_name = action.get('name')
211 from xml.dom import minidom
214 dom_doc = minidom.parseString(view_res['arch'])
216 action_name = dom_doc.documentElement.getAttribute('name')
218 for button in dom_doc.getElementsByTagName('button'):
220 if button.getAttribute('special') == 'cancel':
223 if button.getAttribute('icon') == 'gtk-cancel':
226 if button.getAttribute('default_focus') == '1':
228 if button.getAttribute('string') in wiz_buttons:
230 elif button.getAttribute('icon') in wiz_buttons:
232 string = button.getAttribute('string') or '?%s' % len(buttons)
234 buttons.append( { 'name': button.getAttribute('name'),
236 'type': button.getAttribute('type'),
237 'weight': button_weight,
240 log.warning("Cannot resolve the view arch and locate the buttons!", exc_info=True)
241 raise AssertionError(e.args[0])
243 if not datas['res_id']:
244 # it is probably an orm_memory object, we need to create
246 datas['res_id'] = pool.get(datas['res_model']).create(cr, uid, view_data, context)
249 raise AssertionError("view form doesn't have any buttons to press!")
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]))
255 while buttons and not res:
257 log_test("in the \"%s\" form, I will press the \"%s\" button.", action_name, b['string'])
259 log_test("the \"%s\" button has no type, cannot use it", b['string'])
261 if b['type'] == 'object':
262 #there we are! press the button!
263 fn = getattr(pool.get(datas['res_model']), b['name'])
265 log.error("The %s model doesn't have a %s attribute!", datas['res_model'], b['name'])
267 res = fn(cr, uid, [datas['res_id'],], context)
270 log.warning("in the \"%s\" form, the \"%s\" button has unknown type %s",
271 action_name, b['string'], b['type'])
274 elif action['type']=='ir.actions.report.xml':
275 if 'window' in datas:
278 datas = action.get('datas',{})
280 ids = datas.get('ids')
283 res = try_report(cr, uid, 'report.'+action['report_name'], ids, datas, context, our_module=our_module)
286 raise Exception("Cannot handle action of type %s" % act_model)
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)
294 # This part tries to emulate the loop of the Gtk client
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):
302 datas = result.get('datas', {})
311 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: