diff --git a/Gemfile b/Gemfile index 30e7919c..95a04989 100644 --- a/Gemfile +++ b/Gemfile @@ -14,4 +14,7 @@ group :development do gem "yard-thor" gem "yard" gem "pry" + # this is required to avoid `undefined method last_comment` + # until jeweler is upgraded + gem "rake", "< 11.0" end diff --git a/lib/jenkins_api_client.rb b/lib/jenkins_api_client.rb index 7afdcda3..6d5b77fe 100644 --- a/lib/jenkins_api_client.rb +++ b/lib/jenkins_api_client.rb @@ -23,10 +23,12 @@ require 'jenkins_api_client/version' require 'jenkins_api_client/exceptions' require 'jenkins_api_client/client' +require 'jenkins_api_client/job_build' require 'jenkins_api_client/job' require 'jenkins_api_client/node' require 'jenkins_api_client/system' require 'jenkins_api_client/view' +require 'jenkins_api_client/queue_item' require 'jenkins_api_client/build_queue' require 'jenkins_api_client/plugin_manager' require 'jenkins_api_client/user' diff --git a/lib/jenkins_api_client/build_queue.rb b/lib/jenkins_api_client/build_queue.rb index 09a294e6..cdb9be7e 100644 --- a/lib/jenkins_api_client/build_queue.rb +++ b/lib/jenkins_api_client/build_queue.rb @@ -65,6 +65,16 @@ def list tasks end + # Lists all tasks currently in the build queue as [QueueItem] objects + # + def list_tasks + @logger.info "Obtaining the tasks currently in the build queue" + response_json = @client.api_get_request("/queue") + tasks = response_json["items"].map do |item| + QueueItem.new(@client, item) + end + end + # Gets the time number of seconds the task is in the queue # # @param [String] task_name Name of the task/job diff --git a/lib/jenkins_api_client/job.rb b/lib/jenkins_api_client/job.rb index ef7cd751..317e8043 100644 --- a/lib/jenkins_api_client/job.rb +++ b/lib/jenkins_api_client/job.rb @@ -665,6 +665,17 @@ def list(filter, ignorecase = true) jobs end + # List all builds for a given job + # + # @return an array of [JobBuild] items + def list_builds(job_name) + @logger.info "Obtaining the builds of #{job_name}" + response_json = @client.api_get_request("/job/#{path_encode job_name}", "tree=builds[number]") + tasks = response_json["builds"].map do |item| + JobBuild.new(@client, :id => item['number'],:name => job_name) + end + end + # List all jobs on the Jenkins CI server along with their details # # @return [Array] the details of all jobs in jenkins diff --git a/lib/jenkins_api_client/job_build.rb b/lib/jenkins_api_client/job_build.rb new file mode 100644 index 00000000..f3dd9d49 --- /dev/null +++ b/lib/jenkins_api_client/job_build.rb @@ -0,0 +1,66 @@ +module JenkinsApi + class Client + + # + # This class represents a build with an assingned id. This includes + # jobs already built or that are currently active. + # + class JobBuild + VALID_PARAMS = ['id', 'name', 'params'].freeze + attr_accessor :id, :name + + # Initializes a new [JobBuild] + # + # @param client [Client] the api client object + # @param attr Hash with attributes containing at least name and id, the information not provided + # can be latter retrieved through the json api using the given client + # + # @return [JobBuild] the build item object + def initialize(client, attr = {}) + @client = client + attr.each do |name, value| + name = name.to_s + if VALID_PARAMS.include?(name) + instance_variable_set("@#{name.to_s}", value) + end + end + end + + # @return Hash with build parameters + # + def params + load_attributes + @params + end + + private + + # Queries the server for the missing information + # + def load_attributes + if @params.nil? + json = @client.api_get_request("/job/#{@name}/#{@id}", "depth=2") + if json.member?('actions') + @params = json['actions'].find do |action| + if action.is_a?(Hash) + action.member?('parameters') + elsif action.is_a?(Array) + action.first.is_a?(Hash) and action.first.member?('value') + end + end + if @params.nil? + @params = {} + else + @params = @params['parameters'] if @params.is_a?(Hash) + @params = @params.map do |param| + [param['name'], param['value']] + end + @params = Hash[@params] + end + + end + end + end + end + end +end diff --git a/lib/jenkins_api_client/queue_item.rb b/lib/jenkins_api_client/queue_item.rb new file mode 100644 index 00000000..5537a2f1 --- /dev/null +++ b/lib/jenkins_api_client/queue_item.rb @@ -0,0 +1,51 @@ +module JenkinsApi + class Client + + # + # This class represents an item in the building queue and allows interaction + # with an identified queue item. + # + class QueueItem + VALID_PARAMS = ['id', 'params'].freeze + attr_accessor *VALID_PARAMS.each {|attr| attr.to_sym} + attr_accessor :name + + # Initializes a new QueueItem + # + # @param client [Cliente] the api client object + # @param json the json object with all the information of the queue item (as returned by api call "/queue") + # + # @return [QueueItem] the queue item object + # + def initialize(client, json) + @client = client + json.each do |key, value| + if VALID_PARAMS.include?(key) + instance_variable_set("@#{key}", value) + end + end if json.is_a? Hash + @name = json['task']['name'] + + # parseamos los parametros + @params = @params.split("\n").map do |p| + p.split('=') + end.select do |p| + not p.empty? + end + @params = Hash[@params] + end + + # String representation of the queue item object + # + def to_s + self.name + end + + # Removes the item from the building queue + # + def cancel + @client.api_post_request("/queue/cancelItem?id=#{self.id}") + end + end + end +end diff --git a/spec/func_tests/job_spec.rb b/spec/func_tests/job_spec.rb index 8c44d339..c2374244 100644 --- a/spec/func_tests/job_spec.rb +++ b/spec/func_tests/job_spec.rb @@ -464,6 +464,33 @@ def test_and_validate(name, params, config_line = nil) end end + describe "#list_builds" do + it "Should get builds of a specified job, and query their parameters" do + job = 'dummy_with_params' + begin + tries ||= 1 + xml = @helper.create_job_with_params_xml + @client.job.create(job, xml).to_i.should == 200 + rescue JenkinsApi::Exceptions::JobAlreadyExists + @client.job.delete(job) + tries -= 1 + retry if tries >= 0 + end + + 2.times do |num| + @client.job.build(job, {"PARAM1" => num}) + sleep 10 + while @client.job.get_current_build_status(job) == "running" do + sleep 2 + end + end + builds = @client.job.list_builds(job) + builds.class.should == Array + builds.map {|b| b.id }.sort.should == [1, 2] + builds.map {|b| b.params['PARAM1']}.sort.should == ['0', '1'] + end + end + describe "#get_current_build_status" do it "Should obtain the current build status for the specified job" do build_status = @client.job.get_current_build_status(@job_name) diff --git a/spec/func_tests/queue_spec.rb b/spec/func_tests/queue_spec.rb new file mode 100644 index 00000000..da5fd58a --- /dev/null +++ b/spec/func_tests/queue_spec.rb @@ -0,0 +1,106 @@ +# +# Specifying JenkinsApi::Client::Job class capabilities +# Author: Kannan Manickam +# + +require File.expand_path('../spec_helper', __FILE__) +require 'yaml' + +describe JenkinsApi::Client::BuildQueue do + context "With properly initialized client" do + before(:each) do + @helper = JenkinsApiSpecHelper::Helper.new + @creds_file = '~/.jenkins_api_client/spec.yml' + @creds = YAML.load_file(File.expand_path(@creds_file, __FILE__)) + @job_name_prefix = 'awesome_rspec_test_job' + @filter = "^#{@job_name_prefix}.*" + @job_name = '' + @valid_post_responses = [200, 201, 302] + begin + @client = JenkinsApi::Client.new(@creds) + rescue Exception => e + puts "WARNING: Credentials are not set properly." + puts e.message + end + # Creating 10 jobs to run the spec tests on + begin + @client.node.create_dumb_slave(:name => 'none', :slave_host => '10.10.10.10', :private_key_file => '') + 10.times do |num| + xml = @helper.create_job_with_params_xml + job = "#{@job_name_prefix}_#{num}" + @job_name = job if num == 0 + @client.job.create(job, xml).to_i.should == 200 + end + rescue Exception => e + puts "WARNING: Can't create jobs for preparing to spec tests" + puts e.message + end + end + + describe "InstanceMethods" do + + describe "#initialize" do + it "Initializes without any exception" do + expect( + lambda { job = JenkinsApi::Client::BuildQueue.new(@client) } + ).not_to raise_error + end + it "Raises an error if a reference of client is not passed" do + expect( + lambda { job = JenkinsApi::Client::BuildQueue.new() } + ).to raise_error + end + end + + describe "#list_tasks" do + it "Gets all queued tasks as QueueItem objects" do + 10.times do |num| + job = "#{@job_name_prefix}_#{num}" + @job_name = job if num == 0 + @client.job.build(job, {"PARAM1" => num}) + end + + tasks = @client.queue.list_tasks + expect(tasks).to be_a_kind_of(Array) + tasks.each do |task| + expect(task).to be_a_kind_of(JenkinsApi::Client::QueueItem) + expect(task.params).to include("PARAM1") + expect(task.name).to eq("#{@job_name_prefix}_#{task.params['PARAM1']}") + end + tasks.size.should == 10 + end + end + + end + + describe "QueueItem" do + describe "#cancel" do + it "Cancels an item in the queue" do + 5.times do |num| + job = "#{@job_name_prefix}_3" + @client.job.build(job, {"PARAM1" => num}) + end + + tasks = @client.queue.list_tasks + tasks.size.should == 5 + + tasks.each do |task| + task.cancel if task.params['PARAM1'].to_i.even? + end + + tasks = @client.queue.list_tasks + tasks.map {|t| t.params['PARAM1']}.sort.should == ['1', '3'] + end + end + end + + after(:each) do + job_names = @client.job.list(@filter) + job_names.each { |job_name| + @client.job.delete(job_name) + } + @client.node.delete_all! + end + + end +end diff --git a/spec/func_tests/spec_helper.rb b/spec/func_tests/spec_helper.rb index 20822891..713e4a55 100644 --- a/spec/func_tests/spec_helper.rb +++ b/spec/func_tests/spec_helper.rb @@ -37,5 +37,40 @@ def create_job_xml } builder.to_xml end + + def create_job_with_params_xml + builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') { |xml| + xml.project { + xml.actions + xml.description + xml.keepDependencies "false" + xml.properties { + xml.send("hudson.model.ParametersDefinitionProperty") { + xml.parameterDefinitions { + xml.send("hudson.model.StringParameterDefinition") { + xml.name "PARAM1" + } + } + } + } + xml.scm(:class => "hudson.scm.NullSCM") + xml.canRoam "true" + xml.disabled "false" + xml.assignedNode "none" + xml.blockBuildWhenDownstreamBuilding "false" + xml.blockBuildWhenUpstreamBuilding "false" + xml.triggers.vector + xml.concurrentBuild "false" + xml.builders { + xml.send("hudson.tasks.Shell") { + xml.command "\necho 'going to take a nice nap'\nsleep 15\necho 'took a nice nap'" + } + } + xml.publishers + xml.buildWrappers + } + } + builder.to_xml + end end end diff --git a/spec/unit_tests/build_queue_spec.rb b/spec/unit_tests/build_queue_spec.rb index aa5762d6..3ea77e29 100644 --- a/spec/unit_tests/build_queue_spec.rb +++ b/spec/unit_tests/build_queue_spec.rb @@ -66,6 +66,19 @@ end end + describe "#list_tasks" do + it "returns the list of tasks in the queue as QueueItems" do + @client.should_receive(:api_get_request).with("/queue").and_return( + @sample_queue_json + ) + tasks = @queue.list_tasks + tasks.class.should == Array + tasks.each do |t| + t.class.should == JenkinsApi::Client::QueueItem + end + end + end + describe "#get_age" do it "returns the age of a task" do @client.should_receive(:api_get_request).with("/queue").and_return( diff --git a/spec/unit_tests/job_build_spec.rb b/spec/unit_tests/job_build_spec.rb new file mode 100644 index 00000000..73afb355 --- /dev/null +++ b/spec/unit_tests/job_build_spec.rb @@ -0,0 +1,80 @@ +require File.expand_path('../spec_helper', __FILE__) + +describe JenkinsApi::Client::JobBuild do + context "With properly initialized Client" do + before do + @client = double + mock_logger = Logger.new "/dev/null" + allow(@client).to receive(:logger).and_return(mock_logger) + @build = JenkinsApi::Client::JobBuild.new(@client, id: 23, name: 'build_test') + end + + describe "InstanceMethods" do + describe "#initialize" do + it "initializes by receiving an instance of client object id and name" do + expect( + lambda{ JenkinsApi::Client::JobBuild.new(@client, id: 1, name: 'name') } + ).not_to raise_error + end + end + + describe "#id" do + it "returns the build id" do + @build.id.should == 23 + end + end + + describe "#name" do + it "returns the build name" do + @build.name.should == "build_test" + end + end + + describe "#params" do + it "queries the server when asked about parameters of the build" do + json = { + 'actions' => [ + [ + { + "name" => "PARAM1", + "value" => "VALUE1", + }, + { + "name" => "PARAM2", + "value" => "VALUE2", + } + ] + ] + } + @client.should_receive(:api_get_request).with('/job/build_test/23', anything()).once().and_return(json) + 2.times do + @build.params.should == {'PARAM1' => 'VALUE1', 'PARAM2' => 'VALUE2'} + end + end + + it "queries the server when asked about parameters of the build (with other type of possible json response)" do + json = { + 'actions' => [ + { + 'parameters' => [ + { + "name" => "PARAM1", + "value" => "VALUE1", + }, + { + "name" => "PARAM2", + "value" => "VALUE2", + } + ] + } + ] + } + @client.should_receive(:api_get_request).with('/job/build_test/23', anything()).once().and_return(json) + 2.times do + @build.params.should == {'PARAM1' => 'VALUE1', 'PARAM2' => 'VALUE2'} + end + end + end + end + end +end diff --git a/spec/unit_tests/job_spec.rb b/spec/unit_tests/job_spec.rb index acb96bf5..c02a4fba 100644 --- a/spec/unit_tests/job_spec.rb +++ b/spec/unit_tests/job_spec.rb @@ -273,6 +273,23 @@ end end + describe "#list_builds" do + it "returns the array of builds of a job" do + json = { + "builds" => (1..5).map do |x| + { + "number" => x + } + end + } + @client.should_receive(:api_get_request).with("/job/test_job", anything()).and_return( + json) + response = @job.list_builds('test_job') + response.class.should == Array + response.map { |b| b.id }.should == Array(1..5) + end + end + describe "#exists?" do it "accepts a job name and returns true if the job exists" do @client.should_receive(:api_get_request).and_return( diff --git a/spec/unit_tests/queue_item_spec.rb b/spec/unit_tests/queue_item_spec.rb new file mode 100644 index 00000000..f270526d --- /dev/null +++ b/spec/unit_tests/queue_item_spec.rb @@ -0,0 +1,73 @@ +require File.expand_path('../spec_helper', __FILE__) + +describe JenkinsApi::Client::QueueItem do + context "With properly initialized Client" do + before do + @client = double + mock_logger = Logger.new "/dev/null" + allow(@client).to receive(:logger).and_return(mock_logger) + @sample_queue_item_json = { + "actions" => [ + { + "causes" => [ + { + + } + ] + } + ], + "blocked" => true, + "buildable" => false, + "id" => 2, + "inQueueSince" => 1362906942731, + "params" => "\nPARAM1=VALUE1\nPARAM2=VALUE2", + "stuck" => false, + "task" => { + "name" => "queue_test", + "url" => "http://localhost:8080/job/queue_test/", + "color" => "grey_anime" + }, + "why" => "Build #1 is already in progress (ETA:N/A)", + "buildStartMilliseconds" => 1362906942832 + } + @queue_item = JenkinsApi::Client::QueueItem.new(@client, @sample_queue_item_json) + end + + describe "InstanceMethods" do + describe "#initialize" do + it "initializes by receiving an instance of client object and queue json" do + expect( + lambda{ JenkinsApi::Client::QueueItem.new(@client, @sample_queue_item_json) } + ).not_to raise_error + end + end + + describe "#id" do + it "returns the queue id" do + @queue_item.id.should == 2 + end + end + + describe "#name" do + it "returns the tasks name" do + @queue_item.name.should == "queue_test" + end + end + + describe "#params" do + it "returns the tasks params" do + @queue_item.params.class.should == Hash + @queue_item.params.should == {'PARAM1' => 'VALUE1', 'PARAM2' => 'VALUE2'} + end + end + + describe "#cancel" do + it "signals jenkins to cancel queued item" do + @client.should_receive(:api_post_request).with('/queue/cancelItem?id=2') + @queue_item.cancel + end + end + + end + end +end