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 _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 fp = Popen(['pdftotext', '-enc', 'UTF-8', '-nopgbrk', rfname, '-'], shell=False, stdout=PIPE).stdout
77 res_text = tools.ustr(fp.read())
80 _logger.debug("Unable to parse PDF report: install pdftotext to perform automated tests.")
82 if res_text is not False:
83 for line in res_text.split('\n'):
84 if ('[[' in line) or ('[ [' in line):
85 _logger.error("Report %s may have bad expression near: \"%s\".", rname, line[80:])
86 # TODO more checks, what else can be a sign of a faulty report?
87 elif res_format == 'foobar':
91 _logger.warning("Report %s produced a \"%s\" chunk, cannot examine it", rname, res_format)
94 _logger.log(netsvc.logging.TEST, " + Report %s produced correctly.", rname)
97 def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
98 wiz_data=None, wiz_buttons=None,
99 context=None, our_module=None):
100 """Take an ir.action.act_window and follow it until a report is produced
102 :param action_id: the integer id of an action, or a reference to xml id
103 of the act_window (can search [our_module.]+xml_id
104 :param active_model, active_ids: call the action as if it had been launched
105 from that model+ids (tree/form view action)
106 :param wiz_data: a dictionary of values to use in the wizard, if needed.
107 They will override (or complete) the default values of the
109 :param wiz_buttons: a list of button names, or button icon strings, which
110 should be preferred to press during the wizard.
111 Eg. 'OK' or 'gtk-print'
112 :param our_module: the name of the calling module (string), like 'account'
115 if not our_module and isinstance(action_id, basestring):
117 our_module = action_id.split('.', 1)[0]
122 context = context.copy() # keep it local
123 # TODO context fill-up
125 pool = pooler.get_pool(cr.dbname)
127 def log_test(msg, *args):
128 _logger.log(netsvc.logging.TEST, " - " + msg, *args)
132 datas['model'] = active_model
134 datas['ids'] = active_ids
139 if isinstance(action_id, basestring):
141 act_module, act_xmlid = action_id.split('.', 1)
144 raise ValueError('You cannot only specify action_id "%s" without a module name' % action_id)
145 act_module = our_module
146 act_xmlid = action_id
147 act_model, act_id = pool.get('ir.model.data').get_object_reference(cr, uid, act_module, act_xmlid)
149 assert isinstance(action_id, (long, int))
150 act_model = 'ir.action.act_window' # assume that
152 act_xmlid = '<%s>' % act_id
154 def _exec_action(action, datas, context):
155 # taken from client/modules/action/main.py:84 _exec_action()
156 if isinstance(action, bool) or 'type' not in action:
158 # Updating the context : Adding the context of action in order to use it on Views called from buttons
159 if datas.get('id',False):
160 context.update( {'active_id': datas.get('id',False), 'active_ids': datas.get('ids',[]), 'active_model': datas.get('model',False)})
161 context.update(safe_eval(action.get('context','{}'), context.copy()))
162 if action['type'] in ['ir.actions.act_window', 'ir.actions.submenu']:
163 for key in ('res_id', 'res_model', 'view_type', 'view_mode',
164 'limit', 'auto_refresh', 'search_view', 'auto_search', 'search_view_id'):
165 datas[key] = action.get(key, datas.get(key, None))
168 if action.get('views', []):
169 if isinstance(action['views'],list):
170 view_id = action['views'][0][0]
171 datas['view_mode']= action['views'][0][1]
173 if action.get('view_id', False):
174 view_id = action['view_id'][0]
175 elif action.get('view_id', False):
176 view_id = action['view_id'][0]
178 assert datas['res_model'], "Cannot use the view without a model"
179 # Here, we have a view that we need to emulate
180 log_test("will emulate a %s view: %s#%s",
181 action['view_type'], datas['res_model'], view_id or '?')
183 view_res = pool.get(datas['res_model']).fields_view_get(cr, uid, view_id, action['view_type'], context)
184 assert view_res and view_res.get('arch'), "Did not return any arch for the view"
186 if view_res.get('fields',{}).keys():
187 view_data = pool.get(datas['res_model']).default_get(cr, uid, view_res['fields'].keys(), context)
188 if datas.get('form'):
189 view_data.update(datas.get('form'))
191 view_data.update(wiz_data)
192 _logger.debug("View data is: %r", view_data)
194 for fk, field in view_res.get('fields',{}).items():
195 # Default fields returns list of int, while at create()
196 # we need to send a [(6,0,[int,..])]
197 if field['type'] in ('one2many', 'many2many') \
198 and view_data.get(fk, False) \
199 and isinstance(view_data[fk], list) \
200 and not isinstance(view_data[fk][0], tuple) :
201 view_data[fk] = [(6, 0, view_data[fk])]
203 action_name = action.get('name')
205 from xml.dom import minidom
208 dom_doc = minidom.parseString(view_res['arch'])
210 action_name = dom_doc.documentElement.getAttribute('name')
212 for button in dom_doc.getElementsByTagName('button'):
214 if button.getAttribute('special') == 'cancel':
217 if button.getAttribute('icon') == 'gtk-cancel':
220 if button.getAttribute('default_focus') == '1':
222 if button.getAttribute('string') in wiz_buttons:
224 elif button.getAttribute('icon') in wiz_buttons:
226 string = button.getAttribute('string') or '?%s' % len(buttons)
228 buttons.append( { 'name': button.getAttribute('name'),
230 'type': button.getAttribute('type'),
231 'weight': button_weight,
234 _logger.warning("Cannot resolve the view arch and locate the buttons!", exc_info=True)
235 raise AssertionError(e.args[0])
237 if not datas['res_id']:
238 # it is probably an orm_memory object, we need to create
240 datas['res_id'] = pool.get(datas['res_model']).create(cr, uid, view_data, context)
243 raise AssertionError("view form doesn't have any buttons to press!")
245 buttons.sort(key=lambda b: b['weight'])
246 _logger.debug('Buttons are: %s', ', '.join([ '%s: %d' % (b['string'], b['weight']) for b in buttons]))
249 while buttons and not res:
251 log_test("in the \"%s\" form, I will press the \"%s\" button.", action_name, b['string'])
253 log_test("the \"%s\" button has no type, cannot use it", b['string'])
255 if b['type'] == 'object':
256 #there we are! press the button!
257 fn = getattr(pool.get(datas['res_model']), b['name'])
259 _logger.error("The %s model doesn't have a %s attribute!", datas['res_model'], b['name'])
261 res = fn(cr, uid, [datas['res_id'],], context)
264 _logger.warning("in the \"%s\" form, the \"%s\" button has unknown type %s",
265 action_name, b['string'], b['type'])
268 elif action['type']=='ir.actions.report.xml':
269 if 'window' in datas:
272 datas = action.get('datas',{})
274 ids = datas.get('ids')
277 res = try_report(cr, uid, 'report.'+action['report_name'], ids, datas, context, our_module=our_module)
280 raise Exception("Cannot handle action of type %s" % act_model)
282 log_test("will be using %s action %s #%d", act_model, act_xmlid, act_id)
283 action = pool.get(act_model).read(cr, uid, act_id, context=context)
284 assert action, "Could not read action %s[%s]" %(act_model, act_id)
288 # This part tries to emulate the loop of the Gtk client
290 _logger.error("Passed %d loops, giving up", loop)
291 raise Exception("Too many loops at action")
292 log_test("it is an %s action at loop #%d", action.get('type', 'unknown'), loop)
293 result = _exec_action(action, datas, context)
294 if not isinstance(result, dict):
296 datas = result.get('datas', {})
305 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: