├── .gitignore ├── Gemfile ├── README.markdown ├── Rakefile ├── features ├── build_history.feature ├── configure_slaves.feature ├── freestyle_build.feature ├── install_plugin.feature ├── plugins │ └── buildtimeout.feature ├── step_definitions │ ├── build_history_steps.rb │ ├── buildtimeout_steps.rb │ ├── general_steps.rb │ ├── job_configuration_steps.rb │ ├── job_steps.rb │ ├── plugin_steps.rb │ └── slave_steps.rb └── support │ ├── env.rb │ └── hooks.rb ├── grab-latest-rc.sh ├── lib ├── build.rb ├── controller │ ├── jenkins_controller.rb │ ├── local_controller.rb │ ├── log_watcher.rb │ └── sysv_init_controller.rb ├── job.rb ├── pageobject.rb ├── pluginmanager.rb └── slave.rb ├── setup.sh └── test └── selenium ├── core └── freestyle_test.rb ├── lib └── base.rb ├── pageobjects ├── build.rb ├── globalconfig.rb ├── job.rb ├── newjob.rb ├── newslave.rb ├── pluginmanager.rb └── slave.rb └── plugins └── git.rb /.gitignore: -------------------------------------------------------------------------------- 1 | jenkins.war 2 | *.swp 3 | *.swn 4 | last_test.log 5 | *~ 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem "rake" 4 | gem "selenium-webdriver" 5 | gem "rest-client" 6 | 7 | gem "cucumber" 8 | gem "curb" 9 | gem "capybara" 10 | gem "json" 11 | gem "rspec" 12 | gem "tempdir" 13 | 14 | gem "rdoc" 15 | 16 | if RUBY_VERSION < "1.9" 17 | gem "rdoc-data" 18 | end 19 | 20 | 21 | # vim: set ft=conf 22 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Selenium tests for Jenkins 2 | 3 | This is a project to flesh out some of the [manual test cases for 4 | Jenkins LTS](https://wiki.jenkins-ci.org/display/JENKINS/LTS+1.409.x+RC+Testing) in an automated fashion. 5 | 6 | Right now the project is in a very early state, and is in dire need of some 7 | [Page Objects](https://code.google.com/p/selenium/wiki/PageObjects) for the 8 | more standard components of Jenkins such as the: 9 | 10 | * Root actions link listing (top left sidebar) 11 | * New Job control 12 | * Various plugin configuration sections on the `job/configure` page 13 | * Node configuration 14 | * etc 15 | 16 | Drop me a line (`rtyler` on [Freenode](http://freenode.net)) if you're 17 | interested in helping out 18 | 19 | 20 | ## Current test matrix 21 | 22 | The tests cases that have been completed or nede to be completed can be found 23 | on the [Selenium Test 24 | Cases](https://wiki.jenkins-ci.org/display/JENKINS/Selenium+Test+Cases) page on 25 | the Jenkins wiki 26 | 27 | For historical reasons, there are older tests that are written for `test/unit` (in the `test` directory) 28 | and newer tests that are written for cucumber (in the `features` directory.) 29 | 30 | ## Running tests 31 | 32 | To run the test, `JENKINS_WAR=path/to/your/jenkins.war bundle exec rake`. This will run both 33 | the older `test/unit` tests as well as cucumber tests in one go. 34 | 35 | There is a bit of a delay since we bring up Jenkins for every single test, with 36 | it's own sandboxed workspace as well: 37 | 38 | ![](http://strongspace.com/rtyler/public/selenium-jenkins.png) 39 | 40 | 41 | ### Choosing the JenkinsController 42 | This test harness has an abstraction called `JenkinsController` that allows you to use different logic 43 | for starting/stopping Jenkins. We use this to reuse the same set of tests for testing stand-alone `jenkins.war` 44 | to testing packages. 45 | 46 | See [the source code](tree/master/lib/controller/) for the list of available controllers. If you see a line like 47 | `register :remote_sysv`, that means the ID of that controller is `remote_sysv`. 48 | 49 | To select a controller, run the test with the 'type' environment variable set to the controller ID, such as: 50 | `type=remote_sysv bundle exec rake`. Controllers take their configurations from environment variables. Again, 51 | see the controller source code for details until we document them here. 52 | 53 | ### Running one test 54 | You can run a single cucumber test by pointing to a test scenario in terms of its file name and line number like 55 | `bundle exec cucumber features/freestyle_build.feature:6` 56 | 57 | If someone knows how to run a single `test/unit` test case, please update this document. 58 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | require 'rake' 3 | require 'rake/testtask' 4 | require 'cucumber' 5 | require 'cucumber/rake/task' 6 | 7 | task :default => [:test,:features] 8 | 9 | desc "Run Selenium tests locally" 10 | Rake::TestTask.new("test") do |t| 11 | t.pattern = "test/selenium/**/*_test.rb" 12 | end 13 | 14 | 15 | namespace :cucumber do 16 | desc "Run the 'finished' scenarios (without @wip)" 17 | Cucumber::Rake::Task.new(:ready) do |t| 18 | t.cucumber_opts = "--tags ~@wip --format pretty" 19 | end 20 | 21 | desc "Run the scenarios which don't require network access" 22 | Cucumber::Rake::Task.new(:nonetwork) do |t| 23 | t.cucumber_opts = "--tags ~@wip --tags ~@realupdatecenter --format pretty" 24 | end 25 | 26 | desc "Run the scenarios tagged with @wip" 27 | Cucumber::Rake::Task.new(:wip) do |t| 28 | t.cucumber_opts = "--tags @wip --format pretty" 29 | end 30 | end 31 | 32 | desc "Defaults to running cucumber:ready" 33 | task :cucumber => "cucumber:ready" 34 | -------------------------------------------------------------------------------- /features/build_history.feature: -------------------------------------------------------------------------------- 1 | Feature: Display build history 2 | As a Jenkins user or adminstrator 3 | I should be able to view the build history both globally or per-job 4 | So that I can identify build trends, times, etc. 5 | 6 | Scenario: Viewing global build history 7 | Given a simple job 8 | When I run the job 9 | Then the global build history should show the build 10 | -------------------------------------------------------------------------------- /features/configure_slaves.feature: -------------------------------------------------------------------------------- 1 | Feature: configure slaves 2 | In order to effectively use more machines 3 | As a user 4 | I want to be able to configure slaves and jobs to distribute load 5 | 6 | Scenario: Tie a job to a specified label 7 | Given a job 8 | And a dumb slave 9 | When I add the label "test" to the slave 10 | And I configure the job 11 | And I tie the job to the "test" label 12 | Then I should see the job tied to the "test" label 13 | 14 | Scenario: Tie a job to a specific slave 15 | Given a job 16 | And a dumb slave 17 | When I configure the job 18 | And I tie the job to the slave 19 | Then I should see the job tied to the slave 20 | 21 | Scenario: Create a slave with multiple executors 22 | Given a dumb slave 23 | When I set the executors to "3" 24 | And I visit the home page 25 | Then I should see "3" executors configured 26 | 27 | 28 | # vim: tabstop=2 expandtab shiftwidth=2 29 | -------------------------------------------------------------------------------- /features/freestyle_build.feature: -------------------------------------------------------------------------------- 1 | Feature: Configure/build freestyle jobs 2 | In order to get some basic usage out of freestyle jobs 3 | As a user 4 | I want to configure and run a series of different freestyle-based jobs 5 | 6 | Scenario: Create a simple job 7 | When I create a job named "MAGICJOB" 8 | And I visit the home page 9 | Then the page should say "MAGICJOB" 10 | 11 | Scenario: Run a simple job 12 | Given a job 13 | When I configure the job 14 | And I add a script build step to run "ls" 15 | And I save the job 16 | And I run the job 17 | Then I should see console output matching "+ ls" 18 | 19 | Scenario: Disable a job 20 | Given a job 21 | When I configure the job 22 | And I click the "disable" checkbox 23 | And I save the job 24 | And I visit the job page 25 | Then the page should say "This project is currently disabled" 26 | 27 | Scenario: Enable concurrent builds 28 | Given a job 29 | When I configure the job 30 | And I enable concurrent builds 31 | And I add a script build step to run "sleep 20" 32 | And I save the job 33 | And I build 2 jobs 34 | Then the 2 jobs should run concurrently 35 | 36 | Scenario: Create a parameterized job 37 | Given a job 38 | When I configure the job 39 | And I add a string parameter "Foo" 40 | And I run the job 41 | Then I should be prompted to enter the "Foo" parameter 42 | 43 | Scenario: Configure a job with Ant build steps 44 | Given a job 45 | When I configure the job 46 | And I add an Ant build step for: 47 | """ 48 | 49 | 50 | 51 | 52 | 53 | """ 54 | When I run the job 55 | Then the build should succeed 56 | 57 | Scenario: Disable a job 58 | Given a job 59 | When I disable the job 60 | Then it should be disabled 61 | And it should have an "Enable" button on the job page 62 | 63 | # vim: tabstop=2 expandtab shiftwidth=2 64 | -------------------------------------------------------------------------------- /features/install_plugin.feature: -------------------------------------------------------------------------------- 1 | Feature: Install plugins from the update center 2 | In order to make Jenkins more useful for non-default uses 3 | 4 | As a Jenkins user 5 | 6 | I should be able to browse a number of plugins and install them directly from 7 | within Jenkins itself 8 | 9 | @realupdatecenter 10 | Scenario: Install the Git plugin 11 | When I install the "git" plugin from the update center 12 | And I create a job named "git-test" 13 | Then the job should be able to use the Git SCM 14 | 15 | 16 | -------------------------------------------------------------------------------- /features/plugins/buildtimeout.feature: -------------------------------------------------------------------------------- 1 | Feature: Fail builds that take too long 2 | In order to prevent executors from being blocked for too long 3 | As a Jenkins user 4 | I want to set timeouts with the build-timeout plugin to abort or 5 | fail builds that exceed specied timeout values 6 | 7 | # NOTE: The build-timeout plugin doesn't allow timeouts less than 3 minutes 8 | # in duration, so I'm just going to leave this commented out :( 9 | #@wip @realupdatecenter 10 | #Scenario: Fail a blocked job 11 | # Given I have installed the "build-timeout" plugin 12 | # And a job 13 | # When I configure the job 14 | # And I add a script build step to run "sleep 10000" 15 | # And I set the build timeout to 3 minutes 16 | # When I run the job 17 | # Then the build should fail 18 | 19 | -------------------------------------------------------------------------------- /features/step_definitions/build_history_steps.rb: -------------------------------------------------------------------------------- 1 | Then /^the global build history should show the build$/ do 2 | visit '/view/All/builds' 3 | page.should have_content("#{@job.name} #1") 4 | end 5 | -------------------------------------------------------------------------------- /features/step_definitions/buildtimeout_steps.rb: -------------------------------------------------------------------------------- 1 | When /^I set the build timeout to (\d+) minutes$/ do |timeout| 2 | # Check the [x] Abort the build if it's stuck 3 | find(:xpath, "//input[@name='hudson-plugins-build_timeout-BuildTimeoutWrapper']").set(true) 4 | 5 | 6 | choose 'build-timeout.timeoutType' 7 | fill_in '_.timeoutMunites', :with => timeout 8 | end 9 | 10 | -------------------------------------------------------------------------------- /features/step_definitions/general_steps.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # vim: tabstop=2 expandtab shiftwidth=2 3 | When /^I visit the home page$/ do 4 | visit "/" 5 | end 6 | 7 | When /^I click the "([^"]*)" checkbox$/ do |name| 8 | find(:xpath, "//input[@name='#{name}']").set(true) 9 | end 10 | 11 | Then /^the page should say "([^"]*)"$/ do |content| 12 | page.should have_content(content) 13 | end 14 | 15 | -------------------------------------------------------------------------------- /features/step_definitions/job_configuration_steps.rb: -------------------------------------------------------------------------------- 1 | 2 | When /^I configure the job$/ do 3 | @job.configure 4 | end 5 | 6 | When /^I add a script build step to run "([^"]*)"$/ do |script| 7 | @job.add_script_step(script) 8 | end 9 | 10 | When /^I tie the job to the "([^"]*)" label$/ do |label| 11 | @job.configure do 12 | @job.label_expression = label 13 | end 14 | end 15 | 16 | When /^I tie the job to the slave$/ do 17 | step %{I tie the job to the "#{@slave.name}" label} 18 | end 19 | 20 | When /^I enable concurrent builds$/ do 21 | step %{I click the "_.concurrentBuild" checkbox} 22 | end 23 | 24 | When /^I add a string parameter "(.*?)"$/ do |string_param| 25 | @job.configure do 26 | @job.add_parameter("String Parameter",string_param,string_param) 27 | end 28 | end 29 | When /^I add an Ant build step for:$/ do |ant_xml| 30 | @job.configure do 31 | @job.add_script_step("cat > build.xml < :firefox, :http_client => http_client) 12 | end 13 | 14 | Capybara.run_server = false 15 | Capybara.default_selector = :css 16 | Capybara.default_driver = :selenium 17 | 18 | 19 | # Include Page objects: 20 | 21 | PAGE_OBJECTS_BASE = File.dirname(__FILE__) + "/../../lib/" 22 | 23 | 24 | Dir["#{PAGE_OBJECTS_BASE}/*.rb"].each do |name| 25 | require File.expand_path(name) 26 | end 27 | -------------------------------------------------------------------------------- /features/support/hooks.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # vim: tabstop=2 expandtab shiftwidth=2 3 | 4 | $LOAD_PATH.push File.dirname(__FILE__) + "/../.." 5 | require "lib/controller/jenkins_controller.rb" 6 | require "lib/controller/local_controller.rb" 7 | require "lib/controller/sysv_init_controller.rb" 8 | 9 | Before('@realupdatecenter') do |scenario| 10 | @controller_options = {:real_update_center => true} 11 | end 12 | 13 | Before do |scenario| 14 | # default is to run locally, but allow the parameters to be given as env vars 15 | # so that rake can be invoked like "rake test type=remote_sysv" 16 | if ENV['type'] 17 | controller_args = {} 18 | ENV.each { |k,v| controller_args[k.to_sym]=v } 19 | else 20 | controller_args = { :type => :local } 21 | end 22 | 23 | if @controller_options 24 | controller_args = controller_args.merge(@controller_options) 25 | end 26 | @runner = JenkinsController.create(controller_args) 27 | @runner.start 28 | at_exit do 29 | @runner.stop 30 | @runner.teardown 31 | end 32 | @base_url = @runner.url 33 | Capybara.app_host = @base_url 34 | 35 | # wait for Jenkins to properly boot up and finish initialization 36 | s = Capybara.current_session 37 | for i in 1..20 do 38 | begin 39 | s.visit "/systemInfo" 40 | s.find "TABLE.bigtable" 41 | break # found it 42 | rescue => e 43 | sleep 0.5 44 | end 45 | end 46 | end 47 | 48 | After do |scenario| 49 | @runner.stop # if test fails, stop in at_exit is not called 50 | end 51 | -------------------------------------------------------------------------------- /grab-latest-rc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -xe 2 | 3 | curl -L "http://mirrors.jenkins-ci.org/war-rc/latest/jenkins.war" > jenkins.war 4 | -------------------------------------------------------------------------------- /lib/build.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # vim: tabstop=2 expandtab shiftwidth=2 3 | 4 | require File.dirname(__FILE__) + "/pageobject.rb" 5 | 6 | module Jenkins 7 | class Build < PageObject 8 | attr_accessor :job, :number 9 | 10 | def initialize(base_url, job, number) 11 | @base_url = base_url 12 | @job = job 13 | @number = number 14 | super(base_url, "#{job}/#{number}") 15 | end 16 | 17 | def build_url 18 | @job.job_url + "/#{@number}" 19 | end 20 | 21 | def open 22 | visit(build_url) 23 | end 24 | 25 | def json_api_url 26 | "#{build_url}/api/json" 27 | end 28 | 29 | def console 30 | @console ||= begin 31 | visit("#{build_url}/console") 32 | find(:xpath, "//pre").text 33 | end 34 | end 35 | 36 | def succeeded? 37 | @succeeded ||= begin 38 | visit(build_url) 39 | page.has_xpath? "//img[@title='Success']" 40 | end 41 | end 42 | 43 | def failed? 44 | return !succeeded 45 | end 46 | 47 | def in_progress? 48 | data = self.json 49 | return data['building'] 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/controller/jenkins_controller.rb: -------------------------------------------------------------------------------- 1 | require 'temp_dir' 2 | 3 | # This module defines a contract that various Jenkins controller implementations need to support 4 | # 5 | # JenkinsController encapsulates a Jenkins installation, the subject of the test. 6 | # It maybe a lone bare +jenkins.war+, it may be an installation of Jenkins on Tomcat, 7 | # or it maybe a debian package installation of Jenkins that starts/stops via SysV init script. 8 | # Jenkins may or may not be running on a local machine, etc. 9 | # 10 | # Each JenkinsController goes through the call sequence of +(start,restart*,stop)+ sprinkled with 11 | # calls to +url+ and +diagnose+. 12 | class JenkinsController 13 | attr_accessor :is_running, :log_watcher 14 | 15 | def initialize(*args) 16 | @is_running = false 17 | @log_watcher = nil 18 | end 19 | 20 | # Starts Jenkins, with a brand new temporary JENKINS_HOME. 21 | # 22 | # This method can return as soon as the server becomes accessible via HTTP, 23 | # and it is the caller's responsibility to wait until Jenkins finishes its bootup sequence. 24 | def start! 25 | raise NotImplementedException 26 | end 27 | 28 | def start 29 | start! unless is_running? 30 | @is_running = true 31 | end 32 | 33 | # Restarts Jenkins 34 | def restart 35 | stop 36 | start 37 | end 38 | 39 | # Shuts down the Jenkins process. 40 | def stop! 41 | raise NotImplementedException 42 | end 43 | 44 | def stop 45 | stop! if is_running? 46 | @is_running = false 47 | end 48 | 49 | # return the URL where Jenkins is running, such as "http://localhost:9999/" 50 | # the URL must ends with '/' 51 | def url 52 | raise "Not implemented yet" 53 | end 54 | 55 | # local file path to obtain slave.jar 56 | # TODO: change this to URL. 57 | def slave_jar_path 58 | "#{@tempdir}/war/WEB-INF/slave.jar" 59 | end 60 | 61 | # called when a test failed. Produce diagnostic output to the console, to the extent you can, 62 | # such as printing out the server log, etc. 63 | def diagnose 64 | # default is no-op 65 | end 66 | 67 | # called at the very end to dispose any resources 68 | def teardown 69 | 70 | end 71 | 72 | def is_running? 73 | @is_running 74 | end 75 | 76 | # registered implementations 77 | @@impls = {} 78 | 79 | def self.create(args) 80 | t = @@impls[args[:type].to_sym] 81 | raise "Undefined controller type #{args[:type]}" if t.nil? 82 | t.new(args) 83 | end 84 | 85 | def self.register(type) 86 | @@impls[type] = self 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/controller/local_controller.rb: -------------------------------------------------------------------------------- 1 | %w(jenkins_controller log_watcher).each { |f| require File.dirname(__FILE__)+"/"+f } 2 | 3 | # Runs jenkins.war on the same system with built-in Winstone container 4 | class LocalJenkinsController < JenkinsController 5 | JENKINS_DEBUG_LOG = Dir.pwd + "/last_test.log" 6 | register :local 7 | attr_accessor :real_update_center 8 | 9 | # @param [Hash] opts 10 | # :war => specify the location of jenkins.war 11 | def initialize(opts) 12 | super() 13 | @war = opts[:war] || ENV['JENKINS_WAR'] || File.expand_path("./jenkins.war") 14 | raise "jenkins.war doesn't exist in #{@war}, maybe you forgot to set JENKINS_WAR env var?" if !File.exists?(@war) 15 | 16 | @tempdir = TempDir.create(:rootpath => Dir.pwd) 17 | 18 | # Chose a random port, just to be safe 19 | @httpPort = rand(65000 - 8080) + 8080 20 | @controlPort = rand(65000 - 8080) + 8080 21 | @real_update_center = opts[:real_update_center] || false 22 | 23 | FileUtils.rm JENKINS_DEBUG_LOG if File.exists? JENKINS_DEBUG_LOG 24 | @log = File.open(JENKINS_DEBUG_LOG, "w") 25 | 26 | @base_url = "http://127.0.0.1:#{@httpPort}/" 27 | end 28 | 29 | def start! 30 | ENV["JENKINS_HOME"] = @tempdir 31 | puts 32 | print " Bringing up a temporary Jenkins instance" 33 | @pipe = IO.popen(["java", 34 | @real_update_center ? "" : "-Dhudson.model.UpdateCenter.updateCenterUrl=http://not.resolvable", 35 | "-jar", @war, "--ajp13Port=-1", "--controlPort=#{@controlPort}", 36 | "--httpPort=#{@httpPort}","2>&1"].join(' ')) 37 | @pid = @pipe.pid 38 | 39 | @log_watcher = LogWatcher.new(@pipe,@log) 40 | @log_watcher.wait_for_ready 41 | 42 | # still seeing occasional first page load problem. adding a bit more delay 43 | sleep 1 44 | end 45 | 46 | def stop! 47 | begin 48 | TCPSocket.open("localhost", @controlPort) do |sock| 49 | sock.write("0") 50 | end 51 | @log_watcher.wait_for_ready false 52 | rescue => e 53 | puts "Failed to cleanly shutdown Jenkins #{e}" 54 | puts " "+e.backtrace.join("\n ") 55 | puts "Killing #{@pid}" 56 | Process.kill("KILL",@pid) 57 | end 58 | end 59 | 60 | def teardown 61 | unless @log.nil? 62 | @log.close 63 | end 64 | FileUtils.rm_rf(@tempdir) 65 | end 66 | 67 | def url 68 | @base_url 69 | end 70 | 71 | def diagnose 72 | puts "It looks like the test failed/errored, so here's the console from Jenkins:" 73 | puts "--------------------------------------------------------------------------" 74 | File.open(JENKINS_DEBUG_LOG, 'r') do |fd| 75 | fd.each_line do |line| 76 | puts line 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/controller/log_watcher.rb: -------------------------------------------------------------------------------- 1 | # Mix-in for JenkinsController that watches the log output by Jenkins 2 | class LogWatcher 3 | TIMEOUT = 60 4 | 5 | # Launches a thread that monitors the given +pipe+ for log output and copy them over to +log+ 6 | # @arg [IO] pipe 7 | # @arg [IO] log 8 | def initialize(pipe,log) 9 | @ready = false 10 | @log_regex = nil 11 | @log_found = false 12 | 13 | @log = log 14 | @pipe = pipe 15 | Thread.new do 16 | while (line = @pipe.gets) 17 | log_line(line) 18 | # earlier version of Jenkins doesn't have this line 19 | # if line =~ /INFO: Jenkins is fully up and running/ 20 | if line =~ /INFO: Completed initialization/ 21 | puts " Jenkins completed initialization" 22 | @ready = true 23 | else 24 | unless @ready 25 | print '.' 26 | STDOUT.flush 27 | end 28 | end 29 | end 30 | @ready = false 31 | end 32 | end 33 | 34 | # block until Jenkins is up and running 35 | def wait_for_ready(expected=true) 36 | start_time = Time.now 37 | while @ready!=expected && ((Time.now - start_time) < TIMEOUT) 38 | sleep 0.5 39 | end 40 | 41 | if @ready!=expected 42 | raise expected ? "Could not bring up a Jenkins server" : "Shut down of Jenkins server had timed out" 43 | end 44 | end 45 | 46 | def log_line(line) 47 | @log.write(line) 48 | @log.flush 49 | 50 | unless @log_regex.nil? 51 | if line.match(@log_regex) 52 | @log_found = true 53 | end 54 | end 55 | end 56 | 57 | def wait_until_logged(regex, timeout=60) 58 | start = Time.now.to_i 59 | @log_regex = regex 60 | 61 | while (Time.now.to_i - start) < timeout 62 | if @log_found 63 | @log_regex = nil 64 | @log_found = false 65 | return true 66 | end 67 | sleep 1 68 | end 69 | 70 | return false 71 | end 72 | 73 | def close 74 | @pipe.close if @pipe 75 | end 76 | end -------------------------------------------------------------------------------- /lib/controller/sysv_init_controller.rb: -------------------------------------------------------------------------------- 1 | %w(jenkins_controller log_watcher).each { |f| require File.dirname(__FILE__)+"/"+f } 2 | 3 | # Runs Jenkins controlled by SysV init script, running on another machine 4 | # 5 | # @attr [String] ssh 6 | # ssh command line with parameters that control how to access the remote host, such as "ssh -p 2222 localhost" 7 | # @attr [String] host 8 | # "hostname[:port]" that specifies where it is running 9 | # @attr [String] service 10 | # SysV service name of Jenkins. By default "jenkins" 11 | # @attr [String] logfile 12 | # Path to the log file on the target system, default to "/var/log/jenkins/jenkins.log" 13 | class RemoteSysvInitController < JenkinsController 14 | register :remote_sysv 15 | 16 | JENKINS_DEBUG_LOG = Dir.pwd + "/last_test.log" 17 | 18 | attr_reader :url 19 | 20 | def initialize(args) 21 | @ssh = args[:ssh] 22 | @host = args[:host] 23 | @service = args[:service] || "jenkins" 24 | @logfile = args[:logfile] || "/var/log/jenkins/jenkins.log" 25 | 26 | @url = "http://#{@host}/" 27 | @tempdir = "/tmp/jenkins/"+(rand(500000)+100000).to_s 28 | ssh_exec "'mkdir -p #{@tempdir} && chmod 777 #{@tempdir}'" 29 | 30 | FileUtils.rm JENKINS_DEBUG_LOG if File.exists? JENKINS_DEBUG_LOG 31 | @log = File.open(JENKINS_DEBUG_LOG, "w") 32 | end 33 | 34 | # run the command via ssh 35 | def ssh_exec(cmd) 36 | if !system("#{@ssh} #{cmd}") 37 | raise "Command execution '#{@ssh} #{cmd}' failed" 38 | end 39 | end 40 | 41 | def ssh_popen(cmd) 42 | IO.popen("#{@ssh} #{cmd}") 43 | end 44 | 45 | def start! 46 | # perl needs to get '.*', which means ssh needs to get '.\*' (because ssh executes perl via shell) 47 | # which means system needs to get '.\\\*' (because system runs ssh via shell), 48 | # and that means Ruby literal needs whopping 6 '\'s. Crazy. 49 | ssh_exec "sudo perl -p -i -e s%JENKINS_HOME=.\\\\\\*%JENKINS_HOME=#{@tempdir}% /etc/default/jenkins /etc/sysconfig/jenkins" 50 | ssh_exec "sudo /etc/init.d/#{@service} stop" # make sure it's dead 51 | ssh_exec "sudo /etc/init.d/#{@service} start" 52 | 53 | @pipe = ssh_popen("sudo tail -f #{@logfile}") 54 | 55 | @log_watcher = LogWatcher.new(@pipe,@log) 56 | @log_watcher.wait_for_ready 57 | end 58 | 59 | def stop! 60 | begin 61 | ssh_exec "sudo /etc/init.d/#{@service} stop" 62 | 63 | @log_watcher.close 64 | rescue => e 65 | puts "Failed to cleanly shutdown Jenkins #{e}" 66 | puts " "+e.backtrace.join("\n ") 67 | end 68 | end 69 | 70 | def teardown 71 | unless @log.nil? 72 | @log.close 73 | end 74 | ssh_exec "sudo rm -rf #{@tempdir}" 75 | end 76 | 77 | def diagnose 78 | puts "It looks like the test failed/errored, so here's the console from Jenkins:" 79 | puts "--------------------------------------------------------------------------" 80 | File.open(JENKINS_DEBUG_LOG, 'r') do |fd| 81 | fd.each_line do |line| 82 | puts line 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/job.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # vim: tabstop=2 expandtab shiftwidth=2 3 | 4 | require File.dirname(__FILE__) + "/pageobject.rb" 5 | require File.dirname(__FILE__) + "/build.rb" 6 | 7 | module Jenkins 8 | class Job < PageObject 9 | attr_accessor :timeout 10 | 11 | def initialize(*args) 12 | @timeout = 60 # Default all builds for this job to a 60s timeout 13 | super(*args) 14 | end 15 | 16 | def job_url 17 | @base_url + "/job/#{@name}" 18 | end 19 | 20 | def configure_url 21 | job_url + "/configure" 22 | end 23 | 24 | def configure(&block) 25 | visit configure_url 26 | unless block.nil? 27 | yield 28 | save 29 | end 30 | end 31 | 32 | def add_parameter(type,name,value) 33 | ensure_config_page 34 | find(:xpath, "//input[@name='parameterized']").set(true) 35 | find(:xpath, "//button[text()='Add Parameter']").click 36 | find(:xpath, "//a[text()='#{type}']").click 37 | find(:xpath, "//input[@name='parameter.name']").set(name) 38 | find(:xpath, "//input[@name='parameter.defaultValue']").set(value) 39 | end 40 | 41 | 42 | def add_script_step(script) 43 | ensure_config_page 44 | 45 | # HACK: on a sufficiently busy configuration page, the "add build step" button can end up below 46 | # the sticky "save" button, and Chrome driver says that's not clickable. So we first scroll all 47 | # the way down, so that "add build step" will appear top of the page. 48 | page.execute_script "window.scrollTo(0, document.body.scrollHeight)" 49 | 50 | find(:xpath, "//button[text()='Add build step']").click 51 | find(:xpath, "//a[text()='Execute shell']").click 52 | find(:xpath, "//textarea[@name='command']").set(script) 53 | end 54 | 55 | def add_ant_step(targets, ant_build_file) 56 | click_button 'Add build step' 57 | click_link 'Invoke Ant' 58 | fill_in '_.targets', :with => targets 59 | end 60 | 61 | def open 62 | visit(job_url) 63 | end 64 | 65 | def last_build 66 | return build("lastBuild") # Hacks! 67 | end 68 | 69 | def build(number) 70 | Jenkins::Build.new(@base_url, self, number) 71 | end 72 | 73 | def queue_build 74 | visit("#{job_url}/build?delay=0sec") 75 | # This is kind of silly, but I can't think of a better way to wait for the 76 | # build to complete 77 | sleep 5 78 | end 79 | 80 | def wait_for_build(number) 81 | build = self.build(number) 82 | start = Time.now 83 | while (build.in_progress? && ((Time.now - start) < @timeout)) 84 | sleep 1 85 | end 86 | end 87 | 88 | def label_expression=(expression) 89 | ensure_config_page 90 | find(:xpath, "//input[@name='hasSlaveAffinity']").set(true) 91 | find(:xpath, "//input[@name='_.assignedLabelString']").set(expression) 92 | end 93 | 94 | def disable 95 | check 'disable' 96 | end 97 | 98 | def self.create_freestyle(base_url, name) 99 | visit("#{@base_url}/newJob") 100 | 101 | fill_in "name", :with => name 102 | find(:xpath, "//input[starts-with(@value, 'hudson.model.FreeStyleProject')]").set(true) 103 | click_button "OK" 104 | 105 | self.new(base_url, name) 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/pageobject.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # vim: tabstop=2 expandtab shiftwidth=2 3 | 4 | require 'rubygems' 5 | require 'capybara' 6 | require 'capybara/dsl' 7 | 8 | module Jenkins 9 | class PageObject 10 | include Capybara::DSL 11 | extend Capybara::DSL 12 | 13 | attr_accessor :name 14 | 15 | def initialize(base_url, name) 16 | @base_url = base_url 17 | @name = name 18 | end 19 | 20 | def self.random_name 21 | suffix = (rand() * 10_000_000).to_s[0 .. 20] 22 | return "rand_name_#{suffix}" 23 | end 24 | 25 | def ensure_config_page 26 | current_url.should == configure_url 27 | end 28 | 29 | def configure_url 30 | # Should be overridden by subclasses if they want to use the configure 31 | # block 32 | nil 33 | end 34 | 35 | def configure(&block) 36 | visit(configure_url) 37 | 38 | unless block.nil? 39 | yield 40 | save 41 | end 42 | end 43 | 44 | def save 45 | click_button "Save" 46 | end 47 | 48 | def json_api_url 49 | # Should be overridden by subclasses 50 | nil 51 | end 52 | 53 | def json 54 | url = json_api_url 55 | unless url.nil? 56 | begin 57 | uri = URI.parse(url) 58 | return JSON.parse(Net::HTTP.get_response(uri).body) 59 | rescue => e 60 | puts "Failed to parse JSON from URL #{url}" 61 | end 62 | end 63 | return nil 64 | end 65 | 66 | def wait_for(selector, opts={}) 67 | timeout = opts[:timeout] || 30 68 | selector_kind = opts[:with] || :css 69 | start = Time.now.to_i 70 | begin 71 | find(selector_kind, selector) 72 | rescue Capybara::TimeoutError, Capybara::ElementNotFound => e 73 | retry unless (Time.now.to_i - start) >= timeout 74 | raise 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/pluginmanager.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # vim: tabstop=2 expandtab shiftwidth=2 3 | 4 | require File.dirname(__FILE__) + "/pageobject.rb" 5 | 6 | module Jenkins 7 | class PluginManager < PageObject 8 | def initialize(*args) 9 | super(*args) 10 | @updated = false 11 | end 12 | 13 | def url 14 | @base_url + "/pluginManager" 15 | end 16 | 17 | def open 18 | visit url 19 | end 20 | 21 | def check_for_updates 22 | visit "#{url}/checkUpdates" 23 | 24 | wait_for("//span[@id='completionMarker' and text()='Done']", :with => :xpath) 25 | 26 | @updated = true 27 | # This is totally arbitrary, it seems that the Available page doesn't 28 | # update properly if you don't sleep a bit 29 | sleep 5 30 | end 31 | 32 | def install_plugin(name) 33 | unless @updated 34 | check_for_updates 35 | end 36 | 37 | visit "#{url}/available" 38 | check "plugin.#{name}.default" 39 | click_button 'Install' 40 | end 41 | 42 | def installed?(name) 43 | visit "#{url}/installed" 44 | page.has_xpath?("//input[@url='plugin/#{name}']") 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/slave.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # vim: tabstop=2 expandtab shiftwidth=2 3 | 4 | require 'rubygems' 5 | require 'json' 6 | require 'net/http' 7 | require 'uri' 8 | 9 | require File.dirname(__FILE__) + "/pageobject.rb" 10 | 11 | module Jenkins 12 | class Slave < PageObject 13 | include Capybara::DSL 14 | extend Capybara::DSL 15 | 16 | def configure_url 17 | @base_url + "/computer/#{@name}/configure" 18 | end 19 | 20 | def json_api_url 21 | @base_url + "/computer/#{@name}/api/json" 22 | end 23 | 24 | def executors=(num_exec) 25 | find(:xpath, "//input[@name='_.numExecutors']").set(num_exec.to_s) 26 | # in my chrome, I need to move the focus out from the control to have it recognize the value entered 27 | # perhaps it's related to the way input type=number is emulated? 28 | find(:xpath, "//input[@name='_.remoteFS']").click 29 | end 30 | 31 | def remote_fs=(remote_fs) 32 | find(:xpath, "//input[@name='_.remoteFS']").set(remote_fs) 33 | end 34 | 35 | def labels=(labels) 36 | find(:xpath, "//input[@name='_.labelString']").set(labels) 37 | end 38 | 39 | def online? 40 | data = self.json 41 | return data != nil && !data["offline"] 42 | end 43 | 44 | def executor_count 45 | data = self.json 46 | return data["executors"].length 47 | end 48 | 49 | def self.dumb_slave(base_url) 50 | slave = self.new(base_url, self.random_name) 51 | visit("/computer/new") 52 | 53 | find(:xpath, "//input[@id='name']").set(slave.name) 54 | find(:xpath, "//input[@value='hudson.slaves.DumbSlave']").set(true) 55 | click_button "OK" 56 | # This form submission will drop us on the configure page 57 | 58 | # Just to make sure the dumb slave is set up properly, we should seed it 59 | # with a FS root and executors 60 | slave.executors = 1 61 | slave.remote_fs = "/tmp/#{slave.name}" 62 | 63 | # Configure this slave to be automatically launched from the master 64 | find(:xpath, "//option[@value='hudson.slaves.CommandLauncher']").select_option 65 | find(:xpath, "//input[@name='_.command']").set("sh -c 'curl -s -o slave.jar #{base_url}jnlpJars/slave.jar && java -jar slave.jar'") 66 | 67 | slave.save 68 | 69 | # Fire the slave up before we move on 70 | start = Time.now 71 | while (!slave.online? && (Time.now - start) < 60) 72 | sleep 1 73 | end 74 | 75 | return slave 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | LIB_DIR="./lib" # if changed, JENKINS_LIB_DIR variable in test/selenium/lib/base.rb needs to be changed as well! 4 | JENKINS_WAR="$LIB_DIR/jenkins.war" 5 | 6 | function prepare_lib_dir { 7 | # LIB_DIR doesn't exist 8 | if [ ! -d $LIB_DIR ]; then 9 | echo "Creating lib dir ($LIB_DIR)" 10 | mkdir $LIB_DIR 11 | fi 12 | } 13 | 14 | function clean_lib_dir { 15 | # LIB_DIR is not empty 16 | if [ !`ls -A $LIB_DIR` ]; then 17 | echo "Lib dir ($LIB_DIR) is not empty, cleaning" 18 | rm -rf $LIB_DIR 19 | fi 20 | 21 | } 22 | 23 | function grab_latest_rc { 24 | curl "http://mirrors.jenkins-ci.org/war-rc/latest/jenkins.war" > $JENKINS_WAR 25 | } 26 | 27 | function grab_latest_lts { 28 | curl "http://mirrors.jenkins-ci.org/war-stable/latest/jenkins.war" > $JENKINS_WAR 29 | } 30 | 31 | function extract_slave { 32 | prepare_lib_dir 33 | tmp_dir=`mktemp -d` 34 | unzip $JENKINS_WAR -d $tmp_dir 35 | if [ -r $tmp_dir/WEB-INF/slave.jar ]; then 36 | cp $tmp_dir/WEB-INF/slave.jar $LIB_DIR 37 | else 38 | echo "slave.jar ($tmp_dir/WEB-INF/slave.jar) wasn't found, exit!" 39 | rm -rf $tmp_dir 40 | exit 1 41 | fi 42 | rm -rf $tmp_dir 43 | } 44 | 45 | 46 | extract_slave 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/selenium/core/freestyle_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/../lib/base" 2 | require File.dirname(__FILE__) + "/../pageobjects/newjob" 3 | require File.dirname(__FILE__) + "/../pageobjects/job" 4 | require File.dirname(__FILE__) + "/../pageobjects/newslave" 5 | require File.dirname(__FILE__) + "/../pageobjects/slave" 6 | require File.dirname(__FILE__) + "/../pageobjects/globalconfig" 7 | 8 | class FreestyleJobTests < JenkinsSeleniumTest 9 | def setup 10 | super 11 | @job_name = "Selenium_Test_Job" 12 | NewJob.create_freestyle(@driver, @base_url, @job_name) 13 | @job = Job.new(@driver, @base_url, @job_name) 14 | end 15 | 16 | def test_svn_checkout 17 | @job.configure do 18 | # checkout some small project from SVN 19 | @job.setup_svn("https://svn.jenkins-ci.org/trunk/hudson/plugins/zfs/") 20 | # check workspace if '.svn' dir is present, if not, fail the job 21 | @job.add_build_step "if [ '$(ls .svn)' ]; then \n exit 0 \n else \n exit 1 \n fi" 22 | sleep 10 23 | end 24 | 25 | @job.queue_build 26 | @job.wait_for_build 27 | build = @job.build(1) 28 | #build should fail if the project is not checked out. 29 | #TODO any better way how to check it? Check if all file from repo are present? 30 | assert build.succeeded?, "The build did not succeed!" 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/selenium/lib/base.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | require 'fileutils' 4 | require 'selenium-webdriver' 5 | require 'socket' 6 | require 'temp_dir' 7 | require 'test/unit' 8 | 9 | $LOAD_PATH.push File.dirname(__FILE__) + "/../../.." 10 | require "lib/controller/jenkins_controller.rb" 11 | require "lib/controller/local_controller.rb" 12 | require "lib/controller/sysv_init_controller.rb" 13 | require "test/selenium/pageobjects/globalconfig.rb" 14 | 15 | class JenkinsSeleniumTest < Test::Unit::TestCase 16 | TIMEOUT = 60 17 | 18 | # TODO: get rid of this by downloading slave.jar 19 | JENKINS_LIB_DIR = Dir.pwd + "/lib" 20 | 21 | # @return [JenkinsController] 22 | attr_reader :controller 23 | 24 | # default is to run locally, but allow the parameters to be given as env vars 25 | # so that rake can be invoked like "rake test type=remote_sysv" 26 | if ENV['type'] 27 | @@controller_args = {} 28 | ENV.each { |k,v| @@controller_args[k.to_sym]=v } 29 | else 30 | @@controller_args = { :type => :local } 31 | end 32 | 33 | # set the parameters for creating controller 34 | def self.controller_args=(hash) 35 | @@controller_args = hash 36 | end 37 | 38 | def start_jenkins 39 | @controller.start 40 | @base_url = @controller.url 41 | 42 | go_home 43 | # This hack-ish waiter is in place due to current versions of Jenkins LTS 44 | # not printing "Jenkins is fully up and running" to the logs once Jenkins 45 | # is up and running. 46 | # 47 | # This means LTS releases for now will drop the user on the "Jenkins is 48 | # getting ready to work" page, which means we have to poll until we hit the 49 | # proper "home page" 50 | Selenium::WebDriver::Wait.new(:timeout => 60, :interval => 1).until do 51 | # Due to a current bug with LTS which results in the "Jenkins is getting 52 | # ready to work" page redirecting to a gnarly exception page like this: 53 | # 54 | # 55 | # We're going to refresh the home page every single second >_< 56 | @driver.find_element(:xpath, "//a[@href='/view/All/newJob' and text()='New Job']") if @driver.navigate.refresh 57 | end 58 | end 59 | 60 | def stop_jenkins 61 | @controller.stop 62 | end 63 | 64 | 65 | def restart_jenkins 66 | @controller.restart 67 | end 68 | 69 | def go_home 70 | @driver.navigate.to @base_url 71 | end 72 | 73 | def setup 74 | @controller = JenkinsController.create @@controller_args 75 | @slave_tempdir = TempDir.create(:rootpath => Dir.pwd) 76 | 77 | @driver = Selenium::WebDriver.for(:firefox) 78 | @waiter = Selenium::WebDriver::Wait.new(:timeout => 10) 79 | 80 | start_jenkins 81 | GlobalConfig.instance.init(@driver,@base_url) 82 | end 83 | 84 | def teardown 85 | stop_jenkins 86 | 87 | unless @driver.nil? 88 | @driver.quit 89 | end 90 | 91 | @controller.teardown 92 | FileUtils.rm_rf(@slave_tempdir) 93 | 94 | unless @test_passed 95 | @controller.diagnose 96 | end 97 | end 98 | 99 | def self.suite 100 | # This is a hack to prevent the accidental inclusion of an empty 101 | # "default_test" method in the test suite. The consequences of this extra 102 | # test method means we will bring Jenkins up and down one more time, which 103 | # sucks 104 | r = super 105 | r.tests.collect! do |t| 106 | unless t.method_name == "default_test" 107 | t 108 | end 109 | end 110 | r.tests.compact! 111 | r 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /test/selenium/pageobjects/build.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'selenium-webdriver' 3 | require 'test/unit' 4 | require 'rest_client' 5 | require 'json' 6 | 7 | 8 | class Build 9 | include Test::Unit::Assertions 10 | 11 | BUILD_TIMEOUT = 300 12 | 13 | def initialize(driver, base_url, job, number) 14 | @driver = driver 15 | @base_url = base_url 16 | @job = job 17 | @number = number 18 | end 19 | 20 | def job 21 | @job 22 | end 23 | 24 | def number 25 | @number 26 | end 27 | 28 | def build_url 29 | @job.job_url + "/#{@number}" 30 | end 31 | 32 | def json_rest_url 33 | build_url + "/api/json" 34 | end 35 | 36 | 37 | def console 38 | @console ||= begin 39 | @driver.navigate.to(build_url + "/console") 40 | 41 | console = @driver.find_element(:xpath, "//pre") 42 | assert_not_nil console, "Couldn't find the console text on the page" 43 | console.text 44 | end 45 | end 46 | 47 | def succeeded? 48 | @succeeded ||= begin 49 | @driver.navigate.to(build_url) 50 | status_icon = @driver.find_element(:xpath, "//img[@src='buildStatus']") 51 | 52 | status_icon["tooltip"] == "Success" 53 | end 54 | end 55 | 56 | def failed? 57 | return !succeeded 58 | end 59 | 60 | def in_progress? 61 | response = RestClient.get(json_rest_url) 62 | json = JSON.parse(response.body) 63 | return json['building'] 64 | end 65 | 66 | 67 | end 68 | -------------------------------------------------------------------------------- /test/selenium/pageobjects/globalconfig.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'selenium-webdriver' 3 | require 'test/unit' 4 | 5 | require 'singleton' 6 | 7 | class GlobalConfig 8 | include Test::Unit::Assertions 9 | include Singleton 10 | 11 | attr_reader :driver, :base_url 12 | 13 | def init(driver,base_url) 14 | @driver = driver 15 | @base_url = base_url 16 | end 17 | 18 | def config_url 19 | @base_url + "/configure" 20 | end 21 | 22 | def configure(&block) 23 | @driver.navigate.to(config_url) 24 | 25 | unless block.nil? 26 | yield 27 | save 28 | end 29 | end 30 | 31 | def add_ant_latest 32 | ensure_config_page 33 | button = @driver.find_element(:xpath, "//button[text()='Add Ant']") 34 | ensure_element(button, "Add Ant button") 35 | button.click 36 | 37 | name = @driver.find_element(:xpath, "//input[@name='_.name']") 38 | ensure_element(name,"Ant name") 39 | name.send_keys "Latest" 40 | end 41 | 42 | def save 43 | ensure_config_page 44 | button = @driver.find_element(:xpath, "//button[text()='Save']") 45 | ensure_element(button, "Couldn't find the Save button on the configuration page") 46 | button.click 47 | end 48 | 49 | def ensure_config_page 50 | assert_equal @driver.current_url, config_url, "Cannot configure build steps if I'm not on the configure page" 51 | end 52 | 53 | def ensure_element(element,name) 54 | assert_not_nil element, "Couldn't find element '#{name}'" 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /test/selenium/pageobjects/job.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'selenium-webdriver' 3 | require 'test/unit' 4 | 5 | require File.dirname(__FILE__) + "/build" 6 | 7 | class Job 8 | include Test::Unit::Assertions 9 | 10 | def initialize(driver, base_url, name) 11 | @driver = driver 12 | @base_url = base_url 13 | @name = name 14 | end 15 | 16 | def name 17 | @name 18 | end 19 | 20 | def job_url 21 | @base_url + "/job/#{@name}" 22 | end 23 | 24 | def configure_url 25 | job_url + "/configure" 26 | end 27 | 28 | def configure(&block) 29 | @driver.navigate.to(configure_url) 30 | 31 | unless block.nil? 32 | yield 33 | save 34 | end 35 | end 36 | 37 | def open 38 | @driver.navigate.to(job_url) 39 | end 40 | 41 | def queue_build 42 | @driver.navigate.to(job_url + "/build?delay=0sec") 43 | # This is kind of silly, but I can't think of a better way to wait for the 44 | # build to complete 45 | sleep 5 46 | end 47 | 48 | def queue_param_build 49 | build_button = @driver.find_element(:xpath, "//button[text()='Build']") 50 | ensure_element(build_button,"Param build button") 51 | build_button.click 52 | end 53 | 54 | def build(number) 55 | Build.new(@driver, @base_url, self, number) 56 | end 57 | 58 | def add_parameter(type,name,value) 59 | ensure_config_page 60 | param_check_box = @driver.find_element(:name, "parameterized") 61 | ensure_element(param_check_box,"Parametrized build check box") 62 | param_check_box.click 63 | param_type_list = @driver.find_element(:xpath, "//button[text()='Add Parameter']") 64 | ensure_element(param_type_list,"Parameter type list") 65 | param_type_list.click 66 | param_type_link = @driver.find_element(:link,type) 67 | ensure_element(param_type_link,"Link to parameter fo type '#{type}'") 68 | param_type_link.click 69 | param_name = @driver.find_element(:xpath, "//input[@name='parameter.name']") 70 | ensure_element(param_name,"Parameter name") 71 | param_name.send_keys name 72 | param_def_value = @driver.find_element(:xpath, "//input[@name='parameter.defaultValue']") 73 | ensure_element(param_def_value,"Parameter default value") 74 | param_def_value.send_keys value 75 | end 76 | 77 | 78 | def disable 79 | assert_equal @driver.current_url, configure_url, "Cannot disableif I'm not on the configure page!" 80 | 81 | checkbox = @driver.find_element(:xpath, "//input[@name='disable']") 82 | assert_not_nil checkbox, "Couldn't find the disable button on the configuration page" 83 | checkbox.click 84 | end 85 | 86 | def allow_concurent_builds 87 | ensure_config_page 88 | checkbox = @driver.find_element(:xpath, "//input[@name='_.concurrentBuild']") 89 | ensure_element(checkbox,"Execute concurrent builds if necessary") 90 | checkbox.click 91 | end 92 | 93 | def tie_to(expression) 94 | restrict = @driver.find_element(:xpath,"//input[@name='hasSlaveAffinity']") 95 | ensure_element(restrict,"Restrict where this project can be run") 96 | restrict.click 97 | restrict.click 98 | label_exp = @driver.find_element(:xpath,"//input[@name='_.assignedLabelString']"); 99 | ensure_element(label_exp,"Label Expression") 100 | label_exp.send_keys expression 101 | end 102 | 103 | def setup_svn(repo_url) 104 | ensure_config_page 105 | radio = @driver.find_element(:xpath,"//input[@id='radio-block-24']") 106 | ensure_element(radio,"SVN radio button") 107 | radio.click 108 | remote_loc = @driver.find_element(:xpath,"//input[@id='svn.remote.loc']") 109 | ensure_element(remote_loc,"Repository URL") 110 | remote_loc.send_keys repo_url 111 | end 112 | 113 | def add_build_step(script) 114 | assert_equal @driver.current_url, configure_url, "Cannot configure build steps if I'm not on the configure page" 115 | 116 | add_step = @driver.find_element(:xpath, "//button[text()='Add build step']") 117 | assert_not_nil add_step, "Couldn't find the 'Add build step' button" 118 | add_step.click 119 | 120 | exec_shell = @driver.find_element(:xpath, "//a[text()='Execute shell']") 121 | assert_not_nil exec_shell, "Couldn't find the 'Execute shell' link" 122 | exec_shell.click 123 | 124 | # We need to give the textarea a little bit of time to show up, since the 125 | # JavaScript doesn't seem to make it appear "immediately" as far as the web 126 | # driver is concerned 127 | textarea = nil 128 | Selenium::WebDriver::Wait.new(:timeout => 10).until do 129 | textarea = @driver.find_element(:xpath, "//textarea[@name='command']") 130 | textarea 131 | end 132 | 133 | assert_not_nil textarea, "Couldn't find the command textarea on the page" 134 | textarea.send_keys script 135 | end 136 | 137 | def add_ant_build_step(ant_targets,ant_build_file) 138 | ensure_config_page 139 | add_step = @driver.find_element(:xpath, "//button[text()='Add build step']") 140 | ensure_element(add_step,"Add build step") 141 | add_step.click 142 | 143 | exec_ant = @driver.find_element(:xpath, "//a[text()='Invoke Ant']") 144 | ensure_element(exec_ant,"Invoke Ant") 145 | exec_ant.click 146 | 147 | # choose latest ant version 148 | ant = nil 149 | Selenium::WebDriver::Wait.new(:timeout => 10).until do 150 | ant = @driver.find_element(:name => "ant.antName") 151 | ant 152 | end 153 | ensure_element(ant,"Ant version") 154 | ant.click 155 | #TODO cannot find any select equivalent in Ruby API 156 | ant.send_keys :arrow_down 157 | ant.click 158 | 159 | # setup ant targets 160 | targets = nil 161 | Selenium::WebDriver::Wait.new(:timeout => 10).until do 162 | targets = @driver.find_element(:xpath, "//input[@name='_.targets']") 163 | targets 164 | end 165 | ensure_element(targets,"Ant targets") 166 | targets.send_keys ant_targets 167 | 168 | # advanced section 169 | advanced = @driver.find_element(:xpath, "//div[@name='builder']//button[text()='Advanced...']") 170 | ensure_element(advanced,"Ant advanced") 171 | advanced.click 172 | 173 | # setup build file 174 | build_file = nil 175 | Selenium::WebDriver::Wait.new(:timeout => 10).until do 176 | build_file = @driver.find_element(:xpath, "//input[@id='textarea._.buildFile']") 177 | build_file 178 | end 179 | ensure_element(build_file,"Ant build file") 180 | build_file.send_keys ant_build_file 181 | 182 | end 183 | 184 | def wait_for_build(*args) 185 | number = 1 186 | if args.size == 1 187 | number = args[0] 188 | end 189 | build = self.build(number) 190 | start = Time.now 191 | while (build.in_progress? && ((Time.now - start) < Build::BUILD_TIMEOUT)) 192 | sleep 5 193 | end 194 | end 195 | 196 | def save 197 | assert_equal @driver.current_url, configure_url, "Cannot save if I'm not on the configure page!" 198 | 199 | button = @driver.find_element(:xpath, "//button[text()='Save']") 200 | assert_not_nil button, "Couldn't find the Save button on the configuration page" 201 | button.click 202 | end 203 | 204 | def ensure_config_page 205 | assert_equal @driver.current_url, configure_url, "Cannot configure build steps if I'm not on the configure page" 206 | end 207 | 208 | def ensure_element(element,name) 209 | assert_not_nil element, "Couldn't find element '#{name}'" 210 | end 211 | 212 | 213 | end 214 | -------------------------------------------------------------------------------- /test/selenium/pageobjects/newjob.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'selenium-webdriver' 3 | require 'test/unit' 4 | 5 | class NewJob 6 | extend Test::Unit::Assertions 7 | 8 | def self.goto(driver, base_url) 9 | driver.navigate.to("#{base_url}/newJob") 10 | end 11 | 12 | def self.waiter 13 | Selenium::WebDriver::Wait.new(:timeout => 10) 14 | end 15 | 16 | def self.create_freestyle(driver, base_url, name) 17 | self.goto(driver, base_url) 18 | 19 | self.waiter.until do 20 | driver.find_element(:id, "name") 21 | end 22 | 23 | name_field = driver.find_element(:id, "name") 24 | assert_not_nil name_field, "Couldn't find the Name input field on the new job page" 25 | 26 | name_field.send_keys(name) 27 | job_type = driver.find_element(:xpath, "//input[starts-with(@value, 'hudson.model.FreeStyleProject')]") 28 | assert_not_nil job_type, "Couldn't find the Freestyle job type radio button" 29 | 30 | job_type.click 31 | name_field.submit 32 | 33 | self.waiter.until do 34 | driver.title.match("#{name} Config") 35 | end 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /test/selenium/pageobjects/newslave.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'selenium-webdriver' 3 | require 'test/unit' 4 | 5 | class NewSlave 6 | extend Test::Unit::Assertions 7 | 8 | def self.goto(driver, base_url) 9 | driver.navigate.to("#{base_url}/computer/new") 10 | end 11 | 12 | # TODO DRY, same NewJob, refactor 13 | def self.waiter 14 | Selenium::WebDriver::Wait.new(:timeout => 10) 15 | end 16 | 17 | def self.create_dumb(driver, base_url, name) 18 | self.goto(driver, base_url) 19 | 20 | self.waiter.until do 21 | driver.find_element(:id, "name") 22 | end 23 | 24 | name_field = driver.find_element(:id, "name") 25 | assert_not_nil name_field, "Couldn't find the Name input field on the new slave page" 26 | name_field.send_keys(name) 27 | 28 | job_type = driver.find_element(:name, "mode") 29 | assert_not_nil job_type, "Couldn't find slave mode radio button" 30 | job_type.click 31 | 32 | name_field.submit 33 | 34 | self.waiter.until do 35 | driver.title.match("Jenkins") 36 | end 37 | end 38 | 39 | 40 | end 41 | -------------------------------------------------------------------------------- /test/selenium/pageobjects/pluginmanager.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'selenium-webdriver' 3 | require 'test/unit' 4 | 5 | class PluginManager 6 | include Test::Unit::Assertions 7 | 8 | def initialize(driver, base_url) 9 | @driver = driver 10 | @base_url = base_url 11 | 12 | @updated = false 13 | end 14 | 15 | def waiter 16 | Selenium::WebDriver::Wait.new(:timeout => 10) 17 | end 18 | 19 | def url 20 | @base_url + "/pluginManager" 21 | end 22 | 23 | def open 24 | @driver.navigate.to url() 25 | end 26 | 27 | def check_for_updates 28 | @driver.navigate.to(url + "/checkUpdates") 29 | 30 | waiter.until do 31 | @driver.find_element(:xpath, "//span[@id='completionMarker' and text()='Done']") 32 | end 33 | 34 | @updated = true 35 | # This is totally arbitrary, it seems that the Available page doesn't 36 | # update properly if you don't sleep a bit 37 | sleep 5 38 | end 39 | 40 | def install_plugin(name) 41 | unless @updated 42 | check_for_updates 43 | end 44 | 45 | @driver.navigate.to(url + "/available") 46 | 47 | checkbox = @driver.find_element(:xpath, "//input[@name='plugin.#{name}.default']") 48 | assert_not_nil checkbox, "Couldn't find the plugin checkbox for the #{name} plugin" 49 | checkbox.click 50 | 51 | installbutton = @driver.find_element(:xpath, "//button[text()='Install']") 52 | assert_not_nil installbutton, "Couldn't find the install button on this page" 53 | installbutton.click 54 | end 55 | 56 | def assert_installed(name) 57 | @driver.navigate.to(url + "/installed") 58 | 59 | waiter.until do 60 | @driver.find_element(:xpath, "//input[@url='plugin/#{name}']") 61 | end 62 | 63 | assert true 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/selenium/pageobjects/slave.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'selenium-webdriver' 3 | require 'test/unit' 4 | require 'rest_client' 5 | require 'json' 6 | 7 | require File.dirname(__FILE__) + "/../pageobjects/newjob" 8 | 9 | class Slave 10 | include Test::Unit::Assertions 11 | 12 | def initialize(driver, base_url, name) 13 | @driver = driver 14 | @base_url = base_url 15 | @name = name 16 | end 17 | 18 | def configure_url 19 | @base_url + "/computer/#{@name}/configure" 20 | end 21 | 22 | def json_rest_url 23 | @base_url + "/computer/#{@name}/api/json" 24 | end 25 | 26 | def set_num_executors(num_exec) 27 | #ensure_config_page 28 | param_name = @driver.find_element(:xpath, "//input[@name='_.numExecutors']") 29 | ensure_element(param_name,"# of executors") 30 | param_name.send_keys num_exec 31 | end 32 | 33 | def set_remote_fs(remote_fs) 34 | #ensure_config_page 35 | param_name = @driver.find_element(:xpath, "//input[@name='_.remoteFS']") 36 | ensure_element(param_name,"Remote FS root") 37 | param_name.send_keys remote_fs 38 | end 39 | 40 | def set_labels(labels) 41 | #ensure_config_page 42 | param_name = @driver.find_element(:xpath, "//input[@name='_.labelString']") 43 | ensure_element(param_name,"Labels") 44 | param_name.send_keys labels 45 | end 46 | 47 | def set_command_on_master(launch_command) 48 | method = @driver.find_element(:css,"select.setting-input.dropdownList") 49 | #TODO cannot find any select equivalent in Ruby API 50 | method.click 51 | method.send_keys :arrow_down 52 | method.send_keys :arrow_down 53 | method.click 54 | 55 | #TODO need to move focus somewhere else to command line appers, probebly there is some better way how to do it 56 | usage = @driver.find_element(:xpath,"//select[@name='mode']") 57 | usage.click 58 | usage.click 59 | 60 | command = @driver.find_element(:xpath,"//input[@name='_.command']") 61 | command.send_keys launch_command 62 | end 63 | 64 | def save 65 | button = @driver.find_element(:xpath, "//button[text()='Save']") 66 | assert_not_nil button, "Couldn't find the Save button on the configuration page" 67 | button.click 68 | end 69 | 70 | 71 | 72 | def is_offline 73 | response = RestClient.get(json_rest_url) 74 | js = JSON.parse(response.body) 75 | return js['offline'] 76 | end 77 | 78 | 79 | def ensure_config_page 80 | assert_equal @driver.current_url, configure_url, "Cannot configure build steps if I'm not on the configure page" 81 | end 82 | 83 | def ensure_element(element,name) 84 | assert_not_nil element, "Couldn't find element '#{name}'" 85 | end 86 | 87 | 88 | end 89 | -------------------------------------------------------------------------------- /test/selenium/plugins/git.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/../lib/base" 2 | require File.dirname(__FILE__) + "/../pageobjects/newjob" 3 | require File.dirname(__FILE__) + "/../pageobjects/job" 4 | require File.dirname(__FILE__) + "/../pageobjects/pluginmanager" 5 | 6 | 7 | class GitPluginTests < JenkinsSeleniumTest 8 | def setup 9 | super 10 | 11 | PluginManager.new(@driver, @base_url).install_plugin('git') 12 | restart_jenkins 13 | 14 | @job_name = "Selenium_Git_Test_Job" 15 | NewJob.create_freestyle(@driver, @base_url, @job_name) 16 | @job = Job.new(@driver, @base_url, @job_name) 17 | end 18 | 19 | def test_git_clone 20 | @job.configure do 21 | label = @driver.find_element(:xpath, "//label[text()='Git']") 22 | label.click 23 | 24 | # XXX: This feels brittle as hell! 25 | url = @driver.find_element(:xpath, "//input[@name='_.url']") 26 | url.send_keys "git://github.com/jenkinsci/selenium-tests" 27 | 28 | @job.add_build_step "ls" 29 | end 30 | 31 | @job.queue_build 32 | 33 | build = @job.build(1) 34 | 35 | assert build.succeeded?, "The build did not succeed!" 36 | 37 | assert_not_nil build.console.match("Cloning the remote Git repository") 38 | end 39 | end 40 | --------------------------------------------------------------------------------