Repository: TheFriendlyCoder/pyjen

Issue:
Add support for adding downstream build trigger to job
To facilitate testing the job.py module we need the ability to add downstream jobs as a build trigger publisher. 

Discussion:

Patch:
diff --git a/src/pyjen/job.py b/src/pyjen/job.py
index d7c03b0..8272f17 100644
--- a/src/pyjen/job.py
+++ b/src/pyjen/job.py
@@ -480,6 +480,14 @@ class Job(JenkinsAPI):
         jxml = JobXML(self.config_xml)
         return jxml.publishers
 
+    def add_publisher(self, publisher):
+        """Adds a new job publisher to this job
+
+        :param publisher: job publisher to add"""
+        jxml = JobXML(self.config_xml)
+        jxml.add_publisher(publisher.node)
+        self.config_xml = jxml.xml
+
     @property
     def properties(self):  # pragma: no cover
         """all plugins configured as extra configuration properties"""
diff --git a/src/pyjen/plugins/buildtriggerpublisher.py b/src/pyjen/plugins/buildtriggerpublisher.py
new file mode 100644
index 0000000..56850eb
--- /dev/null
+++ b/src/pyjen/plugins/buildtriggerpublisher.py
@@ -0,0 +1,77 @@
+"""Interface to the Jenkins 'build trigger' publishing plugin"""
+import xml.etree.ElementTree as ElementTree
+
+
+class BuildTriggerPublisher:
+    """Interface to the Jenkins 'build trigger' publishing plugin
+
+    This plugin is a default, built-in plugin which is part of the Jenkins core
+    """
+
+    def __init__(self, node):
+        """
+        :param node: XML node defining the settings for a this plugin
+        :type node: :class:`ElementTree.Element`
+        """
+        self._root = node
+        assert self._root.tag == self.get_jenkins_plugin_name()
+
+    @staticmethod
+    def get_jenkins_plugin_name():
+        """Gets the name of the Jenkins plugin associated with this PyJen plugin
+
+        This static method is used by the PyJen plugin API to associate this
+        class with a specific Jenkins plugin, as it is encoded in the config.xml
+
+        :rtype: :class:`str`
+        """
+        return "hudson.tasks.BuildTrigger"
+
+    @property
+    def job_names(self):
+        """Gets the names of 0 or more downstream jobs managed by this config
+
+        :rtype: :class:`list` of :class:`str`
+        """
+
+        children_node = self._root.find('childProjects')
+        return [i.strip() for i in children_node.text.split(",")]
+
+    @property
+    def node(self):
+        """Gets the XML node associated with this publisher
+
+        :rtype: :class:`ElementTree.Element`
+        """
+        return self._root
+
+    @staticmethod
+    def create(project_name):
+        """Factory method for creating a new build trigger
+
+        The default trigger will run when the parent job is successful
+
+        :param str project_name: name of the downstream job to be triggered
+        :rtype: :class:`pyjen.plugins.buildtriggerpublisher.BuildTriggerPublisher`
+        """
+        default_xml = """<hudson.tasks.BuildTrigger>
+<threshold>
+<name>SUCCESS</name>
+<ordinal>0</ordinal>
+<color>BLUE</color>
+<completeBuild>true</completeBuild>
+</threshold>
+</hudson.tasks.BuildTrigger>"""
+        root_node = ElementTree.fromstring(default_xml)
+
+        child = ElementTree.SubElement(root_node, "childProjects")
+        child.text = project_name
+
+        return BuildTriggerPublisher(root_node)
+
+
+PluginClass = BuildTriggerPublisher
+
+
+if __name__ == "__main__":  # pragma: no cover
+    pass
diff --git a/src/pyjen/utils/jobxml.py b/src/pyjen/utils/jobxml.py
index 3ffeae5..af20cac 100644
--- a/src/pyjen/utils/jobxml.py
+++ b/src/pyjen/utils/jobxml.py
@@ -144,6 +144,13 @@ class JobXML(ConfigXML):
                 self._log.warning("Unsupported job 'publisher' plugin")
         return retval
 
+    def add_publisher(self, node):
+        """Adds a new publisher node to the publisher section of the job XML
+
+        :param node: Elementree XML node for the publisher to assign"""
+        pubs = self._root.find('publishers')
+        pubs.append(node)
+
     @property
     def builders(self):
         """Gets a list of 0 or more build operations associated with this job


Test patch:
diff --git a/tests/test_job.py b/tests/test_job.py
index bd023f9..7073ce9 100644
--- a/tests/test_job.py
+++ b/tests/test_job.py
@@ -281,12 +281,16 @@ def test_build_health(jenkins_env):
     finally:
         jb.delete()
 
-# TODO: add support for publishers to API so we can test upstream/downstream jobs
-# def test_publishers(jenkins_env):
-#     jk = Jenkins(jenkins_env["url"], (jenkins_env["admin_user"], jenkins_env["admin_token"]))
-#     jb = jk.find_job("third")
-#     for cur_pub in jb.publishers:
-#         print(cur_pub)
+
+def test_no_publishers(jenkins_env):
+    jk = Jenkins(jenkins_env["url"], (jenkins_env["admin_user"], jenkins_env["admin_token"]))
+    jb = jk.create_job("test_no_publishers", "project")
+    pubs = jb.publishers
+
+    assert isinstance(pubs, list)
+    assert len(pubs) == 0
+
+
 
 
 # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
diff --git a/tests/test_job_publishers/__init__.py b/tests/test_job_publishers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_job_publishers/test_build_trigger_publisher.py b/tests/test_job_publishers/test_build_trigger_publisher.py
new file mode 100644
index 0000000..dc321ec
--- /dev/null
+++ b/tests/test_job_publishers/test_build_trigger_publisher.py
@@ -0,0 +1,62 @@
+import time
+import pytest
+from pyjen.jenkins import Jenkins
+from pyjen.plugins.buildtriggerpublisher import BuildTriggerPublisher
+
+
+def test_add_build_trigger_publisher(jenkins_env):
+    jk = Jenkins(jenkins_env["url"], (jenkins_env["admin_user"], jenkins_env["admin_token"]))
+    upstream_name = "test_add_build_trigger_publisher1"
+    jb = jk.create_job(upstream_name, "project")
+    jb2 = None
+    try:
+        downstream_name = "test_add_build_trigger_publisher2"
+        jb2 = jk.create_job(downstream_name, "project")
+        publisher = BuildTriggerPublisher.create(downstream_name)
+        jb.add_publisher(publisher)
+        # TODO: Find more robust way to detect when asynchronous calls are finished
+        #       Idea: look at Jenkins server logs to see if there is status info
+        #       we can use to figure this out
+        time.sleep(5)
+
+        # Get a fresh copy of our job to ensure we have an up to date
+        # copy of the config.xml for the job
+        tmp = jk.find_job(upstream_name)
+        pubs = tmp.publishers
+
+        assert isinstance(pubs, list)
+        assert len(pubs) == 1
+        assert isinstance(pubs[0], BuildTriggerPublisher)
+        names = pubs[0].job_names
+        assert isinstance(names, list)
+        assert len(names) == 1
+        assert names[0] == downstream_name
+    finally:
+        jb.delete()
+        if jb2:
+            jb2.delete()
+
+
+def test_downstream_jobs(jenkins_env):
+    jk = Jenkins(jenkins_env["url"], (jenkins_env["admin_user"], jenkins_env["admin_token"]))
+    jb = jk.create_job("test_downstream_jobs1", "project")
+    jb2 = None
+    try:
+        expected_name = "test_downstream_jobs2"
+        jb2 = jk.create_job(expected_name, "project")
+        publisher = BuildTriggerPublisher.create(expected_name)
+        jb.add_publisher(publisher)
+        time.sleep(5)
+        res = jb.downstream_jobs
+
+        assert isinstance(res, list)
+        assert len(res) == 1
+        assert res[0].name == expected_name
+    finally:
+        jb.delete()
+        if jb2:
+            jb2.delete()
+
+
+if __name__ == "__main__":
+    pytest.main([__file__, "-v", "-s"])