├── .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 | 
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 |
--------------------------------------------------------------------------------