From c99cbccb8ea12456c68fa0271239384a098e5df9 Mon Sep 17 00:00:00 2001 From: Darragh Bailey Date: Tue, 15 Jul 2014 14:41:59 +0100 Subject: [PATCH] Ensure dict orders are deterministic Python 3 enables hash randomization by default, additionally tox 1.7 turns on the same randomization for earlier versions of python by default. Need to ensure that order of iteration over the yaml data and resulting XML has deterministic order for testing. Adapts https://gist.github.com/enaeseth/844388 which ensures data read by yaml will have its order retained in a predictable manner across multiple python versions. Additionally it seems more sensible to ensure that the order of generated XML snippets corresponding to the input yaml files are consistently in the same order as the entries in the source files. Closes-Bug: #1333349 Change-Id: I6bf6d298a2609cc6ddbbc6b02b7f1a04413a5c89 --- jenkins_jobs/local_yaml.py | 49 ++++++++++++++++++- jenkins_jobs/modules/publishers.py | 2 +- jenkins_jobs/modules/triggers.py | 10 ++-- test-requirements.txt | 1 + tests/builders/fixtures/ant002.xml | 4 +- .../fixtures/include-raw-escaped001.json | 10 ++-- tests/localyaml/fixtures/include-raw001.json | 6 +-- tests/localyaml/fixtures/include001.json | 30 ++++++------ tests/publishers/fixtures/ruby-metrics.xml | 4 +- tests/publishers/fixtures/xunit001.xml | 4 +- 10 files changed, 84 insertions(+), 36 deletions(-) diff --git a/jenkins_jobs/local_yaml.py b/jenkins_jobs/local_yaml.py index 27df904aa..f66ad4374 100644 --- a/jenkins_jobs/local_yaml.py +++ b/jenkins_jobs/local_yaml.py @@ -54,16 +54,58 @@ Example: """ import codecs +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict import functools import logging import re import os import yaml +from yaml.constructor import BaseConstructor logger = logging.getLogger(__name__) -class LocalLoader(yaml.Loader): +class OrderedConstructor(BaseConstructor): + """The default constructor class for PyYAML loading uses standard python + dictionaries which can have randomized ordering enabled (default in + CPython from version 3.3). The order of the XML elements being outputted + is both important for tests and for ensuring predictable generation based + on the source. This subclass overrides this behaviour to ensure that all + dict's created make use of OrderedDict to have iteration of keys to always + follow the order in which the keys were inserted/created. + """ + + def construct_yaml_map(self, node): + data = OrderedDict() + yield data + value = self.construct_mapping(node) + + if isinstance(node, yaml.MappingNode): + self.flatten_mapping(node) + else: + raise yaml.constructor.ConstructorError( + None, None, + 'expected a mapping node, but found %s' % node.id, + node.start_mark) + + mapping = OrderedDict() + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=False) + try: + hash(key) + except TypeError as exc: + raise yaml.constructor.ConstructorError( + 'while constructing a mapping', node.start_mark, + 'found unacceptable key (%s)' % exc, key_node.start_mark) + value = self.construct_object(value_node, deep=False) + mapping[key] = value + data.update(mapping) + + +class LocalLoader(OrderedConstructor, yaml.Loader): """Subclass for yaml.Loader which handles the local tags 'include', 'include-raw' and 'include-raw-escaped' to specify a file to include data from and whether to parse it as additional yaml, treat it as a data blob @@ -116,6 +158,11 @@ class LocalLoader(yaml.Loader): self.add_constructor('!include-raw-escape', self._include_raw_escape_tag) + # constructor to preserve order of maps and ensure that the order of + # keys returned is consistent across multiple python versions + self.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, + type(self).construct_yaml_map) + if isinstance(self.stream, file): self.search_path.add(os.path.normpath( os.path.dirname(self.stream.name))) diff --git a/jenkins_jobs/modules/publishers.py b/jenkins_jobs/modules/publishers.py index 6bb0b9827..2b02a4453 100644 --- a/jenkins_jobs/modules/publishers.py +++ b/jenkins_jobs/modules/publishers.py @@ -2399,7 +2399,7 @@ postbuildscript003.yaml 'generic': 'GenericScript', 'groovy': 'GroovyScriptFile', } - for script_type in script_types.keys(): + for script_type in sorted(script_types.keys()): if script_type + '-script' not in data: continue diff --git a/jenkins_jobs/modules/triggers.py b/jenkins_jobs/modules/triggers.py index 2caa70e93..a7c846fba 100644 --- a/jenkins_jobs/modules/triggers.py +++ b/jenkins_jobs/modules/triggers.py @@ -107,14 +107,14 @@ def build_gerrit_triggers(xml_parent, data): def build_gerrit_skip_votes(xml_parent, data): - outcomes = {'successful': 'onSuccessful', - 'failed': 'onFailed', - 'unstable': 'onUnstable', - 'notbuilt': 'onNotBuilt'} + outcomes = [('successful', 'onSuccessful'), + ('failed', 'onFailed'), + ('unstable', 'onUnstable'), + ('notbuilt', 'onNotBuilt')] skip_vote_node = XML.SubElement(xml_parent, 'skipVote') skip_vote = data.get('skip-vote', {}) - for result_kind, tag_name in outcomes.iteritems(): + for result_kind, tag_name in outcomes: if skip_vote.get(result_kind, False): XML.SubElement(skip_vote_node, tag_name).text = 'true' else: diff --git a/test-requirements.txt b/test-requirements.txt index 7c487591c..5328c5781 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,6 +2,7 @@ hacking>=0.5.6,<0.8 coverage>=3.6 discover fixtures +ordereddict python-subunit sphinx>=1.1.2,<1.2 setuptools_git>=0.4 diff --git a/tests/builders/fixtures/ant002.xml b/tests/builders/fixtures/ant002.xml index b3f8d69b5..160bfd04a 100644 --- a/tests/builders/fixtures/ant002.xml +++ b/tests/builders/fixtures/ant002.xml @@ -4,8 +4,8 @@ build.xml -ea -Xmx512m - failonerror=True -builddir=/tmp/ + builddir=/tmp/ +failonerror=True debug test install Standard Ant diff --git a/tests/localyaml/fixtures/include-raw-escaped001.json b/tests/localyaml/fixtures/include-raw-escaped001.json index 170985859..67f7796f4 100644 --- a/tests/localyaml/fixtures/include-raw-escaped001.json +++ b/tests/localyaml/fixtures/include-raw-escaped001.json @@ -1,24 +1,24 @@ [ { "template-job": { + "name": "test-job-include-raw-{num}", "builders": [ { "shell": "#!/bin/bash\n#\n# Sample script showing how the yaml include-raw tag can be used\n# to inline scripts that are maintained outside of the jenkins\n# job yaml configuration.\n\necho \"hello world\"\n\nexit 0\n" }, { - "shell": "#!/bin/bash\n#\n# sample script to check that brackets aren't escaped\n# when using the include-raw application yaml tag\n\nVAR1=\"hello\"\nVAR2=\"world\"\nVAR3=\"${{VAR1}} ${{VAR2}}\"\n\n[[ -n \"${{VAR3}}\" ]] && {{\n # this next section is executed as one\n echo \"${{VAR3}}\"\n exit 0\n}}\n\n" + "shell": "#!/bin/bash\n#\n# sample script to check that brackets aren't escaped\n# when using the include-raw application yaml tag\n\nVAR1=\"hello\"\nVAR2=\"world\"\nVAR3=\"${{VAR1}} ${{VAR2}}\"\n\n[[ -n \"${{VAR3}}\" ]] && {{\n # this next section is executed as one\n echo \"${{VAR3}}\"\n exit 0\n}}\n\n" } - ], - "name": "test-job-include-raw-{num}" + ] } }, { "project": { + "name": "test-job-template-1", "num": 1, "jobs": [ "test-job-include-raw-{num}" - ], - "name": "test-job-template-1" + ] } } ] diff --git a/tests/localyaml/fixtures/include-raw001.json b/tests/localyaml/fixtures/include-raw001.json index 7f2ac38b9..e9540524d 100644 --- a/tests/localyaml/fixtures/include-raw001.json +++ b/tests/localyaml/fixtures/include-raw001.json @@ -1,15 +1,15 @@ [ { "job": { + "name": "test-job-include-raw-1", "builders": [ { "shell": "#!/bin/bash\n#\n# Sample script showing how the yaml include-raw tag can be used\n# to inline scripts that are maintained outside of the jenkins\n# job yaml configuration.\n\necho \"hello world\"\n\nexit 0\n" }, { - "shell": "#!/bin/bash\n#\n# sample script to check that brackets aren't escaped\n# when using the include-raw application yaml tag\n\nVAR1=\"hello\"\nVAR2=\"world\"\nVAR3=\"${VAR1} ${VAR2}\"\n\n[[ -n \"${VAR3}\" ]] && {\n # this next section is executed as one\n echo \"${VAR3}\"\n exit 0\n}\n\n" + "shell": "#!/bin/bash\n#\n# sample script to check that brackets aren't escaped\n# when using the include-raw application yaml tag\n\nVAR1=\"hello\"\nVAR2=\"world\"\nVAR3=\"${VAR1} ${VAR2}\"\n\n[[ -n \"${VAR3}\" ]] && {\n # this next section is executed as one\n echo \"${VAR3}\"\n exit 0\n}\n\n" } - ], - "name": "test-job-include-raw-1" + ] } } ] diff --git a/tests/localyaml/fixtures/include001.json b/tests/localyaml/fixtures/include001.json index 93713f8e7..3b29d2402 100644 --- a/tests/localyaml/fixtures/include001.json +++ b/tests/localyaml/fixtures/include001.json @@ -1,43 +1,43 @@ [ { "job": { + "name": "test-job-1", "builders": [ { "copyartifact": { - "filter": "*.tar.gz", "project": "foo", - "parameter-filters": "PUBLISH=true", + "filter": "*.tar.gz", "target": "/home/foo", - "flatten": true, + "which-build": "last-successful", "optional": true, - "which-build": "last-successful" + "flatten": true, + "parameter-filters": "PUBLISH=true" } }, { "copyartifact": { "project": "bar", - "parameter-filters": "PUBLISH=true", - "target": "/home/foo", - "build-number": 123, - "which-build": "specific-build", "filter": "*.tar.gz", + "target": "/home/foo", + "which-build": "specific-build", + "optional": true, "flatten": true, - "optional": true + "parameter-filters": "PUBLISH=true", + "build-number": 123 } }, { "copyartifact": { - "filter": "*.tar.gz", "project": "baz", - "parameter-filters": "PUBLISH=true", + "filter": "*.tar.gz", "target": "/home/foo", - "flatten": true, + "which-build": "upstream-build", "optional": true, - "which-build": "upstream-build" + "flatten": true, + "parameter-filters": "PUBLISH=true" } } - ], - "name": "test-job-1" + ] } } ] diff --git a/tests/publishers/fixtures/ruby-metrics.xml b/tests/publishers/fixtures/ruby-metrics.xml index d0ecff407..75b74e7c3 100644 --- a/tests/publishers/fixtures/ruby-metrics.xml +++ b/tests/publishers/fixtures/ruby-metrics.xml @@ -7,14 +7,14 @@ TOTAL_COVERAGE 80 - 0 0 + 0 CODE_COVERAGE 80 - 0 0 + 0 diff --git a/tests/publishers/fixtures/xunit001.xml b/tests/publishers/fixtures/xunit001.xml index 7d6e5b92c..ce14e0c97 100644 --- a/tests/publishers/fixtures/xunit001.xml +++ b/tests/publishers/fixtures/xunit001.xml @@ -18,15 +18,15 @@ - + - +