1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2010-2012 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.
30 import openerp.tools as tools
32 from openerp.tools.safe_eval import safe_eval
33 from subprocess import Popen, PIPE
37 _logger = logging.getLogger(__name__)
38 _test_logger = logging.getLogger('openerp.tests')
41 def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None):
42 """ Try to render a report <rname> with contents of ids
44 This function should also check for common pitfalls of reports.
50 if rname.startswith('report.'):
54 _test_logger.info(" - Trying %s.create(%r)", rname, ids)
55 res = openerp.report.render_report(cr, uid, ids, rname_s, 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 _logger.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 proc = Popen(['pdftotext', '-enc', 'UTF-8', '-nopgbrk', rfname, '-'], shell=False, stdout=PIPE)
79 stdout, stderr = proc.communicate()
80 res_text = tools.ustr(stdout)
83 _logger.debug("Unable to parse PDF report: install pdftotext to perform automated tests.")
85 if res_text is not False:
86 for line in res_text.split('\n'):
87 if ('[[' in line) or ('[ [' in line):
88 _logger.error("Report %s may have bad expression near: \"%s\".", rname, line[80:])
89 # TODO more checks, what else can be a sign of a faulty report?
90 elif res_format == 'foobar':
94 _logger.warning("Report %s produced a \"%s\" chunk, cannot examine it", rname, res_format)
97 _test_logger.info(" + Report %s produced correctly.", rname)
100 def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
101 wiz_data=None, wiz_buttons=None,
102 context=None, our_module=None):
103 """Take an ir.action.act_window and follow it until a report is produced
105 :param action_id: the integer id of an action, or a reference to xml id
106 of the act_window (can search [our_module.]+xml_id
107 :param active_model, active_ids: call the action as if it had been launched
108 from that model+ids (tree/form view action)
109 :param wiz_data: a dictionary of values to use in the wizard, if needed.
110 They will override (or complete) the default values of the
112 :param wiz_buttons: a list of button names, or button icon strings, which
113 should be preferred to press during the wizard.
114 Eg. 'OK' or 'gtk-print'
115 :param our_module: the name of the calling module (string), like 'account'
118 if not our_module and isinstance(action_id, basestring):
120 our_module = action_id.split('.', 1)[0]
125 context = context.copy() # keep it local
126 # TODO context fill-up
128 registry = openerp.registry(cr.dbname)
130 def log_test(msg, *args):
131 _test_logger.info(" - " + msg, *args)
135 datas['model'] = active_model
137 datas['ids'] = active_ids
142 if isinstance(action_id, basestring):
144 act_module, act_xmlid = action_id.split('.', 1)
147 raise ValueError('You cannot only specify action_id "%s" without a module name' % action_id)
148 act_module = our_module
149 act_xmlid = action_id
150 act_model, act_id = registry['ir.model.data'].get_object_reference(cr, uid, act_module, act_xmlid)
152 assert isinstance(action_id, (long, int))
153 act_model = 'ir.action.act_window' # assume that
155 act_xmlid = '<%s>' % act_id
157 def _exec_action(action, datas, context):
158 # taken from client/modules/action/main.py:84 _exec_action()
159 if isinstance(action, bool) or 'type' not in action:
161 # Updating the context : Adding the context of action in order to use it on Views called from buttons
162 if datas.get('id',False):
163 context.update( {'active_id': datas.get('id',False), 'active_ids': datas.get('ids',[]), 'active_model': datas.get('model',False)})
164 context.update(safe_eval(action.get('context','{}'), context.copy()))
165 if action['type'] in ['ir.actions.act_window', 'ir.actions.submenu']:
166 for key in ('res_id', 'res_model', 'view_type', 'view_mode',
167 'limit', 'auto_refresh', 'search_view', 'auto_search', 'search_view_id'):
168 datas[key] = action.get(key, datas.get(key, None))
171 if action.get('views', []):
172 if isinstance(action['views'],list):
173 view_id = action['views'][0][0]
174 datas['view_mode']= action['views'][0][1]
176 if action.get('view_id', False):
177 view_id = action['view_id'][0]
178 elif action.get('view_id', False):
179 view_id = action['view_id'][0]
181 assert datas['res_model'], "Cannot use the view without a model"
182 # Here, we have a view that we need to emulate
183 log_test("will emulate a %s view: %s#%s",
184 action['view_type'], datas['res_model'], view_id or '?')
186 view_res = registry[datas['res_model']].fields_view_get(cr, uid, view_id, action['view_type'], context)
187 assert view_res and view_res.get('arch'), "Did not return any arch for the view"
189 if view_res.get('fields',{}).keys():
190 view_data = registry[datas['res_model']].default_get(cr, uid, view_res['fields'].keys(), context)
191 if datas.get('form'):
192 view_data.update(datas.get('form'))
194 view_data.update(wiz_data)
195 _logger.debug("View data is: %r", view_data)
197 for fk, field in view_res.get('fields',{}).items():
198 # Default fields returns list of int, while at create()
199 # we need to send a [(6,0,[int,..])]
200 if field['type'] in ('one2many', 'many2many') \
201 and view_data.get(fk, False) \
202 and isinstance(view_data[fk], list) \
203 and not isinstance(view_data[fk][0], tuple) :
204 view_data[fk] = [(6, 0, view_data[fk])]
206 action_name = action.get('name')
208 from xml.dom import minidom
211 dom_doc = minidom.parseString(view_res['arch'])
213 action_name = dom_doc.documentElement.getAttribute('name')
215 for button in dom_doc.getElementsByTagName('button'):
217 if button.getAttribute('special') == 'cancel':
220 if button.getAttribute('icon') == 'gtk-cancel':
223 if button.getAttribute('default_focus') == '1':
225 if button.getAttribute('string') in wiz_buttons:
227 elif button.getAttribute('icon') in wiz_buttons:
229 string = button.getAttribute('string') or '?%s' % len(buttons)
231 buttons.append( { 'name': button.getAttribute('name'),
233 'type': button.getAttribute('type'),
234 'weight': button_weight,
237 _logger.warning("Cannot resolve the view arch and locate the buttons!", exc_info=True)
238 raise AssertionError(e.args[0])
240 if not datas['res_id']:
241 # it is probably an orm_memory object, we need to create
243 datas['res_id'] = registry[datas['res_model']].create(cr, uid, view_data, context)
246 raise AssertionError("view form doesn't have any buttons to press!")
248 buttons.sort(key=lambda b: b['weight'])
249 _logger.debug('Buttons are: %s', ', '.join([ '%s: %d' % (b['string'], b['weight']) for b in buttons]))
252 while buttons and not res:
254 log_test("in the \"%s\" form, I will press the \"%s\" button.", action_name, b['string'])
256 log_test("the \"%s\" button has no type, cannot use it", b['string'])
258 if b['type'] == 'object':
259 #there we are! press the button!
260 fn = getattr(registry[datas['res_model']], b['name'])
262 _logger.error("The %s model doesn't have a %s attribute!", datas['res_model'], b['name'])
264 res = fn(cr, uid, [datas['res_id'],], context)
267 _logger.warning("in the \"%s\" form, the \"%s\" button has unknown type %s",
268 action_name, b['string'], b['type'])
271 elif action['type']=='ir.actions.report.xml':
272 if 'window' in datas:
275 datas = action.get('datas',{})
277 ids = datas.get('ids')
280 res = try_report(cr, uid, 'report.'+action['report_name'], ids, datas, context, our_module=our_module)
283 raise Exception("Cannot handle action of type %s" % act_model)
285 log_test("will be using %s action %s #%d", act_model, act_xmlid, act_id)
286 action = registry[act_model].read(cr, uid, act_id, context=context)
287 assert action, "Could not read action %s[%s]" %(act_model, act_id)
291 # This part tries to emulate the loop of the Gtk client
293 _logger.error("Passed %d loops, giving up", loop)
294 raise Exception("Too many loops at action")
295 log_test("it is an %s action at loop #%d", action.get('type', 'unknown'), loop)
296 result = _exec_action(action, datas, context)
297 if not isinstance(result, dict):
299 datas = result.get('datas', {})
308 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: