155 lines
6.1 KiB
Python
155 lines
6.1 KiB
Python
import ast
|
|
from itertools import starmap
|
|
|
|
from lxml import etree as ET
|
|
from odoo.fields import Command, Domain
|
|
from odoo.addons.web_studio.controllers.export_utils import StudioExportSerializer
|
|
from odoo.tests.common import TransactionCase, tagged
|
|
|
|
XMLPARSER = ET.XMLParser(remove_blank_text=True, strip_cdata=False, resolve_entities=False)
|
|
|
|
|
|
def nodes_equal(n1, n2):
|
|
if n1.tag != n2.tag:
|
|
return False
|
|
if n1.text != n2.text:
|
|
return False
|
|
if n1.tail != n2.tail:
|
|
return False
|
|
if n1.attrib != n2.attrib:
|
|
return False
|
|
if len(n1) != len(n2):
|
|
return False
|
|
if n1.tag == "field":
|
|
# compare a tostring version, to check if CDATA sections are preserved
|
|
n1_str = ET.tostring(n1)
|
|
n2_str = ET.tostring(n2)
|
|
if n1_str != n2_str:
|
|
return False
|
|
if n1.tag == "record":
|
|
# n1 and n2 children order doesn't matter, sort them by tagname, attrib['name']
|
|
n1 = sorted(n1, key=lambda n: (n.tag, n.attrib.get("name")))
|
|
n2 = sorted(n2, key=lambda n: (n.tag, n.attrib.get("name")))
|
|
return all(starmap(nodes_equal, zip(n1, n2)))
|
|
|
|
|
|
@tagged("-at_install", "post_install")
|
|
class StudioExportCase(TransactionCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self._customizations = []
|
|
self._additional_models = self.env["studio.export.model"]
|
|
self._additional_models.search([]).unlink()
|
|
self.exporter = None
|
|
self._export_iter = None
|
|
self.TestModel = self.env["test.studio_export.model1"].with_user(2)
|
|
self.TestModel2 = self.env["test.studio_export.model2"].with_user(2)
|
|
self.TestModel3 = self.env["test.studio_export.model3"].with_user(2)
|
|
|
|
def create_customization(self, _model, **kwargs):
|
|
Model = self.env[_model].with_context(studio=True)
|
|
custo = Model.create(kwargs)
|
|
self._customizations.append(custo)
|
|
return custo
|
|
|
|
def create_export_model(self, _model, **kwargs):
|
|
IrModel = self.env["ir.model"]
|
|
vals = {"model_id": IrModel._get_id(_model)}
|
|
vals.update(kwargs)
|
|
export_model = self.env["studio.export.model"].create(vals)
|
|
self._additional_models |= export_model
|
|
self.addCleanup(export_model.unlink)
|
|
return export_model
|
|
|
|
def get_xml_nodes(self, filepath, xpath=None):
|
|
root = self._get_exported(filepath)
|
|
nodes = root.xpath(xpath) if xpath else root
|
|
return [nodes] if not isinstance(nodes, list) else nodes
|
|
|
|
def get_xmlid(self, record):
|
|
return self.exporter.utils.get_xmlid(record)
|
|
|
|
def studio_export(self):
|
|
if self._export_iter:
|
|
raise RuntimeError("Studio export already in progress: maybe make another test?")
|
|
|
|
# Prepare the export wizard
|
|
domain = Domain.OR(
|
|
Domain("model", "=", custo._name) & Domain("res_id", "=", custo.id)
|
|
for custo in self._customizations
|
|
) & Domain("studio", "=", True)
|
|
custo_data = self.env["ir.model.data"].search(domain)
|
|
custo_data = self.env["studio.export.wizard.data"].create(
|
|
[
|
|
{"model": d.model, "res_id": d.res_id, "studio": d.studio}
|
|
for d in custo_data
|
|
]
|
|
)
|
|
wizard = self.env["studio.export.wizard"].create(
|
|
{
|
|
"default_export_data": [Command.set(custo_data.ids)],
|
|
"additional_models": [Command.set(self._additional_models.ids)],
|
|
"include_additional_data": True,
|
|
"include_demo_data": True,
|
|
}
|
|
)
|
|
export_info = wizard.get_export_info()
|
|
|
|
# Start the export
|
|
studio_module = self.env["ir.module.module"].get_studio_module()
|
|
self.exporter = StudioExportSerializer(self.env, studio_module, export_info)
|
|
self._export_cache = {}
|
|
self._export_iter = iter(self.exporter.serialize())
|
|
|
|
def xml_tostring(self, el):
|
|
return ET.tostring(el, encoding="unicode", pretty_print=True)
|
|
|
|
def _get_exported(self, name=None):
|
|
if not self._export_iter:
|
|
raise RuntimeError("No export has begun, use studio_export() first")
|
|
while name not in self._export_cache:
|
|
try:
|
|
path, content = next(self._export_iter)
|
|
except StopIteration:
|
|
break
|
|
if path.endswith(".xml"):
|
|
self._export_cache[path] = ET.fromstring(content, parser=XMLPARSER)
|
|
elif path.endswith("__manifest__.py"):
|
|
self._export_cache[path] = ast.literal_eval(content.decode("utf-8"))
|
|
else:
|
|
self._export_cache[path] = content
|
|
|
|
return self._export_cache[name] if name else self._export_cache
|
|
|
|
def assertFileContains(self, path, content):
|
|
file = self._get_exported(path)
|
|
self.assertEqual(file, content)
|
|
|
|
def assertFileList(self, *filenames):
|
|
"""You can omit __init__.py and __manifest__.py"""
|
|
filenames += ("__init__.py", "__manifest__.py")
|
|
exported = self._get_exported()
|
|
self.assertEqual(set(exported.keys()), set(filenames))
|
|
|
|
def assertManifest(self, **expected):
|
|
exported = self._get_exported("__manifest__.py")
|
|
for key, value in expected.items():
|
|
if key == "depends":
|
|
self.assertEqual(
|
|
self.exporter.utils.clean_dependencies(set(exported["depends"] + value)),
|
|
exported["depends"],
|
|
)
|
|
else:
|
|
self.assertEqual(exported[key], value)
|
|
|
|
def assertXML(self, actual, expected):
|
|
actual = self.get_xml_nodes(actual)[0] if isinstance(actual, str) else actual
|
|
expected = ET.fromstring(expected, parser=XMLPARSER) if isinstance(expected, str) else expected
|
|
are_equal = nodes_equal(actual, expected)
|
|
message = "Both XMLs are equal"
|
|
if not are_equal:
|
|
expected = self.xml_tostring(expected) if not isinstance(expected, str) else expected
|
|
actual = self.xml_tostring(actual) if not isinstance(actual, str) else actual
|
|
message = "\nExpected:\n%s\nActual:\n%s" % (expected, actual)
|
|
self.assertTrue(are_equal, message)
|