diff --git a/changelogs/fragments/12148-xml-preserve-doctype.yml b/changelogs/fragments/12148-xml-preserve-doctype.yml new file mode 100644 index 0000000000..4381f1e91e --- /dev/null +++ b/changelogs/fragments/12148-xml-preserve-doctype.yml @@ -0,0 +1,2 @@ +bugfixes: + - "xml - preserve DOCTYPE declaration when writing modified XML files (https://github.com/ansible-collections/community.general/issues/2762, https://github.com/ansible-collections/community.general/pull/12148)." diff --git a/plugins/modules/xml.py b/plugins/modules/xml.py index 545246a3cf..66169f753b 100644 --- a/plugins/modules/xml.py +++ b/plugins/modules/xml.py @@ -813,9 +813,15 @@ def children_to_nodes(module=None, children=None, type="yaml"): def make_pretty(module, tree): - xml_string = etree.tostring( - tree, xml_declaration=True, encoding="UTF-8", pretty_print=module.params["pretty_print"] + buf = BytesIO() + tree.write( + buf, + xml_declaration=True, + encoding="UTF-8", + pretty_print=module.params["pretty_print"], + doctype=tree.docinfo.doctype or None, ) + xml_string = buf.getvalue() result = dict( changed=False, @@ -830,7 +836,11 @@ def make_pretty(module, tree): if module.params["backup"]: result["backup_file"] = module.backup_local(module.params["path"]) tree.write( - xml_file, xml_declaration=True, encoding="UTF-8", pretty_print=module.params["pretty_print"] + xml_file, + xml_declaration=True, + encoding="UTF-8", + pretty_print=module.params["pretty_print"], + doctype=tree.docinfo.doctype or None, ) elif module.params["xmlstring"]: @@ -859,10 +869,23 @@ def finish(module, tree, xpath, namespaces, changed=False, msg="", hitcount=0, m if result["changed"]: if module._diff: - result["diff"] = dict( - before=etree.tostring(orig_doc, xml_declaration=True, encoding="UTF-8", pretty_print=True), - after=etree.tostring(tree, xml_declaration=True, encoding="UTF-8", pretty_print=True), + before_buf = BytesIO() + orig_doc.write( + before_buf, + xml_declaration=True, + encoding="UTF-8", + pretty_print=True, + doctype=orig_doc.docinfo.doctype or None, ) + after_buf = BytesIO() + tree.write( + after_buf, + xml_declaration=True, + encoding="UTF-8", + pretty_print=True, + doctype=tree.docinfo.doctype or None, + ) + result["diff"] = dict(before=before_buf.getvalue(), after=after_buf.getvalue()) if module.params["path"] and not module.check_mode: if module.params["backup"]: @@ -872,12 +895,19 @@ def finish(module, tree, xpath, namespaces, changed=False, msg="", hitcount=0, m xml_declaration=True, encoding="UTF-8", pretty_print=module.params["pretty_print"], + doctype=tree.docinfo.doctype or None, ) if module.params["xmlstring"]: - result["xmlstring"] = etree.tostring( - tree, xml_declaration=True, encoding="UTF-8", pretty_print=module.params["pretty_print"] + xmlstring_buf = BytesIO() + tree.write( + xmlstring_buf, + xml_declaration=True, + encoding="UTF-8", + pretty_print=module.params["pretty_print"], + doctype=tree.docinfo.doctype or None, ) + result["xmlstring"] = xmlstring_buf.getvalue() module.exit_json(**result) diff --git a/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml b/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml new file mode 100644 index 0000000000..3b755c1f54 --- /dev/null +++ b/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml.license b/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml.license new file mode 100644 index 0000000000..edff8c7685 --- /dev/null +++ b/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/integration/targets/xml/results/test-preserve-doctype.xml b/tests/integration/targets/xml/results/test-preserve-doctype.xml new file mode 100644 index 0000000000..72aced83d2 --- /dev/null +++ b/tests/integration/targets/xml/results/test-preserve-doctype.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/integration/targets/xml/results/test-preserve-doctype.xml.license b/tests/integration/targets/xml/results/test-preserve-doctype.xml.license new file mode 100644 index 0000000000..edff8c7685 --- /dev/null +++ b/tests/integration/targets/xml/results/test-preserve-doctype.xml.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/integration/targets/xml/tasks/main.yml b/tests/integration/targets/xml/tasks/main.yml index e69a28e578..257f29a100 100644 --- a/tests/integration/targets/xml/tasks/main.yml +++ b/tests/integration/targets/xml/tasks/main.yml @@ -79,6 +79,7 @@ - include_tasks: test-print-match.yml - include_tasks: test-xmlstring.yml - include_tasks: test-children-elements-xml.yml + - include_tasks: test-preserve-doctype.yml # Unicode tests - include_tasks: test-add-children-elements-unicode.yml diff --git a/tests/integration/targets/xml/tasks/test-preserve-doctype.yml b/tests/integration/targets/xml/tasks/test-preserve-doctype.yml new file mode 100644 index 0000000000..5c5b61400b --- /dev/null +++ b/tests/integration/targets/xml/tasks/test-preserve-doctype.yml @@ -0,0 +1,46 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Setup test fixture + copy: + src: fixtures/ansible-xml-doctype.xml + dest: /tmp/ansible-xml-doctype.xml + +- name: Set '/foo/@value' to '1' (file has DOCTYPE) + xml: + path: /tmp/ansible-xml-doctype.xml + xpath: /foo + attribute: value + value: "1" + register: set_attribute_doctype + +- name: Add trailing newline + shell: echo "" >> /tmp/ansible-xml-doctype.xml + +- name: Compare to expected result + copy: + src: results/test-preserve-doctype.xml + dest: /tmp/ansible-xml-doctype.xml + check_mode: true + diff: true + register: comparison + +- name: Test expected result (DOCTYPE is preserved) + assert: + that: + - set_attribute_doctype is changed + - comparison is not changed + +- name: Set '/foo/@value' to '1' again (idempotency) + xml: + path: /tmp/ansible-xml-doctype.xml + xpath: /foo + attribute: value + value: "1" + register: set_attribute_doctype_idempotent + +- name: Test idempotency + assert: + that: + - set_attribute_doctype_idempotent is not changed