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.
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 _logger = logging.getLogger(__name__)
39 def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None):
40 """ Try to render a report <rname> with contents of ids
42 This function should also check for common pitfalls of reports.
48 if rname.startswith('report.'):
52 _logger.log(netsvc.logging.TEST, " - Trying %s.create(%r)", rname, ids)
53 res = netsvc.LocalService(rname).create(cr, uid, ids, data, context)
54 if not isinstance(res, tuple):
55 raise RuntimeError("Result of %s.create() should be a (data,format) tuple, now it is a %s" % \
57 (res_data, res_format) = res
60 raise ValueError("Report %s produced an empty result!" % rname)
62 if tools.config['test_report_directory']:
63 file(os.path.join(tools.config['test_report_directory'], rname+ '.'+res_format), 'wb+').write(res_data)
65 _logger.debug("Have a %s report for %s, will examine it", res_format, rname)
66 if res_format == 'pdf':
67 if res_data[:5] != '%PDF-':
68 raise ValueError("Report %s produced a non-pdf header, %r" % (rname, res_data[:10]))
72 fd, rfname = tempfile.mkstemp(suffix=res_format)
73 os.write(fd, res_data)
76 proc = Popen(['pdftotext', '-enc', 'UTF-8', '-nopgbrk', rfname, '-'], shell=False, stdout=PIPE)
77 stdout, stderr = proc.communicate()
78 res_text = tools.ustr(stdout)
81 _logger.debug("Unable to parse PDF report: install pdftotext to perform automated tests.")
83 if res_text is not False:
84 for line in res_text.split('\n'):
85 if ('[[' in line) or ('[ [' in line):
86 _logger.error("Report %s may have bad expression near: \"%s\".", rname, line[80:])
87 # TODO more checks, what else can be a sign of a faulty report?
88 elif res_format == 'foobar':
92 _logger.warning("Report %s produced a \"%s\" chunk, cannot examine it", rname, res_format)
95 _logger.log(netsvc.logging.TEST, " + Report %s produced correctly.", rname)
98 def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
99 wiz_data=None, wiz_buttons=None,
100 context=None, our_module=None):
101 """Take an ir.action.act_window and follow it until a report is produced
103 :param action_id: the integer id of an action, or a reference to xml id
104 of the act_window (can search [our_module.]+xml_id
105 :param active_model, active_ids: call the action as if it had been launched
106 from that model+ids (tree/form view action)
107 :param wiz_data: a dictionary of values to use in the wizard, if needed.
108 They will override (or complete) the default values of the
110 :param wiz_buttons: a list of button names, or button icon strings, which
111 should be preferred to press during the wizard.
112 Eg. 'OK' or 'gtk-print'
113 :param our_module: the name of the calling module (string), like 'account'
116 if not our_module and isinstance(action_id, basestring):
118 our_module = action_id.split('.', 1)[0]
123 context = context.copy() # keep it local
124 # TODO context fill-up
126 pool = pooler.get_pool(cr.dbname)
128 def log_test(msg, *args):
129 _logger.log(netsvc.logging.TEST, " - " + msg, *args)
133 datas['model'] = active_model
135 datas['ids'] = active_ids
140 if isinstance(action_id, basestring):
142 act_module, act_xmlid = action_id.split('.', 1)
145 raise ValueError('You cannot only specify action_id "%s" without a module name' % action_id)
146 act_module = our_module
147 act_xmlid = action_id
148 act_model, act_id = pool.get('ir.model.data').get_object_reference(cr, uid, act_module, act_xmlid)
150 assert isinstance(action_id, (long, int))
151 act_model = 'ir.action.act_window' # assume that
153 act_xmlid = '<%s>' % act_id
155 def _exec_action(action, datas, context):
156 # taken from client/modules/action/main.py:84 _exec_action()
157 if isinstance(action, bool) or 'type' not in action:
159 # Updating the context : Adding the context of action in order to use it on Views called from buttons
160 if datas.get('id',False):
161 context.update( {'active_id': datas.get('id',False), 'active_ids': datas.get('ids',[]), 'active_model': datas.get('model',False)})
162 context.update(safe_eval(action.get('context','{}'), context.copy()))
163 if action['type'] in ['ir.actions.act_window', 'ir.actions.submenu']:
164 for key in ('res_id', 'res_model', 'view_type', 'view_mode',
165 'limit', 'auto_refresh', 'search_view', 'auto_search', 'search_view_id'):
166 datas[key] = action.get(key, datas.get(key, None))
169 if action.get('views', []):
170 if isinstance(action['views'],list):
171 view_id = action['views'][0][0]
172 datas['view_mode']= action['views'][0][1]
174 if action.get('view_id', False):
175 view_id = action['view_id'][0]
176 elif action.get('view_id', False):
177 view_id = action['view_id'][0]
179 assert datas['res_model'], "Cannot use the view without a model"
180 # Here, we have a view that we need to emulate
181 log_test("will emulate a %s view: %s#%s",
182 action['view_type'], datas['res_model'], view_id or '?')
184 view_res = pool.get(datas['res_model']).fields_view_get(cr, uid, view_id, action['view_type'], context)
185 assert view_res and view_res.get('arch'), "Did not return any arch for the view"
187 if view_res.get('fields',{}).keys():
188 view_data = pool.get(datas['res_model']).default_get(cr, uid, view_res['fields'].keys(), context)
189 if datas.get('form'):
190 view_data.update(datas.get('form'))
192 view_data.update(wiz_data)
193 _logger.debug("View data is: %r", view_data)
195 for fk, field in view_res.get('fields',{}).items():
196 # Default fields returns list of int, while at create()
197 # we need to send a [(6,0,[int,..])]
198 if field['type'] in ('one2many', 'many2many') \
199 and view_data.get(fk, False) \
200 and isinstance(view_data[fk], list) \
201 and not isinstance(view_data[fk][0], tuple) :
202 view_data[fk] = [(6, 0, view_data[fk])]
204 action_name = action.get('name')
206 from xml.dom import minidom
209 dom_doc = minidom.parseString(view_res['arch'])
211 action_name = dom_doc.documentElement.getAttribute('name')
213 for button in dom_doc.getElementsByTagName('button'):
215 if button.getAttribute('special') == 'cancel':
218 if button.getAttribute('icon') == 'gtk-cancel':
221 if button.getAttribute('default_focus') == '1':
223 if button.getAttribute('string') in wiz_buttons:
225 elif button.getAttribute('icon') in wiz_buttons:
227 string = button.getAttribute('string') or '?%s' % len(buttons)
229 buttons.append( { 'name': button.getAttribute('name'),
231 'type': button.getAttribute('type'),
232 'weight': button_weight,
235 _logger.warning("Cannot resolve the view arch and locate the buttons!", exc_info=True)
236 raise AssertionError(e.args[0])
238 if not datas['res_id']:
239 # it is probably an orm_memory object, we need to create
241 datas['res_id'] = pool.get(datas['res_model']).create(cr, uid, view_data, context)
244 raise AssertionError("view form doesn't have any buttons to press!")
246 buttons.sort(key=lambda b: b['weight'])
247 _logger.debug('Buttons are: %s', ', '.join([ '%s: %d' % (b['string'], b['weight']) for b in buttons]))
250 while buttons and not res:
252 log_test("in the \"%s\" form, I will press the \"%s\" button.", action_name, b['string'])
254 log_test("the \"%s\" button has no type, cannot use it", b['string'])
256 if b['type'] == 'object':
257 #there we are! press the button!
258 fn = getattr(pool.get(datas['res_model']), b['name'])
260 _logger.error("The %s model doesn't have a %s attribute!", datas['res_model'], b['name'])
262 res = fn(cr, uid, [datas['res_id'],], context)
265 _logger.warning("in the \"%s\" form, the \"%s\" button has unknown type %s",
266 action_name, b['string'], b['type'])
269 elif action['type']=='ir.actions.report.xml':
270 if 'window' in datas:
273 datas = action.get('datas',{})
275 ids = datas.get('ids')
278 res = try_report(cr, uid, 'report.'+action['report_name'], ids, datas, context, our_module=our_module)
281 raise Exception("Cannot handle action of type %s" % act_model)
283 log_test("will be using %s action %s #%d", act_model, act_xmlid, act_id)
284 action = pool.get(act_model).read(cr, uid, act_id, context=context)
285 assert action, "Could not read action %s[%s]" %(act_model, act_id)
289 # This part tries to emulate the loop of the Gtk client
291 _logger.error("Passed %d loops, giving up", loop)
292 raise Exception("Too many loops at action")
293 log_test("it is an %s action at loop #%d", action.get('type', 'unknown'), loop)
294 result = _exec_action(action, datas, context)
295 if not isinstance(result, dict):
297 datas = result.get('datas', {})
306 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: