diff --git a/.gitignore b/.gitignore index bc1ee12d..af28a163 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,8 @@ config/skylight.yml tmp/ log/ logs/ - -vendor +!vendor/travis-core/lib/travis/logs/ +!vendor/travis-core/lib/travis/model/log/ .yardoc .coverage diff --git a/.rspec b/.rspec index 262c08ba..8db79e75 100644 --- a/.rspec +++ b/.rspec @@ -1,3 +1,2 @@ --colour --tty ---format documentation diff --git a/.ruby-version b/.ruby-version index cd57a8b9..58594069 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1.5 +2.2.3 diff --git a/.travis.yml b/.travis.yml index 3f371b9e..767aa5d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: ruby sudo: false -rvm: 2.1.5 +rvm: 2.2.3 env: global: @@ -19,6 +19,3 @@ services: before_script: - 'RAILS_ENV=test bundle exec rake db:create --trace' - -script: - - bundle exec rspec spec diff --git a/Gemfile b/Gemfile index f1372ff9..e7136ef5 100644 --- a/Gemfile +++ b/Gemfile @@ -1,13 +1,16 @@ source 'https://rubygems.org' gemspec +ruby '2.2.3' if ENV.key?('DYNO') + gem 's3', github: 'travis-ci/s3' -gem 'travis-core', github: 'travis-ci/travis-core' +gem 'travis-core', path: 'vendor' gem 'travis-support', github: 'travis-ci/travis-support' gem 'travis-amqp', github: 'travis-ci/travis-amqp' gem 'travis-config', '~> 0.1.0' -gem 'travis-sidekiqs', github: 'travis-ci/travis-sidekiqs', require: nil +gem 'travis-sidekiqs', github: 'travis-ci/travis-sidekiqs' + gem 'travis-yaml', github: 'travis-ci/travis-yaml' gem 'mustermann', github: 'rkh/mustermann' gem 'sinatra' @@ -17,7 +20,7 @@ gem 'active_model_serializers' gem 'unicorn' gem 'sentry-raven' gem 'yard-sinatra', github: 'rkh/yard-sinatra' -gem 'rack-contrib', github: 'rack/rack-contrib' +gem 'rack-contrib' gem 'rack-cache', github: 'rtomayko/rack-cache' gem 'rack-attack' gem 'gh' diff --git a/Gemfile.lock b/Gemfile.lock index 064187b1..e9f6df04 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,20 +1,13 @@ GIT remote: git://github.com/eric/metriks-librato_metrics.git - revision: ccbeb751ec5fc4edfe446d8a67a423b96ebe86c7 + revision: 2c124f024fd2e34378260cd24b0e8687ff9bd196 specs: - metriks-librato_metrics (1.0.2) + metriks-librato_metrics (1.0.5) metriks (>= 0.9.9.6) -GIT - remote: git://github.com/rack/rack-contrib.git - revision: 1b11346d729efd88b274cd7f704e0bca9eb3de7a - specs: - rack-contrib (1.2.0) - rack (>= 0.9.1) - GIT remote: git://github.com/rkh/mustermann.git - revision: fa22e2cf4cfdb57f452c366eac66f241827b7e9c + revision: 9611951c5c789ad8227a740ae142c157a8bf7401 specs: mustermann (0.4.0) tool (~> 0.2) @@ -42,46 +35,22 @@ GIT GIT remote: git://github.com/travis-ci/travis-amqp.git - revision: c388299757b7eda2cc0e33cdc7d90113cf283e6a + revision: 3966b3651adfd1c544f53d49d5986ac16cd9c4bc specs: travis-amqp (0.0.1) -GIT - remote: git://github.com/travis-ci/travis-core.git - revision: 13061d1474d7b4a71cf7889216410ec3c1d43304 - specs: - travis-core (0.0.1) - actionmailer (~> 3.2.19) - activerecord (~> 3.2.19) - coder (~> 0.4.0) - data_migrations (~> 0.0.1) - gh - google-api-client (~> 0.9.4) - hashr - metriks (~> 0.9.7) - multi_json - pusher (~> 0.14.0) - railties (~> 3.2.19) - rake - redis (~> 3.0) - rollout (~> 1.1.0) - s3 (~> 0.3) - simple_states (~> 1.0.0) - thor - travis-config (~> 0.1.0) - virtus (~> 1.0.0) - GIT remote: git://github.com/travis-ci/travis-migrations.git - revision: bf360857ef7830f7e3ff12de181ab58c33fb29f1 + revision: dc432e45354287c617c3ae07a72e9e3c4be012cd specs: - travis-migrations (0.0.1) + travis-migrations (0.0.2) GIT remote: git://github.com/travis-ci/travis-sidekiqs.git - revision: 21a2fee158e25252dd78f5fa31e81b4f6583be23 + revision: c5d4a4abc6c3737f9c43d3333efb94daa18b9fbb specs: travis-sidekiqs (0.0.1) + redis-namespace sidekiq GIT @@ -113,6 +82,30 @@ PATH travis-support useragent +PATH + remote: vendor + specs: + travis-core (0.0.1) + actionmailer (~> 3.2.19) + activerecord (~> 3.2.19) + coder (~> 0.4.0) + data_migrations (~> 0.0.1) + gh + google-api-client (~> 0.9.4) + hashr + metriks (~> 0.9.7) + multi_json + pusher (~> 0.14.0) + railties (~> 3.2.19) + rake + redis (~> 3.0) + rollout (~> 1.1.0) + s3 (~> 0.3) + simple_states (~> 1.0.0) + thor + travis-config (~> 0.1.0) + virtus (~> 1.0.0) + GEM remote: https://rubygems.org/ specs: @@ -129,7 +122,7 @@ GEM rack-cache (~> 1.2) rack-test (~> 0.6.1) sprockets (~> 2.2.1) - active_model_serializers (0.9.0) + active_model_serializers (0.9.5) activemodel (>= 3.2) activemodel (3.2.22.2) activesupport (= 3.2.22.2) @@ -153,19 +146,17 @@ GEM backports (3.6.8) builder (3.0.4) bunny (0.8.0) - celluloid (0.16.0) - timers (~> 4.0.0) coder (0.4.0) - coderay (1.1.0) + coderay (1.1.1) coercible (1.0.0) descendants_tracker (~> 0.0.1) composite_primary_keys (5.0.14) activerecord (~> 3.2.0, >= 3.2.9) - connection_pool (2.1.1) - customerio (0.6.1) - httparty (>= 0.5, < 0.12) + concurrent-ruby (1.0.2) + connection_pool (2.2.0) + customerio (1.0.0) multi_json (~> 1.0) - dalli (2.7.2) + dalli (2.7.6) data_migrations (0.0.1) activerecord rake @@ -174,17 +165,15 @@ GEM thread_safe (~> 0.3, >= 0.3.1) diff-lcs (1.2.5) docile (1.1.5) - dotenv (0.7.0) equalizer (0.0.11) erubis (2.7.0) factory_girl (2.4.2) activesupport faraday (0.9.2) multipart-post (>= 1.2, < 3) - ffi (1.9.6) - foreman (0.64.0) - dotenv (~> 0.7.0) - thor (>= 0.13.6) + ffi (1.9.10) + foreman (0.82.0) + thor (~> 0.19.1) gh (0.14.0) addressable backports @@ -192,7 +181,8 @@ GEM multi_json (~> 1.0) net-http-persistent (>= 2.7) net-http-pipeline - google-api-client (0.9.6) + git-version-bump (0.15.1) + google-api-client (0.9.8) addressable (~> 2.3) googleauth (~> 0.5) httpclient (~> 2.7) @@ -213,9 +203,6 @@ GEM hashr (0.0.22) hike (1.2.3) hitimes (1.2.4) - httparty (0.11.0) - multi_json (~> 1.0) - multi_xml (>= 0.5.2) httpclient (2.8.0) hurley (0.2) i18n (0.7.0) @@ -224,11 +211,11 @@ GEM journey (1.0.4) json (1.8.3) jwt (1.5.4) - kgio (2.9.2) - listen (1.0.3) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) - rb-kqueue (>= 0.2) + kgio (2.10.0) + listen (3.1.5) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + ruby_dep (~> 1.2) little-plugger (1.1.4) logging (2.1.0) little-plugger (~> 1.1) @@ -250,16 +237,15 @@ GEM mime-types (1.25.1) mocha (0.14.0) metaclass (~> 0.0.1) - multi_json (1.12.0) - multi_xml (0.5.5) + multi_json (1.12.1) multipart-post (2.0.0) net-http-persistent (2.9.4) net-http-pipeline (1.0.1) os (0.9.6) - pg (0.18.2) + pg (0.18.4) polyglot (0.3.5) proxies (0.2.1) - pry (0.10.1) + pry (0.10.3) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) @@ -269,8 +255,11 @@ GEM pusher-signature (~> 0.1.8) pusher-signature (0.1.8) rack (1.4.7) - rack-attack (4.2.0) + rack-attack (4.4.1) rack + rack-contrib (1.4.0) + git-version-bump (~> 0.15) + rack (~> 1.4) rack-protection (1.5.3) rack rack-ssl (1.3.4) @@ -284,23 +273,21 @@ GEM rake (>= 0.8.7) rdoc (~> 3.4) thor (>= 0.14.6, < 2.0) - raindrops (0.13.0) + raindrops (0.16.0) rake (0.9.6) - rb-fsevent (0.9.4) - rb-inotify (0.9.5) - ffi (>= 0.5.0) - rb-kqueue (0.2.3) + rb-fsevent (0.9.7) + rb-inotify (0.9.7) ffi (>= 0.5.0) rdoc (3.12.2) json (~> 1.4) redcarpet (2.3.0) redis (3.3.0) - redis-namespace (1.5.1) + redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) representable (2.3.0) uber (~> 0.0.7) - rerun (0.8.2) - listen (~> 1.0.3) + rerun (0.11.0) + listen (~> 3.0) retriable (2.1.0) rollout (1.1.0) rspec (2.99.0) @@ -313,15 +300,14 @@ GEM rspec-its (1.0.1) rspec-core (>= 2.99.0.beta1) rspec-expectations (>= 2.99.0.beta1) - rspec-mocks (2.99.2) - sentry-raven (0.15.3) + rspec-mocks (2.99.4) + ruby_dep (1.3.1) + sentry-raven (1.0.0) faraday (>= 0.7.6) - sidekiq (3.3.0) - celluloid (>= 0.16.0) - connection_pool (>= 2.0.0) - json - redis (>= 3.0.6) - redis-namespace (>= 1.3.1) + sidekiq (4.1.2) + concurrent-ruby (~> 1.0) + connection_pool (~> 2.2, >= 2.2.0) + redis (~> 3.2, >= 3.2.1) signet (0.7.2) addressable (~> 2.3) faraday (~> 0.9) @@ -330,23 +316,23 @@ GEM simple_states (1.0.1) activesupport hashr (~> 0.0.10) - simplecov (0.9.1) + simplecov (0.11.2) docile (~> 1.1.0) - multi_json (~> 1.0) - simplecov-html (~> 0.8.0) - simplecov-html (0.8.0) + json (~> 1.8) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.0) sinatra (1.4.6) rack (~> 1.4) rack-protection (~> 1.4) tilt (>= 1.3, < 3) - sinatra-contrib (1.4.2) + sinatra-contrib (1.4.7) backports (>= 2.0) multi_json rack-protection rack-test sinatra (~> 1.4.0) - tilt (~> 1.3) - skylight (0.6.0.beta.1) + tilt (>= 1.3, < 3) + skylight (0.6.2.beta.2) activesupport (>= 3.0.0) slop (3.6.0) sprockets (2.2.3) @@ -354,13 +340,11 @@ GEM multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - stackprof (0.2.7) + stackprof (0.2.9) thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) - timecop (0.8.0) - timers (4.0.1) - hitimes + timecop (0.8.1) tool (0.2.3) travis-config (0.1.4) hashr (~> 0.0) @@ -369,11 +353,10 @@ GEM polyglot (>= 0.3.1) tzinfo (0.3.49) uber (0.0.15) - unicorn (4.8.3) + unicorn (5.1.0) kgio (~> 2.6) - rack raindrops (~> 0.7) - useragent (0.13.3) + useragent (0.16.7) virtus (1.0.5) axiom-types (~> 0.1) coercible (~> 1.0) @@ -402,7 +385,7 @@ DEPENDENCIES pry rack-attack rack-cache! - rack-contrib! + rack-contrib rake (~> 0.9.2) rb-fsevent (~> 0.9.1) rerun @@ -426,3 +409,6 @@ DEPENDENCIES travis-yaml! unicorn yard-sinatra! + +BUNDLED WITH + 1.12.5 diff --git a/README.md b/README.md index f73a9ad0..fa2c8741 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ $ popd ### Run tests ```sh-session -$ bundle exec rspec +$ bundle exec rake ``` ### Run the server ```sh-session diff --git a/Rakefile b/Rakefile index 0b13d449..2780f87a 100644 --- a/Rakefile +++ b/Rakefile @@ -9,6 +9,23 @@ namespace :db do end end +begin + require 'rspec' + require 'rspec/core/rake_task' + RSpec::Core::RakeTask.new(:spec) + + task :core_specs do + RSpec::Core::RakeTask.new(:core_spec) do |t| + t.pattern = 'core_specs/**{,/*/**}/*_spec.rb' + end + Rake::Task["core_spec"].execute + end + + task :default => [:spec, :core_specs] +rescue LoadError => e + puts e.inspect +end + desc "generate gemspec" task 'travis-api.gemspec' do content = File.read 'travis-api.gemspec' diff --git a/lib/travis/api/app/endpoint/builds.rb b/lib/travis/api/app/endpoint/builds.rb index cf6a39e0..ffa6b90a 100644 --- a/lib/travis/api/app/endpoint/builds.rb +++ b/lib/travis/api/app/endpoint/builds.rb @@ -1,7 +1,7 @@ require 'travis/api/app' require 'travis/api/workers/build_cancellation' require 'travis/api/workers/build_restart' -require 'travis/api/enqueue/services/restart_model' +require 'travis/api/enqueue/services/enqueue_build' class Travis::Api::App class Endpoint @@ -49,24 +49,27 @@ class Travis::Api::App post '/:id/restart' do Metriks.meter("api.request.restart_build").mark - service = if Travis::Features.owner_active?(:enqueue_to_hub, current_user) - Travis::Enqueue::Services::RestartModel.new(current_user, build_id: params[:id]) + if Travis::Features.owner_active?(:enqueue_to_hub, current_user) + service = Travis::Enqueue::Services::EnqueueBuild.new(current_user, params[:id]) + if !service.accept? + status 400 + result = false + else + payload = {id: params[:id], user_id: current_user.id} + service.push("build:restart", payload) + status 202 + result = true + end else - self.service(:reset_model, build_id: params[:id]) - end - - result = if !service.accept? - status 400 - false - elsif service.respond_to?(:push) - payload = { id: params[:id], user_id: current_user.id } - service.push("build:restart", payload) - status 202 - true - else - Travis::Sidekiq::BuildRestart.perform_async(id: params[:id], user_id: current_user.id) - status 202 - true + service = self.service(:reset_model, build_id: params[:id]) + if !service.accept? + status 400 + result = false + else + Travis::Sidekiq::BuildRestart.perform_async(id: params[:id], user_id: current_user.id) + status 202 + result = true + end end respond_with(result: result, flash: service.messages) diff --git a/lib/travis/api/app/endpoint/jobs.rb b/lib/travis/api/app/endpoint/jobs.rb index c4d77228..9bb2fbc9 100644 --- a/lib/travis/api/app/endpoint/jobs.rb +++ b/lib/travis/api/app/endpoint/jobs.rb @@ -1,7 +1,6 @@ require 'travis/api/app' require 'travis/api/workers/job_cancellation' require 'travis/api/workers/job_restart' -require 'travis/api/enqueue/services/restart_model' class Travis::Api::App class Endpoint @@ -56,26 +55,16 @@ class Travis::Api::App post '/:id/restart' do Metriks.meter("api.request.restart_job").mark - service = if Travis::Features.owner_active?(:enqueue_to_hub, current_user) - Travis::Enqueue::Services::RestartModel.new(current_user, { job_id: params[:id] }) - else - self.service(:reset_model, job_id: params[:id]) - end - result = if !service.accept? + service = self.service(:reset_model, job_id: params[:id]) + if !service.accept? status 400 - false - elsif service.respond_to?(:push) - payload = {id: params[:id], user_id: current_user.id} - service.push("job:restart", payload) - status 202 - true + result = false else Travis::Sidekiq::JobRestart.perform_async(id: params[:id], user_id: current_user.id) status 202 result = true end - respond_with(result: result, flash: service.messages) end diff --git a/lib/travis/api/enqueue/services/enqueue_build.rb b/lib/travis/api/enqueue/services/enqueue_build.rb new file mode 100644 index 00000000..8776a5e6 --- /dev/null +++ b/lib/travis/api/enqueue/services/enqueue_build.rb @@ -0,0 +1,49 @@ +module Travis + module Enqueue + module Services + + class EnqueueBuild + attr_reader :current_user, :build + + def initialize(current_user, build_id) + @current_user = current_user + @build = Build.find(build_id) + end + + def push(event, payload) + ::Sidekiq::Client.push( + 'queue' => 'hub', + 'class' => 'Travis::Hub::Sidekiq::Worker', + 'args' => [event, payload] + ) + end + + def accept? + current_user && permission? && resetable? + end + + def messages + messages = [] + messages << { notice: "The build was successfully restarted." } if accept? + messages << { error: 'You do not seem to have sufficient permissions.' } unless permission? + messages << { error: "This build currently can not be restarted." } unless resetable? + messages + end + + private + + def permission? + current_user.permission?(required_role, repository_id: build.repository_id) + end + + def resetable? + build.resetable? + end + + def required_role + Travis.config.roles.reset_model + end + end + end + end +end diff --git a/lib/travis/api/enqueue/services/restart_model.rb b/lib/travis/api/enqueue/services/restart_model.rb deleted file mode 100644 index ff9f5fec..00000000 --- a/lib/travis/api/enqueue/services/restart_model.rb +++ /dev/null @@ -1,64 +0,0 @@ -module Travis - module Enqueue - module Services - - class RestartModel - attr_reader :current_user, :target - - def initialize(current_user, params) - @current_user = current_user - @params = params - target - end - - def push(event, payload) - if current_user && target && accept? - ::Sidekiq::Client.push( - 'queue' => 'hub', - 'class' => 'Travis::Hub::Sidekiq::Worker', - 'args' => [event, payload] - ) - end - end - - def accept? - current_user && permission? && resetable? - end - - def messages - messages = [] - messages << { notice: "The #{type} was successfully restarted." } if accept? - messages << { error: 'You do not seem to have sufficient permissions.' } unless permission? - messages << { error: "This #{type} currently can not be restarted." } unless resetable? - messages - end - - def type - @type ||= @params[:build_id] ? :build : :job - end - - def target - if type == :build - @target = Build.find(@params[:build_id]) - else - @target = Job.find(@params[:job_id]) - end - end - - private - - def permission? - current_user.permission?(required_role, repository_id: target.repository_id) - end - - def resetable? - target.resetable? - end - - def required_role - Travis.config.roles.reset_model - end - end - end - end -end diff --git a/lib/travis/api/v3/queries/repositories.rb b/lib/travis/api/v3/queries/repositories.rb index 3994c384..7132fb00 100644 --- a/lib/travis/api/v3/queries/repositories.rb +++ b/lib/travis/api/v3/queries/repositories.rb @@ -1,9 +1,10 @@ module Travis::API::V3 class Queries::Repositories < Query params :active, :private, :starred, prefix: :repository + sortable_by :id, :github_id, :owner_name, :name, active: sort_condition(:active), :'default_branch.last_build' => 'builds.started_at' sortable_by :id, :github_id, :owner_name, :name, active: sort_condition(:active), :'default_branch.last_build' => 'builds.started_at', - :current_build_id => "repositories.current_build_id %{order} NULLS LAST" + :current_build => "current_build_id %{order} NULLS LAST" def for_member(user, **options) all(user: user, **options).joins(:users).where(users: user_condition(user), invalidated_at: nil) diff --git a/lib/travis/api/v3/renderer/cron.rb b/lib/travis/api/v3/renderer/cron.rb index 95419867..fdad9263 100644 --- a/lib/travis/api/v3/renderer/cron.rb +++ b/lib/travis/api/v3/renderer/cron.rb @@ -3,7 +3,7 @@ require 'travis/api/v3/renderer/model_renderer' module Travis::API::V3 class Renderer::Cron < Renderer::ModelRenderer representation(:minimal, :id) - representation(:standard, :id, :repository, :branch, :interval, :disable_by_build, :next_enqueuing, :created_at) + representation(:standard, :id, :repository, :branch, :interval, :disable_by_build, :next_enqueuing) def repository model.branch.repository diff --git a/lib/travis/api/v3/result.rb b/lib/travis/api/v3/result.rb index c6875af1..42a6c727 100644 --- a/lib/travis/api/v3/result.rb +++ b/lib/travis/api/v3/result.rb @@ -2,7 +2,7 @@ module Travis::API::V3 class Result attr_accessor :access_control, :type, :resource, :status, :href, :meta_data, :warnings - def initialize(access_control, type, resource = [], status: 200, **meta_data) + def initialize(access_control, type, resource, status: 200, **meta_data) @warnings = [] @access_control, @type, @resource, @status, @meta_data = access_control, type, resource, status, meta_data end @@ -20,11 +20,6 @@ module Travis::API::V3 warn(message, warning_type: :ignored_parameter, parameter: param, **info) end - def <<(value) - resource << value - self - end - def render(params, env) href = self.href href = V3.location(env) if href.nil? and env['REQUEST_METHOD'.freeze] == 'GET'.freeze diff --git a/spec/integration/v1/branches_spec.rb b/spec/integration/v1/branches_spec.rb deleted file mode 100644 index 6e5c8735..00000000 --- a/spec/integration/v1/branches_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'spec_helper' - -describe 'Branches' do - let(:repo) { Repository.by_slug('svenfuchs/minimal').first } - let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.1+json' } } - - it 'GET /branches?repository_id=:repository_id' do - response = get '/branches', { repository_id: repo.id }, headers - response.should deliver_json_for(repo.last_finished_builds_by_branches, version: 'v1', type: 'branches') - end -end diff --git a/spec/integration/v1/builds_spec.rb b/spec/integration/v1/builds_spec.rb deleted file mode 100644 index 73d2fe85..00000000 --- a/spec/integration/v1/builds_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -describe 'Builds' do - let(:repo) { Repository.by_slug('svenfuchs/minimal').first } - let(:build) { repo.builds.first } - let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.1+json' } } - - it 'GET /builds.json?repository_id=1' do - response = get '/builds.json', { repository_id: repo.id }, headers - response.should deliver_json_for(repo.builds.order('id DESC'), version: 'v1') - end - - it 'GET /builds/1.json' do - response = get "/builds/#{build.id}.json", {}, headers - response.should deliver_json_for(build, version: 'v1') - end - - it 'GET /builds/1?repository_id=1.json' do - response = get "/builds/#{build.id}.json", { repository_id: repo.id }, headers - response.should deliver_json_for(build, version: 'v1') - end - - it 'GET /svenfuchs/minimal/builds.json' do - response = get '/svenfuchs/minimal/builds.json', {}, headers - response.should redirect_to('/repositories/svenfuchs/minimal/builds.json') - end - - it 'GET /svenfuchs/minimal/builds/1.json' do - response = get "/svenfuchs/minimal/builds/#{build.id}.json", {}, headers - response.should redirect_to("/repositories/svenfuchs/minimal/builds/#{build.id}.json") - end -end diff --git a/spec/integration/v1/hooks_spec.rb b/spec/integration/v1/hooks_spec.rb deleted file mode 100644 index ccf76a00..00000000 --- a/spec/integration/v1/hooks_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'spec_helper' - -describe 'Hooks' do - before(:each) do - user.permissions.create repository: repo, admin: true - end - - let(:user) { User.where(login: 'svenfuchs').first } - let(:repo) { Repository.first } - let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: -1) } - let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.1+json', 'HTTP_AUTHORIZATION' => "token #{token}" } } - - it 'GET /hooks' do - response = get '/hooks', {}, headers - response.should deliver_json_for(user.service_hooks, version: 'v1', type: 'hooks') - end -end diff --git a/spec/integration/v1/jobs_spec.rb b/spec/integration/v1/jobs_spec.rb deleted file mode 100644 index 99b59862..00000000 --- a/spec/integration/v1/jobs_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe 'Jobs' do - let!(:jobs) {[ - FactoryGirl.create(:test, :number => '3.1', :queue => 'builds.common'), - FactoryGirl.create(:test, :number => '3.2', :queue => 'builds.common') - ]} - let(:job) { jobs.first } - let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.1+json' } } - - it '/jobs?queue=builds.common' do - response = get '/jobs', { queue: 'builds.common' }, headers - response.should deliver_json_for(Job.queued('builds.common'), version: 'v1') - end - - it '/jobs/:job_id' do - response = get "/jobs/#{job.id}", {}, headers - response.should deliver_json_for(job, version: 'v1') - end -end diff --git a/spec/integration/v1/repositories_spec.rb b/spec/integration/v1/repositories_spec.rb deleted file mode 100644 index 54526418..00000000 --- a/spec/integration/v1/repositories_spec.rb +++ /dev/null @@ -1,119 +0,0 @@ -require 'spec_helper' - -describe 'v1 repos' do - let(:repo) { Repository.by_slug('svenfuchs/minimal').first } - let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.1+json' } } - - it 'GET /repositories.json' do - response = get '/repositories.json', {}, headers - response.should deliver_json_for(Repository.timeline, version: 'v1') - end - - it 'GET /repositories.json?owner_name=svenfuchs' do - response = get '/repositories.json', { owner_name: 'svenfuchs' }, headers - response.should deliver_json_for(Repository.by_owner_name('svenfuchs'), version: 'v1') - end - - it 'GET /repositories.json?member=svenfuchs' do - response = get '/repositories.json', { member: 'svenfuchs' }, headers - response.should deliver_json_for(Repository.by_member('svenfuchs'), version: 'v1') - end - - it 'GET /repositories.json?slug=svenfuchs/name=minimal' do - response = get '/repositories.json', { slug: 'svenfuchs/minimal' }, headers - response.should deliver_json_for(Repository.by_slug('svenfuchs/minimal'), version: 'v1') - end - - it 'GET /repositories/1.json' do - response = get "repositories/#{repo.id}.json", {}, headers - response.should deliver_json_for(Repository.by_slug('svenfuchs/minimal').first, version: 'v1') - end - - it 'GET /svenfuchs/minimal.json' do - response = get '/svenfuchs/minimal.json', {}, headers - response.should redirect_to('/repositories/svenfuchs/minimal.json') - end - - it 'GET /svenfuchs/minimal' do - response = get '/svenfuchs/minimal.json', {}, 'HTTP_ACCEPT' => 'application/json; version=2' - response.status.should == 404 - end - - it 'GET /svenfuchs/minimal/cc.xml' do - response = get '/svenfuchs/minimal/cc.xml' - response.should redirect_to('/repositories/svenfuchs/minimal/cc.xml') - end - - describe 'GET /svenfuchs/minimal.png' do - it '"unknown" when it only has one build that is not finished' do - Build.delete_all - Factory(:build, repository: repo, state: :created) - repo.builds.update_all(state: 'started') - get('/svenfuchs/minimal.png').should deliver_result_image_for('unknown') - end - - it '"failing" when the last build has failed' do - repo.builds.update_all(state: 'failed') - get('/svenfuchs/minimal.png').should deliver_result_image_for('failing') - end - - it '"passing" when the last build has passed' do - repo.builds.update_all(state: 'passed') - get('/svenfuchs/minimal.png').should deliver_result_image_for('passing') - end - - it '"passing" when there is a running build but the previous one has passed' do - Factory(:build, repository: repo, state: :passed, previous_state: :passed) - repo.update_attributes!(last_build_state: 'started') - get('/svenfuchs/minimal.png').should deliver_result_image_for('passing') - end - end - - describe 'GET /svenfuchs/minimal.png?branch=foo,bar' do - let(:on_foo) { Factory(:commit, branch: 'foo') } - let(:on_bar) { Factory(:commit, branch: 'bar') } - - it '"unknown" when it only has unfinished builds on the relevant branches' do - Build.delete_all - Factory(:build, repository: repo, state: :started, commit: on_foo) - Factory(:build, repository: repo, state: :started, commit: on_bar) - get('/svenfuchs/minimal.png?branch=foo,bar').should deliver_result_image_for('unknown') - end - - it '"failing" when the last build has failed' do - Factory(:build, repository: repo, state: :failed, commit: on_foo) - Factory(:build, repository: repo, state: :failed, commit: on_bar) - get('/svenfuchs/minimal.png?branch=foo,bar').should deliver_result_image_for('failing') - end - - it '"passing" when the last build has passed' do - Factory(:build, repository: repo, state: :failed, commit: on_foo) - Factory(:build, repository: repo, state: :passed, commit: on_bar) - get('/svenfuchs/minimal.png?branch=foo,bar').should deliver_result_image_for('passing') - end - - it '"passing" when there is a running build but the previous one has passed' do - Factory(:build, repository: repo, state: :passed, commit: on_foo) - Factory(:build, repository: repo, state: :passed, commit: on_bar) - Factory(:build, repository: repo, state: :started, commit: on_bar) - repo.update_attributes!(last_build_state: 'started') - get('/svenfuchs/minimal.png?branch=foo,bar').should deliver_result_image_for('passing') - end - end - - context 'with "Accept: application/atom+xml" header' do - let(:headers) { { 'HTTP_ACCEPT' => 'application/atom+xml' } } - it 'GET /repositories/svenfuchs/minimal/builds' do - response = get '/repositories/svenfuchs/minimal/builds', {}, headers - response.content_type.should =~ /^application\/atom\+xml/ - end - end - - context 'with .atom extension and "Accept: */*" header' do - let(:headers) { { 'HTTP_ACCEPT' => '*/*' } } - it 'GET /repositories/svenfuchs/minimal/builds.atom' do - response = get '/repositories/svenfuchs/minimal/builds.atom', {}, headers - response.content_type.should =~ /^application\/atom\+xml/ - end - end -end diff --git a/spec/integration/v1_spec.backup.rb b/spec/integration/v1_spec.backup.rb deleted file mode 100644 index 14a9957f..00000000 --- a/spec/integration/v1_spec.backup.rb +++ /dev/null @@ -1,174 +0,0 @@ -require 'spec_helper' - -describe 'v1' do - let(:format) { :json } - let(:params) { { :controller => controller, :action => action, :format => format } } - - describe 'GET to repositories.json' do - let(:controller) { 'v1/repositories' } - let(:action) { :index } - - it 'routes to V1::RepositoriesController#index' do - { :get => 'repositories.json' }.should route_to(params) - end - end - - describe 'GET to repositories/1.json' do - let(:controller) { 'v1/repositories' } - let(:action) { :show } - - it 'routes to V1::RepositoriesController#show' do - { :get => 'repositories/1.json' }.should route_to(params.merge(:id => 1)) - end - end - - describe 'GET to builds.json' do - let(:controller) { 'v1/builds' } - let(:action) { :index } - - it 'routes to V1::BuildsController#index' do - { :get => 'builds.json' }.should route_to(params) - end - end - - describe 'GET to builds/1.json' do - let(:controller) { 'v1/builds' } - let(:action) { :show } - - it 'routes to V1::BuildsController#show' do - { :get => 'builds/1.json' }.should route_to(params.merge(:id => 1)) - end - end - - describe 'GET to branches.json' do - let(:controller) { 'v1/branches' } - let(:action) { :index } - - it 'routes to V1::BranchesController#index' do - { :get => 'branches.json' }.should route_to(params) - end - end - - describe 'GET to jobs.json' do - let(:controller) { 'v1/jobs' } - let(:action) { :index } - - it 'routes to V1::JobsController#index' do - { :get => 'jobs.json' }.should route_to(params) - end - end - - describe 'GET to jobs/1.json' do - let(:controller) { 'v1/jobs' } - let(:action) { :show } - - it 'routes to V1::JobsController#show' do - { :get => 'jobs/1.json' }.should route_to(params.merge(:id => 1)) - end - end - - describe 'GET to workers.json' do - let(:controller) { 'v1/workers' } - let(:action) { :index } - - it 'routes to V1::WorkersController#index' do - { :get => 'workers.json' }.should route_to(params) - end - end - - describe 'GET to service_hooks.json' do - let(:controller) { 'v1/service_hooks' } - let(:action) { :index } - - it 'routes to V1::RepositoriesController#index' do - { :get => 'service_hooks.json' }.should route_to(params) - end - end - - describe 'PUT to service_hooks.json' do - let(:controller) { 'v1/service_hooks' } - let(:action) { :update } - - it 'routes to V1::RepositoriesController#update' do - hook_params = params.merge(:id => 'svenfuchs:minimal') - hook_params.delete(:format) - - { :put => 'service_hooks/svenfuchs:minimal' }.should route_to(hook_params) - end - end - - describe 'GET to :owner_name/:name.json' do - let(:controller) { 'v1/repositories' } - let(:action) { :show } - - it 'routes to V1::RepositoriesController#show' do - { :get => 'owner/name.json' }.should route_to(params.merge(:owner_name => 'owner', :name => 'name')) - end - - it 'routes to V1::RepositoriesController#show when owner contains dots' do - { :get => 'some.owner/name.json' }.should route_to(params.merge(:owner_name => 'some.owner', :name => 'name')) - end - - it 'routes to V1::RepositoriesController#show when repository name contains dots' do - { :get => 'owner/some.name.json' }.should route_to(params.merge(:owner_name => 'owner', :name => 'some.name')) - end - - it 'routes to V1::RepositoriesController#show when owner name and repository name contains dots' do - { :get => 'some.owner/some.name.json' }.should route_to(params.merge(:owner_name => 'some.owner', :name => 'some.name')) - end - end - - describe 'GET to :owner_name/:name.png' do - let(:controller) { 'v1/repositories' } - let(:action) { :show } - let(:format) { :png } - - it 'routes to V1::RepositoriesController#show' do - { :get => 'owner/name.png' }.should route_to(params.merge(:owner_name => 'owner', :name => 'name')) - end - - it 'routes to V1::RepositoriesController#show when owner contains dots' do - { :get => 'some.owner/name.png' }.should route_to(params.merge(:owner_name => 'some.owner', :name => 'name')) - end - - it 'routes to V1::RepositoriesController#show when repository name contains dots' do - { :get => 'owner/some.name.png' }.should route_to(params.merge(:owner_name => 'owner', :name => 'some.name')) - end - - it 'routes to V1::RepositoriesController#show when owner name and repository name contains dots' do - { :get => 'some.owner/some.name.png' }.should route_to(params.merge(:owner_name => 'some.owner', :name => 'some.name')) - end - end - - describe 'GET to :owner_name/:name/cc.xml' do - let(:controller) { 'v1/repositories' } - let(:action) { :show } - let(:format) { :xml } - - it 'routes to V1::RepositoriesController#show in XML format with the cctray schema' do - { :get => 'owner/name/cc.xml' }.should route_to(params.merge(:owner_name => 'owner', :name => 'name', :schema => 'cctray')) - end - - it 'routes to V1::RepositoriesController#show in XML format with the cctray schema when owner and repository name contains dots' do - { :get => 'some.owner/some.name/cc.xml' }.should route_to(params.merge(:owner_name => 'some.owner', :name => 'some.name', :schema => 'cctray')) - end - end - - describe 'GET to :owner_name/:name/builds.json' do - let(:controller) { 'v1/builds' } - let(:action) { :index } - - it 'routes to V1::BuildsController#index' do - { :get => 'owner/name/builds.json' }.should route_to(params.merge(:owner_name => 'owner', :name => 'name')) - end - end - - describe 'GET to :owner_name/:name/builds/:id.json' do - let(:controller) { 'v1/builds' } - let(:action) { :show } - - it 'routes to V1::BuildsController#show' do - { :get => 'owner/name/builds/1.json' }.should route_to(params.merge(:owner_name => 'owner', :name => 'name', :id => 1)) - end - end -end diff --git a/spec/integration/v2/builds_spec.rb b/spec/integration/v2/builds_spec.rb index c4e78d44..98a57e26 100644 --- a/spec/integration/v2/builds_spec.rb +++ b/spec/integration/v2/builds_spec.rb @@ -117,7 +117,7 @@ describe 'Builds' do build.update_attribute(:state, 'passed') end - describe 'Enqueues restart event for the Hub' do + describe 'Enqueues restart event to the Hub' do before { Travis::Features.activate_owner(:enqueue_to_hub, repo.owner) } it 'restarts the build' do diff --git a/spec/integration/v2/jobs_spec.rb b/spec/integration/v2/jobs_spec.rb index 72dac3d0..81486845 100644 --- a/spec/integration/v2/jobs_spec.rb +++ b/spec/integration/v2/jobs_spec.rb @@ -274,51 +274,24 @@ describe 'Jobs' do response = post "/jobs/#{job.id}/restart", {}, headers response.status.should == 400 end - - context 'when enqueueing for the Hub' do - before { Travis::Features.activate_owner(:enqueue_to_hub, job.repository.owner) } - - it 'responds with 400' do - response = post "/jobs/#{job.id}/restart", {}, headers - response.status.should == 400 - end - end end context 'when job passed' do - before { job.update_attribute(:state, 'passed') } - - context 'Restart from travis-core' do - before { Travis::Sidekiq::JobCancellation.stubs(:perform_async) } - - it 'restarts the job' do - Travis::Sidekiq::JobRestart.expects(:perform_async).with(id: job.id.to_s, user_id: user.id) - response = post "/jobs/#{job.id}/restart", {}, headers - response.status.should == 202 - end - it 'sends the correct response body' do - Travis::Sidekiq::JobRestart.expects(:perform_async).with(id: job.id.to_s, user_id: user.id) - response = post "/jobs/#{job.id}/restart", {}, headers - body = JSON.parse(response.body) - body.should == {"result"=>true, "flash"=>[{"notice"=>"The job was successfully restarted."}]} - end + before do + Travis::Sidekiq::JobCancellation.stubs(:perform_async) + job.update_attribute(:state, 'passed') end - context 'Enqueues restart event for the Hub' do - before { Travis::Features.activate_owner(:enqueue_to_hub, job.repository.owner) } - - it 'restarts the job' do - ::Sidekiq::Client.expects(:push) - response = post "/jobs/#{job.id}/restart", {}, headers - response.status.should == 202 - end - it 'sends the correct response body' do - ::Sidekiq::Client.expects(:push) - response = post "/jobs/#{job.id}/restart", {}, headers - body = JSON.parse(response.body) - body.should == {"result"=>true, "flash"=>[{"notice"=>"The job was successfully restarted."}]} - end - + it 'restarts the job' do + Travis::Sidekiq::JobRestart.expects(:perform_async).with(id: job.id.to_s, user_id: user.id) + response = post "/jobs/#{job.id}/restart", {}, headers + response.status.should == 202 + end + it 'sends the correct response body' do + Travis::Sidekiq::JobRestart.expects(:perform_async).with(id: job.id.to_s, user_id: user.id) + response = post "/jobs/#{job.id}/restart", {}, headers + body = JSON.parse(response.body) + body.should == {"result"=>true, "flash"=>[{"notice"=>"The job was successfully restarted."}]} end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5161936e..d48d1e61 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -53,7 +53,7 @@ end RSpec.configure do |c| c.mock_framework = :mocha - c.expect_with :rspec, :test_unit + c.expect_with :rspec c.include TestHelpers c.before :suite do diff --git a/spec/spec_helper_core.rb b/spec/spec_helper_core.rb new file mode 100644 index 00000000..47aea2e9 --- /dev/null +++ b/spec/spec_helper_core.rb @@ -0,0 +1,59 @@ +ENV['RAILS_ENV'] = ENV['ENV'] = 'test' + +require 'simplecov' unless RUBY_ENGINE == 'jruby' + +RSpec.configure do |c| + c.before(:each) { Time.now.utc.tap { | now| Time.stubs(:now).returns(now) } } +end + +require 'support' + +require 'travis' + +require 'travis/model' +require 'travis/states_cache' +require 'travis/testing' + +require 'travis/support' +require 'travis/testing/matchers' + +require 'gh' +require 'mocha/api' +require 'stringio' +require 'logger' +require 'patches/rspec_hash_diff' + +Travis.logger = Logger.new(StringIO.new) +Travis.services = Travis::Services +ActionMailer::Base.delivery_method = :test + +include Mocha::API + +RSpec.configure do |c| + c.mock_with :mocha + c.alias_example_to :fit, :focused => true + c.filter_run :focus => true + c.run_all_when_everything_filtered = true + # c.backtrace_clean_patterns.clear + + c.before :each do + Travis.logger.level = Logger::INFO + Travis::Event.instance_variable_set(:@queues, nil) + Travis::Event.instance_variable_set(:@subscriptions, nil) + Travis::Event.stubs(:subscribers).returns [] + Travis.config.oauth2 ||= {} + Travis.config.oauth2.scope = 'public_repo,user' + Travis.config.repository.ssl_key.size = 1024 + Travis::Github.stubs(:scopes_for).returns(['public_repo', 'user']) + GH.reset + end +end + +# this keeps Model.inspect from exploding which happens for +# expected method calls in tests that do not use a db connection +require 'active_record' +ActiveRecord::Base.class_eval do + def self.inspect + super + end +end diff --git a/spec/support.rb b/spec/support.rb new file mode 100644 index 00000000..c53ece58 --- /dev/null +++ b/spec/support.rb @@ -0,0 +1,15 @@ +require 'support/matchers' +require 'support/payloads' + +module Support + autoload :ActiveRecord, 'support/active_record' + autoload :Formats, 'support/formats' + autoload :GCS, 'support/gcs' + autoload :Log, 'support/log' + autoload :Mocks, 'support/mocks' + autoload :Notifications, 'support/notifications' + autoload :Redis, 'support/redis' + autoload :S3, 'support/s3' + autoload :Silence, 'support/silence' +end + diff --git a/spec/support/active_record.rb b/spec/support/active_record.rb new file mode 100644 index 00000000..d0e1874c --- /dev/null +++ b/spec/support/active_record.rb @@ -0,0 +1,47 @@ +require 'active_record' +require 'logger' +require 'fileutils' +require 'database_cleaner' +require 'travis/testing/factories' + +FileUtils.mkdir_p('log') + +# TODO why not make this use Travis::Database.connect ? +config = Travis.config.database.to_h +config.merge!('adapter' => 'jdbcpostgresql', 'username' => ENV['USER']) if RUBY_PLATFORM == 'java' + +ActiveRecord::Base.default_timezone = :utc +ActiveRecord::Base.logger = Logger.new('log/test.db.log') +ActiveRecord::Base.configurations = { 'test' => config } +ActiveRecord::Base.establish_connection('test') + +DatabaseCleaner.clean_with :truncation +DatabaseCleaner.strategy = :transaction + +module Support + module ActiveRecord + extend ActiveSupport::Concern + + included do + before :suite do + DatabaseCleaner.clean_with(:truncation) + end + + before :each do + DatabaseCleaner.strategy = :transaction + end + + before(:each, :truncation => true) do + DatabaseCleaner.strategy = :truncation + end + + before :each do + DatabaseCleaner.start + end + + after :each do + DatabaseCleaner.clean + end + end + end +end diff --git a/spec/support/gcs.rb b/spec/support/gcs.rb new file mode 100644 index 00000000..11ff98d6 --- /dev/null +++ b/spec/support/gcs.rb @@ -0,0 +1,42 @@ +require 'google/apis/storage_v1' + +module Support + module GCS + class FakeObject + attr_accessor :key, :size + def initialize(key, options = {}) + @key = key + @size = options[:size] || "0" + end + end + + class FakeService + def authorization=(auth) + true + end + + def list_objects(*args) + FakeObjects.new + end + end + + class FakeObjects + def items + [] + end + end + + class FakeAuthorization + end + + extend ActiveSupport::Concern + + included do + before :each do + ::Google::Apis::StorageV1::StorageService.stubs(:new).returns(gcs_storage) + ::Google::Auth::ServiceAccountCredentials.stubs(:make_creds).returns(FakeAuthorization.new) + end + let(:gcs_storage) { FakeService.new } + end + end +end \ No newline at end of file diff --git a/spec/support/payloads.rb b/spec/support/payloads.rb new file mode 100644 index 00000000..02ff7d96 --- /dev/null +++ b/spec/support/payloads.rb @@ -0,0 +1,624 @@ +API_PAYLOADS = { + 'custom' => { + 'repository' => { + 'owner_id' => 2208, + 'owner_type' => 'User', + 'owner_name' => 'svenfuchs', + 'name' => 'gem-release' + }, + 'branch' => 'master', + 'config' => { + 'env' => ['FOO=foo', 'BAR=bar'] + }, + 'user' => { + 'id' => 1 + } + } +} + +GITHUB_PAYLOADS = { + "private-repo" => %({ + "repository": { + "url": "http://github.com/svenfuchs/gem-release", + "name": "gem-release", + "private":true, + "owner": { + "email": "svenfuchs@artweb-design.de", + "name": "svenfuchs" + } + }, + "commits": [{ + "id": "9854592", + "message": "Bump to 0.0.15", + "timestamp": "2010-10-27T04:32:37Z", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Christopher Floess", + "email": "chris@flooose.de" + } + }], + "ref": "refs/heads/master" + }), + + "gem-release" => %({ + "repository": { + "id": 100, + "url": "http://github.com/svenfuchs/gem-release", + "name": "gem-release", + "description": "Release your gems with ease", + "owner": { + "id": "2208", + "email": "svenfuchs@artweb-design.de", + "name": "svenfuchs" + } + }, + "commits": [{ + "id": "586374eac43853e5542a2e2faafd48047127e4be", + "message": "Update the readme", + "timestamp": "2010-10-14T04:00:37Z", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Christopher Floess", + "email": "chris@flooose.de" + } + },{ + "id": "46ebe012ef3c0be5542a2e2faafd48047127e4be", + "message": "Bump to 0.0.15", + "timestamp": "2010-10-27T04:32:37Z", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Christopher Floess", + "email": "chris@flooose.de" + } + }], + "ref": "refs/heads/master", + "compare": "https://github.com/svenfuchs/gem-release/compare/af674bd...9854592" + }), + + "skip-last" => %({ + "repository": { + "url": "http://github.com/svenfuchs/gem-release", + "name": "gem-release", + "description": "Release your gems with ease", + "owner": { + "email": "svenfuchs@artweb-design.de", + "name": "svenfuchs" + } + }, + "commits": [{ + "id": "60aaa2faaa5fdbd87719a10e308d396b828e5a01", + "message": "Bump to 0.0.14", + "timestamp": "2010-10-12T08:47:06Z", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + } + },{ + "id": "586374eac43853e5542a2e2faafd48047127e4be", + "message": "Update the readme", + "timestamp": "2010-10-14T04:00:37Z", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Christopher Floess", + "email": "chris@flooose.de" + } + },{ + "id": "46ebe012ef3c0be5542a2e2faafd48047127e4be", + "message": "Bump to 0.0.15\\n\\n[ci skip]", + "timestamp": "2010-10-27T04:32:37Z", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Christopher Floess", + "email": "chris@flooose.de" + } + }], + "ref": "refs/heads/master", + "compare": "https://github.com/svenfuchs/gem-release/compare/af674bd...9854592" + }), + + "skip-all" => %({ + "repository": { + "url": "http://github.com/svenfuchs/gem-release", + "name": "gem-release", + "description": "Release your gems with ease", + "owner": { + "email": "svenfuchs@artweb-design.de", + "name": "svenfuchs" + } + }, + "commits": [{ + "id": "60aaa2faaa5fdbd87719a10e308d396b828e5a01", + "message": "Bump to 0.0.14\\n\\n[ci skip]", + "timestamp": "2010-10-12T08:47:06Z", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + } + },{ + "id": "586374eac43853e5542a2e2faafd48047127e4be", + "message": "Update the readme\\n\\n[ci skip]", + "timestamp": "2010-10-14T04:00:37Z", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Christopher Floess", + "email": "chris@flooose.de" + } + },{ + "id": "46ebe012ef3c0be5542a2e2faafd48047127e4be", + "message": "Bump to 0.0.15\\n\\n[ci skip]", + "timestamp": "2010-10-27T04:32:37Z", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Christopher Floess", + "email": "chris@flooose.de" + } + }], + "ref": "refs/heads/master", + "compare": "https://github.com/svenfuchs/gem-release/compare/af674bd...9854592" + }), + + + "travis-core" => %({ + "repository": { + "id": 111, + "url": "http://github.com/travis-ci/travis-core", + "name": "travis-core", + "description": "description for travis-core", + "organization": "travis-ci", + "owner": { + "email": "contact@travis-ci.org", + "name": "travis-ci" + } + }, + "commits": [{ + "id": "46ebe012ef3c0be5542a2e2faafd48047127e4be", + "message": "Bump to 0.0.15", + "timestamp": "2010-10-27T04:32:37Z", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Josh Kalderimis", + "email": "josh@email.org" + } + }], + "ref": "refs/heads/master", + "compare": "https://github.com/travis-ci/travis-core/compare/af674bd...9854592" + }), + + "travis-core-no-commit" => %({ + "repository": { + "url": "http://github.com/travis-ci/travis-core", + "name": "travis-core", + "description": "description for travis-core", + "organization": "travis-ci", + "owner": { + "email": "contact@travis-ci.org", + "name": "travis-ci" + } + }, + "commits":[], + "ref": "refs/heads/master", + "compare": "https://github.com/travis-ci/travis-core/compare/af674bd...9854592" + }), + + "gh-pages-update" => %({ + "repository": { + "url": "http://github.com/svenfuchs/gem-release", + "name": "gem-release", + "owner": { + "email": "svenfuchs@artweb-design.de", + "name": "svenfuchs" + } + }, + "commits": [{ + "id": "9854592", + "message": "Bump to 0.0.15", + "timestamp": "2010-10-27T04:32:37Z", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Christopher Floess", + "email": "chris@flooose.de" + } + }], + "ref": "refs/heads/gh-pages" + }), + + "gh_pages-update" => %({ + "repository": { + "url": "http://github.com/svenfuchs/gem-release", + "name": "gem-release", + "owner": { + "email": "svenfuchs@artweb-design.de", + "name": "svenfuchs" + } + }, + "commits": [{ + "id": "46ebe012ef3c0be5542a2e2faafd48047127e4be", + "message": "Bump to 0.0.15", + "timestamp": "2010-10-27T04:32:37Z", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Christopher Floess", + "email": "chris@flooose.de" + } + }], + "ref": "refs/heads/gh_pages" + }), + + # it is unclear why this payload was send but it happened quite often. the force option + # seems to indicate something like $ git push --force + "force-no-commit" => %({ + "pusher": { "name": "LTe", "email":"lite.88@gmail.com" }, + "repository":{ + "name":"acts-as-messageable", + "created_at":"2010/08/02 07:41:30 -0700", + "has_wiki":true, + "size":200, + "private":false, + "watchers":13, + "fork":false, + "url":"https://github.com/LTe/acts-as-messageable", + "language":"Ruby", + "pushed_at":"2011/05/31 04:16:01 -0700", + "open_issues":0, + "has_downloads":true, + "homepage":"http://github.com/LTe/acts-as-messageable", + "has_issues":true, + "forks":5, + "description":"ActsAsMessageable", + "owner": { "name":"LTe", "email":"lite.88@gmail.com" } + }, + "ref_name":"v0.3.0", + "forced":true, + "after":"b842078c2f0084bb36cea76da3dad09129b3c26b", + "deleted":false, + "ref":"refs/tags/v0.3.0", + "commits":[], + "base_ref":"refs/heads/master", + "before":"0000000000000000000000000000000000000000", + "compare":"https://github.com/LTe/acts-as-messageable/compare/v0.3.0", + "created":true + }), + + "pull-request" => %({ + "action": "opened", + "number": 1, + "pull_request": { + "deletions": 1, + "merged_by": null, + "comments": 0, + "updated_at": "2012-04-12T17:02:33Z", + "state": "open", + "diff_url": "https://github.com/travis-repos/test-project-1/pull/1.diff", + "_links": { + "comments": { + "href": "https://api.github.com/repos/travis-repos/test-project-1/issues/1/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/travis-repos/test-project-1/pulls/1/comments" + }, + "self": { + "href": "https://api.github.com/repos/travis-repos/test-project-1/pulls/1" + }, + "html": { + "href": "https://github.com/travis-repos/test-project-1/pull/1" + } + }, + "merged_at": null, + "user": { + "gravatar_id": "5c2b452f6eea4a6d84c105ebd971d2a4", + "url": "https://api.github.com/users/rkh", + "avatar_url": "https://secure.gravatar.com/avatar/5c2b452f6eea4a6d84c105ebd971d2a4?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png", + "id": 30442, + "login": "rkh" + }, + "issue_url": "https://github.com/travis-repos/test-project-1/issues/1", + "commits": 1, + "changed_files": 1, + "title": "You must enter a title to submit a Pull Request", + "merged": false, + "closed_at": null, + "created_at": "2012-02-14T14:00:48Z", + "patch_url": "https://github.com/travis-repos/test-project-1/pull/1.patch", + "url": "https://api.github.com/repos/travis-repos/test-project-1/pulls/1", + "base": { + "repo": { + "pushed_at": "2012-04-11T15:50:22Z", + "homepage": "http://travis-ci.org", + "svn_url": "https://github.com/travis-repos/test-project-1", + "has_issues": false, + "updated_at": "2012-04-11T15:50:22Z", + "forks": 6, + "has_downloads": true, + "ssh_url": "git@github.com:travis-repos/test-project-1.git", + "language": "Ruby", + "clone_url": "https://github.com/travis-repos/test-project-1.git", + "fork": false, + "git_url": "git://github.com/travis-repos/test-project-1.git", + "created_at": "2011-04-14T18:23:41Z", + "url": "https://api.github.com/repos/travis-repos/test-project-1", + "has_wiki": false, + "size": 140, + "private": false, + "description": "Test dummy repository for testing Travis CI", + "owner": { + "gravatar_id": "dad32d44d4850d2bc9485ee115ab4227", + "url": "https://api.github.com/users/travis-repos", + "avatar_url": "https://secure.gravatar.com/avatar/dad32d44d4850d2bc9485ee115ab4227?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-orgs.png", + "id": 864347, + "login": "travis-repos" + }, + "name": "test-project-1", + "full_name": "travis-repos/test-project-1", + "watchers": 8, + "html_url": "https://github.com/travis-repos/test-project-1", + "id": 1615549, + "open_issues": 3, + "mirror_url": null + }, + "sha": "4a90c0ad9187c8735e1bcbf39a0291a21284994a", + "label": "travis-repos:master", + "user": { + "gravatar_id": "dad32d44d4850d2bc9485ee115ab4227", + "url": "https://api.github.com/users/travis-repos", + "avatar_url": "https://secure.gravatar.com/avatar/dad32d44d4850d2bc9485ee115ab4227?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-orgs.png", + "id": 864347, + "login": "travis-repos" + }, + "ref": "master" + }, + "number": 1, + "review_comments": 0, + "head": { + "repo": { + "pushed_at": "2012-02-14T14:00:26Z", + "homepage": "http://travis-ci.org", + "svn_url": "https://github.com/rkh/test-project-1", + "has_issues": false, + "updated_at": "2012-02-14T14:00:27Z", + "forks": 0, + "has_downloads": true, + "ssh_url": "git@github.com:rkh/test-project-1.git", + "language": "Ruby", + "clone_url": "https://github.com/rkh/test-project-1.git", + "fork": true, + "git_url": "git://github.com/rkh/test-project-1.git", + "created_at": "2012-02-13T15:17:57Z", + "url": "https://api.github.com/repos/rkh/test-project-1", + "has_wiki": true, + "size": 108, + "private": false, + "description": "Test dummy repository for testing Travis CI", + "owner": { + "gravatar_id": "5c2b452f6eea4a6d84c105ebd971d2a4", + "url": "https://api.github.com/users/rkh", + "avatar_url": "https://secure.gravatar.com/avatar/5c2b452f6eea4a6d84c105ebd971d2a4?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png", + "id": 30442, + "login": "rkh" + }, + "name": "test-project-1", + "full_name": "rkh/test-project-1", + "watchers": 1, + "html_url": "https://github.com/rkh/test-project-1", + "id": 3431064, + "open_issues": 0, + "mirror_url": null + }, + "sha": "9b00989b1a0e7d9b609ad2e28338c060f79a71ac", + "label": "rkh:master", + "user": { + "gravatar_id": "5c2b452f6eea4a6d84c105ebd971d2a4", + "url": "https://api.github.com/users/rkh", + "avatar_url": "https://secure.gravatar.com/avatar/5c2b452f6eea4a6d84c105ebd971d2a4?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png", + "id": 30442, + "login": "rkh" + }, + "ref": "master" + }, + "body": "", + "html_url": "https://github.com/travis-repos/test-project-1/pull/1", + "id": 826379, + "mergeable": true, + "mergeable_state": "clean", + "additions": 1 + }, + "repository": { + "created_at": "2011-04-14T18:23:41Z", + "id": 1615549, + "name": "test-project-1", + "owner": { + "avatar_url": "https:\/\/secure.gravatar.com\/avatar\/dad32d44d4850d2bc9485ee115ab4227?d=https:\/\/a248.e.akamai.net\/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-orgs.png", + "gravatar_id": "dad32d44d4850d2bc9485ee115ab4227", + "id": 864347, + "login": "travis-repos", + "url": "https:\/\/api.github.com\/users\/travis-repos" + }, + "pushed_at": "2011-12-12T06:38:20Z", + "updated_at": "2012-02-13T15:17:57Z", + "url": "https:\/\/api.github.com\/repos\/travis-repos\/test-project-1" + }, + "sender": { + "avatar_url": "https:\/\/secure.gravatar.com\/avatar\/5c2b452f6eea4a6d84c105ebd971d2a4?d=https:\/\/a248.e.akamai.net\/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png", + "gravatar_id": "5c2b452f6eea4a6d84c105ebd971d2a4", + "id": 30442, + "login": "rkh", + "url": "https:\/\/api.github.com\/users\/rkh" + } + }), + + 'hook_inactive' => %({ + "last_response": { + "status": "ok", + "message": "", + "code": 200 + }, + "config": { + "domain": "staging.travis-ci.org", + "user": "svenfuchs", + "token": "token" + }, + "created_at": "2011-09-18T10:49:06Z", + "events": [ + "push", + "pull_request", + "issue_comment", + "public", + "member" + ], + "active": false, + "updated_at": "2012-08-09T09:32:42Z", + "name": "travis", + "_links": { + "self": { + "href": "https://api.github.com/repos/svenfuchs/minimal/hooks/77103" + } + }, + "id": 77103 + }), + + 'hook_active' => %({ + "last_response": { + "status": "ok", + "message": "", + "code": 200 + }, + "config": { + "domain": "staging.travis-ci.org", + "user": "svenfuchs", + "token": "token" + }, + "created_at": "2011-09-18T10:49:06Z", + "events": [ + "push", + "pull_request", + "issue_comment", + "public", + "member" + ], + "active": true, + "updated_at": "2012-08-09T09:32:42Z", + "name": "travis", + "_links": { + "self": { + "href": "https://api.github.com/repos/svenfuchs/minimal/hooks/77103" + } + }, + "id": 77103 + }), + + 'rkh' => %({ + "user": { + "gravatar_id":"5c2b452f6eea4a6d84c105ebd971d2a4", + "company":"Travis GmbH", + "name":"Konstantin Haase", + "created_at":"2008/10/22 11:56:03 -0700", + "location":"Potsdam, Berlin, Portland", + "public_repo_count":108, + "public_gist_count":217, + "blog":"http://rkh.im", + "following_count":477, + "id":30442, + "type":"User", + "permission":null, + "followers_count":369, + "login":"rkh", + "email":"k.haase@finn.de" + } + }), + + :oauth => { + "uid" => "234423", + "info" => { + "name" => "John", + "nickname" => "john", + "email" => "john@email.com" + }, + "credentials" => { + "token" => "1234567890abcdefg" + }, + "extra" => { + "raw_info" => { + "gravatar_id" => "41193cdbffbf06be0cdf231b28c54b18" + } + } + }, +} + +GITHUB_OAUTH_DATA = { + 'name' => 'John', + 'email' => 'john@email.com', + 'login' => 'john', + 'github_id' => 234423, + 'github_oauth_token' => '1234567890abcdefg', + 'gravatar_id' => '41193cdbffbf06be0cdf231b28c54b18' +} + +WORKER_PAYLOADS = { + 'job:test:receive' => { 'id' => 1, 'state' => 'received', 'received_at' => '2011-01-01 00:02:00 +0200', 'worker' => 'ruby3.worker.travis-ci.org:travis-ruby-4' }, + 'job:test:start' => { 'id' => 1, 'state' => 'started', 'started_at' => '2011-01-01 00:02:00 +0200', 'worker' => 'ruby3.worker.travis-ci.org:travis-ruby-4' }, + 'job:test:log' => { 'id' => 1, 'log' => '... appended' }, + 'job:test:log:1' => { 'id' => 1, 'log' => 'the ' }, + 'job:test:log:2' => { 'id' => 1, 'log' => 'full ' }, + 'job:test:log:3' => { 'id' => 1, 'log' => 'log' }, + 'job:test:finish' => { 'id' => 1, 'state' => 'passed', 'finished_at' => '2011-01-01 00:03:00 +0200', 'log' => 'the full log' }, + 'job:test:reset' => { 'id' => 1 } +} + +WORKER_LEGACY_PAYLOADS = { + 'job:test:finished' => { 'id' => 1, 'state' => 'finished', 'finished_at' => '2011-01-01 00:03:00 +0200', 'result' => 0, 'log' => 'the full log' } +} + +QUEUE_PAYLOADS = { + 'job:configure' => { + :type => 'configure', + :repository => { :slug => 'travis-ci/travis-ci' }, + :build => { :id => 1, :commit => '313f61b', :config_url => 'https://raw.github.com/travis-ci/travis-ci/313f61b/.travis.yml' } + }, + 'job:test:1' => { + :build => { :id => 2, :number => '1.1', :commit => '9854592', :branch => 'master', :config => { :rvm => '1.8.7' } }, + :repository => { :id => 1, :slug => 'svenfuchs/gem-release' }, + :queue => 'builds.linux' + }, + 'job:test:2' => { + :build => { :id => 3, :number => '1.2', :commit => '9854592', :branch => 'master', :config => { :rvm => '1.9.2' } }, + :repository => { :id => 1, :slug => 'svenfuchs/gem-release' }, + :queue => 'builds.linux' + } +} diff --git a/spec/support/s3.rb b/spec/support/s3.rb new file mode 100644 index 00000000..2439edf5 --- /dev/null +++ b/spec/support/s3.rb @@ -0,0 +1,48 @@ +require 's3' + +module Support + module S3 + class FakeObject + attr_accessor :key, :size + def initialize(key, options = {}) + @key = key + @size = options[:size] || "0" + end + end + + class FakeService + attr_reader :buckets + def initialize(bucket) + @buckets = [bucket] + @buckets.stubs(:find).returns(bucket) + end + end + + class FakeBucket + def initialize(objects) + @objects = Array(objects) + end + + def objects(params = {}) + params.each_key { |key| raise "cannot fake #{key}" unless key == :prefix } + prefix = params[:prefix] || "" + @objects.select { |o| o.key.start_with? prefix } + end + + def add(key, options = {}) + @objects << FakeObject.new(key, options) + end + + alias_method :<<, :add + end + + extend ActiveSupport::Concern + + included do + before(:each) { ::S3::Service.stubs(:new).returns(s3_service) } + let(:s3_service) { FakeService.new(s3_bucket) } + let(:s3_bucket) { FakeBucket.new(s3_objects) } + let(:s3_objects) { [] } + end + end +end \ No newline at end of file diff --git a/spec/unit/api/v2/http/branch_spec.rb b/spec/unit/api/v2/http/branch_spec.rb index 158c1cd0..ada13b3e 100644 --- a/spec/unit/api/v2/http/branch_spec.rb +++ b/spec/unit/api/v2/http/branch_spec.rb @@ -28,7 +28,6 @@ describe Travis::Api::V2::Http::Branch do 'sha' => '62aae5f70ceee39123ef', 'branch' => 'master', 'message' => 'the commit message', - 'compare_url' => 'https://github.com/svenfuchs/minimal/compare/master...develop', 'committed_at' => json_format_time(Time.now.utc - 1.hour), 'committer_email' => 'svenfuchs@artweb-design.de', 'committer_name' => 'Sven Fuchs', diff --git a/spec/unit/api/v2/http/branches_spec.rb b/spec/unit/api/v2/http/branches_spec.rb index ed25562c..c0cec4e6 100644 --- a/spec/unit/api/v2/http/branches_spec.rb +++ b/spec/unit/api/v2/http/branches_spec.rb @@ -28,7 +28,6 @@ describe Travis::Api::V2::Http::Branches do 'sha' => '62aae5f70ceee39123ef', 'branch' => 'master', 'message' => 'the commit message', - 'compare_url' => 'https://github.com/svenfuchs/minimal/compare/master...develop', 'committed_at' => json_format_time(Time.now.utc - 1.hour), 'committer_email' => 'svenfuchs@artweb-design.de', 'committer_name' => 'Sven Fuchs', diff --git a/spec/unit/api/v2/http/build_spec.rb b/spec/unit/api/v2/http/build_spec.rb index f276b2b8..2997a6ce 100644 --- a/spec/unit/api/v2/http/build_spec.rb +++ b/spec/unit/api/v2/http/build_spec.rb @@ -31,7 +31,6 @@ describe Travis::Api::V2::Http::Build do 'branch' => 'master', 'branch_is_default' => true, 'message' => 'the commit message', - 'compare_url' => 'https://github.com/svenfuchs/minimal/compare/master...develop', 'committed_at' => json_format_time(Time.now.utc - 1.hour), 'committer_email' => 'svenfuchs@artweb-design.de', 'committer_name' => 'Sven Fuchs', diff --git a/spec/unit/api/v2/http/builds_spec.rb b/spec/unit/api/v2/http/builds_spec.rb index d1fc6e31..f59ac427 100644 --- a/spec/unit/api/v2/http/builds_spec.rb +++ b/spec/unit/api/v2/http/builds_spec.rb @@ -30,7 +30,6 @@ describe Travis::Api::V2::Http::Builds do 'sha' => '62aae5f70ceee39123ef', 'branch' => 'master', 'message' => 'the commit message', - 'compare_url' => 'https://github.com/svenfuchs/minimal/compare/master...develop', 'committed_at' => json_format_time(Time.now.utc - 1.hour), 'committer_email' => 'svenfuchs@artweb-design.de', 'committer_name' => 'Sven Fuchs', diff --git a/spec/unit/api/v2/http/job_spec.rb b/spec/unit/api/v2/http/job_spec.rb index 22fda7b6..ca10373b 100644 --- a/spec/unit/api/v2/http/job_spec.rb +++ b/spec/unit/api/v2/http/job_spec.rb @@ -32,7 +32,6 @@ describe Travis::Api::V2::Http::Job do 'message' => 'the commit message', 'branch' => 'master', 'branch_is_default' => true, - 'message' => 'the commit message', 'committed_at' => json_format_time(Time.now.utc - 1.hour), 'committer_name' => 'Sven Fuchs', 'committer_email' => 'svenfuchs@artweb-design.de', diff --git a/spec/unit/api/v2/http/jobs_spec.rb b/spec/unit/api/v2/http/jobs_spec.rb index 4a084973..ba3e0d3f 100644 --- a/spec/unit/api/v2/http/jobs_spec.rb +++ b/spec/unit/api/v2/http/jobs_spec.rb @@ -28,7 +28,6 @@ describe Travis::Api::V2::Http::Jobs do data['commits'].first.should == { 'id' => 1, 'sha' => '62aae5f70ceee39123ef', - 'message' => 'the commit message', 'branch' => 'master', 'message' => 'the commit message', 'committed_at' => json_format_time(Time.now.utc - 1.hour), diff --git a/spec/unit/api/v2/http/request_spec.rb b/spec/unit/api/v2/http/request_spec.rb index 812064c7..c7eb2edf 100644 --- a/spec/unit/api/v2/http/request_spec.rb +++ b/spec/unit/api/v2/http/request_spec.rb @@ -38,7 +38,6 @@ describe Travis::Api::V2::Http::Request do 'sha' => '62aae5f70ceee39123ef', 'branch' => 'master', 'message' => 'the commit message', - 'compare_url' => 'https://github.com/svenfuchs/minimal/compare/master...develop', 'committed_at' => json_format_time(commit.committed_at), 'committer_email' => 'svenfuchs@artweb-design.de', 'committer_name' => 'Sven Fuchs', diff --git a/spec/unit/api/v2/http/requests_spec.rb b/spec/unit/api/v2/http/requests_spec.rb index f783f8d6..2f72bf63 100644 --- a/spec/unit/api/v2/http/requests_spec.rb +++ b/spec/unit/api/v2/http/requests_spec.rb @@ -44,7 +44,6 @@ describe Travis::Api::V2::Http::Requests do 'sha' => '62aae5f70ceee39123ef', 'branch' => 'master', 'message' => 'the commit message', - 'compare_url' => 'https://github.com/svenfuchs/minimal/compare/master...develop', 'committed_at' => json_format_time(Time.now.utc - 1.hour), 'committer_email' => 'svenfuchs@artweb-design.de', 'committer_name' => 'Sven Fuchs', diff --git a/spec/v3/result_spec.rb b/spec/v3/result_spec.rb index 3dd75885..360d686c 100644 --- a/spec/v3/result_spec.rb +++ b/spec/v3/result_spec.rb @@ -2,15 +2,10 @@ require 'spec_helper' describe Travis::API::V3::Result do let(:access_control) { Object.new } - subject(:result) { described_class.new(access_control, :example) } + subject(:result) { described_class.new(access_control, :example, []) } example { expect(result.type) .to be == :example } example { expect(result.resource) .to be == [] } example { expect(result.example) .to be == [] } example { expect(result.access_control) .to be == access_control } - - example do - result << 42 - expect(result.example).to include(42) - end end diff --git a/spec/v3/services/cron/create_spec.rb b/spec/v3/services/cron/create_spec.rb index 859e6a27..dc578e80 100644 --- a/spec/v3/services/cron/create_spec.rb +++ b/spec/v3/services/cron/create_spec.rb @@ -64,8 +64,7 @@ describe Travis::API::V3::Services::Cron::Create do "name" => "#{branch.name}" }, "interval" => "monthly", "disable_by_build" => false, - "next_enqueuing" => current_cron.next_enqueuing.strftime('%Y-%m-%dT%H:%M:%SZ'), - "created_at" => current_cron.created_at.strftime('%Y-%m-%dT%H:%M:%SZ') + "next_enqueuing" => current_cron.next_enqueuing.strftime('%Y-%m-%dT%H:%M:%SZ') }} end diff --git a/spec/v3/services/cron/delete_spec.rb b/spec/v3/services/cron/delete_spec.rb index 54a3b056..601d7bb9 100644 --- a/spec/v3/services/cron/delete_spec.rb +++ b/spec/v3/services/cron/delete_spec.rb @@ -52,8 +52,7 @@ describe Travis::API::V3::Services::Cron::Delete do "name" => branch.name }, "interval" => "daily", "disable_by_build" => true, - "next_enqueuing" => cron.next_enqueuing.strftime('%Y-%m-%dT%H:%M:%SZ'), - "created_at" => cron.created_at.strftime('%Y-%m-%dT%H:%M:%SZ') + "next_enqueuing" => cron.next_enqueuing.strftime('%Y-%m-%dT%H:%M:%SZ') }} end diff --git a/spec/v3/services/cron/find_spec.rb b/spec/v3/services/cron/find_spec.rb index e75e15c0..8ae7f2c6 100644 --- a/spec/v3/services/cron/find_spec.rb +++ b/spec/v3/services/cron/find_spec.rb @@ -46,9 +46,8 @@ describe Travis::API::V3::Services::Cron::Find do "@representation" => "minimal", "name" => branch.name }, "interval" => "daily", - "disable_by_build" => true, - "next_enqueuing" => cron.next_enqueuing.strftime('%Y-%m-%dT%H:%M:%SZ'), - "created_at" => cron.created_at.strftime('%Y-%m-%dT%H:%M:%SZ') + "disable_by_build" => true, + "next_enqueuing" => cron.next_enqueuing.strftime('%Y-%m-%dT%H:%M:%SZ') }} end @@ -106,9 +105,8 @@ describe Travis::API::V3::Services::Cron::Find do "@representation" => "minimal", "name" => branch.name }, "interval" => "daily", - "disable_by_build" => true, - "next_enqueuing" => cron.next_enqueuing.strftime('%Y-%m-%dT%H:%M:%SZ'), - "created_at" => cron.created_at.strftime('%Y-%m-%dT%H:%M:%SZ') + "disable_by_build" => true, + "next_enqueuing" => cron.next_enqueuing.strftime('%Y-%m-%dT%H:%M:%SZ') }} end diff --git a/spec/v3/services/cron/for_branch_spec.rb b/spec/v3/services/cron/for_branch_spec.rb index c5509074..32b0f76b 100644 --- a/spec/v3/services/cron/for_branch_spec.rb +++ b/spec/v3/services/cron/for_branch_spec.rb @@ -47,9 +47,8 @@ describe Travis::API::V3::Services::Cron::ForBranch do "@representation" => "minimal", "name" => branch.name }, "interval" => "daily", - "disable_by_build" => true, - "next_enqueuing" => cron.next_enqueuing.strftime('%Y-%m-%dT%H:%M:%SZ'), - "created_at" => cron.created_at.strftime('%Y-%m-%dT%H:%M:%SZ') + "disable_by_build" => true, + "next_enqueuing" => cron.next_enqueuing.strftime('%Y-%m-%dT%H:%M:%SZ') }} end diff --git a/spec/v3/services/crons/for_repository_spec.rb b/spec/v3/services/crons/for_repository_spec.rb index f81b8e7b..05e37102 100644 --- a/spec/v3/services/crons/for_repository_spec.rb +++ b/spec/v3/services/crons/for_repository_spec.rb @@ -68,8 +68,7 @@ describe Travis::API::V3::Services::Crons::ForRepository do "name" => "#{branch.name}" }, "interval" => "daily", "disable_by_build" => true, - "next_enqueuing" => cron.next_enqueuing.strftime('%Y-%m-%dT%H:%M:%SZ'), - "created_at" => cron.created_at.strftime('%Y-%m-%dT%H:%M:%SZ') + "next_enqueuing" => cron.next_enqueuing.strftime('%Y-%m-%dT%H:%M:%SZ') } ] }} diff --git a/spec_core/core/model/annotation_provider_spec.rb b/spec_core/core/model/annotation_provider_spec.rb new file mode 100644 index 00000000..a0244269 --- /dev/null +++ b/spec_core/core/model/annotation_provider_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper_core' + +describe AnnotationProvider do + include Support::ActiveRecord + + let(:provider) { Factory(:annotation_provider) } + + + describe '.authenticate_provider', truncation: true do + context 'given a valid username and key' do + it 'authenticates the provider' do + described_class.authenticate_provider(provider.api_username, provider.api_key).should eq(provider) + end + end + + context 'given an invalid username' do + it 'does not authenticate the provider' do + described_class.authenticate_provider('someone-else', provider.api_key).should be_nil + end + end + + context 'given an invalid key' do + it 'does not authenticate the provider' do + described_class.authenticate_provider(provider.api_username, 'some-other-key').should be_nil + end + end + + context 'with an encrypted key' do + it 'authenticates the provider' do + provider.update_column(:api_key, 'encrypted-key') + + Travis::Model::EncryptedColumn.any_instance.stubs(encrypt?: true, key: 'abcd', load: '...') + Travis::Model::EncryptedColumn.any_instance.expects(:load).with('encrypted-key').returns('a-key') + + described_class.authenticate_provider(provider.api_username, 'a-key').should eq(provider) + end + end + end + + describe '#annotation_for_job' do + let(:job) { Factory(:test) } + + context 'given an annotation already exists for the job' do + it 'returns the annotation' do + annotation = Factory(:annotation, annotation_provider: provider, job: job) + provider.annotation_for_job(job.id).should eq(annotation) + end + end + + context 'given no annotation exists yet for the job' do + it 'returns a new annotation object' do + provider.annotation_for_job(job.id).new_record?.should be true + end + end + end +end diff --git a/spec_core/core/model/annotation_spec.rb b/spec_core/core/model/annotation_spec.rb new file mode 100644 index 00000000..0ace11c9 --- /dev/null +++ b/spec_core/core/model/annotation_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper_core' + +describe Annotation do + include Support::ActiveRecord + + let(:annotation) { Factory.build(:annotation) } + + describe 'validations' do + it 'only allows http or https URLs' do + annotation.url = 'ftp://travis-ci.org' + annotation.save.should be false + annotation.errors[:url].first.should match(/scheme/) + end + + it 'only allows valid URLs' do + annotation.url = 'http://travis-ci.org:80b/' + annotation.save.should be false + annotation.errors[:url].first.should match(/invalid/) + end + end +end diff --git a/spec_core/core/model/broadcast_spec.rb b/spec_core/core/model/broadcast_spec.rb new file mode 100644 index 00000000..a3480e05 --- /dev/null +++ b/spec_core/core/model/broadcast_spec.rb @@ -0,0 +1,106 @@ +require 'spec_helper_core' + +describe Broadcast do + include Support::ActiveRecord + + let(:org) { Factory(:org) } + let(:repo) { Factory(:repository) } + let(:user) { Factory(:user) } + + before :each do + user.organizations << org + user.repositories << repo + end + + describe 'by_user' do + let(:broadcasts) { Broadcast.by_user(user) } + + it 'finds a global broadcast' do + global = Broadcast.create! + broadcasts.should include(global) + end + + it 'finds a broadcast for the given user' do + to_user = Broadcast.create!(recipient: user) + broadcasts.should include(to_user) + end + + it 'does not find a broadcast for a different user' do + to_user = Broadcast.create!(recipient: Factory(:user, login: 'rkh')) + broadcasts.should_not include(to_user) + end + + it 'finds a broadcast for orgs where the given user is a member' do + to_org = Broadcast.create!(recipient: org) + broadcasts.should include(to_org) + end + + it 'does not find a broadcast for a different org' do + to_org = Broadcast.create!(recipient: Factory(:org, login: 'sinatra')) + broadcasts.should_not include(to_org) + end + + it 'finds a broadcast for a repo where the given user has any permissions' do + to_repo = Broadcast.create!(recipient: repo) + broadcasts.should include(to_repo) + end + + it 'does not find a broadcast for a different repo' do + to_repo = Broadcast.create!(recipient: Factory(:repository, name: 'sinatra')) + broadcasts.should_not include(to_repo) + end + + it 'does not find an expired broadcast' do + expired = Broadcast.create!(created_at: 4.weeks.ago, expired: true) + broadcasts.should_not include(expired) + end + + it 'does not find broadcasts older than 2 weeks' do + too_old = Broadcast.create!(created_at: 4.weeks.ago) + broadcasts.should_not include(too_old) + end + end + + describe 'by_repo' do + let(:broadcasts) { Broadcast.by_repo(repo) } + + it 'finds a global broadcast' do + global = Broadcast.create! + broadcasts.should include(global) + end + + it 'finds a broadcast for the given repo' do + to_repo = Broadcast.create!(recipient: repo) + broadcasts.should include(to_repo) + end + + it 'does not find a broadcast for a different repo' do + to_repo = Broadcast.create!(recipient: Factory(:repository, name: 'sinatra')) + broadcasts.should_not include(to_repo) + end + + it 'finds a broadcast for an org this repo belongs to' do + repo.update_attributes(owner: org) + to_org = Broadcast.create!(recipient: org) + broadcasts.should include(to_org) + end + + it 'does not find a broadcast for a different org' do + repo.update_attributes(owner: org) + to_org = Broadcast.create!(recipient: Factory(:org, login: 'sinatra')) + broadcasts.should_not include(to_org) + end + + it 'finds a broadcast for a user this repo belongs to' do + repo.update_attributes(owner: user) + to_org = Broadcast.create!(recipient: user) + broadcasts.should include(to_org) + end + + it 'does not find a broadcast for a different user' do + repo.update_attributes(owner: org) + to_org = Broadcast.create!(recipient: Factory(:user, login: 'rkh')) + broadcasts.should_not include(to_org) + end + end +end diff --git a/spec_core/core/model/build/compat_spec.rb b/spec_core/core/model/build/compat_spec.rb new file mode 100644 index 00000000..3e5495e2 --- /dev/null +++ b/spec_core/core/model/build/compat_spec.rb @@ -0,0 +1,7 @@ +# require 'spec_helper_core' +# +# describe Build::Compat do +# include Support::ActiveRecord +# +# let(:build) { Factory(:build, :result => nil) } +# end diff --git a/spec_core/core/model/build/config/dist_spec.rb b/spec_core/core/model/build/config/dist_spec.rb new file mode 100644 index 00000000..9da1f40e --- /dev/null +++ b/spec_core/core/model/build/config/dist_spec.rb @@ -0,0 +1,124 @@ +require 'travis/model/build/config/dist' + +describe Build::Config::Dist do + subject { described_class.new(config, options) } + let(:config) { {} } + let(:options) { {} } + + it 'sets dist to the default' do + subject.run[:dist].should eql(described_class::DEFAULT_DIST) + end + + context 'with :dist' do + let(:config) { { dist: 'hambone' } } + + it 'is a no-op' do + subject.run[:dist].should eql('hambone') + end + end + + context "with 'dist'" do + let(:config) { { 'dist' => 'lentil' } } + + it 'is a no-op' do + subject.run['dist'].should eql('lentil') + end + end + + context 'with an override language' do + let(:config) { { language: language } } + let(:language) { described_class::DIST_LANGUAGE_MAP.keys.sample } + + it 'sets the override for that language' do + subject.run[:dist].should eql( + described_class::DIST_LANGUAGE_MAP[language] + ) + end + end + + context 'with an override os' do + let(:config) { { os: os } } + let(:os) { described_class::DIST_OS_MAP.keys.sample } + + it 'sets the override for that os' do + subject.run[:dist].should eql(described_class::DIST_OS_MAP[os]) + end + end + + context 'with an override language and os' do + let(:config) { { language: language, os: os } } + let(:language) { described_class::DIST_LANGUAGE_MAP.keys.sample } + let(:os) { described_class::DIST_OS_MAP.keys.sample } + + it 'sets the override for that language' do + subject.run[:dist].should eql( + described_class::DIST_LANGUAGE_MAP[language] + ) + end + end + + context 'with multi_os option and override language set' do + let(:config) { { language: language } } + let(:options) { { multi_os: true } } + let(:language) { described_class::DIST_LANGUAGE_MAP.keys.sample } + + it 'sets the override for that language' do + subject.run[:dist].should eql( + described_class::DIST_LANGUAGE_MAP[language] + ) + end + end + + context 'with multi_os option and non-override language set' do + let(:config) { { language: 'goober' } } + let(:options) { { multi_os: true } } + let(:language) { described_class::DIST_LANGUAGE_MAP.keys.sample } + + it 'sets dist to the default' do + subject.run[:dist].should eql(described_class::DEFAULT_DIST) + end + end + + context 'without multi_os and os array with override first entry' do + let(:config) { { os: %w(osx linux) } } + let(:options) { { multi_os: false } } + + it 'sets the override for that os' do + subject.run[:dist].should eql(described_class::DIST_OS_MAP['osx']) + end + end + + context 'without multi_os and os array without override first entry' do + let(:config) { { os: %w(freebsd osx linux) } } + let(:options) { { multi_os: false } } + + it 'sets dist to the default' do + subject.run[:dist].should eql(described_class::DEFAULT_DIST) + end + end + + context 'with docker in services' do + let(:config) { { services: %w(docker) } } + + it 'sets the dist to trusty' do + subject.run[:dist].should eql('trusty') + end + end + + context 'with docker in matrix include services' do + let(:config) do + { + matrix: { include: [{ services: %w(docker postgresql) }] }, + services: %w(postgresql) + } + end + + it 'sets the dist to trusty in the include hash' do + subject.run[:matrix][:include].first[:dist].should eql('trusty') + end + + it 'sets the dist to the default at the top level' do + subject.run[:dist].should eql(described_class::DEFAULT_DIST) + end + end +end diff --git a/spec_core/core/model/build/config/group_spec.rb b/spec_core/core/model/build/config/group_spec.rb new file mode 100644 index 00000000..f26bbde0 --- /dev/null +++ b/spec_core/core/model/build/config/group_spec.rb @@ -0,0 +1,26 @@ +require 'travis/model/build/config/group' + +describe Build::Config::Group do + subject { described_class.new(config) } + let(:config) { { dist: 'bork' } } + + it 'sets group to the default' do + subject.run[:group].should eql(described_class::DEFAULT_GROUP) + end + + context 'with :group' do + let(:config) { { group: 'foo' } } + + it 'is a no-op' do + subject.run[:group].should eql('foo') + end + end + + context "with 'group'" do + let(:config) { { 'group' => 'bar' } } + + it 'is a no-op' do + subject.run['group'].should eql('bar') + end + end +end diff --git a/spec_core/core/model/build/config/matrix_spec.rb b/spec_core/core/model/build/config/matrix_spec.rb new file mode 100644 index 00000000..f2ced0e5 --- /dev/null +++ b/spec_core/core/model/build/config/matrix_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper_core' +require 'core_ext/hash/deep_symbolize_keys' + +describe Build::Config::Matrix do + include Support::ActiveRecord + + it 'can handle nil values in exclude matrix' do + -> { Build::Config::Matrix.new(matrix: { exclude: [nil] }).expand }.should_not raise_error + end + + it 'can handle list values in exclude matrix' do + -> { Build::Config::Matrix.new(matrix: []).expand }.should_not raise_error + end +end diff --git a/spec_core/core/model/build/config/obfuscate_spec.rb b/spec_core/core/model/build/config/obfuscate_spec.rb new file mode 100644 index 00000000..838de065 --- /dev/null +++ b/spec_core/core/model/build/config/obfuscate_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper_core' + +describe Build::Config::Obfuscate do + include Support::ActiveRecord + + let(:repo) { Factory(:repository) } + let(:build) { Build.new(repository: repo) } + + it 'normalizes env vars which are hashes to strings' do + encrypted = repo.key.secure.encrypt('BAR=barbaz') + build.config = { + language: 'ruby', + env: [[encrypted, 'FOO=foo'], [{ ONE: 1, TWO: '2' }]] + } + + build.obfuscated_config.should == { + language: 'ruby', + os: 'linux', + group: 'stable', + dist: 'precise', + env: ['BAR=[secure] FOO=foo', 'ONE=1 TWO=2'] + } + end + + it 'leaves regular vars untouched' do + build.config = { + rvm: ['1.8.7'], env: ['FOO=foo'] + } + + build.obfuscated_config.should == { + language: 'ruby', + os: 'linux', + group: 'stable', + dist: 'precise', + rvm: ['1.8.7'], + env: ['FOO=foo'] + } + end + + it 'obfuscates env vars' do + encrypted = build.repository.key.secure.encrypt('BAR=barbaz') + build.config = { + rvm: ['1.8.7'], + env: [[encrypted, 'FOO=foo'], 'BAR=baz'] + } + + build.obfuscated_config.should == { + language: 'ruby', + os: 'linux', + group: 'stable', + dist: 'precise', + rvm: ['1.8.7'], + env: ['BAR=[secure] FOO=foo', 'BAR=baz'] + } + end + + it 'obfuscates env vars which are not in nested array' do + build.config = { + rvm: ['1.8.7'], + env: [build.repository.key.secure.encrypt('BAR=barbaz')] + } + + build.obfuscated_config.should == { + language: 'ruby', + os: 'linux', + group: 'stable', + dist: 'precise', + rvm: ['1.8.7'], + env: ['BAR=[secure]'] + } + end + + it 'works with nil values' do + build.config = { + rvm: ['1.8.7'], + env: [[nil, { secure: '' }]] + } + build.obfuscated_config.should == { + language: 'ruby', + os: 'linux', + group: 'stable', + dist: 'precise', + rvm: ['1.8.7'], + env: [''] + } + end + + it 'does not make an empty env key an array but leaves it empty' do + build.config = { + rvm: ['1.8.7'], + env: nil + } + build.obfuscated_config.should == { + language: 'ruby', + os: 'linux', + group: 'stable', + dist: 'precise', + rvm: ['1.8.7'], + env: nil + } + end + + it 'removes source key' do + build.config = { + rvm: ['1.8.7'], + source_key: '1234' + } + build.obfuscated_config.should == { + language: 'ruby', + os: 'linux', + group: 'stable', + dist: 'precise', + rvm: ['1.8.7'] + } + end +end diff --git a/spec_core/core/model/build/config_spec.rb b/spec_core/core/model/build/config_spec.rb new file mode 100644 index 00000000..708786a8 --- /dev/null +++ b/spec_core/core/model/build/config_spec.rb @@ -0,0 +1,156 @@ +require 'spec_helper_core' + +describe Build::Config do + include Support::ActiveRecord + + it 'keeps the given env if it is an array' do + config = YAML.load %( + env: + - FOO=foo + - BAR=bar + ) + Build::Config.new(config).normalize.slice(:env, :global_env).should == { + env: [ + 'FOO=foo', + 'BAR=bar' + ] + } + end + + # seems odd. is this on purpose? + it 'normalizes an env vars hash to an array of strings' do + config = YAML.load %( + env: + FOO: foo + BAR: bar + ) + Build::Config.new(config).normalize.slice(:env, :global_env).should == { + env: [ + 'FOO=foo BAR=bar' + ] + } + end + + it 'keeps env vars global and matrix arrays' do + config = YAML.load %( + env: + global: + - FOO=foo + - BAR=bar + matrix: + - BAZ=baz + - BUZ=buz + ) + Build::Config.new(config).normalize.slice(:env, :global_env).should == { + global_env: [ + 'FOO=foo', + 'BAR=bar' + ], + env: [ + 'BAZ=baz', + 'BUZ=buz' + ] + } + end + + # seems odd. is this on purpose? + it 'normalizes env vars global and matrix which are hashes to an array of strings' do + config = YAML.load %( + env: + global: + FOO: foo + BAR: bar + matrix: + BAZ: baz + BUZ: buz + ) + Build::Config.new(config).normalize.slice(:env, :global_env).should == { + global_env: [ + 'FOO=foo BAR=bar' + ], + env: [ + 'BAZ=baz BUZ=buz' + ] + } + end + + it 'works fine if matrix part of env is undefined' do + config = YAML.load %( + env: + global: FOO=foo + ) + Build::Config.new(config).normalize.slice(:env, :global_env).should == { + global_env: [ + 'FOO=foo' + ] + } + end + + it 'works fine if global part of env is undefined' do + config = YAML.load %( + env: + matrix: FOO=foo + ) + Build::Config.new(config).normalize.slice(:env, :global_env).should == { + env: [ + 'FOO=foo' + ] + } + end + + # Seems odd. What's the usecase? Broken yaml? + it 'keeps matrix and global config as arrays, not hashes' do + config = YAML.load %( + env: + global: FOO=foo + matrix: + - + - BAR=bar + - BAZ=baz + - BUZ=buz + ) + Build::Config.new(config).normalize.slice(:env, :global_env).should == { + global_env: [ + 'FOO=foo' + ], + env: [ + ['BAR=bar', 'BAZ=baz'], + 'BUZ=buz' + ] + } + end + + # Seems super odd. Do people actually pass such stuff? + it 'keeps wild nested array/hashes structure' do + config = YAML.load %( + env: + - + - + secure: encrypted-value + - FOO=foo + - + - + BAR: bar + BAZ: baz + ) + Build::Config.new(config).normalize.slice(:env, :global_env).should == { + env: [ + [{ secure: 'encrypted-value' }, 'FOO=foo'], + ['BAR=bar BAZ=baz'] + ] + } + end + + it 'sets the os value to osx for objective-c builds' do + config = YAML.load %( + language: objective-c + ) + Build::Config.new(config).normalize[:os].should == 'osx' + end + + it 'sets the os value to linux for other builds' do + config = YAML.load %( + ) + Build::Config.new(config).normalize[:os].should == 'linux' + end +end diff --git a/spec_core/core/model/build/denormalize_spec.rb b/spec_core/core/model/build/denormalize_spec.rb new file mode 100644 index 00000000..0077bb40 --- /dev/null +++ b/spec_core/core/model/build/denormalize_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper_core' + +describe Build, 'denormalization' do + include Support::ActiveRecord + + let(:build) { Factory(:build, state: :started, duration: 30) } + + describe 'on build:started' do + before :each do + build.denormalize(:start) + build.reload + end + + it 'denormalizes last_build_id to its repository' do + build.repository.last_build_id.should == build.id + end + + it 'denormalizes last_build_state to its repository' do + build.repository.last_build_state.should == 'started' + end + + it 'denormalizes last_build_number to its repository' do + build.repository.last_build_number.should == build.number + end + + it 'denormalizes last_build_duration to its repository' do + build.repository.last_build_duration.should == build.duration + end + + it 'denormalizes last_build_started_at to its repository' do + build.repository.last_build_started_at.should == build.started_at + end + + it 'denormalizes last_build_finished_at to its repository' do + build.repository.last_build_finished_at.should == build.finished_at + end + end + + describe 'on build:finished' do + before :each do + build.update_attributes(state: :errored) + build.denormalize(:finish) + build.reload + end + + it 'denormalizes last_build_state to its repository' do + build.repository.last_build_state.should == 'errored' + end + + it 'denormalizes last_build_duration to its repository' do + build.repository.last_build_duration.should == build.duration + end + + it 'denormalizes last_build_finished_at to its repository' do + build.repository.last_build_finished_at.should == build.finished_at + end + end +end + diff --git a/spec_core/core/model/build/matrix_spec.rb b/spec_core/core/model/build/matrix_spec.rb new file mode 100644 index 00000000..d3a76bd2 --- /dev/null +++ b/spec_core/core/model/build/matrix_spec.rb @@ -0,0 +1,889 @@ +require 'spec_helper_core' + +describe Build, 'matrix' do + include Support::ActiveRecord + + describe :matrix_finished? do + context 'if config[:matrix][:finish_fast] is not set' do + context 'if at least one job has not finished and is not allowed to fail' do + it 'returns false' do + build = Factory(:build, config: { rvm: ['1.8.7', '1.9.2'] }) + build.matrix[0].update_attributes(state: :passed) + build.matrix[1].update_attributes(state: :started) + + build.matrix_finished?.should_not be true + end + end + + context 'if at least one job has not finished and is allowed to fail' do + it 'returns false' do + build = Factory(:build, config: { rvm: ['1.8.7', '1.9.2'] }) + build.matrix[0].update_attributes(state: :passed) + build.matrix[1].update_attributes(state: :started, allow_failure: true) + + build.matrix_finished?.should_not be true + end + end + + context 'if all jobs have finished' do + it 'returns true' do + build = Factory(:build, config: { rvm: ['1.8.7', '1.9.2'] }) + build.matrix[0].update_attributes!(state: :passed) + build.matrix[1].update_attributes!(state: :passed) + + build.matrix_finished?.should be true + end + end + end + context 'if config[:matrix][:finish_fast] is set' do + context 'if at least one job has not finished and is not allowed to fail' do + it 'returns false' do + build = Factory(:build, config: { rvm: ['1.8.7', '1.9.2'], matrix: {fast_finish: true} }) + build.matrix[0].update_attributes(state: :passed) + build.matrix[1].update_attributes(state: :started) + + build.matrix_finished?.should be false + end + end + + context 'if at least one job has not finished and is allowed to fail' do + it 'returns true' do + build = Factory(:build, config: { rvm: ['1.8.7', '1.9.2'], matrix: {fast_finish: true} }) + build.matrix[0].update_attributes(state: :passed) + build.matrix[1].update_attributes(state: :started, allow_failure: true) + + build.matrix_finished?.should be true + end + end + + context 'if all jobs have finished' do + it 'returns true' do + build = Factory(:build, config: { rvm: ['1.8.7', '1.9.2'], matrix: {fast_finish: true} }) + build.matrix[0].update_attributes!(state: :passed) + build.matrix[1].update_attributes!(state: :passed) + + build.matrix_finished?.should be true + end + end + end + end + + describe :matrix_state do + let(:build) { Factory(:build, config: { rvm: ['1.8.7', '1.9.2'] }) } + + it 'returns :passed if all jobs have passed' do + build.matrix[0].update_attributes!(state: "passed") + build.matrix[1].update_attributes!(state: "passed") + build.matrix_state.should == :passed + end + + it 'returns :failed if one job has failed' do + build.matrix[0].update_attributes!(state: "passed") + build.matrix[1].update_attributes!(state: "failed") + build.matrix_state.should == :failed + end + + it 'returns :failed if one job has failed and one job has errored' do + build.matrix[0].update_attributes!(state: "errored") + build.matrix[1].update_attributes!(state: "failed") + build.matrix_state.should == :errored + end + + it 'returns :errored if one job has errored' do + build.matrix[0].update_attributes!(state: "passed") + build.matrix[1].update_attributes!(state: "errored") + build.matrix_state.should == :errored + end + + it 'returns :passed if a errored job is allowed to fail' do + build.matrix[0].update_attributes!(state: "passed") + build.matrix[1].update_attributes!(state: "errored", allow_failure: true) + build.matrix_state.should == :passed + end + + it 'returns :passed if a failed job is allowed to fail' do + build.matrix[0].update_attributes!(state: "passed") + build.matrix[1].update_attributes!(state: "failed", allow_failure: true) + build.matrix_state.should == :passed + end + + it 'returns :failed if all jobs have failed and only one is allowed to fail' do + build.matrix[0].update_attributes!(state: "failed") + build.matrix[1].update_attributes!(state: "failed", allow_failure: true) + build.matrix_state.should == :failed + end + + it 'returns :failed if all jobs have failed and only one is allowed to fail' do + build.matrix[0].update_attributes!(state: "finished") + expect { build.matrix_state }.to raise_error(StandardError) + end + + it 'returns :passed if all jobs have passed except a job that is allowed to fail, and config[:matrix][:finish_fast] is set' do + build.config.update(finish_fast: true) + build.matrix[0].update_attributes!(state: "passed") + build.matrix[1].update_attributes!(state: "failed", allow_failure: true) + build.matrix_state.should == :passed + end + end + + context 'matrix with one allow_failure job' do + let(:build) { Factory(:build, config: { rvm: ['1.9.3'] }) } + + it 'returns :passed' do + build.matrix[0].update_attributes!(state: "failed", allow_failure: true) + build.matrix_state.should == :passed + end + end + + describe :matrix_duration do + let(:build) do + Build.new(matrix: [ + Job::Test.new(started_at: 60.seconds.ago, finished_at: 40.seconds.ago), + Job::Test.new(started_at: 20.seconds.ago, finished_at: 10.seconds.ago) + ]) + end + + context 'if the matrix is finished' do + it 'returns the sum of the matrix job durations' do + build.stubs(:matrix_finished?).returns(true) + build.matrix_duration.should == 30 + end + end + + context 'if the matrix is not finished' do + it 'returns nil' do + build.stubs(:matrix_finished?).returns(false) + build.matrix_duration.should be_nil + end + end + end + + describe 'for Ruby projects' do + let(:no_matrix_config) { + YAML.load <<-yml + script: 'rake ci' + yml + } + + let(:single_test_config) { + YAML.load <<-yml + script: 'rake ci' + rvm: + - 1.8.7 + gemfile: + - gemfiles/rails-3.0.6 + env: + - USE_GIT_REPOS=true + yml + } + + let(:env_global_config) { + YAML.load <<-yml + script: 'rake ci' + rvm: + - 1.9.2 + - 1.9.3 + gemfile: + - gemfiles/rails-4.0.0 + env: + global: + - TOKEN=abcdef + matrix: + - FOO=bar + - BAR=baz + yml + } + + let(:multiple_tests_config) { + YAML.load <<-yml + script: 'rake ci' + rvm: + - 1.8.7 + - 1.9.1 + - 1.9.2 + gemfile: + - gemfiles/rails-3.0.6 + - gemfiles/rails-3.0.7 + - gemfiles/rails-3-0-stable + - gemfiles/rails-master + env: + - USE_GIT_REPOS=true + yml + } + + let(:multiple_tests_config_with_exculsion) { + YAML.load <<-yml + rvm: + - 1.8.7 + - 1.9.2 + gemfile: + - gemfiles/rails-2.3.x + - gemfiles/rails-3.0.x + - gemfiles/rails-3.1.x + matrix: + exclude: + - rvm: 1.8.7 + gemfile: gemfiles/rails-3.1.x + - rvm: 1.9.2 + gemfile: gemfiles/rails-2.3.x + yml + } + + let(:multiple_tests_config_with_global_env_and_exclusion) { + YAML.load <<-yml + rvm: + - 1.9.2 + - 2.0.0 + gemfile: + - gemfiles/rails-3.1.x + - gemfiles/rails-4.0.x + env: + global: + - FOO=bar + matrix: + exclude: + - rvm: 1.9.2 + gemfile: gemfiles/rails-4.0.x + yml + } + + let(:multiple_tests_config_with_invalid_exculsion) { + YAML.load <<-yml + rvm: + - 1.8.7 + - 1.9.2 + gemfile: + - gemfiles/rails-3.0.x + - gemfiles/rails-3.1.x + env: + - FOO=bar + - BAR=baz + matrix: + exclude: + - rvm: 1.9.2 + gemfile: gemfiles/rails-3.0.x + yml + } + + let(:multiple_tests_config_with_inclusion) { + YAML.load <<-yml + rvm: + - 1.8.7 + - 1.9.2 + env: + - FOO=bar + - BAR=baz + matrix: + include: + - rvm: 1.9.2 + env: BAR=xyzzy + yml + } + + let(:matrix_with_inclusion_only) { + YAML.load <<-yml + language: ruby + matrix: + include: + - rvm: "2.1.0" + env: FOO=true + - rvm: "2.1.0" + env: BAR=true + - rvm: "1.9.3" + env: BAZ=true + yml + } + + let(:matrix_with_empty_include) { + YAML.load <<-yml + language: ruby + matrix: + include: + yml + } + + let(:multiple_tests_config_with_allow_failures) { + YAML.load <<-yml + language: objective-c + rvm: + - 1.8.7 + - 1.9.2 + xcode_sdk: + - iphonesimulator6.1 + - iphonesimulator7.0 + matrix: + allow_failures: + - rvm: 1.8.7 + xcode_sdk: iphonesimulator7.0 + yml + } + + let(:allow_failures_with_global_env) { + YAML.load <<-yml + rvm: + - 1.9.3 + - 2.0.0 + env: + global: + - "GLOBAL=global NEXT_GLOBAL=next" + matrix: + - "FOO=bar" + - "FOO=baz" + matrix: + allow_failures: + - rvm: 1.9.3 + env: "FOO=bar" + yml + } + + let(:scalar_allow_failures) { + YAML.load <<-yml + env: + global: + - "GLOBAL=global NEXT_GLOBAL=next" + matrix: + - "FOO=bar" + - "FOO=baz" + matrix: + allow_failures: + "FOO=bar" + yml + } + + let(:matrix_with_unwanted_expansion_ruby) { + YAML.load <<-yml + language: ruby + python: + - 3.3 + - 2.7 + rvm: + - 2.0.0 + - 1.9.3 + gemfile: + - 'gemfiles/rails-4' + yml + } + + let(:matrix_with_unwanted_expansion_python) { + YAML.load <<-yml + language: python + python: + - "3.3" + - "2.7" + rvm: + - 2.0.0 + - 1.9.3 + gemfile: + - 'gemfiles/rails-4' + yml + } + + let(:ruby_matrix_with_incorrect_allow_failures) { + YAML.load <<-yml + language: ruby + + rvm: + - "1.9.3" + - "2.1.0" + + matrix: + fast_finish: true + allow_failures: + - what: "ever" + yml + } + + describe :expand_matrix, truncation: true do + it 'does not expand on :os' do + build = Factory.create(:build, config: { rvm: ['1.9.3', '2.0.0'], os: ['osx', 'linux']}) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.3' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '2.0.0' } + ] + end + + it 'does not clobber env and global_env vars' do + build = Factory(:build, config: env_global_config) + + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.9.2', gemfile: 'gemfiles/rails-4.0.0', env: 'FOO=bar', global_env: ['TOKEN=abcdef'] }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.9.2', gemfile: 'gemfiles/rails-4.0.0', env: 'BAR=baz', global_env: ['TOKEN=abcdef'] }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.9.3', gemfile: 'gemfiles/rails-4.0.0', env: 'FOO=bar', global_env: ['TOKEN=abcdef'] }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.9.3', gemfile: 'gemfiles/rails-4.0.0', env: 'BAR=baz', global_env: ['TOKEN=abcdef'] } + ] + end + + it 'sets the config to the jobs (no config)' do + build = Factory(:build, config: {}) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise' } + ] + end + + it 'sets the config to the jobs (no matrix config)' do + build = Factory(:build, config: no_matrix_config) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci' } + ] + end + + it 'sets the config to the jobs (single test config)' do + build = Factory(:build, config: single_test_config) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.8.7', gemfile: 'gemfiles/rails-3.0.6', env: 'USE_GIT_REPOS=true' } + ] + end + + it 'sets the config to the jobs (multiple tests config)' do + build = Factory(:build, config: multiple_tests_config) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.8.7', gemfile: 'gemfiles/rails-3.0.6', env: 'USE_GIT_REPOS=true' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.8.7', gemfile: 'gemfiles/rails-3.0.7', env: 'USE_GIT_REPOS=true' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.8.7', gemfile: 'gemfiles/rails-3-0-stable', env: 'USE_GIT_REPOS=true' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.8.7', gemfile: 'gemfiles/rails-master', env: 'USE_GIT_REPOS=true' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.9.1', gemfile: 'gemfiles/rails-3.0.6', env: 'USE_GIT_REPOS=true' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.9.1', gemfile: 'gemfiles/rails-3.0.7', env: 'USE_GIT_REPOS=true' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.9.1', gemfile: 'gemfiles/rails-3-0-stable', env: 'USE_GIT_REPOS=true' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.9.1', gemfile: 'gemfiles/rails-master', env: 'USE_GIT_REPOS=true' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.9.2', gemfile: 'gemfiles/rails-3.0.6', env: 'USE_GIT_REPOS=true' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.9.2', gemfile: 'gemfiles/rails-3.0.7', env: 'USE_GIT_REPOS=true' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.9.2', gemfile: 'gemfiles/rails-3-0-stable', env: 'USE_GIT_REPOS=true' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', script: 'rake ci', rvm: '1.9.2', gemfile: 'gemfiles/rails-master', env: 'USE_GIT_REPOS=true' } + ] + end + + it 'sets the config to the jobs (allow failures config)' do + build = Factory(:build, config: multiple_tests_config_with_allow_failures) + build.matrix.map(&:allow_failure).should == [false, true, false, false] + end + + it 'ignores global env config when setting allow failures' do + build = Factory(:build, config: allow_failures_with_global_env) + build.matrix.map(&:allow_failure).should == [true, false, false, false] + end + + context 'when matrix specifies incorrect allow_failures' do + before :each do + @build = Factory(:build, config: ruby_matrix_with_incorrect_allow_failures) + end + + it 'excludes matrices correctly' do + @build.matrix.map(&:allow_failure).should == [false, false] + end + end + + context 'when matrix specifies scalar allow_failures' do + before :each do + @build = Factory(:build, config: scalar_allow_failures) + end + + it 'ignores allow_failures silently' do + @build.matrix.map(&:allow_failure).should == [false, false] + end + end + + context 'when ruby project contains unwanted key' do + before :each do + @build_ruby = Factory(:build, config: matrix_with_unwanted_expansion_ruby) + end + + it 'ignores irrelevant matrix dimensions' do + @build_ruby.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '2.0.0', gemfile: 'gemfiles/rails-4' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.3', gemfile: 'gemfiles/rails-4' } + ] + end + + it 'creates jobs whose config does not contain unwanted keys' do + configs = @build_ruby.matrix.map { |job| job.config[:python] }.flatten.compact + configs.should be_empty + end + + # it 'does not touch config' do + # @build_ruby.config.keys.should include(:python) + # end + end + + context 'when python project contains unwanted key' do + before :each do + @build_python = Factory(:build, config: matrix_with_unwanted_expansion_python) + end + + it 'ignores irrelevant matrix dimensions' do + @build_python.matrix.map(&:config).should == [ + { os: 'linux', language: 'python', group: 'stable', dist: 'precise', python: '3.3' }, + { os: 'linux', language: 'python', group: 'stable', dist: 'precise', python: '2.7' } + ] + end + + # it 'does not touch config' do + # @build_python.config.keys.should include(:rvm) + # end + end + + it 'copies build attributes' do + # TODO spec other attributes! + build = Factory(:build, config: multiple_tests_config) + build.matrix.map(&:commit_id).uniq.should == [build.commit_id] + end + + it 'adds a sub-build number to the job number' do + build = Factory(:build, config: multiple_tests_config) + build.matrix.map(&:number)[0..3].should == ['1.1', '1.2', '1.3', '1.4'] + end + + describe :exclude_matrix_config do + it 'excludes a matrix config when all config items are defined in the exclusion' do + build = Factory(:build, config: multiple_tests_config_with_exculsion) + matrix_exclusion = { + exclude: [ + { rvm: '1.8.7', gemfile: 'gemfiles/rails-3.1.x' }, + { rvm: '1.9.2', gemfile: 'gemfiles/rails-2.3.x' } + ] + } + + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', gemfile: 'gemfiles/rails-2.3.x' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', gemfile: 'gemfiles/rails-3.0.x' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.2', gemfile: 'gemfiles/rails-3.0.x' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.2', gemfile: 'gemfiles/rails-3.1.x' } + ] + end + + it "excludes a matrix config without specifying global env vars in the exclusion" do + build = Factory(:build, config: multiple_tests_config_with_global_env_and_exclusion) + matrix_exclusion = { exclude: [{ rvm: "1.9.2", gemfile: "gemfiles/rails-4.0.x" }] } + + build.matrix.map(&:config).should eq([ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: "1.9.2", gemfile: "gemfiles/rails-3.1.x", global_env: ["FOO=bar"] }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: "2.0.0", gemfile: "gemfiles/rails-3.1.x", global_env: ["FOO=bar"] }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: "2.0.0", gemfile: "gemfiles/rails-4.0.x", global_env: ["FOO=bar"] }, + ]) + end + + it 'excludes jobs from a matrix config when the matrix exclusion definition is incomplete' do + build = Factory(:build, config: multiple_tests_config_with_invalid_exculsion) + + matrix_exclusion = { exclude: [{ rvm: '1.9.2', gemfile: 'gemfiles/rails-3.0.x' }] } + + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', gemfile: 'gemfiles/rails-3.0.x', env: 'FOO=bar' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', gemfile: 'gemfiles/rails-3.0.x', env: 'BAR=baz' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', gemfile: 'gemfiles/rails-3.1.x', env: 'FOO=bar' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', gemfile: 'gemfiles/rails-3.1.x', env: 'BAR=baz' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.2', gemfile: 'gemfiles/rails-3.1.x', env: 'FOO=bar' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.2', gemfile: 'gemfiles/rails-3.1.x', env: 'BAR=baz' } + ] + end + end + end + + describe :include_matrix_config do + it 'includes a matrix config' do + build = Factory(:build, config: multiple_tests_config_with_inclusion) + + matrix_inclusion = { + include: [ + { rvm: '1.9.2', env: 'BAR=xyzzy' } + ] + } + + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', env: 'FOO=bar' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', env: 'BAR=baz' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.2', env: 'FOO=bar' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.2', env: 'BAR=baz' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.2', env: 'BAR=xyzzy' } + ] + end + + it 'does not include "empty" matrix config' do + build = Factory(:build, config: matrix_with_inclusion_only) + + matrix_inclusion = { + include: [ + { rvm: '2.1.0', env: 'FOO=true' }, + { rvm: '2.1.0', env: 'BAR=true' }, + { rvm: '1.9.3', env: 'BAZ=true' } + ] + } + + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '2.1.0', env: 'FOO=true' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '2.1.0', env: 'BAR=true' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.3', env: 'BAZ=true' } + ] + end + + it 'includes "empty" matrix config when matrix.include is null' do + build = Factory(:build, config: matrix_with_empty_include) + + matrix_inclusion = { + include: nil + } + + build.matrix.map(&:config).should == [] + end + end + + describe 'matrix expansion' do + let(:repository) { Factory(:repository) } + + it 'with string values' do + build = Factory(:build, config: { rvm: '1.8.7', gemfile: 'gemfiles/rails-2.3.x', env: 'FOO=bar' }) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', gemfile: 'gemfiles/rails-2.3.x', env: 'FOO=bar' } + ] + end + + it 'does not decrypt secure env vars' do + env = repository.key.secure.encrypt('FOO=bar').symbolize_keys + config = { rvm: '1.8.7', gemfile: 'gemfiles/rails-2.3.x', env: env } + build = Factory(:build, repository: repository, config: config) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', gemfile: 'gemfiles/rails-2.3.x', env: env } + ] + end + + it 'with two Rubies and Gemfiles' do + build = Factory(:build, config: { rvm: ['1.8.7', '1.9.2'], gemfile: ['gemfiles/rails-2.3.x', 'gemfiles/rails-3.0.x'] }) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', gemfile: 'gemfiles/rails-2.3.x' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', gemfile: 'gemfiles/rails-3.0.x' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.2', gemfile: 'gemfiles/rails-2.3.x' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.2', gemfile: 'gemfiles/rails-3.0.x' } + ] + end + + it 'with unequal number of Rubies, env variables and Gemfiles' do + build = Factory(:build, config: { rvm: ['1.8.7', '1.9.2', 'ree'], gemfile: ['gemfiles/rails-3.0.x'], env: ['DB=postgresql', 'DB=mysql'] }) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', gemfile: 'gemfiles/rails-3.0.x', env: 'DB=postgresql' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', gemfile: 'gemfiles/rails-3.0.x', env: 'DB=mysql' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.2', gemfile: 'gemfiles/rails-3.0.x', env: 'DB=postgresql' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.2', gemfile: 'gemfiles/rails-3.0.x', env: 'DB=mysql' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: 'ree', gemfile: 'gemfiles/rails-3.0.x', env: 'DB=postgresql' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: 'ree', gemfile: 'gemfiles/rails-3.0.x', env: 'DB=mysql' }, + ] + end + + it 'with an array of Rubies and a single Gemfile' do + build = Factory(:build, config: { rvm: ['1.8.7', '1.9.2'], gemfile: 'gemfiles/rails-2.3.x' }) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.8.7', gemfile: 'gemfiles/rails-2.3.x' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.2', gemfile: 'gemfiles/rails-2.3.x' } + ] + end + end + end + + describe 'for Scala projects' do + it 'with a single Scala version given as a string' do + build = Factory(:build, config: { language: 'scala', scala: '2.8.2', env: 'NETWORK=false' }) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'scala', group: 'stable', dist: 'precise', scala: '2.8.2', env: 'NETWORK=false' } + ] + end + + it 'with multiple Scala versions and no env variables' do + build = Factory(:build, config: { language: 'scala', scala: ['2.8.2', '2.9.1']}) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'scala', group: 'stable', dist: 'precise', scala: '2.8.2' }, + { os: 'linux', language: 'scala', group: 'stable', dist: 'precise', scala: '2.9.1' } + ] + end + + it 'with a single Scala version passed in as array and two env variables' do + build = Factory(:build, config: { language: 'scala', scala: ['2.8.2'], env: ['STORE=postgresql', 'STORE=redis'] }) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'scala', group: 'stable', dist: 'precise', scala: '2.8.2', env: 'STORE=postgresql' }, + { os: 'linux', language: 'scala', group: 'stable', dist: 'precise', scala: '2.8.2', env: 'STORE=redis' } + ] + end + end + + describe 'multi_os' do + let(:matrix_with_os_ruby) { + YAML.load(%( + language: ruby + os: + - osx + - linux + rvm: + - 2.0.0 + - 1.9.3 + gemfile: + - 'gemfiles/rails-4' + )).deep_symbolize_keys + } + + let(:repository) { Factory(:repository)} + let(:test) { Factory(:test, repository: repository) } + + context 'the feature is active' do + it 'expands on :os' do + repository.stubs(:multi_os_enabled?).returns(true) + build = Factory(:build, config: matrix_with_os_ruby, repository: repository) + + build.matrix.map(&:config).should == [ + { os: 'osx', language: 'ruby', group: 'stable', dist: 'precise', rvm: '2.0.0', gemfile: 'gemfiles/rails-4' }, + { os: 'osx', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.3', gemfile: 'gemfiles/rails-4' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '2.0.0', gemfile: 'gemfiles/rails-4' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.3', gemfile: 'gemfiles/rails-4' }, + ] + end + end + + context 'the feature is inactive' do + it 'does not expand on :os' do + repository.stubs(:multi_os_enabled?).returns(false) + build = Factory(:build, config: matrix_with_os_ruby, repository: repository) + + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '2.0.0', gemfile: 'gemfiles/rails-4' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', rvm: '1.9.3', gemfile: 'gemfiles/rails-4' } + ] + end + end + end + + describe 'os expansion' do + let(:matrix_with_includes_os_ruby) { + YAML.load(%( + language: ruby + matrix: + include: + - os: linux + compiler: gcc + - os: linux + compiler: clang + - os: osx + compiler: gcc + - os: osx + compiler: clang + )).deep_symbolize_keys + } + let(:repository) { Factory(:repository) } + let(:build) { Factory(:build, repository: repository, config: matrix_with_includes_os_ruby) } + + it 'expands on :os if the feature is active' do + repository.stubs(:multi_os_enabled?).returns(true) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', compiler: 'gcc' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', compiler: 'clang' }, + { os: 'osx', language: 'ruby', group: 'stable', dist: 'precise', compiler: 'gcc' }, + { os: 'osx', language: 'ruby', group: 'stable', dist: 'precise', compiler: 'clang' } + ] + end + + it 'ignores the os key if the feature is inactive' do + repository.stubs(:multi_os_enabled?).returns(false) + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', compiler: 'gcc' }, + { os: 'linux', language: 'ruby', group: 'stable', dist: 'precise', compiler: 'clang' } + ] + end + end + + describe 'dist_group_expansion' do + let(:matrix_with_dist_and_group_ruby) { + YAML.load(%( + language: ruby + dist: + - precise + - trusty + group: + - current + - update + rvm: + - 2.0.0 + - 1.9.3 + gemfile: + - 'gemfiles/rails-4' + )).deep_symbolize_keys + } + let(:repository) { Factory(:repository) } + + context 'the feature is active' do + it 'expands on :dist and :group' do + repository.stubs(:dist_group_expansion_enabled?).returns(true) + build = Factory(:build, repository: repository, config: matrix_with_dist_and_group_ruby) + + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', dist: 'precise', group: 'current', rvm: '2.0.0', gemfile: 'gemfiles/rails-4' }, + { os: 'linux', language: 'ruby', dist: 'precise', group: 'current', rvm: '1.9.3', gemfile: 'gemfiles/rails-4' }, + { os: 'linux', language: 'ruby', dist: 'precise', group: 'update', rvm: '2.0.0', gemfile: 'gemfiles/rails-4' }, + { os: 'linux', language: 'ruby', dist: 'precise', group: 'update', rvm: '1.9.3', gemfile: 'gemfiles/rails-4' }, + { os: 'linux', language: 'ruby', dist: 'trusty', group: 'current', rvm: '2.0.0', gemfile: 'gemfiles/rails-4' }, + { os: 'linux', language: 'ruby', dist: 'trusty', group: 'current', rvm: '1.9.3', gemfile: 'gemfiles/rails-4' }, + { os: 'linux', language: 'ruby', dist: 'trusty', group: 'update', rvm: '2.0.0', gemfile: 'gemfiles/rails-4' }, + { os: 'linux', language: 'ruby', dist: 'trusty', group: 'update', rvm: '1.9.3', gemfile: 'gemfiles/rails-4' }, + ] + end + end + + context 'the feature is inactive' do + it 'does not expand on :dist or :group' do + Build.any_instance.stubs(:dist_group_expansion_enabled?).returns(false) + build = Factory(:build, config: matrix_with_dist_and_group_ruby) + + build.matrix.map(&:config).should == [ + { os: 'linux', language: 'ruby', dist: ['precise', 'trusty'], group: ['current', 'update'], rvm: '2.0.0', gemfile: 'gemfiles/rails-4' }, + { os: 'linux', language: 'ruby', dist: ['precise', 'trusty'], group: ['current', 'update'], rvm: '1.9.3', gemfile: 'gemfiles/rails-4' } + ] + end + end + end + + describe 'filter_matrix' do + it 'selects matching builds' do + build = Factory(:build, config: { rvm: ['1.8.7', '1.9.2'], env: ['DB=sqlite3', 'DB=postgresql'] }) + build.filter_matrix({ rvm: '1.8.7', env: 'DB=sqlite3' }).should == [build.matrix[0]] + end + + it 'does not select builds with non-matching values' do + build = Factory(:build, config: { rvm: ['1.8.7', '1.9.2'], env: ['DB=sqlite3', 'DB=postgresql'] }) + build.filter_matrix({ rvm: 'nomatch', env: 'DB=sqlite3' }).should be_empty + end + + it 'does not select builds with non-matching keys' do + build = Factory(:build, config: { rvm: ['1.8.7', '1.9.2'], env: ['DB=sqlite3', 'DB=postgresql'] }) + build.filter_matrix({ rvm: '1.8.7', nomatch: 'DB=sqlite3' }).should == [build.matrix[0], build.matrix[1]] + end + end + + describe 'does not explode' do + it 'on a config key that is `true`' do + config = { true => 'broken' } + build = Factory(:build, config: config, repository: Factory(:repository)) + expect { build.expand_matrix }.to_not raise_error + end + + it 'on bad matrix include values' do + config = { matrix: { include: ['broken'] } } + build = Factory(:build, config: config, repository: Factory(:repository)) + expect { build.expand_matrix }.to_not raise_error + end + + it 'on config[:matrix] being an array' do + config = { matrix: [{ foo: 'kaputt' }] } + build = Factory(:build, config: config, repository: Factory(:repository)) + expect { build.expand_matrix }.to_not raise_error + end + end + + + # describe 'matrix_keys_for' do + # let(:config_default_lang) { { 'rvm' => ['1.8.7', '1.9.2'], 'env' => ['DB=sqlite3', 'DB=postgresql'] } } + # let(:config_non_def_lang) { { 'language' => 'scala', 'rvm' => ['1.8.7', '1.9.2'], 'env' => ['DB=sqlite3', 'DB=postgresql'] } } + # let(:config_lang_array) { { 'language' => ['scala'], 'rvm' => ['1.8.7', '1.9.2'], 'env' => ['DB=sqlite3', 'DB=postgresql'] } } + # let(:config_unrecognized) { { 'language' => 'bash', 'rvm' => ['1.8.7', '1.9.2'], 'env' => ['DB=sqlite3', 'DB=postgresql'] } } + + # it 'only selects appropriate keys' do + # Build.matrix_keys_for(config_default_lang).should == [:rvm, :env] + # Build.matrix_keys_for(config_non_def_lang).should == [:env] + # Build.matrix_keys_for(config_lang_array).should == [:env] + # Build.matrix_keys_for(config_unrecognized).should == [:rvm, :env] + # end + # end +end diff --git a/spec_core/core/model/build/metrics_spec.rb b/spec_core/core/model/build/metrics_spec.rb new file mode 100644 index 00000000..cc9b8769 --- /dev/null +++ b/spec_core/core/model/build/metrics_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper_core' + +class BuildMetricsMock + include do + attr_accessor :state, :request + + def initialize(request) + @request = request + end + + def start(data = {}) + self.state = :started + end + + def started_at + Time.now + end + + end + + include Build::Metrics +end + +describe Build::Metrics do + let(:request) { stub('request', created_at: Time.now - 60) } + let(:build) { BuildMetricsMock.new(request) } + let(:timer) { stub('meter', :update) } + + before :each do + Metriks.stubs(:timer).returns(timer) + end + + it 'measures on "travis.builds.start.delay"' do + Metriks.expects(:timer).with('travis.builds.start.delay').returns(timer) + build.start(started_at: Time.now) + end + + it 'measures the time it takes from creating the request until starting the build' do + timer.expects(:update).with(60) + build.start(started_at: Time.now) + end +end diff --git a/spec_core/core/model/build/result_message_spec.rb b/spec_core/core/model/build/result_message_spec.rb new file mode 100644 index 00000000..68371312 --- /dev/null +++ b/spec_core/core/model/build/result_message_spec.rb @@ -0,0 +1,148 @@ +require 'spec_helper_core' + +describe Build::ResultMessage do + def message(data) + described_class.new(data) + end + + describe "short" do + it 'returns :pending if the build is pending' do + data = { state: :created, previous_state: nil } + message(data).short.should == 'Pending' + end + + it 'returns :passed if the build has passed for the first time' do + data = { state: :passed, previous_state: nil } + message(data).short.should == 'Passed' + end + + it 'returns :failed if the build has failed for the first time' do + data = { state: :failed, previous_state: nil } + message(data).short.should == 'Failed' + end + + it 'returns :passed if the build has passed again' do + data = { state: :passed, previous_state: :passed } + message(data).short.should == 'Passed' + end + + it 'returns :broken if the build was broken' do + data = { state: :failed, previous_state: :passed } + message(data).short.should == 'Broken' + end + + it 'returns :fixed if the build was fixed' do + data = { state: :passed, previous_state: :failed } + message(data).short.should == 'Fixed' + end + + it 'returns :failing if the build has failed again' do + data = { state: :failed, previous_state: :failed } + message(data).short.should == 'Still Failing' + end + + it 'returns :errored if the build has errored' do + data = { state: :errored, previous_state: :failed } + message(data).short.should == 'Errored' + end + + it 'returns :canceled if the build has canceled' do + data = { state: :canceled, previous_state: :failed } + message(data).short.should == 'Canceled' + end + end + + describe "full" do + it 'returns :pending if the build is pending' do + data = { state: :created, previous_state: nil } + message(data).full.should == 'The build is pending.' + end + + it 'returns :passed if the build has passed for the first time' do + data = { state: :passed, previous_state: nil } + message(data).full.should == 'The build passed.' + end + + it 'returns :failed if the build has failed for the first time' do + data = { state: :failed, previous_state: nil } + message(data).full.should == 'The build failed.' + end + + it 'returns :passed if the build has passed again' do + data = { state: :passed, previous_state: :passed } + message(data).full.should == 'The build passed.' + end + + it 'returns :broken if the build was broken' do + data = { state: :failed, previous_state: :passed } + message(data).full.should == 'The build was broken.' + end + + it 'returns :fixed if the build was fixed' do + data = { state: :passed, previous_state: :failed } + message(data).full.should == 'The build was fixed.' + end + + it 'returns :failing if the build has failed again' do + data = { state: :failed, previous_state: :failed } + message(data).full.should == 'The build is still failing.' + end + + it 'returns :errored if the build has errored' do + data = { state: :errored, previous_state: :failed } + message(data).full.should == 'The build has errored.' + end + + it 'returns :canceled if the build has canceled' do + data = { state: :canceled, previous_state: :failed } + message(data).full.should == 'The build was canceled.' + end + end + + describe "email" do + it 'returns :pending if the build is pending' do + data = { state: :created, previous_state: nil, number: 2 } + message(data).email.should == 'Build #2 is pending.' + end + + it 'returns :passed if the build has passed for the first time' do + data = { state: :passed, previous_state: nil, number: 2 } + message(data).email.should == 'Build #2 passed.' + end + + it 'returns :failed if the build has failed for the first time' do + data = { state: :failed, previous_state: nil, number: 2 } + message(data).email.should == 'Build #2 failed.' + end + + it 'returns :passed if the build has passed again' do + data = { state: :passed, previous_state: :passed, number: 2 } + message(data).email.should == 'Build #2 passed.' + end + + it 'returns :broken if the build was broken' do + data = { state: :failed, previous_state: :passed, number: 2 } + message(data).email.should == 'Build #2 was broken.' + end + + it 'returns :fixed if the build was fixed' do + data = { state: :passed, previous_state: :failed, number: 2 } + message(data).email.should == 'Build #2 was fixed.' + end + + it 'returns :failing if the build has failed again' do + data = { state: :failed, previous_state: :failed, number: 2 } + message(data).email.should == 'Build #2 is still failing.' + end + + it 'returns :errored if the build has errored' do + data = { state: :errored, previous_state: :failed, number: 2 } + message(data).email.should == 'Build #2 has errored.' + end + + it 'returns :canceled if the build has canceled' do + data = { state: :canceled, previous_state: :failed, number: 2 } + message(data).email.should == 'Build #2 was canceled.' + end + end +end diff --git a/spec_core/core/model/build/states_spec.rb b/spec_core/core/model/build/states_spec.rb new file mode 100644 index 00000000..86d0a249 --- /dev/null +++ b/spec_core/core/model/build/states_spec.rb @@ -0,0 +1,249 @@ +require 'spec_helper_core' + +class BuildMock + include Build::States + class << self; def name; 'Build'; end; end + attr_accessor :state, :received_at, :started_at, :finished_at, :duration + def denormalize(*) end +end + +describe Build::States do + include Support::ActiveRecord + + let(:build) { BuildMock.new } + + describe 'events' do + describe 'cancel' do + it 'cancels all the cancelable jobs' do + build = Factory(:build) + build.matrix.destroy_all + + created_job = Factory(:test, source: build, state: :created) + finished_jobs = Job::Test::FINISHED_STATES.map do |state| + Factory(:test, source: build, state: state) + end + build.reload + + expect { + build.cancel! + }.to change { created_job.reload.state } + + created_job.state.should == 'canceled' + finished_jobs.map { |j| j.state.to_sym }.should == Job::Test::FINISHED_STATES + end + end + + describe 'reset' do + before :each do + build.stubs(:write_attribute) + end + it 'does not set the state to created if any jobs in the matrix are running' do + build.stubs(matrix: [stub(state: :started)]) + build.reset + build.state.should_not == :started + end + it 'sets the state to created if none of the jobs in the matrix are running' do + build.stubs(matrix: [stub(state: :passed)]) + build.reset + build.state.should == :created + end + end + + describe 'receive' do + let(:data) { WORKER_PAYLOADS['job:test:receive'] } + + it 'does not denormalize attributes' do + build.denormalize?('job:test:receive').should be false + end + + describe 'when the build is not already received' do + it 'sets the state to :received' do + build.receive(data) + build.state.should == :received + end + + it 'notifies observers' do + Travis::Event.expects(:dispatch).with('build:received', build, data) + build.receive(data) + end + end + + describe 'when the build is already received' do + before :each do + build.state = :received + end + + it 'does not notify observers' do + Travis::Event.expects(:dispatch).never + build.receive(data) + end + end + + describe 'when the build has failed' do + before :each do + build.state = :failed + end + + it 'does not notify observers' do + Travis::Event.expects(:dispatch).never + build.receive(data) + end + end + + describe 'when the build has errored' do + before :each do + build.state = :errored + end + + it 'does not notify observers' do + Travis::Event.expects(:dispatch).never + build.receive(data) + end + end + end + + describe 'start' do + let(:data) { WORKER_PAYLOADS['job:test:start'] } + + describe 'when the build is not already started' do + it 'sets the state to :started' do + build.start(data) + build.state.should == :started + end + + it 'denormalizes attributes' do + build.expects(:denormalize) + build.start(data) + end + + it 'notifies observers' do + Travis::Event.expects(:dispatch).with('build:started', build, data) + build.start(data) + end + end + + describe 'when the build is already started' do + before :each do + build.state = :started + end + + it 'does not denormalize attributes' do + build.expects(:denormalize).never + build.start(data) + end + + it 'does not notify observers' do + Travis::Event.expects(:dispatch).never + build.start(data) + end + end + + describe 'when the build has failed' do + before :each do + build.state = :failed + end + + it 'does not denormalize attributes' do + build.expects(:denormalize).never + build.start(data) + end + + it 'does not notify observers' do + Travis::Event.expects(:dispatch).never + build.start(data) + end + end + + describe 'when the build has errored' do + before :each do + build.state = :errored + end + + it 'does not denormalize attributes' do + build.expects(:denormalize).never + build.start(data) + end + + it 'does not notify observers' do + Travis::Event.expects(:dispatch).never + build.start(data) + end + end + end + + describe 'finish' do + let(:data) { WORKER_PAYLOADS['job:test:finish'] } + + describe 'when the matrix is not finished' do + before(:each) do + build.stubs(matrix_finished?: false) + end + + describe 'when the build is already finished' do + before(:each) do + build.state = :finished + end + + it 'does not denormalize attributes' do + build.expects(:denormalize).never + build.finish(data) + end + + it 'does not notify observers' do + Travis::Event.expects(:dispatch).never + build.finish(data) + end + end + end + + describe 'when the matrix is finished' do + before(:each) do + build.stubs(matrix_finished?: true, matrix_state: :passed, matrix_duration: 30) + end + + describe 'when the build has not finished' do + before(:each) do + build.state = :started + build.expects(:save!) + end + + it 'sets the state to the matrix state' do + build.finish(data) + build.state.should == :passed + end + + it 'calculates the duration based on the matrix durations' do + build.finish(data) + build.duration.should == 30 + end + + it 'denormalizes attributes' do + build.expects(:denormalize).with(:finish, data) + build.finish(data) + end + + it 'notifies observers' do + Travis::Event.expects(:dispatch).with('build:finished', build, data) + build.finish(data) + end + end + + describe 'when the build has already finished' do + before(:each) do + build.state = :passed + end + + it 'does not denormalize attributes' do + build.expects(:denormalize).never + build.finish(data) + end + + it 'does not notify observers' do + Travis::Event.expects(:dispatch).never + build.finish(data) + end + end + end + end + end +end diff --git a/spec_core/core/model/build/update_branch_spec.rb b/spec_core/core/model/build/update_branch_spec.rb new file mode 100644 index 00000000..4a5c0a8e --- /dev/null +++ b/spec_core/core/model/build/update_branch_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper_core' + +describe Build::UpdateBranch, truncation: true do + include Support::ActiveRecord + + let(:request) { Factory.create(:request, event_type: event_type) } + let(:build) { Factory.build(:build, request: request, state: :started, duration: 30, branch: 'master') } + let(:branch) { Branch.where(repository_id: build.repository_id, name: build.branch).first } + + subject { described_class.new(build) } + + shared_examples_for 'updates the branch' do + describe 'creates branch if missing' do + before { build.save! } + it { branch.should_not be_nil } + it { branch.last_build_id.should be == build.id } + end + + describe 'updates an existing branch' do + before { Branch.create!(repository_id: build.repository_id, name: 'master', last_build_id: 0) } + before { build.save! } + it { branch.should_not be_nil } + it { branch.last_build_id.should be == build.id } + end + end + + shared_examples_for 'does not update the branch' do + describe 'does not create a branch' do + before { build.save! } + it { branch.should be_nil } + end + + describe 'does update existing branchs' do + before { Branch.create!(repository_id: build.repository_id, name: 'master', last_build_id: 0) } + before { build.save! } + it { branch.should_not be_nil } + it { branch.last_build_id.should be == 0 } + end + end + + describe 'on build creation' do + describe 'for push events' do + let(:event_type) { 'push' } + include_examples 'updates the branch' + end + + describe 'for api events' do + let(:event_type) { 'api' } + include_examples 'updates the branch' + end + + describe 'for pull request events' do + let(:event_type) { 'pull_request' } + include_examples 'does not update the branch' + end + end +end diff --git a/spec_core/core/model/build_spec.rb b/spec_core/core/model/build_spec.rb new file mode 100644 index 00000000..5e43b209 --- /dev/null +++ b/spec_core/core/model/build_spec.rb @@ -0,0 +1,337 @@ +require 'spec_helper_core' + +describe Build, truncation: true do + include Support::ActiveRecord + + let(:repository) { Factory(:repository) } + + it 'caches matrix ids' do + build = Factory.create(:build, config: { rvm: ['1.9.3', '2.0.0'] }) + build.cached_matrix_ids.should == build.matrix_ids + end + + it 'returns nil if cached_matrix_ids are not set' do + build = Factory.create(:build) + build.update_column(:cached_matrix_ids, nil) + build.reload.cached_matrix_ids.should be_nil + end + + it 'is cancelable if at least one job is cancelable' do + jobs = [Factory.build(:test), Factory.build(:test)] + jobs.first.stubs(:cancelable?).returns(true) + jobs.second.stubs(:cancelable?).returns(false) + + build = Factory.build(:build, matrix: jobs) + build.should be_cancelable + end + + it 'is not cancelable if none of the jobs are cancelable' do + jobs = [Factory.build(:test), Factory.build(:test)] + jobs.first.stubs(:cancelable?).returns(false) + jobs.second.stubs(:cancelable?).returns(false) + + build = Factory.build(:build, matrix: jobs) + build.should_not be_cancelable + end + + describe '#secure_env_enabled?' do + it 'returns true if we\'re not dealing with pull request' do + build = Factory.build(:build) + build.stubs(:pull_request?).returns(false) + build.secure_env_enabled?.should be true + end + + it 'returns true if pull request is from the same repository' do + build = Factory.build(:build) + build.stubs(:pull_request?).returns(true) + build.stubs(:same_repo_pull_request?).returns(true) + build.secure_env_enabled?.should be true + end + + it 'returns false if pull request is not from the same repository' do + build = Factory.build(:build) + build.stubs(:pull_request?).returns(true) + build.stubs(:same_repo_pull_request?).returns(false) + build.secure_env_enabled?.should be false + end + end + + describe 'class methods' do + describe 'recent' do + it 'returns recent finished builds ordered by id descending' do + Factory(:build, state: 'passed') + Factory(:build, state: 'failed') + Factory(:build, state: 'created') + + Build.recent.all.map(&:state).should == ['failed', 'passed'] + end + end + + describe 'was_started' do + it 'returns builds that are either started or finished' do + Factory(:build, state: 'passed') + Factory(:build, state: 'started') + Factory(:build, state: 'created') + + Build.was_started.map(&:state).sort.should == ['passed', 'started'] + end + end + + describe 'on_branch' do + it 'returns builds that are on any of the given branches' do + Factory(:build, commit: Factory(:commit, branch: 'master')) + Factory(:build, commit: Factory(:commit, branch: 'develop')) + Factory(:build, commit: Factory(:commit, branch: 'feature')) + + Build.on_branch('master,develop').map(&:commit).map(&:branch).sort.should == ['develop', 'master'] + end + + it 'does not include pull requests' do + Factory(:build, commit: Factory(:commit, branch: 'no-pull'), request: Factory(:request, event_type: 'pull_request')) + Factory(:build, commit: Factory(:commit, branch: 'no-pull'), request: Factory(:request, event_type: 'push')) + Build.on_branch('no-pull').count.should be == 1 + end + end + + describe 'older_than' do + before do + 5.times { |i| Factory(:build, number: i) } + Build.stubs(:per_page).returns(2) + end + + context "when a Build is passed in" do + subject { Build.older_than(Build.new(number: 3)) } + + it "should limit the results" do + expect(subject.size).to eq(2) + end + + it "should return older than the passed build" do + subject.map(&:number).should == ['2', '1'] + end + end + + context "when a number is passed in" do + subject { Build.older_than(3) } + + it "should limit the results" do + expect(subject.size).to eq(2) + end + + it "should return older than the passed build" do + subject.map(&:number).should == ['2', '1'] + end + end + + context "when not passing a build" do + subject { Build.older_than() } + + it "should limit the results" do + expect(subject.size).to eq(2) + end + end + end + + describe 'paged' do + it 'limits the results to the `per_page` value' do + 3.times { Factory(:build) } + Build.stubs(:per_page).returns(1) + + expect(Build.descending.paged({}).size).to eq(1) + end + + it 'uses an offset' do + 3.times { |i| Factory(:build) } + Build.stubs(:per_page).returns(1) + + builds = Build.descending.paged({page: 2}) + expect(builds.size).to eq(1) + builds.first.number.should == '2' + end + end + + describe 'pushes' do + before do + Factory(:build) + Factory(:build, request: Factory(:request, event_type: 'pull_request')) + end + + it "returns only builds which have Requests with an event_type of push" do + Build.pushes.all.count.should == 1 + end + end + + describe 'pull_requests' do + before do + Factory(:build) + Factory(:build, request: Factory(:request, event_type: 'pull_request')) + end + + it "returns only builds which have Requests with an event_type of pull_request" do + Build.pull_requests.all.count.should == 1 + end + end + end + + describe 'creation' do + describe 'previous_state' do + it 'is set to the last finished build state on the same branch' do + Factory(:build, state: 'failed') + Factory(:build).reload.previous_state.should == 'failed' + end + + it 'is set to the last finished build state on the same branch (disregards non-finished builds)' do + Factory(:build, state: 'failed') + Factory(:build, state: 'started') + Factory(:build).reload.previous_state.should == 'failed' + end + + it 'is set to the last finished build state on the same branch (disregards other branches)' do + Factory(:build, state: 'failed') + Factory(:build, state: 'passed', commit: Factory(:commit, branch: 'something')) + Factory(:build).reload.previous_state.should == 'failed' + end + end + + it "updates the last_build on the build's branch" do + build = FactoryGirl.create(:build) + branch = Branch.where(repository_id: build.repository_id, name: build.branch).first + branch.last_build.should == build + end + end + + describe 'instance methods' do + it 'sets its number to the next build number on creation' do + 1.upto(3) do |number| + Factory(:build).reload.number.should == number.to_s + end + end + + it 'sets previous_state to nil if no last build exists on the same branch' do + build = Factory(:build, commit: Factory(:commit, branch: 'master')) + build.reload.previous_state.should == nil + end + + it 'sets previous_state to the result of the last build on the same branch if exists' do + build = Factory(:build, state: :canceled, commit: Factory(:commit, branch: 'master')) + build = Factory(:build, commit: Factory(:commit, branch: 'master')) + build.reload.previous_state.should == 'canceled' + end + + describe 'config' do + it 'defaults to a hash with language and os set' do + build = Build.new(repository: Repository.new(owner: User.new)) + build.config.should == { language: 'ruby', group: 'stable', dist: 'precise', os: 'linux' } + end + + it 'deep_symbolizes keys on write' do + build = Factory(:build, config: { 'foo' => { 'bar' => 'bar' } }) + build.read_attribute(:config)[:foo].should == { bar: 'bar' } + end + + it 'downcases the language on config' do + build = Factory.create(:build, config: { language: "PYTHON" }) + Build.last.config[:language].should == "python" + end + + it 'sets ruby as default language' do + build = Factory.create(:build, config: { 'foo' => { 'bar' => 'bar' } }) + Build.last.config[:language].should == "ruby" + end + end + + describe :pending? do + it 'returns true if the build is finished' do + build = Factory(:build, state: :finished) + build.pending?.should be false + end + + it 'returns true if the build is not finished' do + build = Factory(:build, state: :started) + build.pending?.should be true + end + end + + describe :passed? do + it 'passed? returns true if state equals :passed' do + build = Factory(:build, state: :passed) + build.passed?.should be true + end + + it 'passed? returns true if result does not equal :passed' do + build = Factory(:build, state: :failed) + build.passed?.should be false + end + end + + describe :color do + it 'returns "green" if the build has passed' do + build = Factory(:build, state: :passed) + build.color.should == 'green' + end + + it 'returns "red" if the build has failed' do + build = Factory(:build, state: :failed) + build.color.should == 'red' + end + + it 'returns "yellow" if the build is pending' do + build = Factory(:build, state: :started) + build.color.should == 'yellow' + end + end + + it 'saves event_type before create' do + build = Factory(:build, request: Factory(:request, event_type: 'pull_request')) + build.event_type.should == 'pull_request' + + build = Factory(:build, request: Factory(:request, event_type: 'push')) + build.event_type.should == 'push' + end + + it 'saves pull_request_title before create' do + payload = { 'pull_request' => { 'title' => 'A pull request' } } + build = Factory(:build, request: Factory(:request, event_type: 'pull_request', payload: payload)) + build.pull_request_title.should == 'A pull request' + end + + it 'saves branch before create' do + build = Factory(:build, commit: Factory(:commit, branch: 'development')) + build.branch.should == 'development' + end + + describe 'reset' do + let(:build) { Factory(:build, state: 'finished') } + + before :each do + build.matrix.each { |job| job.stubs(:reset) } + end + + it 'sets the state to :created' do + build.reset + build.state.should == :created + end + + it 'resets related attributes' do + build.reset + build.duration.should be_nil + build.finished_at.should be_nil + end + + it 'resets each job if :reset_matrix is given' do + build.matrix.each { |job| job.expects(:reset) } + build.reset(reset_matrix: true) + end + + it 'does not reset jobs if :reset_matrix is not given' do + build.matrix.each { |job| job.expects(:reset).never } + build.reset + end + + it 'notifies obsevers' do + Travis::Event.expects(:dispatch).with('build:created', build) + build.reset + end + end + end +end diff --git a/spec_core/core/model/commit_spec.rb b/spec_core/core/model/commit_spec.rb new file mode 100644 index 00000000..0e7db7bf --- /dev/null +++ b/spec_core/core/model/commit_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper_core' + +describe Commit do + include Support::ActiveRecord + + let(:commit) { Commit.new(:commit => '12345678') } + + describe 'pull_request_number' do + context 'when commit is from pull request' do + before { commit.ref = 'refs/pull/180/merge' } + + it 'returns pull request\'s number' do + commit.pull_request_number.should == 180 + end + end + + context 'when commit is not from pull request' do + before { commit.ref = 'refs/branch/master' } + + it 'returns nil' do + commit.pull_request_number.should be_nil + end + end + end + + describe 'pull_request?' do + it 'is false for a nil ref' do + commit.ref = nil + commit.pull_request?.should be_falsey + end + + it 'is false for a ref named ref/branch/master' do + commit.ref = 'refs/branch/master' + commit.pull_request?.should be_falsey + end + + it 'is false for a ref named ref/pull/180/head' do + commit.ref = 'refs/pull/180/head' + commit.pull_request?.should be_falsey + end + + it 'is true for a ref named ref/pull/180/merge' do + commit.ref = 'refs/pull/180/merge' + commit.pull_request?.should be_truthy + end + end + + describe '#range' do + context 'with compare_url present' do + before { commit.compare_url = 'https://github.com/rails/rails/compare/ffaab2c4ffee...60790e852a4f' } + + it 'returns range' do + commit.range.should == 'ffaab2c4ffee...60790e852a4f' + end + end + + context 'with a compare_url with ^ in it' do + before { commit.compare_url = 'https://github.com/rails/rails/compare/ffaab2c4ffee^...60790e852a4f' } + + it 'returns range' do + commit.range.should == 'ffaab2c4ffee^...60790e852a4f' + end + end + + context 'with invalid compare_url' do + before { commit.compare_url = 'https://github.com/rails/rails/compare/ffaab2c4ffee.....60790e852a4f' } + + it 'returns nil' do + commit.range.should be_nil + end + end + + context 'without compare_url' do + before { commit.compare_url = nil } + + it 'returns nil' do + commit.range.should be_nil + end + end + + context 'for a pull request' do + before do + commit.ref = 'refs/pull/1/merge' + commit.request = Request.new(:base_commit => 'abcdef', :head_commit => '123456') + end + + it 'returns range' do + commit.range.should == 'abcdef...123456' + end + end + end +end diff --git a/spec_core/core/model/encrypted_column_spec.rb b/spec_core/core/model/encrypted_column_spec.rb new file mode 100644 index 00000000..b8cf0006 --- /dev/null +++ b/spec_core/core/model/encrypted_column_spec.rb @@ -0,0 +1,126 @@ +require 'spec_helper_core' + +class Travis::Model < ActiveRecord::Base + describe EncryptedColumn do + def encode str + Base64.strict_encode64 str + end + + let(:options){ { key: 'secret-key' } } + let(:column) { EncryptedColumn.new(options) } + let(:iv) { 'a' * 16 } + let(:aes) { stub('aes', :final => '') } + + describe '#encrypt?' do + it 'does not encrypt if given data is empty' do + column.encrypt?(nil).should be false + column.encrypt?('').should be false + end + + context 'when disabled' do + let(:options) { { disable: true, key: 'secret-key' } } + it 'does not encrypt' do + column.encrypt?('--ENCR--abc').should be false + end + end + end + + describe '#decrypt?' do + it 'does not decrypt if given data is empty' do + column.decrypt?(nil).should be false + column.decrypt?('').should be false + end + end + + context 'when encryption is disabled' do + before { column.stubs :encrypt? => false } + + describe '#dump' do + it 'does not encrypt data' do + column.dump('123qwe').should == '123qwe' + end + end + end + + it 'allows to pass use_prefix as an option' do + EncryptedColumn.new(use_prefix: true).use_prefix?.should be true + end + + it 'allows to pass key as an option' do + EncryptedColumn.new(key: 'foobarbaz').key.should == 'foobarbaz' + + end + + context 'when encryption is enabled' do + before { column.stubs :encrypt? => true } + + context 'when prefix usage is disabled' do + before { column.stubs :use_prefix? => false } + + describe '#load' do + it 'decrypts data even with no prefix' do + data = encode "to-decrypt#{iv}" + + column.expects(:create_aes).with(:decrypt, 'secret-key', iv).returns(aes) + aes.expects(:update).with('to-decrypt').returns('decrypted') + + column.load(data).should == 'decrypted' + end + + it 'removes prefix if prefix is still used' do + data = encode "to-decrypt#{iv}" + data = "#{column.prefix}#{data}" + + column.expects(:create_aes).with(:decrypt, 'secret-key', iv).returns(aes) + aes.expects(:update).with('to-decrypt').returns('decrypted') + + column.load(data).should == 'decrypted' + end + end + + describe '#dump' do + it 'attaches iv to encrypted string' do + column.stubs(:iv => iv) + column.expects(:create_aes).with(:encrypt, 'secret-key', iv).returns(aes) + aes.expects(:update).with('to-encrypt').returns('encrypted') + + column.dump('to-encrypt').should == encode("encrypted#{iv}") + end + end + end + + context 'when prefix usage is enabled' do + before { column.stubs :use_prefix? => true } + + describe '#load' do + it 'does not decrypt data if prefix is not used' do + data = 'abc' + + column.load(data).should == data + end + + it 'decrypts data if prefix is used' do + data = encode "to-decrypt#{iv}" + data = "#{column.prefix}#{data}" + + column.expects(:create_aes).with(:decrypt, 'secret-key', iv).returns(aes) + aes.expects(:update).with('to-decrypt').returns('decrypted') + + column.load(data).should == 'decrypted' + end + end + + describe '#dump' do + it 'attaches iv and prefix to encrypted string' do + column.stubs(:iv => iv) + column.expects(:create_aes).with(:encrypt, 'secret-key', iv).returns(aes) + aes.expects(:update).with('to-encrypt').returns('encrypted') + + result = encode "encrypted#{iv}" + column.dump('to-encrypt').should == "#{column.prefix}#{result}" + end + end + end + end + end +end diff --git a/spec_core/core/model/job/cleanup_spec.rb b/spec_core/core/model/job/cleanup_spec.rb new file mode 100644 index 00000000..4115da19 --- /dev/null +++ b/spec_core/core/model/job/cleanup_spec.rb @@ -0,0 +1,45 @@ +# require 'spec_helper_core' +# +# describe Job::Cleanup do +# include Support::ActiveRecord +# +# let(:job) { Factory(:test) } +# +# describe 'scopes' do +# let! :jobs do +# [ Factory(:test, :state => :created, :created_at => Time.now.utc - Travis.config.jobs.retry.after - 60), +# Factory(:test, :state => :started, :created_at => Time.now.utc - Travis.config.jobs.retry.after - 120), +# Factory(:test, :state => :finished, :created_at => Time.now.utc - Travis.config.jobs.retry.after + 10) ] +# end +# +# describe :unfinished do +# it 'finds unfinished jobs' do +# # TODO fixme +# # Job.unfinished.should == jobs[0, 2] +# Job.unfinished.should include(jobs.first) +# Job.unfinished.should include(jobs.second) +# end +# end +# +# describe :stalled do +# it 'finds stalled jobs' do +# Job.stalled.order(:id).should == jobs[0, 2] +# end +# end +# end +# +# describe :force_finish do +# # TODO @flippingbits, could you look into this? +# xit 'appends a message to the log' do +# job.force_finish +# job.reload.log.content.should == "some log.\n#{Job::Requeueing::FORCE_FINISH_MESSAGE}" +# end +# +# it 'finishes the job' do +# job.force_finish +# job.finished?.should be_truthy +# end +# end +# end +# +# diff --git a/spec_core/core/model/job/queue_spec.rb b/spec_core/core/model/job/queue_spec.rb new file mode 100644 index 00000000..10970b36 --- /dev/null +++ b/spec_core/core/model/job/queue_spec.rb @@ -0,0 +1,310 @@ +require 'spec_helper_core' + +describe 'Job::Queue' do + def queue(*args) + Job::Queue.new(*args) + end + + let(:the_past) { Time.parse("1982-06-23") } + let(:recently) { 7.days.ago } + + before do + Travis.config.queues = [ + { queue: 'builds.rails', slug: 'rails/rails' }, + { queue: 'builds.mac_osx', os: 'osx' }, + { queue: 'builds.docker', sudo: false }, + { queue: 'builds.gce', services: %w(docker) }, + { queue: 'builds.gce', dist: 'trusty' }, + { queue: 'builds.cloudfoundry', owner: 'cloudfoundry' }, + { queue: 'builds.clojure', language: 'clojure' }, + { queue: 'builds.erlang', language: 'erlang' }, + ] + Job::Queue.instance_variable_set(:@queues, nil) + Job::Queue.instance_variable_set(:@default, nil) + Travis::Features.stubs(:owner_active?).returns(true) + Travis::Github::Education.stubs(:education_queue?).returns(false) + end + + after do + Travis.config.default_queue = 'builds.linux' + end + + it 'returns builds.linux as the default queue' do + Job::Queue.default.name.should == 'builds.linux' + end + + it 'returns builds.common as the default queue if configured to in Travis.config' do + Travis.config.default_queue = 'builds.common' + Job::Queue.default.name.should == 'builds.common' + end + + describe 'Queue.sudo_detected?' do + [ + [{ script: 'sudo echo' }, true], + [{ bogus: 'sudo echo' }, false], + [{ before_install: ['# no sudo', 'ping -c 1 google.com'] }, true], + [{ before_install: ['docker run busybox echo whatever'] }, true], + [{ before_script: ['echo ; echo ; echo ; sudo echo ; echo'] }, true], + [{ install: '# no sudo needed here' }, false], + [{ install: true }, false], + ].each do |config, expected| + it "returns #{expected} for #{config}" do + Job::Queue.sudo_detected?(config).should == expected + end + end + end + + describe 'Queue.for' do + it 'returns the default build queue when neither slug or language match the given configuration hash' do + job = stub('job', :config => {}, :repository => stub('repository', :owner_name => 'travis-ci', :name => 'travis-ci', :owner => stub, :created_at => the_past)) + Job::Queue.for(job).name.should == 'builds.linux' + end + + it 'returns the queue when slug matches the given configuration hash' do + job = stub('job', :config => {}, :repository => stub('repository', :owner_name => 'rails', :name => 'rails', :owner => stub, :created_at => the_past)) + Job::Queue.for(job).name.should == 'builds.rails' + end + + it 'returns the queue when language matches the given configuration hash' do + job = stub('job', :config => { :language => 'clojure' }, :repository => stub('repository', :owner_name => 'travis-ci', :name => 'travis-ci', :owner => stub, :created_at => the_past)) + Job::Queue.for(job).name.should == 'builds.clojure' + end + + it 'returns the queue when the owner matches the given configuration hash' do + job = stub('job', :config => {}, :repository => stub('repository', :owner_name => 'cloudfoundry', :name => 'bosh', :owner => stub, :created_at => the_past)) + Job::Queue.for(job).name.should == 'builds.cloudfoundry' + end + + it 'returns the queue when sudo requirements matches the given configuration hash' do + job = stub('job', :config => { sudo: false }, :repository => stub('repository', :owner_name => 'markronson', :name => 'recordcollection', :owner => stub, :created_at => the_past)) + Job::Queue.for(job).name.should == 'builds.docker' + end + + it 'returns the docker queue by default for educational repositories' do + Travis::Github::Education.stubs(:education_queue?).returns(true) + owner = stub('owner', :education => true) + job = stub('job', :config => { }, :repository => stub('repository', :owner_name => 'markronson', :name => 'recordcollection', :owner => owner, :created_at => the_past)) + Job::Queue.for(job).name.should == 'builds.docker' + end + + it 'returns the queue matching configuration for educational repository' do + Travis::Github::Education.stubs(:education_queue?).returns(true) + owner = stub('owner', :education => true) + job = stub('job', :config => { :os => 'osx' }, :repository => stub('repository', :owner_name => 'markronson', :name => 'recordcollection', :owner => owner, :created_at => the_past)) + Job::Queue.for(job).name.should == 'builds.mac_osx' + end + + it 'handles language being passed as an array gracefully' do + job = stub('job', :config => { :language => ['clojure'] }, :repository => stub('repository', :owner_name => 'travis-ci', :name => 'travis-ci', :owner => stub, :created_at => the_past)) + Job::Queue.for(job).name.should == 'builds.clojure' + end + + context 'when "os" value matches the given configuration hash' do + it 'returns the matching queue' do + job = stub('job', :config => { :os => 'osx'}, :repository => stub('travis-core', :owner_name => 'travis-ci', :name => 'bosh', :owner => stub, :created_at => the_past)) + Job::Queue.for(job).name.should == 'builds.mac_osx' + end + + it 'returns the matching queue when language is also given' do + job = stub('job', :config => {:language => 'clojure', :os => 'osx'}, :repository => stub('travis-core', :owner_name => 'travis-ci', :name => 'bosh', :owner => stub, :created_at => the_past)) + Job::Queue.for(job).name.should == 'builds.mac_osx' + end + end + + context 'when "services" value matches the given configuration hash' do + it 'returns the matching queue' do + job = stub('job', config: { services: %w(redis docker postgresql) }, repository: stub('travis-core', owner_name: 'travis-ci', name: 'bosh', owner: stub, created_at: the_past)) + Job::Queue.for(job).name.should == 'builds.gce' + end + + it 'returns the matching queue when language is also given' do + job = stub('job', config: { language: 'clojure', services: %w(redis docker postgresql) }, repository: stub('travis-core', owner_name: 'travis-ci', name: 'bosh', owner: stub, created_at: the_past)) + Job::Queue.for(job).name.should == 'builds.gce' + end + end + + context 'when "docker_default_queue" feature is active' do + before do + Travis::Features.stubs(:feature_active?).with(:docker_default_queue).returns(true) + Travis::Features.stubs(:feature_active?).with(:education).returns(true) + end + + it 'returns "builds.docker" when sudo: nil and the repo created_at is nil' do + job = stub('job', :config => { }, :repository => stub('repository', :owner_name => 'travis-ci', :name => 'travis-core', :owner => stub, :created_at => nil)) + Job::Queue.for(job).name.should == 'builds.docker' + end + + it 'returns "builds.docker" when sudo: nil and the repo created_at is after cutoff' do + Travis.config.docker_default_queue_cutoff = recently.to_s + job = stub('job', :config => { }, :repository => stub('repository', :owner_name => 'travis-ci', :name => 'travis-core', :owner => stub, :created_at => Time.now)) + Job::Queue.for(job).name.should == 'builds.docker' + end + + it 'returns "builds.linux" when sudo: nil and the repo created_at is before cutoff' do + Travis.config.docker_default_queue_cutoff = recently.to_s + job = stub('job', :config => { }, :repository => stub('repository', :owner_name => 'travis-ci', :name => 'travis-core', :owner => stub, :created_at => recently - 7.days)) + Job::Queue.for(job).name.should == 'builds.linux' + end + + it 'returns "builds.linux" when sudo: nil and the repo created_at is after cutoff and sudo is detected' do + Travis.config.docker_default_queue_cutoff = recently.to_s + job = stub('job', :config => { script: 'sudo echo whatever' }, :repository => stub('repository', :owner_name => 'travis-ci', :name => 'travis-core', :owner => stub, :created_at => recently - 7.days)) + Job::Queue.for(job).name.should == 'builds.linux' + end + + it 'returns "builds.docker" when sudo: false and the repo created_at is nil' do + job = stub('job', :config => { sudo: false }, :repository => stub('repository', :owner_name => 'travis-ci', :name => 'travis-core', :owner => stub, :created_at => nil)) + Job::Queue.for(job).name.should == 'builds.docker' + end + + it 'returns "builds.docker" when sudo: false and the repo created_at is after cutoff' do + Travis.config.docker_default_queue_cutoff = recently.to_s + job = stub('job', :config => { sudo: false }, :repository => stub('repository', :owner_name => 'travis-ci', :name => 'travis-core', :owner => stub, :created_at => Time.now)) + Job::Queue.for(job).name.should == 'builds.docker' + end + + it 'returns "builds.docker" when sudo: false and the repo created_at is before cutoff' do + Travis.config.docker_default_queue_cutoff = recently.to_s + job = stub('job', :config => { sudo: false }, :repository => stub('repository', :owner_name => 'travis-ci', :name => 'travis-core', :owner => stub, :created_at => recently - 7.days)) + Job::Queue.for(job).name.should == 'builds.docker' + end + + [true, 'required'].each do |sudo| + it %{returns "builds.linux" when sudo: #{sudo} and the repo created_at is nil} do + Travis.config.docker_default_queue_cutoff = recently.to_s + job = stub('job', :config => { sudo: sudo }, :repository => stub('repository', :owner_name => 'travis-ci', :name => 'travis-core', :owner => stub, :created_at => nil)) + Job::Queue.for(job).name.should == 'builds.linux' + end + + it %{returns "builds.linux" when sudo: #{sudo} and the repo created_at is after cutoff} do + Travis.config.docker_default_queue_cutoff = recently.to_s + job = stub('job', :config => { sudo: sudo }, :repository => stub('repository', :owner_name => 'travis-ci', :name => 'travis-core', :owner => stub, :created_at => nil)) + Job::Queue.for(job).name.should == 'builds.linux' + end + + it %{returns "builds.linux" when sudo: #{sudo} and the repo created_at is before cutoff} do + Travis.config.docker_default_queue_cutoff = recently.to_s + job = stub('job', :config => { sudo: sudo }, :repository => stub('repository', :owner_name => 'travis-ci', :name => 'travis-core', :owner => stub, :created_at => nil)) + Job::Queue.for(job).name.should == 'builds.linux' + end + end + end + end + + context 'when "sudo" value matches the given configuration hash' do + it 'returns the matching queue' do + job = stub('job', config: { sudo: false }, repository: stub('travis-core', owner_name: 'travis-ci', name: 'travis-core', owner: stub, :created_at => the_past)) + Job::Queue.for(job).name.should == 'builds.docker' + end + + it 'returns the matching queue when language is also given' do + job = stub('job', config: { language: 'clojure', sudo: false }, repository: stub('travis-core', owner_name: 'travis-ci', name: 'travis-core', owner: stub, :created_at => the_past)) + Job::Queue.for(job).name.should == 'builds.docker' + end + end + + describe 'Queue.queues' do + it 'returns an array of Queues for the config hash' do + rails, _, docker, _, _, cloudfoundry, clojure, _ = Job::Queue.send(:queues) + + rails.name.should == 'builds.rails' + rails.attrs[:slug].should == 'rails/rails' + + docker.name.should == 'builds.docker' + docker.attrs[:sudo].should == false + + cloudfoundry.name.should == 'builds.cloudfoundry' + cloudfoundry.attrs[:owner].should == 'cloudfoundry' + + clojure.name.should == 'builds.clojure' + clojure.attrs[:language].should == 'clojure' + end + end + + describe 'matches?' do + it "returns false when neither of slug or language match" do + queue = queue('builds.linux', {}) + queue.matches?(stub('job', repository: stub('repository', owner_name: 'foo', name: 'bar', owner: nil), config: { language: 'COBOL' })).should be false + end + + it "returns true when the given owner matches" do + queue = queue('builds.cloudfoundry', { owner: 'cloudfoundry' }) + queue.matches?(stub('job', repository: stub('repository', owner_name: 'cloudfoundry', name: 'bosh', owner: nil), config: {})).should be true + end + + it "returns true when the given slug matches" do + queue = queue('builds.rails', { slug: 'rails/rails' }) + queue.matches?(stub('job', repository: stub('repository', owner_name: 'rails', name: 'rails', owner: nil), config: {})).should be true + end + + it "returns true when the given language matches" do + queue = queue('builds.linux', { language: 'clojure' }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: { language: 'clojure' })).should be true + end + + it 'returns true when os is missing' do + queue = queue('builds.linux', { language: 'clojure' }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: { language: 'clojure' })).should be true + end + + it 'returns true when sudo is false' do + queue = queue('builds.docker', { sudo: false }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: { sudo: false })).should be true + end + + it 'returns false when sudo is true' do + queue = queue('builds.docker', { sudo: false }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: { sudo: true })).should be false + end + + it 'returns false when sudo is not specified' do + queue = queue('builds.docker', { sudo: false }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: {})).should be false + end + + it 'returns true when dist matches' do + queue = queue('builds.gce', { dist: 'trusty' }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: { dist: 'trusty' })).should be true + end + + it 'returns false when dist does not match' do + queue = queue('builds.docker', { dist: 'precise' }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: { dist: 'trusty' })).should be false + end + + it 'returns true when osx_image matches' do + queue = queue('builds.mac_beta', { osx_image: 'beta' }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: { osx_image: 'beta' })).should be true + end + + it 'returns false when osx_image does not match' do + queue = queue('builds.mac_stable', { osx_image: 'stable' }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: { osx_image: 'beta' })).should be false + end + + it 'returns true when services match' do + queue = queue('builds.gce', { services: %w(docker) }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: { services: %w(redis docker postgresql) })).should be true + end + + it 'returns false when services do not match' do + queue = queue('builds.gce', { services: %w(docker) }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: { services: %w(redis postgresql) })).should be false + end + + it 'returns false if no valid matchers are specified' do + queue = queue('builds.invalid', { foobar_donotmatch: true }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: {})).should be false + end + + it 'returns true for percentage: 100' do + queue = queue('builds.always', { percentage: 100 }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: {})).should be true + end + + it 'returns false for percentage: 0' do + queue = queue('builds.always', { percentage: 0 }) + queue.matches?(stub('job', repository: stub('repository', owner_name: nil, name: nil, owner: nil), config: {})).should be false + end + end +end diff --git a/spec_core/core/model/job/test_spec.rb b/spec_core/core/model/job/test_spec.rb new file mode 100644 index 00000000..c2ebdadf --- /dev/null +++ b/spec_core/core/model/job/test_spec.rb @@ -0,0 +1,190 @@ +require 'spec_helper_core' + +describe Job::Test do + include Support::ActiveRecord + + let(:job) { Factory(:test) } + + before :each do + Travis::Event.stubs(:dispatch) + end + + it 'is cancelable if the job has not finished yet' do + job = Factory(:test, state: :created) + job.should be_cancelable + + job = Factory(:test, state: :started) + job.should be_cancelable + end + + it 'is not cancelable if the job has already been finished' do + job = Factory(:test, state: :passed) + job.should_not be_cancelable + end + + describe 'cancelling' do + it 'should not propagate cancel state to source' do + build = Factory(:build, state: :started) + build.matrix.destroy_all + job = Factory(:test, state: :created, source: build) + Factory(:test, state: :started, source: build) + build.reload + + expect { + job.cancel! + }.to_not change { job.source.reload.state } + end + + it 'should put a build into canceled state if all the jobs in matrix are in finished state' do + build = Factory(:build, state: :started) + build.matrix.destroy_all + job = Factory(:test, state: :created, source: build) + Job::Test::FINISHED_STATES.each do |state| + Factory(:test, source: build, state: state) + end + build.reload + + expect { + expect { + expect { + job.cancel! + }.to change { build.state } + }.to change { build.canceled_at } + }.to change { build.repository.reload.last_build_state } + + build.reload.state.should == 'canceled' + build.repository.last_build_state.should == 'canceled' + end + + it 'should set canceled_at and finished_at on job' do + job = Factory(:test, state: :created) + + expect { + expect { + job.cancel! + }.to change { job.canceled_at } + }.to change { job.finished_at } + end + end + + describe 'events' do + describe 'receive' do + let(:data) { WORKER_PAYLOADS['job:test:receive'] } + + it 'sets the state to :received' do + job.receive(data) + job.state.should == :received + end + + it 'sets the worker from the payload' do + job.receive(data) + job.worker.should == 'ruby3.worker.travis-ci.org:travis-ruby-4' + end + + it 'resets the log content' do + job.log.expects(:update_attributes!).with(content: '', removed_at: nil, removed_by: nil) + job.receive(data) + end + + it 'notifies observers' do + Travis::Event.expects(:dispatch).with('job:test:received', job, data) + job.receive(data) + end + + it 'propagates the event to the source' do + job.source.expects(:receive) + job.receive(data) + end + + it 'sets log\'s removed_at and removed_by to nil' do + job.log.removed_at = Time.now + job.log.removed_by = job.repository.owner + job.receive(data) + job.log.removed_at.should be_nil + job.log.removed_by.should be_nil + end + end + + describe 'start' do + let(:data) { WORKER_PAYLOADS['job:test:start'] } + + it 'sets the state to :started' do + job.start(data) + job.state.should == :started + end + + it 'notifies observers' do + Travis::Event.expects(:dispatch).with('job:test:started', job, data) + job.start(data) + end + + it 'propagates the event to the source' do + job.source.expects(:start) + job.start(data) + end + end + + describe 'finish' do + let(:data) { WORKER_PAYLOADS['job:test:finish'] } + + it 'sets the state to the given result state' do + job.finish(data) + job.state.should == 'passed' + end + + it 'notifies observers' do + Travis::Event.expects(:dispatch).with('job:test:finished', job, data) + job.finish(data) + end + + it 'propagates the event to the source' do + job.source.expects(:finish).with(data) + job.finish(data) + end + end + + describe 'reset' do + let(:job) { Factory(:test, state: 'finished', queued_at: Time.now, finished_at: Time.now) } + + it 'sets the state to :created' do + job.reset! + job.reload.state.should == 'created' + end + + it 'resets job attributes' do + job.reset! + job.reload.queued_at.should be_nil + job.reload.finished_at.should be_nil + end + + it 'resets log attributes' do + job.log.update_attributes!(content: 'foo', aggregated_at: Time.now) + job.reset! + job.reload.log.aggregated_at.should be_nil + job.reload.log.content.should be_blank + end + + it 'recreates log if it\'s removed' do + job.log.destroy + job.reload + job.reset! + job.reload.log.should_not be_nil + end + + xit 'clears log parts' do + end + + it 'destroys annotations' do + job.annotations << Factory(:annotation) + job.reload + job.reset! + job.reload.annotations.should be_empty + end + + it 'triggers a :created event' do + job.expects(:notify).with(:reset) + job.reset + end + end + end +end diff --git a/spec_core/core/model/job_spec.rb b/spec_core/core/model/job_spec.rb new file mode 100644 index 00000000..2a580a22 --- /dev/null +++ b/spec_core/core/model/job_spec.rb @@ -0,0 +1,471 @@ +require 'spec_helper_core' + +describe Job do + include Support::ActiveRecord + + describe '.result' do + it 'returns 1 for failed builds' do + job = Factory.build(:test, state: :failed) + job.result.should == 1 + end + + it 'returns 0 for passed builds' do + job = Factory.build(:test, state: :passed) + job.result.should == 0 + end + end + + describe ".queued" do + let(:jobs) { [Factory.create(:test), Factory.create(:test), Factory.create(:test)] } + + it "returns jobs that are created but not started or finished" do + jobs.first.start! + jobs.third.start! + jobs.third.finish!(state: 'passed') + + Job.queued.should include(jobs.second) + Job.queued.should_not include(jobs.first) + Job.queued.should_not include(jobs.third) + end + end + + describe 'before_create' do + let(:job) { Job::Test.create!(owner: Factory(:user), repository: Factory(:repository), commit: Factory(:commit), source: Factory(:build)) } + + before :each do + Job::Test.any_instance.stubs(:enqueueable?).returns(false) # prevent jobs to enqueue themselves on create + end + + it 'instantiates the log' do + job.reload.log.should be_instance_of(Log) + end + + it 'sets the state attribute' do + job.reload.should be_created + end + + it 'sets the queue attribute' do + job.reload.queue.should == 'builds.linux' + end + end + + describe 'duration' do + it 'returns nil if both started_at is not populated' do + job = Job.new(finished_at: Time.now) + job.duration.should be_nil + end + + it 'returns nil if both finished_at is not populated' do + job = Job.new(started_at: Time.now) + job.duration.should be_nil + end + + it 'returns the duration if both started_at and finished_at are populated' do + job = Job.new(started_at: 20.seconds.ago, finished_at: 10.seconds.ago) + job.duration.should == 10 + end + end + + describe 'obfuscated config' do + it 'handles nil env' do + job = Job.new(repository: Factory(:repository)) + job.config = { rvm: '1.8.7', env: nil } + + job.obfuscated_config.should == { + rvm: '1.8.7', + env: nil + } + end + + it 'leaves regular vars untouched' do + job = Job.new(repository: Factory(:repository)) + job.expects(:secure_env_enabled?).at_least_once.returns(true) + job.config = { rvm: '1.8.7', env: 'FOO=foo' } + + job.obfuscated_config.should == { + rvm: '1.8.7', + env: 'FOO=foo' + } + end + + it 'obfuscates env vars' do + job = Job.new(repository: Factory(:repository)) + job.expects(:secure_env_enabled?).at_least_once.returns(true) + config = { rvm: '1.8.7', + env: [job.repository.key.secure.encrypt('BAR=barbaz'), 'FOO=foo'] + } + job.config = config + + job.obfuscated_config.should == { + rvm: '1.8.7', + env: 'BAR=[secure] FOO=foo' + } + end + + it 'normalizes env vars which are hashes to strings' do + job = Job.new(repository: Factory(:repository)) + job.expects(:secure_env_enabled?).at_least_once.returns(true) + + config = { rvm: '1.8.7', + env: [{FOO: 'bar', BAR: 'baz'}, + job.repository.key.secure.encrypt('BAR=barbaz')] + } + job.config = config + + job.obfuscated_config.should == { + rvm: '1.8.7', + env: 'FOO=bar BAR=baz BAR=[secure]' + } + end + + it 'removes addons config if it is not a hash' do + job = Job.new(repository: Factory(:repository)) + config = { rvm: '1.8.7', + addons: "foo", + } + job.config = config + + job.obfuscated_config.should == { + rvm: '1.8.7' + } + end + + it 'removes addons items which are not whitelisted' do + job = Job.new(repository: Factory(:repository)) + config = { rvm: '1.8.7', + addons: { sauce_connect: true, firefox: '22.0' }, + } + job.config = config + + job.obfuscated_config.should == { + rvm: '1.8.7', + addons: { + firefox: '22.0' + } + } + end + + it 'removes source key' do + job = Job.new(repository: Factory(:repository)) + config = { rvm: '1.8.7', + source_key: '1234' + } + job.config = config + + job.obfuscated_config.should == { + rvm: '1.8.7', + } + end + + context 'when job has secure env disabled' do + let :job do + job = Job.new(repository: Factory(:repository)) + job.expects(:secure_env_enabled?).returns(false).at_least_once + job + end + + it 'removes secure env vars' do + config = { rvm: '1.8.7', + env: [job.repository.key.secure.encrypt('BAR=barbaz'), 'FOO=foo'] + } + job.config = config + + job.obfuscated_config.should == { + rvm: '1.8.7', + env: 'FOO=foo' + } + end + + it 'works even if it removes all env vars' do + config = { rvm: '1.8.7', + env: [job.repository.key.secure.encrypt('BAR=barbaz')] + } + job.config = config + + job.obfuscated_config.should == { + rvm: '1.8.7', + env: nil + } + end + + it 'normalizes env vars which are hashes to strings' do + config = { rvm: '1.8.7', + env: [{FOO: 'bar', BAR: 'baz'}, + job.repository.key.secure.encrypt('BAR=barbaz')] + } + job.config = config + + job.obfuscated_config.should == { + rvm: '1.8.7', + env: 'FOO=bar BAR=baz' + } + end + end + end + + describe '#pull_request?' do + it 'is delegated to commit' do + commit = Commit.new + commit.expects(:pull_request?).returns(true) + + job = Job.new + job.commit = commit + job.pull_request?.should be true + end + end + + describe 'decrypted config' do + it 'handles nil env' do + job = Job.new(repository: Factory(:repository)) + job.config = { rvm: '1.8.7', env: nil, global_env: nil } + + job.decrypted_config.should == { + rvm: '1.8.7', + env: nil, + global_env: nil + } + end + + it 'normalizes env vars which are hashes to strings' do + job = Job.new(repository: Factory(:repository)) + job.expects(:secure_env_enabled?).at_least_once.returns(true) + + config = { rvm: '1.8.7', + env: [{FOO: 'bar', BAR: 'baz'}, + job.repository.key.secure.encrypt('BAR=barbaz')], + global_env: [{FOO: 'foo', BAR: 'bar'}, + job.repository.key.secure.encrypt('BAZ=baz')] + } + job.config = config + + job.decrypted_config.should == { + rvm: '1.8.7', + env: ["FOO=bar BAR=baz", "SECURE BAR=barbaz"], + global_env: ["FOO=foo BAR=bar", "SECURE BAZ=baz"] + } + end + + it 'does not change original config' do + job = Job.new(repository: Factory(:repository)) + job.expects(:secure_env_enabled?).at_least_once.returns(true) + + config = { + env: [{secure: 'invalid'}], + global_env: [{secure: 'invalid'}] + } + job.config = config + + job.decrypted_config + job.config.should == { + env: [{ secure: 'invalid' }], + global_env: [{ secure: 'invalid' }] + } + end + + it 'leaves regular vars untouched' do + job = Job.new(repository: Factory(:repository)) + job.expects(:secure_env_enabled?).returns(true).at_least_once + job.config = { rvm: '1.8.7', env: 'FOO=foo', global_env: 'BAR=bar' } + + job.decrypted_config.should == { + rvm: '1.8.7', + env: ['FOO=foo'], + global_env: ['BAR=bar'] + } + end + + context 'when secure env is not enabled' do + let :job do + job = Job.new(repository: Factory(:repository)) + job.expects(:secure_env_enabled?).returns(false).at_least_once + job + end + + it 'removes secure env vars' do + config = { rvm: '1.8.7', + env: [job.repository.key.secure.encrypt('BAR=barbaz'), 'FOO=foo'], + global_env: [job.repository.key.secure.encrypt('BAR=barbaz'), 'BAR=bar'] + } + job.config = config + + job.decrypted_config.should == { + rvm: '1.8.7', + env: ['FOO=foo'], + global_env: ['BAR=bar'] + } + end + + it 'removes only secured env vars' do + config = { rvm: '1.8.7', + env: [job.repository.key.secure.encrypt('BAR=barbaz'), 'FOO=foo'] + } + job.config = config + + job.decrypted_config.should == { + rvm: '1.8.7', + env: ['FOO=foo'] + } + end + end + + context 'when addons are disabled' do + let :job do + job = Job.new(repository: Factory(:repository)) + job.expects(:addons_enabled?).returns(false).at_least_once + job + end + + it 'removes addons if it is not a hash' do + config = { rvm: '1.8.7', + addons: [] + } + job.config = config + + job.decrypted_config.should == { + rvm: '1.8.7' + } + end + + it 'removes addons items which are not whitelisted' do + config = { rvm: '1.8.7', + addons: { + sauce_connect: { + username: 'johndoe', + access_key: job.repository.key.secure.encrypt('foobar') + }, + firefox: '22.0', + mariadb: '10.1', + postgresql: '9.3', + hosts: %w(travis.dev), + apt_packages: %w(curl git), + apt_sources: %w(deadsnakes) + } + } + job.config = config + + job.decrypted_config.should == { + rvm: '1.8.7', + addons: { + firefox: '22.0', + mariadb: '10.1', + postgresql: '9.3', + hosts: %w(travis.dev), + apt_packages: %w(curl git), + apt_sources: %w(deadsnakes) + } + } + end + end + + context 'when job has secure env enabled' do + let :job do + job = Job.new(repository: Factory(:repository)) + job.expects(:secure_env_enabled?).returns(true).at_least_once + job + end + + it 'decrypts env vars' do + config = { rvm: '1.8.7', + env: job.repository.key.secure.encrypt('BAR=barbaz'), + global_env: job.repository.key.secure.encrypt('BAR=bazbar') + } + job.config = config + + job.decrypted_config.should == { + rvm: '1.8.7', + env: ['SECURE BAR=barbaz'], + global_env: ['SECURE BAR=bazbar'] + } + end + + it 'decrypts only secure env vars' do + config = { rvm: '1.8.7', + env: [job.repository.key.secure.encrypt('BAR=bar'), 'FOO=foo'], + global_env: [job.repository.key.secure.encrypt('BAZ=baz'), 'QUX=qux'] + } + job.config = config + + job.decrypted_config.should == { + rvm: '1.8.7', + env: ['SECURE BAR=bar', 'FOO=foo'], + global_env: ['SECURE BAZ=baz', 'QUX=qux'] + } + end + end + + context 'when job has addons enabled' do + let :job do + job = Job.new(repository: Factory(:repository)) + job.expects(:addons_enabled?).returns(true).at_least_once + job + end + + it 'decrypts addons config' do + config = { rvm: '1.8.7', + addons: { + sauce_connect: { + username: 'johndoe', + access_key: job.repository.key.secure.encrypt('foobar') + } + } + } + job.config = config + + job.decrypted_config.should == { + rvm: '1.8.7', + addons: { + sauce_connect: { + username: 'johndoe', + access_key: 'foobar' + } + } + } + end + + it 'decrypts deploy addon config' do + config = { rvm: '1.8.7', + deploy: { foo: job.repository.key.secure.encrypt('foobar') } + } + job.config = config + + job.decrypted_config.should == { + rvm: '1.8.7', + addons: { + deploy: { foo: 'foobar' } + } + } + end + + it 'removes addons config if it is an array and deploy is present' do + config = { rvm: '1.8.7', + addons: ["foo"], + deploy: { foo: 'bar'} + } + job.config = config + + job.decrypted_config.should == { + rvm: '1.8.7', + addons: { + deploy: { foo: 'bar' } + } + } + end + + end + end + + describe 'log_content=' do + let(:job) { Job::Test.create!(owner: Factory(:user), repository: Factory(:repository), commit: Factory(:commit), source: Factory(:build), log: Factory(:log)) } + + it 'sets the log content' do + job.log_content = 'Hello, world' + job.log.content.should == 'Hello, world' + end + + it 'blanks out any old log content' do + job.log_content = 'foo' + job.log_content = 'bar' + job.log.content.should == 'bar' + end + end +end diff --git a/spec_core/core/model/organization_spec.rb b/spec_core/core/model/organization_spec.rb new file mode 100644 index 00000000..27a0f2e4 --- /dev/null +++ b/spec_core/core/model/organization_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper_core' + +describe User do + include Support::ActiveRecord + + let(:org) { Factory.create(:org, :login => 'travis-organization') } + + describe 'educational_org' do + after do + Travis::Features.deactivate_owner(:educational_org, org) + end + + it 'returns true if organization is flagged as educational_org' do + Travis::Features.activate_owner(:educational_org, org) + org.education?.should be true + end + + it 'returns false if the organization has not been flagged as educational_org' do + org.education?.should be false + end + end +end diff --git a/spec_core/core/model/permission_spec.rb b/spec_core/core/model/permission_spec.rb new file mode 100644 index 00000000..2b475840 --- /dev/null +++ b/spec_core/core/model/permission_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper_core' + +describe Permission do + include Support::ActiveRecord + + describe 'by_roles' do + before :each do + Permission::ROLES.each { |role| Permission.create!(role => true) } + end + + it 'returns matching permissions if two roles given as symbols' do + Permission.by_roles([:admin, :pull]).size.should == 2 + end + + it 'returns a single permission if one role given' do + Permission.by_roles('admin').size.should == 1 + end + + it 'returns an empty scope if no roles given' do + Permission.by_roles('').size.should == 0 + end + end +end diff --git a/spec_core/core/model/repository/settings/ssh_key_spec.rb b/spec_core/core/model/repository/settings/ssh_key_spec.rb new file mode 100644 index 00000000..d2739643 --- /dev/null +++ b/spec_core/core/model/repository/settings/ssh_key_spec.rb @@ -0,0 +1,104 @@ +# encoding: utf-8 +require 'spec_helper_core' + +describe Repository::Settings::SshKey do + let(:private_key) { +"-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuHVhiw/2qaGmIiRbKjO5bZmQI0UEQ1vQVVxujL5SFAUsEB3w +YvcsdK+EDrmFfhrvVde1PnWzuwdq4seftZvTni+5KIjpNJ6/YKfYVUEOQ5ORvuqb +zrYPWzJShXnqvrFpbF82unHs4ceb+XzV/2Tciy/p5Yv535yweLRJKtcwK+ANLz8O +wF+IZf20StugF7tZaXMCXD1ieGg6fv5eV7lohfxYCeRTVJsMUxnwcrLxeqi8IgAa +q44IsIayTn5jcBZMwir8W+PrlXq44WHyLWnErXCH0Pds1UrbL6HFz5+uU0xoMO0N +vG1T0er1KIOooQ2dnbcH8UoDnrYSsn5mWFq1fwIDAQABAoIBABB5Qz3lLhVWP30b +HB03w167cTkFJ+1QHNoSyDi/oprxH09NLTPZeVnudu/Nt9NcWnWjLyel4WhZsD0S +sPvKL+sXvgSVvaYaa2MZemOazMhSPJj9YO7kKZjudJpBGirvs0efdUbPd+VuK0rr +0Dzf6CZyIASFLMrAtq4BA+vUjhPM5tmQqwhuZVkrr+GstCvJa2W2K4hbpZ+1XyhX +++XX4QnvQ1HXjVxo4LSXV05oJ9OBbiCh+OEkME3X3vPuy62E0WngFyH67NlryR1O +AyqrDPALf0Fl/1IXwGOZNsjHUQr8j+lbAE3uxwS7KNwlvEmJ8Hc2LdRwmlvTim9f +xWRGaWkCgYEA6GJ/FBgSykBs0FvYqvAs8O2Y1Rh1YJrInwSI/nG4yXEUDvmM+rFB +7Cb2AhTamw4VlHbu2dvUuVh4I2u1GVESrIy2+SV2xfzieoEG6+HWvRAm5owqKq2u +HSM3GFN2VGZzYl1J1260J9wlWHoPZV8vpgzD/VulN7x/TAXXwv/u0FMCgYEAyzQU +rIlbqsomx3G4Nzi4Nr9nLaKkRiAmSITzEVojItRWJRAKZtrMPV57JlGfNIqzAzAS +MWkcZr4XLvn6gxks37rrl7NtgRTTyq9MLTx5opMqQalYUIpp8PHMJ0vevdqVGgmS +FOP17SEyO2Tnc0UYAezyS8VuQ30u2ReJx/PJ0KUCgYBI3vIok+/4ekFlCRglalE9 +b9Q4JoZQN9lnfB2VZIXkrU/z7i9WQZWBfyovtuhiLQV5W95EdNn9ERADU3gjqzem +4i1SbXwUU9uVPLa160jSWqlILHXgkjwCKRPSzgFSMBpIoyZPpwhZY4BWgVgomrOv +Z1tiLIXft31XkpF5NZZmvwKBgGsBJu3geywJvbgDE13I+YCi9CNc5SKkZWSE1jbJ +/3yk0iQ8OS4Gg8zBRxpbmvmhHDlOhBYO4szbxvuO2bNVe4LpPIyrCLwTip/OBdBA +a1EILBVdpsrqyHT/72C2HDpfs2p9pbZogKV5eKk8LoFN3iGNc94gvjq93gCl24E2 +yIydAoGBALrYhMbK+ljTaqn4IsC0CwG5S6dLA/uXLn4QosDGtCqi1iXWKb8ixRho +x9giBf4WDeH3Gb2TBF1QnB8sbhHJAzTW/CO3vOjRiFSSF7EjxjCFier/LfuDU1Kr +tFns8eTxHpZOYOftxpX91vS3tzKCKgkdPhnYBDrvFFWnGgRLXFpb +-----END RSA PRIVATE KEY----- +" + } + + it 'validates correctness of private key' do + ssh_key = described_class.new(value: private_key) + ssh_key.should be_valid + + ssh_key.value = 'foo' + ssh_key.should_not be_valid + + ssh_key.errors[:value].should == [:not_a_private_key] + end + + it 'allows only private key' do + public_key = OpenSSL::PKey::RSA.new(private_key).public_key.to_s + ssh_key = described_class.new(value: public_key) + + ssh_key.should_not be_valid + ssh_key.errors[:value].should == [:not_a_private_key] + end + + it 'does not check key if a value is nil' do + ssh_key = described_class.new({}) + + ssh_key.should_not be_valid + ssh_key.errors[:value].should == [:blank] + end + + describe 'with a passphrase' do + let(:private_key) { + "-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,A2EDFC4C1196A9F3A58F327650CA6A1A + +hfJtbnTxMhnqNLbRi1KGKE1rX3ypef5tIQPC+2OyUH4MQDTpaz5INoMqk1AjOz+O +LfmPZcy+5g3w9HJCHkGFU2kNmseHWCyoPQ3t9BKzaVWun4IxoMn7K2hZaebsyPQx +sq7vyDKAflgUKlkZgHWIimJ1lJH0CJB/3mplc16NeNqv1AaICFJagYHwPGHfVxa/ +CQLPd2nw4LGmxEvmfuVq4qiSsYTqUkBA4wqgEX4bWGbkDZF2mKJvL/5AM5Cei+cy +ZhB4zm0mORoNQnGHbolYstHfm6h7RjlYDV60WC7iBnRRNuWmktzA+oOjH8RqQ29Z +LDcjyg4Rl0BsRwjkppOSScO6QAt4h740ZYHv7I/m3UAIl33xHjlz7PJb8aOzYqgC +QDFNJOr+AJx3tamY0Hg7v2l77oWQX8hHeJEbIySbftzIX+UrpSWFlcTNm4xpffI0 +yO0Id2wY1mMOSs3yzNQ0AlGJR9Ns3P+2RjbyAJnuKI2ZctcBdlZSEiz/aavNW3ql +mv5FAzP4tSkTaWCnJaf8RAL0CSr5ppycWYGnZYbem6Bh9Cwe4f7PmQL5RJ/Y2rMc +V5ir+CFiVPFZP7by0OVz1Hg8XjynyCXej6J21el2hUyTI7oLxh/CxHW2lmqgUwYA +SNGGqMrYVQKxs+yp6i62OhKTl93jmW+8mE9VX6jIKBQJ7GBf44YLALwgxLyO6lCP +yY3dXI2QTb1StBHZhyQHazoghs3/6vEAEC8kj2U2NLBxlk8+caSEIrNoWRNCOAo+ +1p75ZHrPDuTirDsyascOJ0Yff+O+uzCiqf9aPxxVJllhC2l6LGCwU1dsZ+O2RuaI +anH8OvFpSUhQY97vEnRDuPei/jz/C2/oZJzIdXCUmPvVn7Ut23m9A/1x6Mq3FA1X +iN09gB3XzsJZpxQ3TEMg+pp6bV64O5yZghAAWmKzJOmZ2j2BxCbuX3H399EAW3hc +sB/TxCh6kjiMOltbHtKsclsNZ9YQmk5+x4LwX4BSCV6YnytS+I49eMx1ikLX5nJR +tVAVsF5oE31pgg8lUgIWRJdK7EjjX/cDfkJYf9NWSwFCYxUKB+2adX/ix0eI1NGH +oJ+AD9tUrMAnNDTgFM4n5rtYDwf0fuA1C9RjJP8NcPm7oNlpyP2VQbtr3rSwmx+c +xYQuIZxqYbO+iwlJDuAst1n7dDIzPtnea/KUEQy4u7jmONKQ1VdA9dyGvqy4y8ie +bVDfnAzAvO17Zbvmqk0zQRmYXRsLuIN6QyWsfi8e2O7FctcpRVgc4e5xmBTfztBL +Q5feJ50wqE+JNfL5dQp8N0NtWFA6d4RLMN1T8zLhYASO/NOOzQtR7X+NlMrtm0wQ +aNDoTrIrg7xpuQOlMOe9UCwfcHu+DoPxUzZrqzhPlCGYSbecBW6G4+S4FPL5LpsW +NrYx30C8A87A0eEUNzLxO3CoPv7XhN/b0xf7W2CA79gbZnhgPtF12/11VmRo8ckl +zkrhrvtsjexdwYje7xjngPXrZ9USh13CoYNlduTlWB72m+wN8W7zyCLn1Zl/grTI +76Z2FZiqBEuPEcoDRrNUmX6MeNcMRo8Zq1FRi8imYnKYC0YsJMU0N+kIsiuGQsOI +-----END RSA PRIVATE KEY----- +" + } + + it 'returns key_with_a_passphrase validation error' do + ssh_key = described_class.new(value: private_key) + + ssh_key.should_not be_valid + + ssh_key.errors[:value].should == [:key_with_a_passphrase] + end + end +end diff --git a/spec_core/core/model/repository/settings_spec.rb b/spec_core/core/model/repository/settings_spec.rb new file mode 100644 index 00000000..e2b48caf --- /dev/null +++ b/spec_core/core/model/repository/settings_spec.rb @@ -0,0 +1,140 @@ +# encoding: utf-8 +require 'spec_helper_core' + +describe Repository::Settings do + describe 'env_vars' do + it 'can be filtered to get only public vars' do + settings = Repository::Settings.load(env_vars: [ + { name: 'PUBLIC_VAR', value: 'public var', public: true }, + { name: 'SECRET_VAR', value: 'secret var', public: false } + ]) + settings.env_vars.public.length.should == 1 + settings.env_vars.public.first.name.should == 'PUBLIC_VAR' + end + end + + describe '#maximum_number_of_builds' do + it 'defaults to 0' do + settings = Repository::Settings.new(maximum_number_of_builds: nil) + settings.maximum_number_of_builds.should == 0 + end + end + + describe '#restricts_number_of_builds?' do + it 'returns true if number of builds is restricted' do + settings = Repository::Settings.new(maximum_number_of_builds: 2) + settings.restricts_number_of_builds?.should be true + end + + it 'returns false if builds are not restricted' do + settings = Repository::Settings.new(maximum_number_of_builds: 0) + settings.restricts_number_of_builds?.should be false + end + end + + it 'validates maximum_number_of_builds' do + settings = Repository::Settings.new + settings.maximum_number_of_builds = nil + settings.should be_valid + + settings.maximum_number_of_builds = 'foo' + settings.should_not be_valid + + settings.errors[:maximum_number_of_builds].should == [:not_a_number] + + settings.maximum_number_of_builds = 0 + settings.should be_valid + end + + describe '#api_builds_rate_limit' do + it 'saves new api_builds_rate_limit if rate is under 200' do + settings = Repository::Settings.new(api_builds_rate_limit: 2) + settings.should be_valid + end + + it 'does not save new api_builds_rate_limit if rate is over 200' do + settings = Repository::Settings.new(api_builds_rate_limit: 201) + settings.should_not be_valid + end + + it 'returns nil if no api_builds_rate_limit is set on settings' do + settings = Repository::Settings.new() + settings.api_builds_rate_limit.should eq(nil) + end + end + + describe 'timeouts' do + MAX = { + off: { hard_limit: 50, log_silence: 10 }, + on: { hard_limit: 180, log_silence: 60 } + } + + [:hard_limit, :log_silence].each do |type| + describe type do + def settings(type, value) + Repository::Settings.load({ :"timeout_#{type}" => value }, repository_id: 1) + end + + it 'defaults to nil' do + settings(type, nil).send(:"timeout_#{type}").should be_nil + end + + it "is valid if #{type} is nil" do + settings(type, nil).should be_valid + end + + it 'returns nil if set to 0' do + settings(type, 0).send(:"timeout_#{type}").should be_nil + end + + it "is valid if #{type} is set to 0" do + settings(type, 0).should be_valid + end + + [:off, :on].each do |status| + describe "with :custom_timeouts feature flag turned #{status}" do + max = MAX[status][type] + + before :each do + Travis::Features.stubs(:repository_active?).with(:custom_timeouts, 1).returns true if status == :on + end + + describe 'is valid' do + it "if #{type} is nil" do + settings(type, nil).should be_valid + end + + it "if #{type} is > 0" do + settings(type, 1).should be_valid + end + + it "if #{type} is < #{max}" do + settings(type, max - 1).should be_valid + end + + it "if #{type} equals #{max}" do + settings(type, max).should be_valid + end + end + + describe 'is invalid' do + it "if #{type} is < 0" do + settings(type, -1).should_not be_valid + end + + it "if #{type} is > #{max}" do + settings(type, max + 1).should_not be_valid + end + end + + it 'adds an error message if invalid' do + model = settings(type, - 1) + model.valid? + model.errors[:"timeout_#{type}"].should == ["Invalid #{type} timout value (allowed: 0 - #{max})"] + end + end + end + end + end + end +end diff --git a/spec_core/core/model/repository/status_image_spec.rb b/spec_core/core/model/repository/status_image_spec.rb new file mode 100644 index 00000000..1c166cae --- /dev/null +++ b/spec_core/core/model/repository/status_image_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper_core' + +describe Repository::StatusImage do + include Support::ActiveRecord + + let(:cache) { stub('states cache', fetch: nil, write: nil, fetch_state: nil) } + let!(:request) { Factory(:request, event_type: 'push', repository: repo) } + let!(:build) { Factory(:build, repository: repo, request: request, state: :passed) } + let(:repo) { Factory(:repository) } + + before do + described_class.any_instance.stubs(cache: cache) + described_class.any_instance.stubs(:cache_enabled? => true) + end + + describe('with cache') do + it 'tries to get state from cache first' do + image = described_class.new(repo, 'foobar') + cache.expects(:fetch_state).with(repo.id, 'foobar').returns(:passed) + + image.result.should == :passing + end + + it 'saves state to the cache if it needs to be fetched from the db' do + image = described_class.new(repo, 'master') + cache.expects(:fetch_state).with(repo.id, 'master').returns(nil) + cache.expects(:write).with(repo.id, 'master', build) + + image.result.should == :passing + end + + it 'saves state of the build to the cache with its branch even if brianch is not given' do + image = described_class.new(repo, nil) + cache.expects(:fetch_state).with(repo.id, nil).returns(nil) + cache.expects(:write).with(repo.id, 'master', build) + + image.result.should == :passing + end + + it 'handles cache failures gracefully' do + image = described_class.new(repo, nil) + cache.expects(:fetch_state).raises(Travis::StatesCache::CacheError) + expect { + image.result.should == :passing + }.to_not raise_error + end + end + + describe 'given no branch' do + it 'returns the status of the last finished build' do + image = described_class.new(repo, nil) + image.result.should == :passing + end + + it 'returns :failing if the status of the last finished build is failed' do + build.update_attributes(state: :failed) + image = described_class.new(repo, nil) + image.result.should == :failing + end + + it 'returns :error if the status of the last finished build is errored' do + build.update_attributes(state: :errored) + image = described_class.new(repo, nil) + image.result.should == :error + end + + it 'returns :canceled if the status of the last finished build is canceled' do + build.update_attributes(state: 'canceled') + image = described_class.new(repo, nil) + image.result.should == :canceled + end + + it 'returns :unknown if the status of the last finished build is unknown' do + build.update_attributes(state: :created) + image = described_class.new(repo, nil) + image.result.should == :unknown + end + end + + describe 'given a branch' do + it 'returns :passed if the last build on that branch has passed' do + build.update_attributes(state: :passed, branch: 'master') + image = described_class.new(repo, 'master') + image.result.should == :passing + end + + it 'returns :failed if the last build on that branch has failed' do + build.update_attributes(state: :failed, branch: 'develop') + image = described_class.new(repo, 'develop') + image.result.should == :failing + end + + it 'returns :error if the last build on that branch has errored' do + build.update_attributes(state: :errored, branch: 'develop') + image = described_class.new(repo, 'develop') + image.result.should == :error + end + + it 'returns :canceled if the last build on that branch was canceled' do + build.update_attributes(state: :canceled, branch: 'develop') + image = described_class.new(repo, 'develop') + image.result.should == :canceled + end + end +end diff --git a/spec_core/core/model/repository_spec.rb b/spec_core/core/model/repository_spec.rb new file mode 100644 index 00000000..90d9b304 --- /dev/null +++ b/spec_core/core/model/repository_spec.rb @@ -0,0 +1,435 @@ +require 'spec_helper_core' + +describe Repository, truncation: true do + include Support::ActiveRecord + + describe '#last_completed_build' do + let(:repo) { Factory(:repository, name: 'foobarbaz', builds: [build1, build2]) } + let(:build1) { Factory(:build, finished_at: 1.hour.ago, state: :passed) } + let(:build2) { Factory(:build, finished_at: Time.now, state: :failed) } + + before do + build1.update_attributes(branch: 'master') + build2.update_attributes(branch: 'development') + end + + it 'returns last completed build' do + repo.last_completed_build.should == build2 + end + + it 'returns last completed build for a branch' do + repo.last_completed_build('master').should == build1 + end + end + + describe '#regenerate_key!' do + it 'regenerates key' do + repo = Factory(:repository) + + expect { repo.regenerate_key! }.to change { repo.key.private_key } + end + end + + describe 'associations' do + describe 'owner' do + let(:user) { Factory(:user) } + let(:org) { Factory(:org) } + + it 'can be a user' do + repo = Factory(:repository, owner: user) + repo.reload.owner.should == user + end + + it 'can be an organization' do + repo = Factory(:repository, owner: org) + repo.reload.owner.should == org + end + end + end + + describe 'class methods' do + describe 'find_by' do + let(:minimal) { Factory(:repository) } + + it "should find a repository by it's github_id" do + Repository.find_by(github_id: minimal.github_id).should == minimal + end + + it "should find a repository by it's id" do + Repository.find_by(id: minimal.id).id.should == minimal.id + end + + it "should find a repository by it's name and owner_name" do + repo = Repository.find_by(name: minimal.name, owner_name: minimal.owner_name) + repo.owner_name.should == minimal.owner_name + repo.name.should == minimal.name + end + + it "returns nil when a repository couldn't be found using params" do + Repository.find_by(name: 'emptiness').should be_nil + end + end + + describe 'timeline' do + before do + Factory(:repository, name: 'unbuilt 1', active: true, last_build_started_at: nil, last_build_finished_at: nil) + Factory(:repository, name: 'unbuilt 2', active: true, last_build_started_at: nil, last_build_finished_at: nil) + Factory(:repository, name: 'finished 1', active: true, last_build_started_at: '2011-11-12 12:00:00', last_build_finished_at: '2011-11-12 12:00:05') + Factory(:repository, name: 'finished 2', active: true, last_build_started_at: '2011-11-12 12:00:01', last_build_finished_at: '2011-11-11 12:00:06') + Factory(:repository, name: 'started 1', active: true, last_build_started_at: '2011-11-11 12:00:00', last_build_finished_at: nil) + Factory(:repository, name: 'started 2', active: true, last_build_started_at: '2011-11-11 12:00:01', last_build_finished_at: nil) + Factory(:repository, name: 'invalidated', active: true, last_build_started_at: '2011-11-11 12:00:01', last_build_finished_at: nil, invalidated_at: '2012-11-11 12:00:06') + end + + it 'sorts repositories with running builds to the top, most recent builds next, un-built repos last' do + repositories = Repository.timeline + repositories.map(&:name).should == ['started 2', 'started 1', 'finished 2', 'finished 1', 'unbuilt 2', 'unbuilt 1'] + end + + it 'does not include invalidated repos' do + repositories = Repository.timeline + repositories.map(&:name).should_not include('invalidated') + end + end + + describe 'with_builds' do + it 'gets only projects with existing builds' do + one = Factory(:repository, name: 'one', last_build_started_at: '2011-11-11', last_build_id: nil) + two = Factory(:repository, name: 'two', last_build_started_at: '2011-11-12', last_build_id: 101) + three = Factory(:repository, name: 'three', last_build_started_at: nil, last_build_id: 100) + + repositories = Repository.with_builds.all + repositories.map(&:id).sort.should == [two, three].map(&:id).sort + end + end + + describe 'active' do + let(:active) { Factory(:repository, active: true) } + let(:inactive) { Factory(:repository, active: false) } + let(:invalidated) { Factory(:repository, invalidated_at: Time.now) } + + it 'contains active repositories' do + Repository.active.should include(active) + end + + it 'does not include inactive repositories' do + Repository.active.should_not include(inactive) + end + + it 'does not include invalidated repositories' do + Repository.active.should_not include(invalidated) + end + end + + describe 'search' do + before(:each) do + Factory(:repository, name: 'repo 1', last_build_started_at: '2011-11-11') + Factory(:repository, name: 'repo 2', last_build_started_at: '2011-11-12') + Factory(:repository, name: 'invalidated', invalidated_at: Time.now) + end + + it 'performs searches case-insensitive' do + Repository.search('rEpO').to_a.count.should == 2 + end + + it 'performs searches with / entered' do + Repository.search('fuchs/').to_a.count.should == 2 + end + + it 'performs searches with \ entered' do + Repository.search('fuchs\\').to_a.count.should == 2 + end + + it 'does not find invalidated repos' do + Repository.search('fuchs').map(&:name).should_not include('invalidated') + end + end + + describe 'by_member' do + let(:user) { Factory(:user) } + let(:org) { Factory(:org) } + let(:user_repo) { Factory(:repository, owner: user)} + let(:org_repo) { Factory(:repository, owner: org, name: 'globalize')} + let(:invalidated) { Factory(:repository, owner: org, name: 'invalidated', invalidated_at: Time.now)} + before do + Permission.create!(user: user, repository: user_repo, pull: true, push: true) + Permission.create!(user: user, repository: org_repo, pull: true) + Permission.create!(user: user, repository: invalidated, pull: true) + end + + it 'returns all repositories a user has rights to' do + expect(Repository.by_member('svenfuchs').size).to eq(2) + end + + it 'does not find invalidated repos' do + Repository.by_member('svenfuchs').map(&:name).should_not include('invalidated') + end + end + + describe 'counts_by_owner_names' do + let!(:repositories) do + Factory(:repository, owner_name: 'svenfuchs', name: 'minimal') + Factory(:repository, owner_name: 'travis-ci', name: 'travis-ci') + Factory(:repository, owner_name: 'travis-ci', name: 'invalidated', invalidated_at: Time.now) + end + + it 'returns repository counts per owner_name for the given owner_names' do + counts = Repository.counts_by_owner_names(%w(svenfuchs travis-ci)) + counts.should == { 'svenfuchs' => 1, 'travis-ci' => 1 } + end + end + end + + describe 'api_url' do + let(:repo) { Repository.new(owner_name: 'travis-ci', name: 'travis-ci') } + + before :each do + Travis.config.github.api_url = 'https://api.github.com' + end + + it 'returns the api url for the repository' do + repo.api_url.should == 'https://api.github.com/repos/travis-ci/travis-ci' + end + end + + describe 'source_url' do + describe 'default source endpoint' do + let(:repo) { Repository.new(owner_name: 'travis-ci', name: 'travis-ci') } + + before :each do + Travis.config.github.source_host = nil + end + + it 'returns the public git source url for a public repository' do + repo.private = false + repo.source_url.should == 'git://github.com/travis-ci/travis-ci.git' + end + + it 'returns the private git source url for a private repository' do + repo.private = true + repo.source_url.should == 'git@github.com:travis-ci/travis-ci.git' + end + end + + describe 'custom source endpoint' do + let(:repo) { Repository.new(owner_name: 'travis-ci', name: 'travis-ci') } + + before :each do + Travis.config.github.source_host = 'localhost' + end + + it 'returns the private git source url for a public repository' do + repo.private = false + repo.source_url.should == 'git@localhost:travis-ci/travis-ci.git' + end + + it 'returns the private git source url for a private repository' do + repo.private = true + repo.source_url.should == 'git@localhost:travis-ci/travis-ci.git' + end + end + end + + describe 'source_host' do + before :each do + Travis.config.github.stubs(:source_host).returns('localhost') + end + + it 'returns the source_host name from Travis.config' do + Repository.new.source_host.should == 'localhost' + end + end + + describe "#last_build" do + let(:repo) { Factory(:repository) } + let(:attributes) { { repository: repo, state: 'finished' } } + let(:api_req) { Factory(:request, {event_type: 'api'}) } + + before :each do + Factory(:build, attributes) + Factory(:build, attributes) + end + + context 'when last build is a push build' do + before :each do + @build = Factory(:build, attributes) + end + + it 'returns the most recent build' do + repo.last_build('master').id.should == @build.id + end + end + + context 'when last build is an API build' do + before :each do + @build = Factory(:build, attributes.merge({request: api_req})) + end + + it 'returns the most recent build' do + repo.last_build('master').id.should == @build.id + end + end + end + + describe '#last_build_on' do + let(:repo) { Factory(:repository) } + let(:attributes) { { repository: repo, state: 'finished' } } + let(:api_req) { Factory(:request, {event_type: 'api'}) } + + before :each do + Factory(:build, attributes) + end + + context 'when last build is a push build' do + before :each do + @build = Factory(:build, attributes) + end + + it 'returns the most recent build' do + repo.last_build_on('master').id.should == @build.id + end + end + + context 'when last build is an API build' do + before :each do + @build = Factory(:build, attributes.merge({request: api_req})) + end + + it 'returns the most recent build' do + repo.last_build_on('master').id.should == @build.id + end + end + end + + describe "keys" do + let(:repo) { Factory(:repository) } + + it "should return the public key" do + repo.public_key.should == repo.key.public_key + end + + it "should create a new key when the repository is created" do + repo = Repository.create!(owner_name: 'travis-ci', name: 'travis-ci') + repo.key.should_not be_nil + end + end + + describe 'branches' do + let(:repo) { Factory(:repository) } + + it 'returns branches for the given repository' do + %w(master production).each do |branch| + 2.times { Factory(:build, repository: repo, commit: Factory(:commit, branch: branch)) } + end + repo.branches.sort.should == %w(master production) + end + + it 'is empty for empty repository' do + repo.branches.should eql [] + end + end + + describe 'settings' do + let(:repo) { Factory.build(:repository) } + + it 'adds repository_id to collection records' do + repo.save + + env_var = repo.settings.env_vars.create(name: 'FOO') + env_var.repository_id.should == repo.id + + repo.settings.save + + repo.reload + + repo.settings.env_vars.first.repository_id.should == repo.id + end + + it "is reset on reload" do + repo.save + + repo.settings = {} + repo.update_column(:settings, { 'build_pushes' => false }.to_json) + repo.reload + repo.settings.build_pushes?.should be false + repo.update_column(:settings, { 'build_pushes' => true }.to_json) + repo.reload + repo.settings.build_pushes?.should be true + end + + it "allows to set nil for settings" do + repo.settings = nil + repo.settings.to_hash.should == Repository::Settings.new.to_hash + end + + it "allows to set settings as JSON string" do + repo.settings = '{"maximum_number_of_builds": 44}' + repo.settings.to_hash.should == Repository::Settings.new(maximum_number_of_builds: 44).to_hash + end + + it "allows to set settings as a Hash" do + repo.settings = { maximum_number_of_builds: 44} + repo.settings.to_hash.should == Repository::Settings.new(maximum_number_of_builds: 44).to_hash + end + + it 'updates settings in the DB' do + repo.settings = {'build_pushes' => false} + repo.save + + repo.reload.settings.build_pushes?.should == false + + repo.settings.merge('build_pushes' => true) + repo.settings.save + + repo.reload.settings.build_pushes?.should == true + end + end + + describe 'last_finished_builds_by_branches' do + let(:repo) { Factory(:repository) } + + it 'properly orders branches by last build' do + Build.delete_all + one = Factory(:build, repository: repo, finished_at: 2.hours.ago, state: 'finished', commit: Factory(:commit, branch: '1one')) + two = Factory(:build, repository: repo, finished_at: 1.hours.ago, state: 'finished', commit: Factory(:commit, branch: '2two')) + + builds = repo.last_finished_builds_by_branches(1) + builds.should == [two] + end + + it 'retrieves last builds on all branches' do + Build.delete_all + old = Factory(:build, repository: repo, finished_at: 1.hour.ago, state: 'finished', commit: Factory(:commit, branch: 'one')) + one = Factory(:build, repository: repo, finished_at: 1.hour.from_now, state: 'finished', commit: Factory(:commit, branch: 'one')) + two = Factory(:build, repository: repo, finished_at: 1.hour.from_now, state: 'finished', commit: Factory(:commit, branch: 'two')) + three = Factory(:build, repository: repo, finished_at: 1.hour.from_now, state: 'finished', commit: Factory(:commit, branch: 'three')) + three.update_attribute(:event_type, 'pull_request') + + builds = repo.last_finished_builds_by_branches + builds.size.should == 2 + builds.should include(one) + builds.should include(two) + builds.should_not include(old) + end + end + + describe '#users_with_permission' do + it 'returns users with the given permission linked to that repository' do + repo = Factory(:repository) + other_repo = Factory(:repository) + + user_with_permission = Factory(:user) + user_with_permission.permissions.create!(repository: repo, admin: true) + + user_wrong_repo = Factory(:user) + user_wrong_repo.permissions.create!(repository: other_repo, admin: true) + + user_wrong_permission = Factory(:user) + user_wrong_permission.permissions.create!(repository: repo, push: true) + + repo.users_with_permission(:admin).should include(user_with_permission) + repo.users_with_permission(:admin).should_not include(user_wrong_repo) + repo.users_with_permission(:admin).should_not include(user_wrong_permission) + end + end +end diff --git a/spec_core/core/model/request/approval_spec.rb b/spec_core/core/model/request/approval_spec.rb new file mode 100644 index 00000000..50d0727b --- /dev/null +++ b/spec_core/core/model/request/approval_spec.rb @@ -0,0 +1,272 @@ +require 'spec_helper_core' + +describe Request::Approval do + include Travis::Testing::Stubs + + let(:approval) { Request::Approval.new(request) } + + before do + approval.stubs(:build_pull_requests?).returns(true) + approval.stubs(:build_pushes?).returns(true) + request.stubs(:creates_jobs?).returns(true) + end + + describe 'config_accepted?' do + it 'approves the build when .travis.yml is missing, but builds with .travis.yml are allowed' do + request.config['.result'] = 'not_found' + approval.config_accepted?.should be true + end + + it 'does not approve the build if .travis.yml is missing and builds without it are not allowed' do + request.repository.stubs(:builds_only_with_travis_yml?).returns(true) + request.config['.result'] = 'not_found' + + approval.config_accepted?.should be false + approval.message.should == '.travis.yml is missing and builds without .travis.yml are disabled' + end + + it 'approves the build when .travis.yml is present' do + request.config['.result'] = 'configured' + approval.config_accepted?.should be true + end + end + + describe 'branch_accepted?' do + it 'does not accept a request that belongs to the github_pages branch' do + request.commit.stubs(:branch).returns('gh_pages') + approval.branch_accepted?.should be false + end + + it 'accepts a request that belongs to the gh-pages branch if it\'s specified in branches:only' do + request.commit.stubs(:branch).returns('gh_pages') + request.config['branches'] = { 'only' => ['gh-pages'] } + approval.branch_accepted?.should be_truthy + end + + it "doesn't fail when the branch configuration is an array" do + request.config['branches'] = [{ 'only' => ['gh-pages'] }] + approval.branch_accepted?.should be true + end + end + + describe 'accepted?' do + it 'accepts a request that has a commit, belongs to a public repository, is not skipped and does not belong to the github_pages branch and it is not a rails fork' do + approval.should be_accepted + end + + it 'does not accept a request that does not have a commit' do + approval.stubs(:commit).returns(nil) + approval.should_not be_accepted + end + + it 'does not accept a request that belongs to a private repository' do + request.repository.stubs(:private?).returns(true) + approval.should_not be_accepted + end + + it 'does not accept a request that belongs to an excluded repository' do + request.repository.stubs(:slug).returns('svenfuchs/rails') + approval.should_not be_accepted + end + + it 'does not accept a request that is skipped (using the commit message)' do + request.commit.stubs(:message).returns('update README [ci:skip]') + approval.should_not be_accepted + end + + it 'accepts a request that belongs to the github_pages branch and is explicitly set to build that branch (String)' do + request.commit.stubs(:branch).returns('gh_pages') + request.stubs(:config).returns('branches' => { 'only' => 'gh_pages' }) + approval.should be_accepted + end + + it 'accepts a request that belongs to the github_pages branch and is explicitly set to build that branch (Array)' do + request.commit.stubs(:branch).returns('gh_pages') + request.stubs(:config).returns('branches' => { 'only' => ['gh_pages'] }) + approval.should be_accepted + end + + it 'does not accept a request when it is disabled in settings' do + approval.stubs(:enabled_in_settings?).returns(false) + approval.should_not be_accepted + end + + it 'does not accept a request when compare URL is too long' do + request.commit.stubs(:compare_url).returns('a'*256) + approval.should_not be_accepted + end + end + + describe 'approved?' do + xit 'should be specified' + end + + describe 'message' do + it 'returns "pull requests disabled" if pull requests are disabled' do + approval.stubs(:enabled_in_settings?).returns(false) + request.stubs(:pull_request?).returns(true) + approval.message.should == 'pull requests disabled' + end + + it 'returns "pushes disabled" if pushes are disabled' do + approval.stubs(:enabled_in_settings?).returns(false) + request.stubs(:pull_request?).returns(false) + approval.message.should == 'pushes disabled' + end + + it 'returns "missing commit" if the commit is missing' do + approval.stubs(:commit).returns(nil) + approval.message.should == 'missing commit' + end + + it 'returns "private repository" if the repository is private' do + request.repository.stubs(:private?).returns(true) + request.stubs(:config).returns({key: 'value'}) + approval.message.should == 'private repository' + end + + it 'returns "excluded repository" if the repository is an excluded repository' do + request.repository.stubs(:slug).returns('svenfuchs/rails') + approval.message.should == 'excluded repository' + end + + it 'returns "excluded repository" if the repository is an excluded repository and exclude rule is a string' do + Travis.config.repository_filter.stubs(:exclude).returns(["\\/rails$"]) + request.repository.stubs(:slug).returns('svenfuchs/rails') + approval.message.should == 'excluded repository' + end + + it 'returns "github pages branch" if the branch is a github pages branch' do + request.commit.stubs(:branch).returns('gh-pages') + approval.message.should == 'github pages branch' + end + + it 'returns "config is missing or contains YAML syntax error" if the config is not present' do + request.stubs(:config).returns(nil) + approval.message.should == 'config is missing or contains YAML syntax error' + end + + it 'returns "branch not included or excluded" if the branch was not approved' do + request.commit.stubs(:branch).returns('feature') + request.stubs(:config).returns('branches' => { 'only' => 'master' }) + approval.message.should == 'branch not included or excluded' + end + + it 'returns "compare URL too long; branch/tag names may be too long" if the compare URL is too long' do + request.stubs(:config).returns({key: 'value'}) + request.commit.stubs(:compare_url).returns('a'*256) + approval.message.should == 'compare URL too long; branch/tag names may be too long' + end + end + + describe 'skipped?' do + it 'returns true when the commit message contains [ci skip]' do + request.commit.stubs(:message).returns 'lets party like its 1999 [ci skip]' + approval.send(:skipped?).should be true + end + end + + describe 'github_pages?' do + it 'returns true for a branch named gh-pages' do + request.commit.stubs(:branch).returns 'gh-pages' + approval.send(:github_pages?).should be_truthy + end + + it 'returns true for a branch named gh_pages' do + request.commit.stubs(:branch).returns 'gh_pages' + approval.send(:github_pages?).should be_truthy + end + + it 'returns true when a PR is for gh_pages' do + request.commit.stubs(:ref).returns 'refs/pulls/1/merge' + request.commit.stubs(:branch).returns 'gh_pages' + approval.send(:github_pages?).should be_truthy + end + + it 'returns false for a branch named master' do + commit.stubs(:branch).returns 'master' + approval.send(:github_pages?).should be_falsy + end + end + + describe 'included_repository?' do + it 'returns true if the repository is an included repository' do + request.repository.stubs(:slug).returns 'rails/rails' + approval.send(:included_repository?).should be true + end + + it 'returns true if the repository is an included repository with rule as a string' do + Travis.config.repository_filter.stubs(:include).returns(["rails\\/rails"]) + request.repository.stubs(:slug).returns 'rails/rails' + approval.send(:included_repository?).should be true + end + + it 'returns false if the repository is not included' do + request.repository.stubs(:slug).returns 'josh/completeness-fu' + approval.send(:included_repository?).should be false + end + + it 'returns false if the repository is not included with rule as a string' do + Travis.config.repository_filter.stubs(:include).returns(["rails\\/rails"]) + request.repository.stubs(:slug).returns 'josh/completeness-fu' + approval.send(:included_repository?).should be false + end + end + + describe 'excluded_repository?' do + it 'returns true if the repository is an excluded repository' do + request.repository.stubs(:slug).returns 'josh/rails' + approval.send(:excluded_repository?).should be true + end + + it 'returns false if the repository is not excluded' do + request.repository.stubs(:slug).returns 'josh/completeness-fu' + approval.send(:excluded_repository?).should be false + end + + it 'returns true if the repository is an excluded repository with rule as a string' do + Travis.config.repository_filter.stubs(:exclude).returns(["\\/rails$"]) + request.repository.stubs(:slug).returns 'josh/rails' + approval.send(:excluded_repository?).should be true + end + + it 'returns false if the repository is not excluded with rule as a string' do + Travis.config.repository_filter.stubs(:exclude).returns(["\\/rails$"]) + request.repository.stubs(:slug).returns 'josh/completeness-fu' + approval.send(:excluded_repository?).should be false + end + end + + describe 'enabled_in_settings?' do + it 'returns true if a request is an api request' do + request.stubs(:api_request?).returns(true) + approval.enabled_in_settings?.should be true + end + + it 'returns true if pull requests are enabled and a request is a pull request' do + request.stubs(:pull_request?).returns(true) + approval.stubs(:build_pull_requests?).returns(true) + approval.enabled_in_settings?.should be true + end + + it 'returns true if pushes are enabled and a request is a push' do + request.stubs(:pull_request?).returns(false) + approval.stubs(:build_pushes?).returns(true) + approval.enabled_in_settings?.should be true + + end + + it 'returns false if pull requests are disabled and a request is a pull request' do + request.stubs(:pull_request?).returns(true) + approval.stubs(:build_pull_requests?).returns(false) + approval.enabled_in_settings?.should be false + end + + it 'returns false if pushes are disabled and a request is a push' do + request.stubs(:pull_request?).returns(false) + approval.stubs(:build_pushes?).returns(false) + approval.enabled_in_settings?.should be false + + end + end +end diff --git a/spec_core/core/model/request/branches_spec.rb b/spec_core/core/model/request/branches_spec.rb new file mode 100644 index 00000000..2d8174d4 --- /dev/null +++ b/spec_core/core/model/request/branches_spec.rb @@ -0,0 +1,159 @@ +require 'spec_helper_core' + +describe Request::Branches do + include Travis::Testing::Stubs + + let(:branches) { Request::Branches.new(request) } + + describe '#included?' do + it 'defaults to true if no branches are included' do + request.config['branches'] = { 'only' => nil } + branches.included?('feature').should be true + end + + describe 'returns true if the included branches include the given branch' do + it 'given as a string' do + request.config['branches'] = { 'only' => 'feature' } + branches.included?('feature').should be true + end + + it 'given as a comma separated list of branches' do + request.config['branches'] = { 'only' => 'feature, develop' } + branches.included?('feature').should be true + end + + it 'given as an array of branches' do + request.config['branches'] = { 'only' => %w(feature develop) } + branches.included?('feature').should be true + end + end + + describe 'returns true if the given branch matches a pattern from the included branches' do + it 'given as a string' do + request.config['branches'] = { 'only' => '/^feature-\d+$/' } + branches.included?('feature-42').should be true + end + + it 'given as a comma separated list of patterns' do + request.config['branches'] = { 'only' => '/^feature-\d+$/,/^develop-\d+$/' } + branches.included?('feature-42').should be true + end + + it 'given as an array of patterns' do + request.config['branches'] = { 'only' => %w(/^feature-\d+$/ /^develop-\d+$/) } + branches.included?('feature-42').should be true + end + end + + describe 'returns false if the included branches do not include the given branch' do + it 'given as a string' do + request.config['branches'] = { 'only' => 'feature' } + branches.included?('master').should be false + end + + it 'given as a comma separated list of branches' do + request.config['branches'] = { 'only' => 'feature, develop' } + branches.included?('master').should be false + end + + it 'given as an array of branches' do + request.config['branches'] = { 'only' => %w(feature develop) } + branches.included?('master').should be false + end + end + + describe 'returns false if the given branch does not match any pattern from the included branches' do + it 'given as a string' do + request.config['branches'] = { 'only' => '/^feature-\d+$/' } + branches.included?('master').should be false + end + + it 'given as a comma separated list of patterns' do + request.config['branches'] = { 'only' => '/^feature-\d+$/,/^develop-\d+$/' } + branches.included?('master').should be false + end + + it 'given as an array of patterns' do + request.config['branches'] = { 'only' => %w(/^feature-\d+$/ /^develop-\d+$/) } + branches.included?('master').should be false + end + end + end + + describe '#excluded?' do + it 'defaults to false if no branches are excluded' do + request.config['branches'] = { 'except' => nil } + branches.excluded?('feature').should be_falsy + end + + describe 'returns true if the excluded branches include the given branch' do + it 'given as a string' do + request.config['branches'] = { 'except' => 'feature' } + branches.excluded?('feature').should be true + end + + it 'given as a comma separated list of branches' do + request.config['branches'] = { 'except' => 'feature, develop' } + branches.excluded?('feature').should be true + end + + it 'given as an array of branches' do + request.config['branches'] = { 'except' => %w(feature develop) } + branches.excluded?('feature').should be true + end + end + + describe 'returns true if the given branch matches a pattern from the excluded branches' do + it 'given as a string' do + request.config['branches'] = { 'except' => '/^feature-\d+$/' } + branches.excluded?('feature-42').should be true + end + + it 'given as a comma separated list of patterns' do + request.config['branches'] = { 'except' => '/^feature-\d+$/,/^develop-\d+$/' } + branches.excluded?('feature-42').should be true + end + + it 'given as an array of patterns' do + request.config['branches'] = { 'except' => %w(/^feature-\d+$/ /^develop-\d+$/) } + branches.excluded?('feature-42').should be true + + end + end + + describe 'returns false if the excluded branches do not include the given branch' do + it 'given as a string' do + request.config['branches'] = { 'except' => 'feature' } + branches.excluded?('master').should be false + end + + it 'given as a comma separated list of branches' do + request.config['branches'] = { 'except' => 'feature, develop' } + branches.excluded?('master').should be false + end + + it 'given as an array of branches' do + request.config['branches'] = { 'except' => %w(feature develop) } + branches.excluded?('master').should be false + end + end + + describe 'returns false if the given branch does not match any pattern from the excluded branches' do + it 'given as a string' do + request.config['branches'] = { 'except' => '/^feature-\d+$/' } + branches.excluded?('master').should be false + end + + it 'given as a comma separated list of patterns' do + request.config['branches'] = { 'except' => '/^feature-\d+$/,/^develop-\d+$/' } + branches.excluded?('master').should be false + end + + it 'given as an array of patterns' do + request.config['branches'] = { 'except' => %w(/^feature-\d+$/ /^develop-\d+$/) } + branches.excluded?('master').should be false + + end + end + end +end diff --git a/spec_core/core/model/request/states_spec.rb b/spec_core/core/model/request/states_spec.rb new file mode 100644 index 00000000..2b08e7b2 --- /dev/null +++ b/spec_core/core/model/request/states_spec.rb @@ -0,0 +1,254 @@ +require 'spec_helper_core' + +describe Request::States do + include Support::ActiveRecord + + let(:owner) { User.new(:login => 'joshk') } + let(:repository) { Repository.new(:name => 'travis-ci', :owner => owner, :owner_name => 'travis-ci') } + let(:commit) { Commit.new(:repository => repository, :commit => '12345', :branch => 'master', :message => 'message', :committed_at => Time.now, :compare_url => 'https://github.com/svenfuchs/minimal/compare/master...develop') } + let(:request) { Request.new(:repository => repository, :commit => commit) } + + let(:approval) { Request::Approval.any_instance } + let(:config) { { :from => '.travis.yml' } } + + before :each do + repository.save! + Travis.stubs(:run_service).with(:github_fetch_config, is_a(Hash)).returns(config) + request.stubs(:add_build) + request.stubs(:creates_jobs?).returns(true) + end + + it 'has the state :created when just created' do + request.state.should == :created + end + + describe 'start' do + describe 'with an accepted request' do + before :each do + approval.stubs(:accepted?).returns(true) + end + + it 'configures the request' do + request.expects(:configure) + request.start + end + + it 'finishes the request' do + request.expects(:finish) + request.start + end + + it 'sets the state to started' do + request.start + request.was_started?.should be true + end + + it 'sets the result to :accepted' do + request.start + request.result.should == :accepted + end + + describe 'but rejected config' do + before :each do + approval.stubs(:config_accepted?).returns(false) + end + + it 'does config, but resets it to nil' do + request.expects(:fetch_config).returns({}) + + request.start + + request.config.should be_nil + end + end + + describe 'but rejected branch' do + before :each do + approval.stubs(:branch_accepted?).returns(false) + end + + it 'does config, but resets it to nil' do + request.expects(:fetch_config).returns({}) + + request.start + + request.config.should be_nil + end + end + end + + describe 'with a rejected request' do + before :each do + approval.stubs(:accepted?).returns(false) + end + + it 'does not configure the request' do + request.expects(:fetch_config).never + request.start + end + + it 'finishes the request' do + request.expects(:finish) + request.start + end + + it 'sets the state to started' do + request.start + request.was_started?.should be true + end + + it 'sets the result to :rejected' do + request.start + request.result.should == :rejected + end + end + end + + describe 'configure' do + it 'fetches the .travis.yml config from Github' do + Travis.expects(:run_service).returns(config) + request.configure + end + + it 'merges existing configuration (e.g. from an api request)' do + request.config = { env: 'FOO=foo' } + request.configure + request.config.should == config.merge(env: 'FOO=foo') + end + + it 'stores the config on the request' do + request.configure + request.config.should == config + end + + it 'sets the state to configured' do + request.configure + request.was_configured?.should be true + end + end + + describe 'finish' do + before :each do + request.stubs(:config).returns('.configured' => true) + end + + describe 'with an approved request' do + before :each do + approval.stubs(:approved?).returns(true) + end + + it 'builds the build' do + request.expects(:add_build) + request.finish + end + + it 'sets the state to finished' do + request.finish + request.should be_finished + end + end + + describe 'with an unapproved request' do + before :each do + approval.stubs(:approved?).returns(false) + end + + it 'does not build the build' do + request.expects(:add_build).never + request.finish + end + + it 'sets the state to finished' do + request.finish + request.should be_finished + end + end + + describe 'with a config parse error' do + let(:job) { stub(start!: nil, finish!: nil, :log_content= => nil) } + let(:build) { stub(matrix: [job], finish!: nil) } + + before :each do + request.stubs(:add_build).returns(build) + request.stubs(:config).returns('.result' => 'parse_error') + end + + it 'builds the build' do + request.expects(:add_build).returns(build) + request.finish + end + + it 'prints an error to the log' do + job.expects(:log_content=) + request.finish + end + end + + describe 'with a config server error' do + let(:job) { stub(start!: nil, finish!: nil, :log_content= => nil) } + let(:build) { stub(matrix: [job], finish!: nil) } + + before :each do + request.stubs(:add_build).returns(build) + request.stubs(:config).returns('.result' => 'server_error') + end + + it 'builds the build' do + request.expects(:add_build).returns(build) + request.finish + end + + it 'prints an error to the log' do + job.expects(:log_content=) + request.finish + end + end + end + + describe 'start!' do + before :each do + request.stubs(:config).returns('.configured' => true) + approval.stubs(:approved?).returns(true) + end + + it 'finally sets the state to finished' do + request.repository.save! + request.repository_id = request.repository.id + request.save! + request.start! + request.reload.should be_finished + end + end + + describe "adding a build" do + before do + request.unstub(:add_build) + Travis.config.notify_on_build_created = true + end + + after do + request.stubs(:add_build) + Travis.config.notify_on_build_created = false + end + + it "should create a build" do + request.save + request.add_build_and_notify.should be_a(Build) + end + + it "should notify the build" do + request.save + Travis::Event.expects(:dispatch).with do |event, *args| + event.should == "build:created" + end + request.add_build_and_notify + end + + it "shouldn't notify the build when the flag is disabled" do + Travis.config.notify_on_build_created = false + request.save + Travis::Event.expects(:dispatch).with { |e, *| e.should == "build:created" }.never + request.add_build_and_notify + end + end +end diff --git a/spec_core/core/model/request_spec.rb b/spec_core/core/model/request_spec.rb new file mode 100644 index 00000000..434d617c --- /dev/null +++ b/spec_core/core/model/request_spec.rb @@ -0,0 +1,155 @@ +require 'spec_helper_core' + +describe Request do + include Support::ActiveRecord + + let(:repo) { Repository.new(owner_name: 'travis-ci', name: 'travis-ci') } + let(:commit) { Commit.new(commit: '12345678') } + let(:request) { Request.new(repository: repo, commit: commit) } + + describe 'config_url' do + before :each do + GH.options.delete(:api_url) + GH.current = nil + end + + after :each do + GH.set api_url: nil + end + + it 'returns the api url to the .travis.yml file on github' do + request.config_url.should == 'https://api.github.com/repos/travis-ci/travis-ci/contents/.travis.yml?ref=12345678' + end + + it 'returns the api url to the .travis.yml file on github with a gh endpoint given' do + GH.set api_url: 'http://localhost/api/v3' + request.config_url.should == 'http://localhost/api/v3/repos/travis-ci/travis-ci/contents/.travis.yml?ref=12345678' + end + end + + describe 'api_request?' do + it 'returns true if the event_type is api' do + request.event_type = 'api' + request.api_request?.should == true + end + + it 'returns false if the event_type is not api' do + request.event_type = 'push' + request.api_request?.should == false + end + end + + describe 'pull_request?' do + it 'returns true if the event_type is pull_request' do + request.event_type = 'pull_request' + request.pull_request?.should == true + end + + it 'returns false if the event_type is not pull_request' do + request.event_type = 'push' + request.pull_request?.should == false + end + end + + describe 'pull_request_title' do + it 'returns the title of the pull request from payload' do + request.event_type = 'pull_request' + request.payload = { 'pull_request' => { 'title' => 'A pull request' } } + + request.pull_request_title.should == 'A pull request' + end + + it 'returns nil for non pull request' do + request.event_type = 'build' + request.payload = { 'pull_request' => { 'title' => 'A pull request' } } + + request.pull_request_title.should be_nil + end + end + + describe 'tag_name' do + it 'returns a tag name if available' do + request.payload = { 'ref' => 'refs/tags/foo' } + + request.tag_name.should == 'foo' + end + + it 'returns nil if a tag name is not available' do + request.payload = { 'ref' => 'refs/heads/foo' } + + request.tag_name.should be_nil + end + end + + describe 'branch_name' do + it 'returns a branch name if available' do + request.payload = { 'ref' => 'refs/heads/foo' } + + request.branch_name.should == 'foo' + end + + it 'returns nil if a branch name is not available' do + request.payload = { 'ref' => 'refs/tags/foo' } + + request.branch_name.should be_nil + end + end + + describe '#head_repo' do + it 'returns a branch name if available' do + request.payload = { 'pull_request' => { 'head' => { 'repo' => { 'full_name' => 'foo/bar' } } } } + + request.head_repo.should == 'foo/bar' + end + + it 'returns nil if this is not a pull request' do + request.payload = { } + + request.head_repo.should be_nil + end + end + + describe '#head_branch' do + it 'returns a branch name if available' do + request.payload = { 'pull_request' => { 'head' => { 'ref' => 'foo' } } } + + request.head_branch.should == 'foo' + end + + it 'returns nil if this is not a pull request' do + request.payload = { } + + request.head_branch.should be_nil + end + end + + describe 'same_repo_pull_request?' do + it 'returns true if the base and head repos match' do + request.payload = { + 'pull_request' => { + 'base' => { 'repo' => { 'full_name' => 'travis-ci/travis-core' } }, + 'head' => { 'repo' => { 'full_name' => 'travis-ci/travis-core' } }, + } + } + + request.same_repo_pull_request?.should be true + end + + it 'returns false if the base and head repos do not match' do + request.payload = { + 'pull_request' => { + 'base' => { 'repo' => { 'full_name' => 'travis-ci/travis-core' } }, + 'head' => { 'repo' => { 'full_name' => 'BanzaiMan/travis-core' } }, + } + } + + request.same_repo_pull_request?.should be false + end + + it 'returns false if repo data is not available' do + request.payload = {} + + request.same_repo_pull_request?.should be_falsy + end + end +end diff --git a/spec_core/core/model/ssl_key_spec.rb b/spec_core/core/model/ssl_key_spec.rb new file mode 100644 index 00000000..a621e301 --- /dev/null +++ b/spec_core/core/model/ssl_key_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper_core' + +describe SslKey do + include Support::ActiveRecord + + let(:key) { SslKey.new } + + before(:each) do + key.generate_keys + end + + it "is a SslKey" do + key.should be_a(SslKey) + end + + describe "generate_keys" do + it "generates the public key" do + key.public_key.should be_a(String) + end + + it "generates the private key" do + key.private_key.should be_a(String) + end + + it "does not generate a new public key if one already exists" do + public_key = key.public_key + key.generate_keys + key.public_key.should == public_key + end + + it "does not generate a new private key if one already exists" do + private_key = key.private_key + key.generate_keys + key.private_key.should == private_key + end + end + + describe "generate_keys!" do + it "generates a new public key even if one already exists" do + public_key = key.public_key + key.generate_keys! + key.public_key.should_not == public_key + end + + it "generates a new private key even if one already exists" do + private_key = key.private_key + key.generate_keys! + key.private_key.should_not == private_key + end + end + + describe "encrypt" do + it "encrypts something" do + key.encrypt("hello").should_not be_nil + key.encrypt("hello").should_not eql("hello") + end + + it "is decryptable" do + encrypted = key.encrypt("hello") + key.decrypt(encrypted).should eql("hello") + end + end + + describe "decrypt" do + it "decrypts something" do + encrypted_string = key.encrypt("hello world") + key.decrypt(encrypted_string).should_not be_nil + key.decrypt(encrypted_string).should_not eql("hello") + end + end + + describe 'encoding' do + SSL_KEYS = { + :public_key => "-----BEGIN PUBLIC KEY-----\nMDwwDQYJKoZIhvcNAQEBBQADKwAwKAIhALlyZuHmCjZf8pGCUmqz1NESpeVMJoes\nWQblf1p2WhnZAgMBAAE=\n-----END PUBLIC KEY-----\n", + :private_key => "-----BEGIN RSA PRIVATE KEY-----\nMIGrAgEAAiEAuXJm4eYKNl/ykYJSarPU0RKl5Uwmh6xZBuV/WnZaGdkCAwEAAQIg\nVHk9Tjd4fW5VU1z25+4EyXQNnMvaJGr0vP/iG2xSRpECEQD0k/AbOvzsxT5KDXP9\nnsxNAhEAwhuFRSrB1ef6EIPEyLDZvQIRAMGkH4ZvvbD4uciHvj4fbEECEBAl0fRr\nFi0BW2A8VgaMD9ECEQCYSndvz+Vw6SnR9YqElWqc\n-----END RSA PRIVATE KEY-----\n", + :public_base64 => "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAIQC5cmbh5go2X/KRglJqs9TREqXlTCaHrFkG5X9adloZ2Q==", + :private_base64 => "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUdyQWdFQUFpRUF1\nWEptNGVZS05sL3lrWUpTYXJQVTBSS2w1VXdtaDZ4WkJ1Vi9XblphR2RrQ0F3\nRUFBUUlnClZIazlUamQ0Zlc1VlUxejI1KzRFeVhRTm5NdmFKR3IwdlAvaUcy\neFNScEVDRVFEMGsvQWJPdnpzeFQ1S0RYUDkKbnN4TkFoRUF3aHVGUlNyQjFl\nZjZFSVBFeUxEWnZRSVJBTUdrSDRadnZiRDR1Y2lIdmo0ZmJFRUNFQkFsMGZS\ncgpGaTBCVzJBOFZnYU1EOUVDRVFDWVNuZHZ6K1Z3NlNuUjlZcUVsV3FjCi0t\nLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==" + } + + let(:key) { SslKey.new(SSL_KEYS.slice(:private_key, :public_key)) } + + it 'generates the correct key format to export to github' do + key.encoded_public_key.should == SSL_KEYS[:public_base64] + end + + it 'encodes the private key properly for the build' do + key.encoded_private_key.should == SSL_KEYS[:private_base64] + end + end +end diff --git a/spec_core/core/model/token_spec.rb b/spec_core/core/model/token_spec.rb new file mode 100644 index 00000000..3bdd17ac --- /dev/null +++ b/spec_core/core/model/token_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper_core' + +describe Token do + include Support::ActiveRecord + + it 'generate_token sets the token to a 20 character value' do + Token.new.send(:generate_token).length.should == 20 + end + + it 'does not generate new token on save' do + token = Token.create! + + expect { + token.save + }.to_not change { token.token } + end +end diff --git a/spec_core/core/model/url_spec.rb b/spec_core/core/model/url_spec.rb new file mode 100644 index 00000000..9001ab4f --- /dev/null +++ b/spec_core/core/model/url_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper_core' + +describe Url do + include Support::ActiveRecord + + subject { Url.create(:url => "http://example.com") } + + describe ".shorten" do + it "creates a new Url object if the url has not been shortened" do + expect { Url.shorten("http://example.com") }.to change(Url, :count).from(0).to(1) + end + + it "retrieves a Url which has already been shortened" do + Url.shorten("http://example.com") + expect { Url.shorten("http://example.com") }.not_to change(Url, :count) + end + end + + describe "#code" do + it "sets the code automatically" do + subject.code.should_not be_nil + end + end + + describe "#short_url" do + it "returns the full short url" do + subject.short_url.should match(%r(^http://trvs.io/\w{10}$)) + end + end + +end diff --git a/spec_core/core/model/user/oauth_spec.rb b/spec_core/core/model/user/oauth_spec.rb new file mode 100644 index 00000000..39920520 --- /dev/null +++ b/spec_core/core/model/user/oauth_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper_core' + +describe User::Oauth do + include Support::ActiveRecord + + let(:user) { Factory(:user, :github_oauth_token => 'token') } + let(:payload) { GITHUB_PAYLOADS[:oauth] } + + describe 'find_or_create_by' do + def call(payload) + User::Oauth.find_or_create_by(payload) + end + + it 'marks users as recently_signed_up' do + call(payload).should be_recently_signed_up + end + + it 'does not mark existing users as recently_signed_up' do + call(payload) + call(payload).should_not be_recently_signed_up + end + + it 'updates changed attributes' do + call(payload).attributes.slice(*GITHUB_OAUTH_DATA.keys).should == GITHUB_OAUTH_DATA + end + end + + describe 'attributes_from' do + it 'returns required data' do + User::Oauth.attributes_from(payload).should == GITHUB_OAUTH_DATA + end + end +end diff --git a/spec_core/core/model/user_spec.rb b/spec_core/core/model/user_spec.rb new file mode 100644 index 00000000..907e39e2 --- /dev/null +++ b/spec_core/core/model/user_spec.rb @@ -0,0 +1,236 @@ +require 'spec_helper_core' + +describe User, truncation: true do + include Support::ActiveRecord + + let(:user) { Factory(:user, :github_oauth_token => 'token') } + let(:payload) { GITHUB_PAYLOADS[:oauth] } + + describe 'find_or_create_for_oauth' do + def user(payload) + User.find_or_create_for_oauth(payload) + end + + it 'marks new users as such' do + user(payload).should be_recently_signed_up + user(payload).should_not be_recently_signed_up + end + + it 'updates changed attributes' do + user(payload).attributes.slice(*GITHUB_OAUTH_DATA.keys).should == GITHUB_OAUTH_DATA + end + end + + describe '#to_json' do + it 'returns JSON representation of user' do + json = JSON.parse(user.to_json) + json['user']['login'].should == 'svenfuchs' + end + end + + describe 'permission?' do + let!(:repo) { Factory(:org, :login => 'travis') } + + it 'given roles and a condition it returns true if the user has a matching permission for this role' do + user.permissions.create!(push: true, repository_id: repo.id) + user.permission?(['push'], repository_id: repo.id).should be true + end + + it 'given roles and a condition it returns false if the user does not have a matching permission for this role' do + user.permissions.create!(pull: true, repository_id: repo.id) + user.permission?(['push'], repository_id: repo.id).should be false + end + + it 'given a condition it returns true if the user has a matching permission' do + user.permissions.create!(push: true, repository_id: repo.id) + user.permission?(repository_id: repo.id).should be true + end + + it 'given a condition it returns true if the user has a matching permission' do + user.permission?(repository_id: repo.id).should be false + end + end + + describe 'organization_ids' do + let!(:travis) { Factory(:org, :login => 'travis') } + let!(:sinatra) { Factory(:org, :login => 'sinatra') } + + before :each do + user.organizations << travis + user.save! + end + + it 'contains the ids of organizations that the user is a member of' do + user.organization_ids.should include(travis.id) + end + + it 'does not contain the ids of organizations that the user is not a member of' do + user.organization_ids.should_not include(sinatra.id) + end + end + + describe 'repository_ids' do + let!(:travis) { Factory(:repository, :name => 'travis', :owner => Factory(:org, :name => 'travis')) } + let!(:sinatra) { Factory(:repository, :name => 'sinatra', :owner => Factory(:org, :name => 'sinatra')) } + + before :each do + user.repositories << travis + user.save! + user.reload + end + + it 'contains the ids of repositories the user is permitted to see' do + user.repository_ids.should include(travis.id) + end + + it 'does not contain the ids of repositories the user is not permitted to see' do + user.repository_ids.should_not include(sinatra.id) + end + end + + describe 'profile_image_hash' do + it "returns gravatar_id if it's present" do + user.gravatar_id = '41193cdbffbf06be0cdf231b28c54b18' + user.profile_image_hash.should == '41193cdbffbf06be0cdf231b28c54b18' + end + + it 'returns a MD5 hash of the email if no gravatar_id and an email is set' do + user.gravatar_id = nil + user.profile_image_hash.should == Digest::MD5.hexdigest(user.email) + end + + it 'returns 32 zeros if no gravatar_id or email is set' do + user.gravatar_id = nil + user.email = nil + user.profile_image_hash.should == '0' * 32 + end + end + + describe 'authenticate_by' do + describe 'given a valid token and login' do + it 'authenticates the user' do + User.authenticate_by('login' => user.login, 'token' => user.tokens.first.token).should == user + end + end + + describe 'given a wrong token' do + it 'does not authenticate the user' do + User.authenticate_by('login' => 'someone-else', 'token' => user.tokens.first.token).should be_nil + end + end + + describe 'given a wrong login' do + it 'does not authenticate the user' do + User.authenticate_by('login' => user.login, 'token' => 'some-other-token').should be_nil + end + end + + describe 'with encrypted token' do + it 'authenticates the user' do + user.tokens.first.update_column :token, 'encrypted-token' + + Travis::Model::EncryptedColumn.any_instance.stubs(:encrypt? => true, :key => 'abcd', :load => '...') + Travis::Model::EncryptedColumn.any_instance.expects(:load).with('encrypted-token').returns('a-token') + + User.authenticate_by('login' => user.login, 'token' => 'a-token').should == user + end + end + end + + describe 'service_hooks' do + let(:own_repo) { Factory(:repository, :name => 'own-repo', :description => 'description', :active => true) } + let(:admin_repo) { Factory(:repository, :name => 'admin-repo') } + let(:other_repo) { Factory(:repository, :name => 'other-repo') } + let(:push_repo) { Factory(:repository, :name => 'push-repo') } + + before :each do + user.permissions.create! :user => user, :repository => own_repo, :admin => true + user.permissions.create! :user => user, :repository => admin_repo, :admin => true + user.permissions.create! :user => user, :repository => push_repo, :push => true + other_repo + end + + it "contains repositories where the user has an admin role" do + user.service_hooks.should include(own_repo) + end + + it "does not contain repositories where the user does not have an admin role" do + user.service_hooks.should_not include(other_repo) + end + + it "includes all repositories if :all options is passed" do + hooks = user.service_hooks(:all => true) + hooks.should include(own_repo) + hooks.should include(push_repo) + hooks.should include(admin_repo) + hooks.should_not include(other_repo) + end + end + + describe 'track_github_scopes' do + before { user.save! } + + it "does not resolve github scopes if the token didn't change" do + Travis::Github.expects(:scopes_for).never + user.save! + end + + it "it resolves github scopes if the token did change" do + Travis::Github.expects(:scopes_for).with(user).returns(['foo', 'bar']) + user.github_oauth_token = 'new_token' + user.save! + user.github_scopes.should be == ['foo', 'bar'] + end + + it "it resolves github scopes if they haven't been resolved already" do + Travis::Github.expects(:scopes_for).with(user).returns(['foo', 'bar']) + user.github_scopes = nil + user.save! + user.github_scopes.should be == ['foo', 'bar'] + end + + it 'returns an empty list if the token is missing' do + user.github_scopes = ['foo'] + user.github_oauth_token = nil + user.github_scopes.should be_empty + end + end + + describe 'correct_scopes?' do + it "accepts correct scopes" do + user.should be_correct_scopes + end + + it "complains about missing scopes" do + user.github_scopes.pop + user.should_not be_correct_scopes + end + + it "accepts additional scopes" do + user.github_scopes << "foo" + user.should be_correct_scopes + end + end + + describe 'inspect' do + context 'when user has GitHub OAuth token' do + before :each do + user.github_oauth_token = 'foobarbaz' + end + + it 'does not include the user\'s GitHub OAuth token' do + user.inspect.should_not include('foobarbaz') + end + end + + context 'when user has no GitHub OAuth token' do + before :each do + user.github_oauth_token = nil + end + + it 'indicates nil GitHub OAuth token' do + user.inspect.should include('github_oauth_token: nil') + end + end + end +end diff --git a/spec_core/core/services/cancel_build_spec.rb b/spec_core/core/services/cancel_build_spec.rb new file mode 100644 index 00000000..9a708b29 --- /dev/null +++ b/spec_core/core/services/cancel_build_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Travis::Services::CancelBuild do + include Support::ActiveRecord + + let(:repo) { Factory(:repository) } + let!(:job) { Factory(:test, repository: repo, state: :created) } + let!(:passed_job) { Factory(:test, repository: repo, state: :passed) } + let(:build) { Factory(:build, repository: repo) } + let(:params) { { id: build.id, source: 'tests' } } + let(:user) { Factory(:user) } + let(:service) { described_class.new(user, params) } + + before do + build.matrix.destroy_all + build.matrix << passed_job + build.matrix << job + end + + describe 'run' do + it 'should cancel the build if it\'s cancelable' do + job.stubs(:cancelable?).returns(true) + service.stubs(:authorized?).returns(true) + + publisher = mock('publisher') + service.stubs(:publisher).returns(publisher) + publisher.expects(:publish).with(type: 'cancel_job', job_id: job.id, source: 'tests') + publisher.expects(:publish).with(type: 'cancel_job', job_id: passed_job.id, source: 'tests') + + expect { + expect { + service.run + }.to change { build.reload.state } + }.to change { job.reload.state } + + job.state.should == 'canceled' + build.state.should == 'canceled' + end + + it 'should not cancel the job if it\'s not cancelable' do + job.stubs(:cancelable?).returns(false) + + expect { + service.run + }.to_not change { build.reload.state } + end + + it 'should not be able to cancel job if user does not have any permissions' do + user.permissions.destroy_all + + service.can_cancel?.should be false + end + end +end diff --git a/spec_core/core/services/cancel_job_spec.rb b/spec_core/core/services/cancel_job_spec.rb new file mode 100644 index 00000000..abf5bb2b --- /dev/null +++ b/spec_core/core/services/cancel_job_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe Travis::Services::CancelJob do + include Support::ActiveRecord + + let(:repo) { Factory(:repository) } + let!(:job) { Factory(:test, repository: repo, state: :created) } + let(:params) { { id: job.id, source: 'tests' } } + let(:user) { Factory(:user) } + let(:service) { described_class.new(user, params) } + + describe 'run' do + it 'should cancel the job if it\'s cancelable' do + job.stubs(:cancelable?).returns(true) + service.stubs(:authorized?).returns(true) + + publisher = mock('publisher') + service.stubs(:publisher).returns(publisher) + publisher.expects(:publish).with(type: 'cancel_job', job_id: job.id, source: 'tests') + + expect { + service.run + }.to change { job.reload.state } + + job.state.should == 'canceled' + end + + it 'should not cancel the job if it\'s not cancelable' do + job.state.should == :created + job.stubs(:cancelable?).returns(false) + + expect { + service.run + }.to_not change { job.state } + end + + it 'should not be able to cancel job if user does not have pull permission' do + user.permissions.destroy_all + + service.can_cancel?.should be false + end + end +end + diff --git a/spec_core/core/services/find_admin_spec.rb b/spec_core/core/services/find_admin_spec.rb new file mode 100644 index 00000000..e4e20aa2 --- /dev/null +++ b/spec_core/core/services/find_admin_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe Travis::Services::FindAdmin do + include Travis::Testing::Stubs + + describe 'find' do + let(:result) { described_class.new(nil, repository: repository).run } + + before :each do + User.stubs(:with_permissions).with(:repository_id => repository.id, :admin => true).returns [user] + end + + describe 'given a user has admin access to a repository (as seen by github)' do + before :each do + GH.stubs(:[]).with("repos/#{repository.slug}").returns('permissions' => { 'admin' => true }) + end + + it 'returns that user' do + result.should == user + end + end + + describe 'given a user does not have access to a repository' do + before :each do + GH.stubs(:[]).with("repos/#{repository.slug}").returns('permissions' => { 'admin' => false }) + user.stubs(:update_attributes!) + end + + xit 'raises an exception' do + lambda { result }.should raise_error(Travis::AdminMissing, 'no admin available for svenfuchs/minimal') + end + + xit 'revokes admin permissions for that user on our side' do + user.expects(:update_attributes!).with(:permissions => { 'admin' => false }) + ignore_exception { result } + end + end + + describe 'given an error occurs while retrieving the repository info' do + let(:error) { stub('error', :backtrace => [], :response => stub('reponse')) } + + before :each do + GH.stubs(:[]).with("repos/#{repository.slug}").raises(GH::Error.new(error)) + end + + xit 'raises an exception' do + lambda { result }.should raise_error(Travis::AdminMissing, 'no admin available for svenfuchs/minimal') + end + + it 'does not revoke permissions' do + user.expects(:update_permissions!).never + ignore_exception { result } + end + end + + describe 'missing repository' do + it 'raises Travis::RepositoryMissing' do + expect { described_class.new.run }.to raise_error(Travis::RepositoryMissing) + end + end + + def ignore_exception(&block) + block.call + rescue Travis::AdminMissing + end + end +end + +describe Travis::Services::FindAdmin::Instrument do + include Travis::Testing::Stubs + + let(:publisher) { Travis::Notification::Publisher::Memory.new } + let(:event) { publisher.events[1] } + let(:service) { Travis::Services::FindAdmin.new(nil, repository: repository) } + + before :each do + Travis::Notification.publishers.replace([publisher]) + User.stubs(:with_permissions).with(repository_id: repository.id, admin: true).returns [user] + GH.stubs(:[]).with("repos/#{repository.slug}").returns('permissions' => { 'admin' => true }) + service.run + end + + it 'publishes a event' do + event.should publish_instrumentation_event( + event: 'travis.services.find_admin.run:completed', + message: 'Travis::Services::FindAdmin#run:completed for svenfuchs/minimal: svenfuchs', + result: user, + ) + end +end diff --git a/spec_core/core/services/find_annotation_spec.rb b/spec_core/core/services/find_annotation_spec.rb new file mode 100644 index 00000000..9b9fa77c --- /dev/null +++ b/spec_core/core/services/find_annotation_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Travis::Services::FindAnnotations do + include Support::ActiveRecord + + let(:job) { Factory(:test) } + let!(:annotation) { Factory(:annotation, job: job) } + let(:service) { described_class.new(params) } + + attr_reader :params + + describe 'run' do + it 'finds annotations by a given list of ids' do + @params = { ids: [annotation.id] } + service.run.should eq([annotation]) + end + + it 'finds annotations by job_id' do + @params = { job_id: job.id } + service.run.should eq([annotation]) + end + end +end diff --git a/spec_core/core/services/find_branch_spec.rb b/spec_core/core/services/find_branch_spec.rb new file mode 100644 index 00000000..0394e074 --- /dev/null +++ b/spec_core/core/services/find_branch_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Travis::Services::FindBranch do + include Support::ActiveRecord + + let(:repo) { Factory(:repository, :owner_name => 'travis-ci', :name => 'travis-core') } + let!(:build) { Factory(:build, :repository => repo, :state => :finished) } + let(:service) { described_class.new(stub('user'), params) } + + attr_reader :params + + it 'finds the last builds of the given repository and branch' do + @params = { :repository_id => repo.id, :branch => 'master' } + service.run.should be == build + end + + it 'scopes to the given repository' do + @params = { :repository_id => repo.id, :branch => 'master' } + build = Factory(:build, :repository => Factory(:repository), :state => :finished) + service.run.should_not be == build + end + + it 'returns an empty build scope when the repository could not be found' do + @params = { :repository_id => repo.id + 1, :branch => 'master' } + service.run.should be_nil + end + + it 'finds branches by a given id' do + @params = { :id => build.id } + service.run.should be == build + end +end diff --git a/spec_core/core/services/find_branches_spec.rb b/spec_core/core/services/find_branches_spec.rb new file mode 100644 index 00000000..d9ca70f2 --- /dev/null +++ b/spec_core/core/services/find_branches_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Travis::Services::FindBranches do + include Support::ActiveRecord + + let(:repo) { Factory(:repository, :owner_name => 'travis-ci', :name => 'travis-core') } + let!(:build) { Factory(:build, :repository => repo, :state => :finished) } + let(:service) { described_class.new(stub('user'), params) } + + attr_reader :params + + it 'finds the last builds of the given repository grouped per branch' do + @params = { :repository_id => repo.id } + service.run.should include(build) + end + + it 'scopes to the given repository' do + @params = { :repository_id => repo.id } + build = Factory(:build, :repository => Factory(:repository), :state => :finished) + service.run.should_not include(build) + end + + it 'returns an empty build scope when the repository could not be found' do + @params = { :repository_id => repo.id + 1 } + service.run.should == Build.none + end + + it 'finds branches by a given list of ids' do + @params = { :ids => [build.id] } + service.run.should == [build] + end +end diff --git a/spec_core/core/services/find_build_spec.rb b/spec_core/core/services/find_build_spec.rb new file mode 100644 index 00000000..ca229e80 --- /dev/null +++ b/spec_core/core/services/find_build_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe Travis::Services::FindBuild do + include Support::ActiveRecord + + let(:repo) { Factory(:repository, owner_name: 'travis-ci', name: 'travis-core') } + let!(:build) { Factory(:build, repository: repo, state: :finished, number: 1, config: {'sudo' => false}) } + let(:params) { { id: build.id } } + let(:service) { described_class.new(stub('user'), params) } + + describe 'run' do + it 'finds a build by the given id' do + service.run.should == build + end + + it 'does not raise if the build could not be found' do + @params = { :id => build.id + 1 } + lambda { service.run }.should_not raise_error + end + + it 'includes config by default' do + service.run.config.should include(:sudo) + end + + it 'excludes config when requested' do + params[:exclude_config] = '1' + service.run.config.should_not include(:sudo) + end + end + + describe 'updated_at' do + it 'returns builds updated_at attribute' do + service.updated_at.to_s.should == build.updated_at.to_s + end + end + + describe 'with newer associated record' do + it 'returns updated_at of newest result' do + build.update_attribute(:updated_at, 5.minutes.ago) + build.reload.updated_at.should < build.matrix.first.updated_at + service.updated_at.to_s.should == build.matrix.first.updated_at.to_s + end + end + + describe 'without updated_at in one of the resources' do + it 'returns updated_at of newest result' do + Build.any_instance.stubs(updated_at: nil) + expect { + service.updated_at + }.to_not raise_error + end + end + + # TODO builds can be requeued, so finished builds are no more final + # + # describe 'final?' do + # it 'returns true if the build is finished' do + # build.update_attributes!(:state => :errored) + # service.final?.should be_truthy + # end + + # it 'returns false if the build is not finished' do + # build.update_attributes!(:state => :started) + # service.final?.should be false + # end + # end +end diff --git a/spec_core/core/services/find_builds_spec.rb b/spec_core/core/services/find_builds_spec.rb new file mode 100644 index 00000000..4f7e74c5 --- /dev/null +++ b/spec_core/core/services/find_builds_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe Travis::Services::FindBuilds do + include Support::ActiveRecord + + let(:repo) { Factory(:repository, owner_name: 'travis-ci', name: 'travis-core') } + let!(:push) { Factory(:build, repository: repo, event_type: 'push', state: :failed, number: 1) } + let(:service) { described_class.new(stub('user'), params) } + + attr_reader :params + + describe 'run' do + it 'finds recent builds when empty params given' do + @params = { :repository_id => repo.id } + service.run.should == [push] + end + + it 'finds running builds when running param is passed' do + running = Factory(:build, repository: repo, event_type: 'push', state: 'started', number: 2) + @params = { :running => true } + service.run.should == [running] + end + + it 'finds recent builds when no repo given' do + @params = nil + service.run.should == [push] + end + + it 'finds builds older than the given number' do + @params = { :repository_id => repo.id, :after_number => 2 } + service.run.should == [push] + end + + it 'finds builds with a given number, scoped by repository' do + @params = { :repository_id => repo.id, :number => 1 } + Factory(:build, :repository => Factory(:repository), :state => :finished, :number => 1) + Factory(:build, :repository => repo, :state => :finished, :number => 2) + service.run.should == [push] + end + + it 'does not find by number if repository_id is missing' do + @params = { :number => 1 } + service.run.should == Build.none + end + + it 'scopes to the given repository_id' do + @params = { :repository_id => repo.id } + Factory(:build, :repository => Factory(:repository), :state => :finished) + service.run.should == [push] + end + + it 'returns an empty build scope when the repository could not be found' do + @params = { :repository_id => repo.id + 1 } + service.run.should == Build.none + end + + it 'finds builds by a given list of ids' do + @params = { :ids => [push.id] } + service.run.should == [push] + end + + describe 'finds recent builds when event_type' do + let!(:pull_request) { Factory(:build, repository: repo, state: :finished, number: 2, request: Factory(:request, :event_type => 'pull_request')) } + let!(:api) { Factory(:build, repository: repo, state: :finished, number: 2, request: Factory(:request, :event_type => 'api')) } + + it 'given as push' do + @params = { repository_id: repo.id, event_type: 'push' } + service.run.should == [push] + end + + it 'given as pull_request' do + @params = { repository_id: repo.id, event_type: 'pull_request' } + service.run.should == [pull_request] + end + + it 'given as api' do + @params = { repository_id: repo.id, event_type: 'api' } + service.run.should == [api] + end + + it 'given as [push, api]' do + @params = { repository_id: repo.id, event_type: ['push', 'api'] } + service.run.sort.should == [push, api] + end + end + end +end diff --git a/spec_core/core/services/find_caches_spec.rb b/spec_core/core/services/find_caches_spec.rb new file mode 100644 index 00000000..a6bc5dc0 --- /dev/null +++ b/spec_core/core/services/find_caches_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +describe Travis::Services::FindCaches do + include Support::ActiveRecord, Support::S3, Support::GCS + + let(:user) { User.first || Factory(:user) } + let(:service) { described_class.new(user, params) } + let(:repo) { Factory(:repository, :owner_name => 'travis-ci', :name => 'travis-core') } + let(:cache_options) {{ s3: { bucket_name: '' , access_key_id: '', secret_access_key: ''} }} + let(:has_access) { true } + let(:result) { service.run } + subject { result } + + before :each do + Travis.config.roles = {} + Travis.config.cache_options = cache_options + user.stubs(:permission?).returns(has_access) + end + + describe 'given a repository_id' do + let(:params) {{ repository_id: repo.id }} + + describe 'without any caches' do + it { should be == [] } + end + + describe 'with caches' do + before do + s3_bucket << "#{repo.github_id}/master/cache--example1.tbz" + s3_bucket << "#{repo.github_id}/other/cache--example2.tbz" + s3_bucket << "#{repo.github_id.succ}/master/cache--example3.tbz" + end + + its(:size) { should be == 2 } + + describe 'the cache instances' do + subject { result.first } + its(:slug) { should be == 'cache--example1' } + its(:branch) { should be == 'master' } + its(:repository) { should be == repo } + its(:size) { should be == 0 } + end + + describe 'with branch' do + let(:params) {{ repository_id: repo.id, branch: 'other' }} + its(:size) { should be == 1 } + end + + describe 'with match' do + let(:params) {{ repository_id: repo.id, match: 'example1' }} + its(:size) { should be == 1 } + end + + describe 'without access' do + let(:has_access) { false } + its(:size) { should be == 0 } + end + + describe 'without s3 credentials' do + let(:cache_options) {{ }} + before { service.logger.expects(:warn).with("[services:find-caches] cache settings incomplete") } + it { should be == [] } + end + + describe 'with multiple buckets' do + let(:cache_options) {[{ s3: { bucket_name: '', access_key_id: '', secret_access_key: '' } }, { s3: { bucket_name: '', access_key_id: '', secret_access_key: '' } }]} + its(:size) { should be == 4 } + end + end + + context 'with GCS configuration' do + let(:cache_options) { { gcs: { bucket_name: '', json_key: '' } } } + its(:size) { should be == 0 } + end + end +end \ No newline at end of file diff --git a/spec_core/core/services/find_daily_repos_stats_spec.rb b/spec_core/core/services/find_daily_repos_stats_spec.rb new file mode 100644 index 00000000..63f1cbdb --- /dev/null +++ b/spec_core/core/services/find_daily_repos_stats_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' +require 'travis/testing/scenario' + +describe Travis::Services::FindDailyReposStats do + include Support::ActiveRecord + + let(:service) { described_class.new(stub('user'), {}) } + + before { Scenario.default } + + it 'should include the date' do + stats = service.run + expect(stats.size).to eq(1) + stats.first['date'].should == Repository.first.created_at.to_date.to_s(:date) + end + + it 'should include the number per day' do + stats = service.run + expect(stats.size).to eq(1) + stats.first['count'].to_i.should == 2 + end +end diff --git a/spec_core/core/services/find_daily_tests_stats_spec.rb b/spec_core/core/services/find_daily_tests_stats_spec.rb new file mode 100644 index 00000000..a9aa43c7 --- /dev/null +++ b/spec_core/core/services/find_daily_tests_stats_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' +require 'travis/testing/scenario' + +describe Travis::Services::FindDailyTestsStats do + include Support::ActiveRecord + + let(:service) { described_class.new(stub('user'), {}) } + + before { Scenario.default } + + it 'should return the jobs per day' do + stats = service.run + expect(stats.size).to eq(1) + stats.first['date'].should == Job.first.created_at.to_date.to_s(:date) + stats.first['count'].to_i.should == 13 + end +end diff --git a/spec_core/core/services/find_hooks_spec.rb b/spec_core/core/services/find_hooks_spec.rb new file mode 100644 index 00000000..6415f211 --- /dev/null +++ b/spec_core/core/services/find_hooks_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Travis::Services::FindHooks do + include Support::ActiveRecord + + let(:user) { User.first || Factory(:user) } + let(:repo) { Factory(:repository) } + let(:push_repo) { Factory(:repository, name: 'push-repo') } + let(:service) { described_class.new(user, params) } + + before :each do + user.permissions.create!(:repository => repo, :admin => true) + user.permissions.create!(:repository => push_repo, :push => true) + end + + attr_reader :params + + it 'finds repositories where the current user has access with :all option' do + @params = { all: true } + hooks = service.run + hooks.should include(repo) + hooks.should include(push_repo) + expect(hooks.size).to eq(2) + + # hooks should include admin information + hooks.sort_by(&:id).map(&:admin?).should == [true, false] + end + + it 'finds repositories where the current user has admin access' do + @params = {} + service.run.should include(repo) + end + + it 'does not find repositories where the current user does not have admin access' do + @params = {} + user.permissions.delete_all + service.run.should_not include(repo) + end + + it 'finds repositories by a given owner_name where the current user has admin access' do + @params = { :owner_name => repo.owner_name } + service.run.should include(repo) + end + + it 'does not find repositories by a given owner_name where the current user does not have admin access' do + @params = { :owner_name => 'rails' } + service.run.should_not include(repo) + end +end diff --git a/spec_core/core/services/find_job_spec.rb b/spec_core/core/services/find_job_spec.rb new file mode 100644 index 00000000..85d954b5 --- /dev/null +++ b/spec_core/core/services/find_job_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe Travis::Services::FindJob do + include Support::ActiveRecord + + let(:repo) { Factory(:repository) } + let!(:job) { Factory(:test, repository: repo, state: :created, queue: 'builds.linux', config: {'sudo' => false}) } + let(:params) { { id: job.id } } + let(:service) { described_class.new(stub('user'), params) } + + describe 'run' do + it 'finds the job with the given id' do + @params = { id: job.id } + service.run.should == job + end + + it 'does not raise if the job could not be found' do + @params = { id: job.id + 1 } + lambda { service.run }.should_not raise_error + end + + it 'raises RecordNotFound if a SubclassNotFound error is raised during find' do + find_by_id = stub.tap do |s| + s.stubs(:column_names).returns(%w(id config)) + s.stubs(:select).returns(s) + s.stubs(:find_by_id).raises(ActiveRecord::SubclassNotFound) + end + service.stubs(:scope).returns(find_by_id) + lambda { service.run }.should raise_error(ActiveRecord::RecordNotFound) + end + + it 'includes config by default' do + service.run.config.should include(:sudo) + end + + it 'excludes config when requested' do + params[:exclude_config] = '1' + service.run.config.should_not include(:sudo) + end + end + + describe 'updated_at' do + it 'returns jobs updated_at attribute' do + service.updated_at.to_s.should == job.updated_at.to_s + end + end + + # TODO jobs can be requeued, so finished jobs are no more final + # + # describe 'final?' do + # it 'returns true if the job is finished' do + # job.update_attributes!(state: :errored) + # service.final?.should be_truthy + # end + + # it 'returns false if the job is not finished' do + # job.update_attributes!(state: :started) + # service.final?.should be false + # end + # end +end diff --git a/spec_core/core/services/find_jobs_spec.rb b/spec_core/core/services/find_jobs_spec.rb new file mode 100644 index 00000000..3b71e271 --- /dev/null +++ b/spec_core/core/services/find_jobs_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe Travis::Services::FindJobs do + include Support::ActiveRecord + + let(:repo) { Factory(:repository) } + let!(:job) { Factory(:test, :repository => repo, :state => :created, :queue => 'builds.linux') } + let(:service) { described_class.new(stub('user'), params) } + + attr_reader :params + + describe 'run' do + it 'finds jobs on the given queue' do + @params = { :queue => 'builds.linux' } + service.run.should include(job) + end + + it 'does not find jobs on other queues' do + @params = { :queue => 'builds.nodejs' } + service.run.should_not include(job) + end + + it 'finds jobs by a given list of ids' do + @params = { :ids => [job.id] } + service.run.should == [job] + end + + it 'finds jobs by state' do + build = Factory(:build) + + Job::Test.destroy_all + + started = Factory(:test, :state => :started, :source => build) + passed = Factory(:test, :state => :passed, :source => build) + created = Factory(:test, :state => :created, :source => build) + + @params = { :state => ['created', 'passed'] } + service.run.sort_by(&:id).should == [created, passed].sort_by(&:id) + end + + it 'finds jobs that are about to run without any args' do + build = Factory(:build) + + Job::Test.destroy_all + + started = Factory(:test, :state => :started, :source => build) + queued = Factory(:test, :state => :queued, :source => build) + passed = Factory(:test, :state => :passed, :source => build) + created = Factory(:test, :state => :created, :source => build) + received = Factory(:test, :state => :received, :source => build) + + @params = {} + service.run.sort_by(&:id).should == [started, queued, created, received].sort_by(&:id) + end + end + + describe 'updated_at' do + it 'returns the latest updated_at time' do + skip 'rack cache is disabled, so not much need for caching now' + + @params = { :queue => 'builds.linux' } + Job.delete_all + Factory(:test, :repository => repo, :state => :queued, :queue => 'build.common', :updated_at => Time.now - 1.hour) + Factory(:test, :repository => repo, :state => :queued, :queue => 'build.common', :updated_at => Time.now) + service.updated_at.to_s.should == Time.now.to_s + end + end +end diff --git a/spec_core/core/services/find_log_spec.rb b/spec_core/core/services/find_log_spec.rb new file mode 100644 index 00000000..190ff21f --- /dev/null +++ b/spec_core/core/services/find_log_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Travis::Services::FindLog do + include Support::ActiveRecord + + let!(:job) { Factory(:test) } + let(:log) { job.log } + let(:service) { described_class.new(stub('user'), params) } + + attr_reader :params + + describe 'run' do + it 'finds the log with the given id' do + @params = { id: log.id } + service.run.should == log + end + + it 'finds the log with the given job_id' do + @params = { job_id: job.id } + service.run.should == log + end + + it 'does not raise if the log could not be found' do + @params = { id: log.id + 1 } + lambda { service.run }.should_not raise_error + end + end + + # TODO jobs can be requeued, so finished jobs are no more final + # + # describe 'final?' do + # it 'returns true if the job is finished' do + # log.job.update_attributes!(:state => :finished) + # service.final?.should be_truthy + # end + + # it 'returns false if the job is not finished' do + # log.job.update_attributes!(:state => :started) + # service.final?.should be false + # end + # end +end diff --git a/spec_core/core/services/find_repo_key_spec.rb b/spec_core/core/services/find_repo_key_spec.rb new file mode 100644 index 00000000..70c60fa1 --- /dev/null +++ b/spec_core/core/services/find_repo_key_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Travis::Services::FindRepoKey do + include Support::ActiveRecord + + let!(:repo) { Factory(:repository, :owner_name => 'travis-ci', :name => 'travis-core') } + let(:service) { described_class.new(stub('user'), params) } + + attr_reader :params + + describe 'run' do + it 'finds a key by the given repository id' do + @params = { :id => repo.id } + service.run.should == repo.key + end + + it 'finds a key by the given owner_name and name' do + @params = { :owner_name => repo.owner_name, :name => repo.name } + service.run.should == repo.key + end + end + + describe 'updated_at' do + it 'returns key\'s updated_at attribute' do + @params = { :id => repo.id } + service.updated_at.to_s.should == repo.key.updated_at.to_s + end + end +end diff --git a/spec_core/core/services/find_repo_settings_spec.rb b/spec_core/core/services/find_repo_settings_spec.rb new file mode 100644 index 00000000..17842406 --- /dev/null +++ b/spec_core/core/services/find_repo_settings_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Travis::Services::FindRepoSettings do + include Support::ActiveRecord + + let(:repo) { Factory(:repository) } + let(:params) { { id: repo.id } } + let(:user) { Factory(:user) } + let(:service) { described_class.new(user, params) } + + before do + repo.settings.merge('build_pushes' => false) + repo.settings.save + repo.save + end + + describe 'authorized?' do + let(:service) { described_class.new(nil, params) } + + it 'should be unauthorized with current_user' do + service.should_not be_authorized + end + end + + describe 'run' do + it 'should return nil without a repo' do + repo.destroy + service.run.should be_nil + end + + it 'should return repo settings' do + user.permissions.create(repository_id: repo.id, push: true) + service.run.to_hash.should == repo.settings.to_hash + end + + it 'should not be able to get settings if user does not have push permission' do + user.permissions.create(repository_id: repo.id, push: false) + + service.run.should be_nil + end + end +end + diff --git a/spec_core/core/services/find_repo_spec.rb b/spec_core/core/services/find_repo_spec.rb new file mode 100644 index 00000000..d13c8372 --- /dev/null +++ b/spec_core/core/services/find_repo_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Travis::Services::FindRepo do + include Support::ActiveRecord + + let!(:repo) { Factory(:repository, :owner_name => 'travis-ci', :name => 'travis-core') } + let(:service) { described_class.new(stub('user'), params) } + + attr_reader :params + + describe 'run' do + it 'finds a repository by the given id' do + @params = { :id => repo.id } + service.run.should == repo + end + + it 'finds a repository by the given owner_name and name' do + @params = { :owner_name => repo.owner_name, :name => repo.name } + service.run.should == repo + end + + it 'does not raise if the repository could not be found' do + @params = { :id => repo.id + 1 } + lambda { service.run }.should_not raise_error + end + end + + describe 'updated_at' do + it 'returns jobs updated_at attribute' do + @params = { :id => repo.id } + service.updated_at.to_s.should == repo.updated_at.to_s + end + end +end diff --git a/spec_core/core/services/find_repos_spec.rb b/spec_core/core/services/find_repos_spec.rb new file mode 100644 index 00000000..6033f71e --- /dev/null +++ b/spec_core/core/services/find_repos_spec.rb @@ -0,0 +1,127 @@ +require 'spec_helper' + +describe Travis::Services::FindRepos do + include Support::ActiveRecord + + let!(:repo) { Factory(:repository, :owner_name => 'travis-ci', :name => 'travis-core', :active => true) } + let(:service) { described_class.new(stub('user'), params) } + + attr_reader :params + + it 'limits the repositories list' do + Factory(:repository) + @params = { :limit => 1 } + service.run.length.should == 1 + end + + it 'ignores the limit if it is not a number' do + Factory(:repository) + @params = { :limit => 'a' } + service.run.length.should == 2 + end + + it 'does not allow for limit higher than 50' do + @params = { :limit => 60 } + service.send(:limit).should == 50 + end + + it 'finds repositories by a given list of ids' do + @params = { :ids => [repo.id] } + service.run.should == [repo] + end + + it 'returns the recent timeline when given empty params' do + @params = {} + service.run.should include(repo) + end + + it 'applies timeline only if no other params are given' do + repo = Factory(:repository, :owner_name => 'foo', :name => 'bar', :last_build_started_at => nil, :active => true) + @params = { slug: 'foo/bar' } + service.run.should include(repo) + end + + describe 'given a member name' do + it 'finds a repository where that member has permissions' do + @params = { :member => 'joshk' } + repo.users << Factory(:user, :login => 'joshk') + service.run.should include(repo) + end + + it 'does not find a repository where the member does not have permissions' do + @params = { :member => 'joshk' } + service.run.should_not include(repo) + end + + # TODO ... we now include all :active repos (i.e. including those that haven't built yet) + # and last_build_started_at is nil for them, too. since there's no easy way to detect + # queued builds on the repo timeline i'm just disabling this for now. + # + # it 'sorts by latest build, putting queued (no last_build_started_at) at the front' do + # repo.update_column(:last_build_started_at, Time.now - 10) + # queued = Factory(:repository, name: 'queued', last_build_started_at: nil, :active => true) + # just_started = Factory(:repository, name: 'just-started',last_build_started_at: Time.now, :active => true) + # josh = Factory(:user, :login => 'joshk') + # [repo, queued, just_started].each { |r| r.users << josh } + # @params = { :member => 'joshk' } + # service.run.map(&:name).should == [queued, just_started, repo].map(&:name) + # end + end + + describe 'given an owner_name name' do + it 'finds a repository with that owner_name' do + @params = { :owner_name => 'travis-ci' } + service.run.should include(repo) + end + + it 'does not find a repository with another owner name' do + @params = { :owner_name => 'sinatra' } + service.run.should_not include(repo) + end + end + + describe 'given an owner_name name and active param' do + it 'finds a repository with that owner_name even if it does not have any builds' do + repo.update_column(:last_build_id, nil) + repo.update_column(:active, true) + @params = { :owner_name => 'travis-ci', :active => true } + service.run.should include(repo) + end + end + + describe 'given a slug name' do + it 'finds a repository with that slug' do + @params = { :slug => 'travis-ci/travis-core' } + service.run.should include(repo) + end + + it 'does not find a repository with a different slug' do + @params = { :slug => 'travis-ci/travis-hub' } + service.run.should_not include(repo) + end + end + + describe 'given a search phrase' do + it 'finds a repository matching that phrase' do + @params = { :search => 'travis' } + service.run.should include(repo) + end + + it 'does not find a repository that does not match that phrase' do + @params = { :search => 'sinatra' } + service.run.should_not include(repo) + end + end + + describe 'given a list of ids' do + it 'finds included repositories' do + @params = { :ids => [repo.id] } + service.run.should include(repo) + end + + it 'does not find a repositories that are not included' do + @params = { :ids => [repo.id + 1] } + service.run.should_not include(repo) + end + end +end diff --git a/spec_core/core/services/find_request_spec.rb b/spec_core/core/services/find_request_spec.rb new file mode 100644 index 00000000..559c7989 --- /dev/null +++ b/spec_core/core/services/find_request_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Travis::Services::FindRequest do + include Support::ActiveRecord + + let(:repo) { Factory(:repository, :owner_name => 'travis-ci', :name => 'travis-core') } + let!(:request) { Factory(:request, :repository => repo) } + let(:params) { { :id => request.id } } + let(:service) { described_class.new(stub('user'), params) } + + describe 'run' do + it 'finds a request by the given id' do + service.run.should == request + end + + it 'does not raise if the request could not be found' do + @params = { :id => request.id + 1 } + lambda { service.run }.should_not raise_error + end + end + + describe 'updated_at' do + it 'returns request\'s updated_at attribute' do + service.updated_at.to_s.should == request.updated_at.to_s + end + end +end diff --git a/spec_core/core/services/find_requests_spec.rb b/spec_core/core/services/find_requests_spec.rb new file mode 100644 index 00000000..d680c9f1 --- /dev/null +++ b/spec_core/core/services/find_requests_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe Travis::Services::FindRequests do + include Support::ActiveRecord + + let(:repo) { Factory(:repository, :owner_name => 'travis-ci', :name => 'travis-core') } + let!(:request) { Factory(:request, :repository => repo) } + let!(:newer_request) { Factory(:request, :repository => repo) } + let(:service) { described_class.new(stub('user'), params) } + + attr_reader :params + + describe 'run' do + it 'finds recent requests when older_than is not given' do + @params = { :repository_id => repo.id } + service.run.should == [newer_request, request] + end + + it 'finds requests older than the given id' do + @params = { :repository_id => repo.id, :older_than => newer_request.id } + service.run.should == [request] + end + + it 'raises an error if repository params are missing' do + @params = { } + expect { + service.run + }.to raise_error(Travis::RepositoryNotFoundError, "Repository could not be found") + end + + it 'scopes to the given repository_id' do + @params = { :repository_id => repo.id } + Factory(:request, :repository => Factory(:repository)) + service.run.should == [newer_request, request] + end + + it 'raises when the repository could not be found' do + @params = { :repository_id => repo.id + 1 } + expect { + service.run + }.to raise_error(Travis::RepositoryNotFoundError, "Repository with id=#{repo.id + 1} could not be found") + end + + it 'limits requests if limit is passed' do + @params = { :repository_id => repo.id, :limit => 1 } + service.run.should == [newer_request] + end + + it 'limits requests to Travis.config.services.find_requests.max_limit if limit is higher' do + Travis.config.services.find_requests.expects(:max_limit).returns(1) + @params = { :repository_id => repo.id, :limit => 2 } + service.run.should == [newer_request] + end + + it 'limits requests to Travis.config.services.find_requests.default_limit if limit is not given' do + Travis.config.services.find_requests.expects(:default_limit).returns(1) + @params = { :repository_id => repo.id } + service.run.should == [newer_request] + end + end +end diff --git a/spec_core/core/services/find_user_accounts_spec.rb b/spec_core/core/services/find_user_accounts_spec.rb new file mode 100644 index 00000000..44cb3fcf --- /dev/null +++ b/spec_core/core/services/find_user_accounts_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Travis::Services::FindUserAccounts do + include Support::ActiveRecord + + let!(:sven) { Factory(:user, :login => 'sven') } + let!(:travis) { Factory(:org, :login => 'travis-ci') } + let!(:sinatra) { Factory(:org, :login => 'sinatra') } + + let!(:repos) do + Factory(:repository, :owner => sven, :owner_name => 'sven', :name => 'minimal') + Factory(:repository, :owner => travis, :owner_name => 'travis-ci', :name => 'travis-ci') + Factory(:repository, :owner => travis, :owner_name => 'travis-ci', :name => 'travis-core') + Factory(:repository, :owner => sinatra, :owner_name => 'sinatra', :name => 'sinatra') + end + + let(:service) { described_class.new(sven, params || {}) } + + attr_reader :params + + before :each do + Repository.all.each do |repo| + permissions = repo.name == 'sinatra' ? { :push => true } : { :admin => true } + sven.permissions.create!(permissions.merge :repository => repo) + end + + sven.organizations << travis + end + + it 'includes all repositories with :all param' do + @params = { all: true } + service.run.should include(Account.from(sven), Account.from(travis), Account.from(sinatra)) + end + + it 'includes the user' do + service.run.should include(Account.from(sven)) + end + + it 'includes accounts where the user has admin access' do + service.run.should include(Account.from(travis)) + end + + it 'does not include accounts where the user does not have admin access' do + service.run.should_not include(Account.from(sinatra)) + end + + it 'includes repository counts' do + service.run.map(&:repos_count).should == [1, 2] + end +end diff --git a/spec_core/core/services/next_build_number_spec.rb b/spec_core/core/services/next_build_number_spec.rb new file mode 100644 index 00000000..887a958b --- /dev/null +++ b/spec_core/core/services/next_build_number_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' +require 'travis/services/next_build_number' + +describe Travis::Services::NextBuildNumber do + include Support::ActiveRecord + + let(:service) { described_class.new(user, params) } + let!(:user) { Factory(:user) } + let(:result) { service.run } + let(:params) { { repository_id: 1234 } } + let(:repo) do + Factory(:repository, owner_name: 'travis-ci', name: 'travis-core') + end + + subject { result } + + before do + Repository.expects(:find).with(1234).returns(repo) + end + + context 'with a new repository' do + before(:each) { repo.next_build_number = nil } + + it 'returns 1' do + subject.should == 1 + end + + it 'initializes the next_build_number' do + repo.next_build_number.should be_nil + subject + repo.next_build_number.should == 2 + end + end + + context 'with an existing repository' do + let(:repo) do + Factory(:repository, + owner_name: 'travis-ci', name: 'travis-core', next_build_number: 4 + ) + end + + it 'returns the next_build_number' do + subject.should == 4 + end + + it 'increments the next_build_number' do + subject + repo.next_build_number.should == 5 + end + end +end diff --git a/spec_core/core/services/regenerate_repo_key_spec.rb b/spec_core/core/services/regenerate_repo_key_spec.rb new file mode 100644 index 00000000..487839f0 --- /dev/null +++ b/spec_core/core/services/regenerate_repo_key_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Travis::Services::RegenerateRepoKey do + include Support::ActiveRecord + + let(:user) { User.first || Factory(:user) } + let!(:repo) { Factory(:repository, :owner_name => 'travis-ci', :name => 'travis-core') } + let(:service) { described_class.new(user, :id => repo.id) } + + before :each do + service.expects(:service).with(:find_repo, :id => repo.id).returns(stub(:run => repo)) + user.permissions.create!(:repository_id => repo.id, :admin => true) + end + + describe 'given the request is authorized' do + it 'regenerates the key' do + repo.expects(:regenerate_key!) + service.run.should == repo.reload.key + end + end + + describe 'given the request is not authorized' do + it 'does not regenerate key' do + user.permissions.destroy_all + repo.expects(:regenerate_key!).never + service.run.should be_falsey + end + end +end diff --git a/spec_core/core/services/remove_log_spec.rb b/spec_core/core/services/remove_log_spec.rb new file mode 100644 index 00000000..5475d0d5 --- /dev/null +++ b/spec_core/core/services/remove_log_spec.rb @@ -0,0 +1,113 @@ +require 'spec_helper' + +describe Travis::Services::RemoveLog do + include Support::ActiveRecord + + let(:repo) { Factory(:repository) } + let(:job) { Factory(:test, repository: repo, state: :created) } + let(:user) { Factory(:user) } + let(:service) { described_class.new(user, params) } + let(:params) { { id: job.id, reason: 'Because reason!'} } + + context 'when job is not finished' do + before :each do + job.stubs(:finished?).returns false + user.stubs(:permission?).with(:push, anything).returns true + end + + it 'raises JobUnfinished error' do + lambda { + service.run + }.should raise_error Travis::JobUnfinished + end + end + + context 'when user does not have push permissions' do + before :each do + user.stubs(:permission?).with(:push, anything).returns false + end + + it 'raises AuthorizationDenied' do + lambda { + service.run + }.should raise_error Travis::AuthorizationDenied + end + end + + context 'when a job is found' do + before do + find_by_id = stub + find_by_id.stubs(:find_by_id).returns job + job.stubs(:finished?).returns true + service.stubs(:scope).returns find_by_id + user.stubs(:permission?).with(:push, anything).returns true + end + + it 'runs successfully' do + result = service.run + result.removed_by.should == user + result.removed_at.should be_truthy + result.should be_truthy + end + + it "updates logs with desired information" do + service.run + service.log.content.should =~ Regexp.new(user.name) + service.log.content.should =~ Regexp.new(params[:reason]) + end + + it "uses a log part for storing the content" do + service.run + service.log.parts.first.content.should =~ Regexp.new(user.name) + service.log.parts.first.content.should =~ Regexp.new(params[:reason]) + end + + context 'when log is already removed' do + it 'raises LogAlreadyRemoved error' do + service.run + lambda { + service.run + }.should raise_error Travis::LogAlreadyRemoved + end + end + end + + context 'when a job is not found' do + before :each do + find_by_id = stub + find_by_id.stubs(:find_by_id).raises(ActiveRecord::SubclassNotFound) + service.stubs(:scope).returns(find_by_id) + end + + it 'raises ActiveRecord::RecordNotFound exception' do + lambda { service.run }.should raise_error(ActiveRecord::RecordNotFound) + end + end + +end + +describe Travis::Services::RemoveLog::Instrument do + include Support::ActiveRecord + + let(:service) { Travis::Services::RemoveLog.new(user, params) } + let(:repo) { Factory(:repository) } + let(:user) { Factory(:user) } + let(:job) { Factory(:test, repository: repo, state: :passed) } + let(:params) { { id: job.id, reason: 'Because Science!' } } + let(:publisher) { Travis::Notification::Publisher::Memory.new } + let(:event) { publisher.events.last } + + before :each do + Travis::Notification.publishers.replace([publisher]) + service.stubs(:run_service) + user.stubs(:permission?).with(:push, anything).returns true + end + + it 'publishes a event' do + service.run + event.should publish_instrumentation_event( + event: 'travis.services.remove_log.run:completed', + message: "Travis::Services::RemoveLog#run:completed for (svenfuchs)", + ) + end +end diff --git a/spec_core/core/services/reset_model_spec.rb b/spec_core/core/services/reset_model_spec.rb new file mode 100644 index 00000000..c738ec49 --- /dev/null +++ b/spec_core/core/services/reset_model_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper' + +describe Travis::Services::ResetModel do + include Support::ActiveRecord + + let(:user) { User.first || Factory(:user) } + + before :each do + Travis.config.roles = {} + end + + describe 'given a job_id' do + let(:service) { described_class.new(user, job_id: job.id, token: 'token') } + let(:job) { Factory(:test, state: :passed) } + + before :each do + service.stubs(:service).with(:find_job, id: job.id).returns(stub(run: job)) + end + + it 'resets the job' do + user.permissions.create!(repository_id: job.repository_id, pull: true) + job.expects(:reset!) + service.run + end + + it 'has message: all cool' do + user.permissions.create!(repository_id: job.repository_id, pull: true) + service.run + service.messages.should == [{ notice: 'The job was successfully restarted.' }] + end + + it 'has message: missing permissions and can not be enqueued' do + job.stubs(:resetable?).returns(false) + service.run + service.messages.should == [ + { error: 'You do not seem to have sufficient permissions.' }, + { error: 'This job currently can not be restarted.' } + ] + end + end + + describe 'given a build_id' do + let(:service) { described_class.new(user, build_id: build.id, token: 'token') } + let(:build) { Factory(:build, state: 'passed') } + + before :each do + service.stubs(:service).with(:find_build, id: build.id).returns(stub(run: build)) + end + + it 'resets the build (given no roles configuration and the user has permissions)' do + user.permissions.create!(repository_id: build.repository_id, pull: true) + build.expects(:reset!) + service.run + end + + it 'resets the build (given roles configuration and the user has permissions)' do + Travis.config.roles.reset_model = 'push' + user.permissions.create!(repository_id: build.repository_id, push: true) + build.expects(:reset!) + service.run + end + + it 'does not reset the build (given no roles configuration and the user does not have permissions)' do + build.expects(:reset!).never + service.run + end + + it 'does not reset the build (given roles configuration and the user does not have permissions)' do + Travis.config.roles.reset_model = 'push' + build.expects(:reset!).never + service.run + end + + describe 'Instrument' do + let(:publisher) { Travis::Notification::Publisher::Memory.new } + let(:event) { publisher.events.last } + + before :each do + Travis::Notification.publishers.replace([publisher]) + end + + it 'publishes a event' do + service.run + event.should publish_instrumentation_event( + event: 'travis.services.reset_model.run:completed', + message: "Travis::Services::ResetModel#run:completed build_id=#{build.id} not accepted", + data: { + type: :build, + id: build.id, + accept?: false + } + ) + end + end + end +end diff --git a/spec_core/core/services/sync_user_spec.rb b/spec_core/core/services/sync_user_spec.rb new file mode 100644 index 00000000..2c8a284f --- /dev/null +++ b/spec_core/core/services/sync_user_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Travis::Services::SyncUser do + include Travis::Testing::Stubs + + let(:publisher) { stub('publisher', :publish => true) } + let(:service) { described_class.new(user, {}) } + + describe 'given the user is not currently syncing' do + before :each do + user.stubs(:update_column) + user.stubs(:syncing?).returns(false) + end + + it 'enqueues a sync job' do + Travis::Sidekiq::SynchronizeUser.expects(:perform_async).with(user.id) + service.run + end + + it 'sets the user to syncing' do + user.expects(:update_column).with(:is_syncing, true) + service.run + end + end + + describe 'given the user is currently syncing' do + before :each do + user.stubs(:syncing?).returns(true) + end + + it 'does not set the user to syncing' do + user.expects(:update_column).never + service.run + end + end +end diff --git a/spec_core/core/services/update_annotation_spec.rb b/spec_core/core/services/update_annotation_spec.rb new file mode 100644 index 00000000..fe94a23b --- /dev/null +++ b/spec_core/core/services/update_annotation_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe Travis::Services::UpdateAnnotation do + include Support::ActiveRecord + + let(:annotation_provider) { Factory(:annotation_provider) } + let(:job) { Factory(:test) } + let(:service) { described_class.new(params) } + let(:repository) { Factory(:repository) } + + attr_reader :params + + context 'when annotation is enabled' do + before :each do + job.stubs(:repository).returns(repository) + Travis::Features.stubs(:active?).returns(true) + end + + it 'creates the annotation if it doesn\'t exist already' do + @params = { + username: annotation_provider.api_username, + key: annotation_provider.api_key, + job_id: job.id, + description: 'Foo bar baz', + } + + expect { + @annotation = service.run + }.to change(Annotation, :count).by(1) + @annotation.description.should eq(params[:description]) + end + + it 'updates an existing annotation if one exists' do + @params = { + username: annotation_provider.api_username, + key: annotation_provider.api_key, + job_id: job.id, + description: 'Foo bar baz', + } + + annotation = Factory(:annotation, annotation_provider: annotation_provider, job: job) + service.run.id.should eq(annotation.id) + end + end + + context 'when annotation is disabled' do + before :each do + job.stubs(:repository).returns(repository) + Travis::Features.stubs(:active?).returns(false) + end + + it 'returns nil' do + @params = { + username: annotation_provider.api_username, + key: annotation_provider.api_key, + job_id: job.id, + description: 'Foo bar baz', + } + + service.run.should be_nil + end + end + + it 'returns nil when given invalid provider credentials' do + @params = { + username: 'some-invalid-provider', + key: 'some-invalid-key', + job_id: job.id, + description: 'Foo bar baz', + } + + service.run.should be_nil + end +end diff --git a/spec_core/core/services/update_hook_spec.rb b/spec_core/core/services/update_hook_spec.rb new file mode 100644 index 00000000..a5dac014 --- /dev/null +++ b/spec_core/core/services/update_hook_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +describe Travis::Services::UpdateHook do + include Travis::Testing::Stubs + + let(:service) { described_class.new(user, params) } + let(:params) { { id: repo.id, active: true } } + + before :each do + repo.stubs(:update_column) + service.stubs(:run_service) + user.stubs(:service_hook).returns(repo) + end + + it 'finds the repo by the given params' do + user.expects(:service_hook).with(id: repo.id).returns(repo) + service.run + end + + it 'sets the given :active param to the hook' do + service.expects(:run_service).with(:github_set_hook, is_a(Hash)) + service.run + end + + describe 'sets the repo to the active param' do + it 'given true' do + service.params.update(active: true) + repo.expects(:update_column).with(:active, true) + service.run + end + + it 'given false' do + service.params.update(active: false) + repo.expects(:update_column).with(:active, false) + service.run + end + + it 'given "true"' do + service.params.update(active: 'true') + repo.expects(:update_column).with(:active, true) + service.run + end + + it 'given "false"' do + service.params.update(active: 'false') + repo.expects(:update_column).with(:active, false) + service.run + end + end +end + +describe Travis::Services::UpdateHook::Instrument do + include Travis::Testing::Stubs + + let(:service) { Travis::Services::UpdateHook.new(user, params) } + let(:params) { { id: repository.id, active: 'true' } } + let(:publisher) { Travis::Notification::Publisher::Memory.new } + let(:event) { publisher.events.last } + + before :each do + Travis::Notification.publishers.replace([publisher]) + service.stubs(:run_service) + user.stubs(:service_hook).returns(repo) + repo.stubs(:update_column).returns(true) + end + + it 'publishes a event' do + service.run + event.should publish_instrumentation_event( + event: 'travis.services.update_hook.run:completed', + message: 'Travis::Services::UpdateHook#run:completed for svenfuchs/minimal active=true (svenfuchs)', + result: true + ) + end +end + diff --git a/spec_core/core/services/update_job_spec.rb b/spec_core/core/services/update_job_spec.rb new file mode 100644 index 00000000..5a49db5b --- /dev/null +++ b/spec_core/core/services/update_job_spec.rb @@ -0,0 +1,244 @@ +require 'spec_helper' + +describe Travis::Services::UpdateJob do + include Support::ActiveRecord + + let(:service) { described_class.new(event: event, data: payload) } + let(:payload) { WORKER_PAYLOADS["job:test:#{event}"].merge('id' => job.id) } + let(:build) { Factory(:build, state: :created, started_at: nil, finished_at: nil) } + let(:job) { Factory(:test, source: build, state: :started, started_at: nil, finished_at: nil) } + + before :each do + build.matrix.delete_all + end + + describe '#cancel_job_in_worker' do + let(:event) { :start } + + it 'sends cancel event to the worker' do + publisher = mock('publisher') + service.stubs(:publisher).returns(publisher) + + publisher.expects(:publish).with(type: 'cancel_job', job_id: job.id, source: 'update_job_service') + + service.cancel_job_in_worker + end + end + + describe 'event: receive' do + let(:event) { :receive } + + before :each do + job.repository.update_attributes(last_build_state: :passed) + end + + context 'when job is canceled' do + before { job.update_attribute(:state, :canceled) } + + it 'does not update state' do + service.expects(:cancel_job_in_worker) + + service.run + job.reload.state.should == 'canceled' + end + end + + it 'sets the job state to received' do + service.run + job.reload.state.should == 'received' + end + + it 'sets the job received_at' do + service.run + job.reload.received_at.to_s.should == '2011-01-01 00:02:00 UTC' + end + + it 'sets the job worker name' do + service.run + job.reload.worker.should == 'ruby3.worker.travis-ci.org:travis-ruby-4' + end + + it 'sets the build state to received' do + service.run + job.reload.source.state.should == 'received' + end + + it 'sets the build received_at' do + service.run + job.reload.source.received_at.to_s.should == '2011-01-01 00:02:00 UTC' + end + + it 'sets the build state to received' do + service.run + job.reload.source.state.should == 'received' + end + end + + + describe 'event: start' do + let(:event) { :start } + + before :each do + job.repository.update_attributes(last_build_state: :passed) + end + + context 'when job is canceled' do + before { job.update_attribute(:state, :canceled) } + + it 'does not update state' do + service.expects(:cancel_job_in_worker) + + service.run + job.reload.state.should == 'canceled' + end + end + + it 'sets the job state to started' do + service.run + job.reload.state.should == 'started' + end + + it 'sets the job started_at' do + service.run + job.reload.started_at.to_s.should == '2011-01-01 00:02:00 UTC' + end + + it 'sets the build state to started' do + service.run + job.reload.source.state.should == 'started' + end + + it 'sets the build started_at' do + service.run + job.reload.source.started_at.to_s.should == '2011-01-01 00:02:00 UTC' + end + + it 'sets the build state to started' do + service.run + job.reload.source.state.should == 'started' + end + + it 'sets the repository last_build_state to started' do + service.run + job.reload.repository.last_build_state.should == 'started' + end + + it 'sets the repository last_build_started_at' do + service.run + job.reload.repository.last_build_started_at.to_s.should == '2011-01-01 00:02:00 UTC' + end + end + + describe 'event: finish' do + let(:event) { :finish } + + before :each do + job.repository.update_attributes(last_build_state: :started) + end + + context 'when job is canceled' do + before { job.update_attribute(:state, :canceled) } + + it 'does not update state' do + service.expects(:cancel_job_in_worker) + + service.run + job.reload.state.should == 'canceled' + end + end + + it 'sets the job state to passed' do + service.run + job.reload.state.should == 'passed' + end + + it 'sets the job finished_at' do + service.run + job.reload.finished_at.to_s.should == '2011-01-01 00:03:00 UTC' + end + + it 'sets the build state to passed' do + service.run + job.reload.source.state.should == 'passed' + end + + it 'sets the build finished_at' do + service.run + job.reload.source.finished_at.to_s.should == '2011-01-01 00:03:00 UTC' + end + + it 'sets the repository last_build_state to passed' do + service.run + job.reload.repository.last_build_state.should == 'passed' + end + + it 'sets the repository last_build_finished_at' do + service.run + job.reload.repository.last_build_finished_at.to_s.should == '2011-01-01 00:03:00 UTC' + end + end + + describe 'compat' do + let(:event) { :finish } + + it 'swaps :result for :state (passed) if present' do + payload.delete(:state) + payload.merge!(result: 0) + service.data[:state].should == :passed + end + + it 'swaps :result for :state (failed) if present' do + payload.delete(:state) + payload.merge!(result: 1) + service.data[:state].should == :failed + end + end + + describe 'event: reset' do + let(:event) { :reset } + + before :each do + job.repository.update_attributes(last_build_state: :passed) + end + + it 'sets the job state to created' do + service.run + job.reload.state.should == 'created' + end + + it 'resets the job started_at' do + service.run + job.reload.started_at.should be_nil + end + + it 'resets the job worker name' do + service.run + job.reload.worker.should be_nil + end + + it 'resets the build state to started' do + service.run + job.reload.source.state.should == 'created' + end + + it 'resets the build started_at' do + service.run + job.reload.source.started_at.should be_nil + end + + it 'resets the build state to started' do + service.run + job.reload.source.state.should == 'created' + end + + it 'resets the repository last_build_state to started' do + service.run + job.reload.repository.last_build_state.should == 'created' + end + + it 'resets the repository last_build_started_at' do + service.run + job.reload.repository.last_build_started_at.should be_nil + end + end +end diff --git a/spec_core/core/services/update_log_spec.rb b/spec_core/core/services/update_log_spec.rb new file mode 100644 index 00000000..ebc0a5cd --- /dev/null +++ b/spec_core/core/services/update_log_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Travis::Services::UpdateLog do + include Travis::Testing::Stubs + + let(:service) { described_class.new(user, params) } + let(:params) { { id: log.id, archived_at: Time.now, archive_verified: true } } + + before :each do + log.stubs(:update_attributes).returns(true) + service.stubs(:run_service).with(:find_log, id: log.id).returns(log) + end + + it 'updates the log' do + log.expects(:update_attributes).with(archived_at: params[:archived_at], archive_verified: true) + service.run + end + + + describe 'the instrument' do + let(:publisher) { Travis::Notification::Publisher::Memory.new } + let(:event) { publisher.events.last } + + before :each do + Travis::Notification.publishers.replace([publisher]) + end + + it 'publishes a event' do + service.run + event.should publish_instrumentation_event( + event: 'travis.services.update_log.run:completed', + message: "Travis::Services::UpdateLog#run:completed for # params=#{params}", + result: true + ) + end + end +end diff --git a/spec_core/core/services/update_user_spec.rb b/spec_core/core/services/update_user_spec.rb new file mode 100644 index 00000000..9b31107b --- /dev/null +++ b/spec_core/core/services/update_user_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Travis::Services::UpdateUser do + include Travis::Testing::Stubs + + let(:service) { described_class.new(user, params) } + + before :each do + user.stubs(:update_attributes!) + end + + attr_reader :params + + it 'updates the locale if valid' do + @params = { :locale => 'en' } + user.expects(:update_attributes!).with(params) + service.run + end + + it 'does not update the locale if invalid' do + @params = { :locale => 'foo' } + user.expects(:update_attributes!).never + service.run + end +end diff --git a/spec_core/core/services_spec.rb b/spec_core/core/services_spec.rb new file mode 100644 index 00000000..55cb5124 --- /dev/null +++ b/spec_core/core/services_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +module Test + module Services + extend Travis::Services::Registry + + class DoStuff < Travis::Services::Base + attr_reader :current_user, :params + + def initialize(current_user, params) + @current_user, @params = current_user, params + end + end + end + + class Foo + include Travis::Services::Helpers + end +end + +describe Travis::Services::Helpers do + include Travis::Testing::Stubs + + let(:object) { Test::Foo.new } + + before :each do + Travis.stubs(:services).returns(Test::Services) + Test::Services.add(:do_stuff, Test::Services::DoStuff) + end + + describe 'service' do + it 'given :foo as a type and :stuff as a name it returns an instance of Foo::Stuff' do + object.service(:do_stuff, {}).should be_instance_of(Test::Services::DoStuff) + end + + it 'passes the given user' do + object.service(:do_stuff, user).current_user.should == user + end + + it 'passes the given params' do + params = { some: :thing } + object.service(:do_stuff, params).params.should == params + end + + it 'defaults params to {}' do + object.service(:do_stuff).params.should == {} + end + + it 'defaults the user to the current user if the object responds to :current_user' do + object.stubs(:current_user).returns(user) + object.service(:do_stuff, {}).current_user.should == user + end + + it 'defaults the user to nil if the object does not respond to :current_user' do + object.service(:do_stuff, {}).current_user.should be_nil + end + end +end diff --git a/travis-api.gemspec b/travis-api.gemspec index 8d25e9c8..c42ae5af 100644 --- a/travis-api.gemspec +++ b/travis-api.gemspec @@ -18,24 +18,24 @@ Gem::Specification.new do |s| "Josh Kalderimis", "Henrik Hodne", "Steffen Kötte", - "Ana Rosas", + "Renée Hendricksen", "Tyranja", + "Ana Rosas", "Lennard Wolf", "Steffen", "Jonas Chromik", "Dan Buch", "Andre Arko", + "Christopher Weyand", "Erik Michaels-Ober", "C. Scott Ananian", - "Christopher Weyand", "Lisa P", "Brian Ford", - "Renée Hendricksen", "Steve Richert", "Nick Schonning", - "Lucas CHERIFI", + "María de Antón", "Puneeth Chaganti", - "Bryan Goldstein", + "Lucas CHERIFI", "James Dennes", "Igor Wiedler", "rainsun", @@ -44,7 +44,7 @@ Gem::Specification.new do |s| "Tim Carey-Smith", "Dan Rice", "Zachary Scott", - "María de Antón", + "Bryan Goldstein", "Patrick Williams" ] @@ -59,27 +59,27 @@ Gem::Specification.new do |s| "steffen.koette@gmail.com", "carlad@users.noreply.github.com", "me@henrikhodne.com", + "renee@travis-ci.org", "henrik@hodne.io", "carla@travis-ci.org", - "a.rosas10@gmail.com", "tyranja@cassiopeia.uberspace.de", + "a.rosas10@gmail.com", "konstantin.haase@gmail.com", "lennardwolf@live.de", "steffen.koette@gmail.com", "Jonas.Chromik@student.hpi.uni-potsdam.de", "dan@travis-ci.org", - "svenfuchs@artweb-design.de", "andre@arko.net", + "svenfuchs@artweb-design.de", + "cscott@cscott.net", "christopher.weyand@student.hpi.de", "sferik@gmail.com", - "cscott@cscott.net", "steve.richert@gmail.com", "bford@engineyard.com", "henrik@travis-ci.com", "mail@lislis.de", - "renee@travis-ci.org", - "patrick@bittorrent.com", "rainsuner@gmail.com", + "patrick@bittorrent.com", "nschonni@gmail.com", "MariadeAnton@users.noreply.github.com", "lucas@cherifi.info", @@ -376,12 +376,6 @@ Gem::Specification.new do |s| "spec/integration/settings_endpoint_spec.rb", "spec/integration/singleton_settings_endpoint_spec.rb", "spec/integration/uptime_spec.rb", - "spec/integration/v1/branches_spec.rb", - "spec/integration/v1/builds_spec.rb", - "spec/integration/v1/hooks_spec.rb", - "spec/integration/v1/jobs_spec.rb", - "spec/integration/v1/repositories_spec.rb", - "spec/integration/v1_spec.backup.rb", "spec/integration/v2/branches_spec.rb", "spec/integration/v2/builds_spec.rb", "spec/integration/v2/hooks_spec.rb", @@ -394,9 +388,15 @@ Gem::Specification.new do |s| "spec/integration/v2_spec.backup.rb", "spec/integration/version_spec.rb", "spec/spec_helper.rb", + "spec/spec_helper_core.rb", + "spec/support.rb", + "spec/support/active_record.rb", "spec/support/coverage.rb", "spec/support/formats.rb", + "spec/support/gcs.rb", "spec/support/matchers.rb", + "spec/support/payloads.rb", + "spec/support/s3.rb", "spec/unit/access_token_spec.rb", "spec/unit/api/v2/http/accounts_spec.rb", "spec/unit/api/v2/http/annotations_spec.rb", @@ -488,8 +488,343 @@ Gem::Specification.new do |s| "spec/v3/services/user/current_spec.rb", "spec/v3/services/user/find_spec.rb", "spec/v3/services/user/sync_spec.rb", + "spec_core/core/model/annotation_provider_spec.rb", + "spec_core/core/model/annotation_spec.rb", + "spec_core/core/model/broadcast_spec.rb", + "spec_core/core/model/build/compat_spec.rb", + "spec_core/core/model/build/config/dist_spec.rb", + "spec_core/core/model/build/config/group_spec.rb", + "spec_core/core/model/build/config/matrix_spec.rb", + "spec_core/core/model/build/config/obfuscate_spec.rb", + "spec_core/core/model/build/config_spec.rb", + "spec_core/core/model/build/denormalize_spec.rb", + "spec_core/core/model/build/matrix_spec.rb", + "spec_core/core/model/build/metrics_spec.rb", + "spec_core/core/model/build/result_message_spec.rb", + "spec_core/core/model/build/states_spec.rb", + "spec_core/core/model/build/update_branch_spec.rb", + "spec_core/core/model/build_spec.rb", + "spec_core/core/model/commit_spec.rb", + "spec_core/core/model/encrypted_column_spec.rb", + "spec_core/core/model/job/cleanup_spec.rb", + "spec_core/core/model/job/queue_spec.rb", + "spec_core/core/model/job/test_spec.rb", + "spec_core/core/model/job_spec.rb", + "spec_core/core/model/organization_spec.rb", + "spec_core/core/model/permission_spec.rb", + "spec_core/core/model/repository/settings/ssh_key_spec.rb", + "spec_core/core/model/repository/settings_spec.rb", + "spec_core/core/model/repository/status_image_spec.rb", + "spec_core/core/model/repository_spec.rb", + "spec_core/core/model/request/approval_spec.rb", + "spec_core/core/model/request/branches_spec.rb", + "spec_core/core/model/request/states_spec.rb", + "spec_core/core/model/request_spec.rb", + "spec_core/core/model/ssl_key_spec.rb", + "spec_core/core/model/token_spec.rb", + "spec_core/core/model/url_spec.rb", + "spec_core/core/model/user/oauth_spec.rb", + "spec_core/core/model/user_spec.rb", + "spec_core/core/services/cancel_build_spec.rb", + "spec_core/core/services/cancel_job_spec.rb", + "spec_core/core/services/find_admin_spec.rb", + "spec_core/core/services/find_annotation_spec.rb", + "spec_core/core/services/find_branch_spec.rb", + "spec_core/core/services/find_branches_spec.rb", + "spec_core/core/services/find_build_spec.rb", + "spec_core/core/services/find_builds_spec.rb", + "spec_core/core/services/find_caches_spec.rb", + "spec_core/core/services/find_daily_repos_stats_spec.rb", + "spec_core/core/services/find_daily_tests_stats_spec.rb", + "spec_core/core/services/find_hooks_spec.rb", + "spec_core/core/services/find_job_spec.rb", + "spec_core/core/services/find_jobs_spec.rb", + "spec_core/core/services/find_log_spec.rb", + "spec_core/core/services/find_repo_key_spec.rb", + "spec_core/core/services/find_repo_settings_spec.rb", + "spec_core/core/services/find_repo_spec.rb", + "spec_core/core/services/find_repos_spec.rb", + "spec_core/core/services/find_request_spec.rb", + "spec_core/core/services/find_requests_spec.rb", + "spec_core/core/services/find_user_accounts_spec.rb", + "spec_core/core/services/next_build_number_spec.rb", + "spec_core/core/services/regenerate_repo_key_spec.rb", + "spec_core/core/services/remove_log_spec.rb", + "spec_core/core/services/reset_model_spec.rb", + "spec_core/core/services/sync_user_spec.rb", + "spec_core/core/services/update_annotation_spec.rb", + "spec_core/core/services/update_hook_spec.rb", + "spec_core/core/services/update_job_spec.rb", + "spec_core/core/services/update_log_spec.rb", + "spec_core/core/services/update_user_spec.rb", + "spec_core/core/services_spec.rb", "tmp/.gitkeep", - "travis-api.gemspec" + "travis-api.gemspec", + "vendor/travis-core/lib/travis.rb", + "vendor/travis-core/lib/travis/README.markdown", + "vendor/travis-core/lib/travis/addons.rb", + "vendor/travis-core/lib/travis/addons/README.markdown", + "vendor/travis-core/lib/travis/addons/archive.rb", + "vendor/travis-core/lib/travis/addons/archive/event_handler.rb", + "vendor/travis-core/lib/travis/addons/archive/task.rb", + "vendor/travis-core/lib/travis/addons/campfire.rb", + "vendor/travis-core/lib/travis/addons/campfire/event_handler.rb", + "vendor/travis-core/lib/travis/addons/campfire/instruments.rb", + "vendor/travis-core/lib/travis/addons/email.rb", + "vendor/travis-core/lib/travis/addons/email/event_handler.rb", + "vendor/travis-core/lib/travis/addons/email/instruments.rb", + "vendor/travis-core/lib/travis/addons/flowdock.rb", + "vendor/travis-core/lib/travis/addons/flowdock/event_handler.rb", + "vendor/travis-core/lib/travis/addons/flowdock/instruments.rb", + "vendor/travis-core/lib/travis/addons/github_status.rb", + "vendor/travis-core/lib/travis/addons/github_status/event_handler.rb", + "vendor/travis-core/lib/travis/addons/github_status/instruments.rb", + "vendor/travis-core/lib/travis/addons/hipchat.rb", + "vendor/travis-core/lib/travis/addons/hipchat/event_handler.rb", + "vendor/travis-core/lib/travis/addons/hipchat/instruments.rb", + "vendor/travis-core/lib/travis/addons/irc.rb", + "vendor/travis-core/lib/travis/addons/irc/event_handler.rb", + "vendor/travis-core/lib/travis/addons/irc/instruments.rb", + "vendor/travis-core/lib/travis/addons/pusher.rb", + "vendor/travis-core/lib/travis/addons/pusher/event_handler.rb", + "vendor/travis-core/lib/travis/addons/pusher/instruments.rb", + "vendor/travis-core/lib/travis/addons/pushover.rb", + "vendor/travis-core/lib/travis/addons/pushover/event_handler.rb", + "vendor/travis-core/lib/travis/addons/pushover/instruments.rb", + "vendor/travis-core/lib/travis/addons/slack.rb", + "vendor/travis-core/lib/travis/addons/slack/event_handler.rb", + "vendor/travis-core/lib/travis/addons/slack/instruments.rb", + "vendor/travis-core/lib/travis/addons/sqwiggle.rb", + "vendor/travis-core/lib/travis/addons/sqwiggle/event_handler.rb", + "vendor/travis-core/lib/travis/addons/sqwiggle/instruments.rb", + "vendor/travis-core/lib/travis/addons/states_cache.rb", + "vendor/travis-core/lib/travis/addons/states_cache/event_handler.rb", + "vendor/travis-core/lib/travis/addons/util.rb", + "vendor/travis-core/lib/travis/addons/webhook.rb", + "vendor/travis-core/lib/travis/addons/webhook/event_handler.rb", + "vendor/travis-core/lib/travis/addons/webhook/instruments.rb", + "vendor/travis-core/lib/travis/advisory_locks.rb", + "vendor/travis-core/lib/travis/api.rb", + "vendor/travis-core/lib/travis/api/README.markdown", + "vendor/travis-core/lib/travis/api/formats.rb", + "vendor/travis-core/lib/travis/api/v0.rb", + "vendor/travis-core/lib/travis/api/v0/event.rb", + "vendor/travis-core/lib/travis/api/v0/event/build.rb", + "vendor/travis-core/lib/travis/api/v0/event/job.rb", + "vendor/travis-core/lib/travis/api/v0/notification.rb", + "vendor/travis-core/lib/travis/api/v0/notification/build.rb", + "vendor/travis-core/lib/travis/api/v0/notification/repository.rb", + "vendor/travis-core/lib/travis/api/v0/notification/user.rb", + "vendor/travis-core/lib/travis/api/v0/pusher.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/annotation.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/annotation/created.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/annotation/updated.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/build.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/build/canceled.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/build/created.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/build/finished.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/build/received.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/build/received/job.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/build/started.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/build/started/job.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/job.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/job/canceled.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/job/created.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/job/finished.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/job/log.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/job/received.rb", + "vendor/travis-core/lib/travis/api/v0/pusher/job/started.rb", + "vendor/travis-core/lib/travis/api/v0/worker.rb", + "vendor/travis-core/lib/travis/api/v0/worker/job.rb", + "vendor/travis-core/lib/travis/api/v0/worker/job/test.rb", + "vendor/travis-core/lib/travis/api/v1.rb", + "vendor/travis-core/lib/travis/api/v1/archive.rb", + "vendor/travis-core/lib/travis/api/v1/archive/build.rb", + "vendor/travis-core/lib/travis/api/v1/archive/build/job.rb", + "vendor/travis-core/lib/travis/api/v1/helpers.rb", + "vendor/travis-core/lib/travis/api/v1/helpers/legacy.rb", + "vendor/travis-core/lib/travis/api/v1/http.rb", + "vendor/travis-core/lib/travis/api/v1/http/branches.rb", + "vendor/travis-core/lib/travis/api/v1/http/build.rb", + "vendor/travis-core/lib/travis/api/v1/http/build/job.rb", + "vendor/travis-core/lib/travis/api/v1/http/builds.rb", + "vendor/travis-core/lib/travis/api/v1/http/hooks.rb", + "vendor/travis-core/lib/travis/api/v1/http/job.rb", + "vendor/travis-core/lib/travis/api/v1/http/jobs.rb", + "vendor/travis-core/lib/travis/api/v1/http/repositories.rb", + "vendor/travis-core/lib/travis/api/v1/http/repository.rb", + "vendor/travis-core/lib/travis/api/v1/http/user.rb", + "vendor/travis-core/lib/travis/api/v1/webhook.rb", + "vendor/travis-core/lib/travis/api/v1/webhook/build.rb", + "vendor/travis-core/lib/travis/api/v1/webhook/build/finished.rb", + "vendor/travis-core/lib/travis/api/v1/webhook/build/finished/job.rb", + "vendor/travis-core/lib/travis/api/v2.rb", + "vendor/travis-core/lib/travis/chunkifier.rb", + "vendor/travis-core/lib/travis/commit_command.rb", + "vendor/travis-core/lib/travis/config/database.rb", + "vendor/travis-core/lib/travis/config/defaults.rb", + "vendor/travis-core/lib/travis/config/url.rb", + "vendor/travis-core/lib/travis/engine.rb", + "vendor/travis-core/lib/travis/enqueue.rb", + "vendor/travis-core/lib/travis/enqueue/services.rb", + "vendor/travis-core/lib/travis/enqueue/services/enqueue_jobs.rb", + "vendor/travis-core/lib/travis/enqueue/services/enqueue_jobs/limit.rb", + "vendor/travis-core/lib/travis/errors.rb", + "vendor/travis-core/lib/travis/event.rb", + "vendor/travis-core/lib/travis/event/config.rb", + "vendor/travis-core/lib/travis/event/handler.rb", + "vendor/travis-core/lib/travis/event/handler/metrics.rb", + "vendor/travis-core/lib/travis/event/handler/trail.rb", + "vendor/travis-core/lib/travis/event/subscription.rb", + "vendor/travis-core/lib/travis/features.rb", + "vendor/travis-core/lib/travis/github.rb", + "vendor/travis-core/lib/travis/github/education.rb", + "vendor/travis-core/lib/travis/github/services.rb", + "vendor/travis-core/lib/travis/github/services/fetch_config.rb", + "vendor/travis-core/lib/travis/github/services/find_or_create_org.rb", + "vendor/travis-core/lib/travis/github/services/find_or_create_repo.rb", + "vendor/travis-core/lib/travis/github/services/find_or_create_user.rb", + "vendor/travis-core/lib/travis/github/services/set_hook.rb", + "vendor/travis-core/lib/travis/github/services/sync_user.rb", + "vendor/travis-core/lib/travis/github/services/sync_user/organizations.rb", + "vendor/travis-core/lib/travis/github/services/sync_user/repositories.rb", + "vendor/travis-core/lib/travis/github/services/sync_user/repository.rb", + "vendor/travis-core/lib/travis/github/services/sync_user/reset_token.rb", + "vendor/travis-core/lib/travis/github/services/sync_user/user_info.rb", + "vendor/travis-core/lib/travis/logs.rb", + "vendor/travis-core/lib/travis/logs/services.rb", + "vendor/travis-core/lib/travis/logs/services/aggregate.rb", + "vendor/travis-core/lib/travis/logs/services/archive.rb", + "vendor/travis-core/lib/travis/logs/services/receive.rb", + "vendor/travis-core/lib/travis/mailer.rb", + "vendor/travis-core/lib/travis/mailer/user_mailer.rb", + "vendor/travis-core/lib/travis/mailer/views/layouts/contact_email.html.erb", + "vendor/travis-core/lib/travis/mailer/views/user_mailer/welcome_email.html.erb", + "vendor/travis-core/lib/travis/model.rb", + "vendor/travis-core/lib/travis/model/account.rb", + "vendor/travis-core/lib/travis/model/annotation.rb", + "vendor/travis-core/lib/travis/model/annotation_provider.rb", + "vendor/travis-core/lib/travis/model/branch.rb", + "vendor/travis-core/lib/travis/model/broadcast.rb", + "vendor/travis-core/lib/travis/model/build.rb", + "vendor/travis-core/lib/travis/model/build/config.rb", + "vendor/travis-core/lib/travis/model/build/config/dist.rb", + "vendor/travis-core/lib/travis/model/build/config/env.rb", + "vendor/travis-core/lib/travis/model/build/config/features.rb", + "vendor/travis-core/lib/travis/model/build/config/group.rb", + "vendor/travis-core/lib/travis/model/build/config/language.rb", + "vendor/travis-core/lib/travis/model/build/config/matrix.rb", + "vendor/travis-core/lib/travis/model/build/config/obfuscate.rb", + "vendor/travis-core/lib/travis/model/build/config/os.rb", + "vendor/travis-core/lib/travis/model/build/config/yaml.rb", + "vendor/travis-core/lib/travis/model/build/denormalize.rb", + "vendor/travis-core/lib/travis/model/build/matrix.rb", + "vendor/travis-core/lib/travis/model/build/metrics.rb", + "vendor/travis-core/lib/travis/model/build/result_message.rb", + "vendor/travis-core/lib/travis/model/build/states.rb", + "vendor/travis-core/lib/travis/model/build/update_branch.rb", + "vendor/travis-core/lib/travis/model/commit.rb", + "vendor/travis-core/lib/travis/model/email.rb", + "vendor/travis-core/lib/travis/model/encrypted_column.rb", + "vendor/travis-core/lib/travis/model/env_helpers.rb", + "vendor/travis-core/lib/travis/model/job.rb", + "vendor/travis-core/lib/travis/model/job/cleanup.rb", + "vendor/travis-core/lib/travis/model/job/queue.rb", + "vendor/travis-core/lib/travis/model/job/test.rb", + "vendor/travis-core/lib/travis/model/log.rb", + "vendor/travis-core/lib/travis/model/log/part.rb", + "vendor/travis-core/lib/travis/model/logs_model.rb", + "vendor/travis-core/lib/travis/model/membership.rb", + "vendor/travis-core/lib/travis/model/organization.rb", + "vendor/travis-core/lib/travis/model/permission.rb", + "vendor/travis-core/lib/travis/model/repository.rb", + "vendor/travis-core/lib/travis/model/repository/settings.rb", + "vendor/travis-core/lib/travis/model/repository/status_image.rb", + "vendor/travis-core/lib/travis/model/request.rb", + "vendor/travis-core/lib/travis/model/request/approval.rb", + "vendor/travis-core/lib/travis/model/request/branches.rb", + "vendor/travis-core/lib/travis/model/request/pull_request.rb", + "vendor/travis-core/lib/travis/model/request/states.rb", + "vendor/travis-core/lib/travis/model/ssl_key.rb", + "vendor/travis-core/lib/travis/model/token.rb", + "vendor/travis-core/lib/travis/model/url.rb", + "vendor/travis-core/lib/travis/model/user.rb", + "vendor/travis-core/lib/travis/model/user/oauth.rb", + "vendor/travis-core/lib/travis/model/user/renaming.rb", + "vendor/travis-core/lib/travis/notification.rb", + "vendor/travis-core/lib/travis/notification/instrument.rb", + "vendor/travis-core/lib/travis/notification/instrument/event_handler.rb", + "vendor/travis-core/lib/travis/notification/instrument/task.rb", + "vendor/travis-core/lib/travis/notification/publisher.rb", + "vendor/travis-core/lib/travis/notification/publisher/log.rb", + "vendor/travis-core/lib/travis/notification/publisher/memory.rb", + "vendor/travis-core/lib/travis/notification/publisher/redis.rb", + "vendor/travis-core/lib/travis/overwritable_method_definitions.rb", + "vendor/travis-core/lib/travis/redis_pool.rb", + "vendor/travis-core/lib/travis/requests.rb", + "vendor/travis-core/lib/travis/requests/services.rb", + "vendor/travis-core/lib/travis/requests/services/receive.rb", + "vendor/travis-core/lib/travis/requests/services/receive/api.rb", + "vendor/travis-core/lib/travis/requests/services/receive/cron.rb", + "vendor/travis-core/lib/travis/requests/services/receive/pull_request.rb", + "vendor/travis-core/lib/travis/requests/services/receive/push.rb", + "vendor/travis-core/lib/travis/secure_config.rb", + "vendor/travis-core/lib/travis/services.rb", + "vendor/travis-core/lib/travis/services/base.rb", + "vendor/travis-core/lib/travis/services/cancel_build.rb", + "vendor/travis-core/lib/travis/services/cancel_job.rb", + "vendor/travis-core/lib/travis/services/delete_caches.rb", + "vendor/travis-core/lib/travis/services/find_admin.rb", + "vendor/travis-core/lib/travis/services/find_annotations.rb", + "vendor/travis-core/lib/travis/services/find_branch.rb", + "vendor/travis-core/lib/travis/services/find_branches.rb", + "vendor/travis-core/lib/travis/services/find_build.rb", + "vendor/travis-core/lib/travis/services/find_builds.rb", + "vendor/travis-core/lib/travis/services/find_caches.rb", + "vendor/travis-core/lib/travis/services/find_daily_repos_stats.rb", + "vendor/travis-core/lib/travis/services/find_daily_tests_stats.rb", + "vendor/travis-core/lib/travis/services/find_hooks.rb", + "vendor/travis-core/lib/travis/services/find_job.rb", + "vendor/travis-core/lib/travis/services/find_jobs.rb", + "vendor/travis-core/lib/travis/services/find_log.rb", + "vendor/travis-core/lib/travis/services/find_repo.rb", + "vendor/travis-core/lib/travis/services/find_repo_key.rb", + "vendor/travis-core/lib/travis/services/find_repo_settings.rb", + "vendor/travis-core/lib/travis/services/find_repos.rb", + "vendor/travis-core/lib/travis/services/find_request.rb", + "vendor/travis-core/lib/travis/services/find_requests.rb", + "vendor/travis-core/lib/travis/services/find_user_accounts.rb", + "vendor/travis-core/lib/travis/services/find_user_broadcasts.rb", + "vendor/travis-core/lib/travis/services/find_user_permissions.rb", + "vendor/travis-core/lib/travis/services/helpers.rb", + "vendor/travis-core/lib/travis/services/next_build_number.rb", + "vendor/travis-core/lib/travis/services/regenerate_repo_key.rb", + "vendor/travis-core/lib/travis/services/registry.rb", + "vendor/travis-core/lib/travis/services/remove_log.rb", + "vendor/travis-core/lib/travis/services/reset_model.rb", + "vendor/travis-core/lib/travis/services/sync_user.rb", + "vendor/travis-core/lib/travis/services/update_annotation.rb", + "vendor/travis-core/lib/travis/services/update_hook.rb", + "vendor/travis-core/lib/travis/services/update_job.rb", + "vendor/travis-core/lib/travis/services/update_log.rb", + "vendor/travis-core/lib/travis/services/update_user.rb", + "vendor/travis-core/lib/travis/settings.rb", + "vendor/travis-core/lib/travis/settings/collection.rb", + "vendor/travis-core/lib/travis/settings/encrypted_value.rb", + "vendor/travis-core/lib/travis/settings/model.rb", + "vendor/travis-core/lib/travis/settings/model_extensions.rb", + "vendor/travis-core/lib/travis/states_cache.rb", + "vendor/travis-core/lib/travis/task.rb", + "vendor/travis-core/lib/travis/testing.rb", + "vendor/travis-core/lib/travis/testing/factories.rb", + "vendor/travis-core/lib/travis/testing/matchers.rb", + "vendor/travis-core/lib/travis/testing/payloads.rb", + "vendor/travis-core/lib/travis/testing/scenario.rb", + "vendor/travis-core/lib/travis/testing/stubs.rb", + "vendor/travis-core/lib/travis/testing/stubs/stub.rb", + "vendor/travis-core/lib/travis/travis_yml_stats.rb", + "vendor/travis-core/lib/travis_core/version.rb", + "vendor/travis-core/travis-core.gemspec" ] s.add_dependency 'travis-support' @@ -506,4 +841,3 @@ Gem::Specification.new do |s| s.add_dependency 'memcachier' s.add_dependency 'useragent' end - diff --git a/vendor/travis-core/lib/travis.rb b/vendor/travis-core/lib/travis.rb new file mode 100644 index 00000000..6bb017cd --- /dev/null +++ b/vendor/travis-core/lib/travis.rb @@ -0,0 +1,99 @@ +require 'pusher' +require 'travis/support' +require 'travis/support/database' +require 'travis_core/version' +require 'travis/redis_pool' +require 'travis/errors' + +# travis-core holds the central parts of the model layer used in both travis-ci +# (i.e. the web application) as well as travis-hub (a non-rails ui-less JRuby +# application that receives, processes and distributes messages from/to the +# workers and issues various events like email, pusher, irc notifications and +# so on). +# +# travis/model - contains ActiveRecord models that and model the main +# parts of the domain logic (e.g. repository, build, job +# etc.) and issue events on state changes (e.g. +# build:created, job:test:finished etc.) +# travis/event - contains event handlers that register for certain +# events and send out such things as email, pusher, irc +# notifications, archive builds or queue jobs for the +# workers. +# travis/mailer - contains ActionMailers for sending out email +# notifications +# +# travis-core also contains some helper classes and modules like Travis::Database +# (needed in travis-hub in order to connect to the database) and Travis::Renderer +# (our inferior layer on top of Rabl). +module Travis + class << self + def services=(services) + # Travis.logger.info("Using services: #{services}") + @services = services + end + + def services + @services ||= Travis::Services + end + end + + require 'travis/model' + require 'travis/task' + require 'travis/event' + require 'travis/addons' + require 'travis/api' + require 'travis/config/defaults' + require 'travis/commit_command' + require 'travis/enqueue' + require 'travis/features' + require 'travis/github' + require 'travis/logs' + require 'travis/mailer' + require 'travis/notification' + require 'travis/requests' + require 'travis/services' + + class UnknownRepository < StandardError; end + class GithubApiError < StandardError; end + class AdminMissing < StandardError; end + class RepositoryMissing < StandardError; end + class LogAlreadyRemoved < StandardError; end + class AuthorizationDenied < StandardError; end + class JobUnfinished < StandardError; end + + class << self + def setup(options = {}) + @config = Config.load(*options[:configs]) + @redis = Travis::RedisPool.new(config.redis.to_h) + + Travis.logger.info('Setting up Travis::Core') + + Github.setup + Addons.register + Services.register + Enqueue::Services.register + Github::Services.register + Logs::Services.register + Requests::Services.register + end + + attr_accessor :redis, :config + + def pusher + @pusher ||= ::Pusher.tap do |pusher| + pusher.app_id = config.pusher.app_id + pusher.key = config.pusher.key + pusher.secret = config.pusher.secret + pusher.scheme = config.pusher.scheme if config.pusher.scheme + pusher.host = config.pusher.host if config.pusher.host + pusher.port = config.pusher.port if config.pusher.port + end + end + + def states_cache + @states_cache ||= Travis::StatesCache.new + end + end + + setup +end diff --git a/vendor/travis-core/lib/travis/README.markdown b/vendor/travis-core/lib/travis/README.markdown new file mode 100644 index 00000000..a24a9c90 --- /dev/null +++ b/vendor/travis-core/lib/travis/README.markdown @@ -0,0 +1,16 @@ +# Travis Core directory overview + +This folder, `lib/travis` contains the main code for the Travis Core repository. It contains several sub-section/subdirectories: + +- [`addons`](addons): Event handlers that take events such as "build finished" and sends out notifications to GitHub, Pusher, Campfire, etc. +- [`api`](api): Serializers for models and events used in our API and in some other places (for example to generate Pusher payloads). +- [`enqueue`](enqueue): Logic for enqueueing jobs. +- [`event`](event): Code for sending and subscribing to events. Used by the `addons` code to subscribe to changes in the models. +- [`github`](github): Services for communicating with the GitHub API. +- [`mailer`](mailer): ActionMailer mailers. +- [`model`](model): All of our ActiveRecord models. +- [`notification`](notification): Code for adding instrumentation. +- [`requests`](requests): Handles requests received from GitHub. +- [`secure_config.rb`](secure_config.rb): Logic for encrypting and decrypting build/job configs. +- [`services`](services): Most of the business logic behind our [API](https://github.com/travis-ci/travis-api). +- [`testing`](testing): Code used by our tests, such as model stubs and factories. diff --git a/vendor/travis-core/lib/travis/addons.rb b/vendor/travis-core/lib/travis/addons.rb new file mode 100644 index 00000000..240c66cc --- /dev/null +++ b/vendor/travis-core/lib/travis/addons.rb @@ -0,0 +1,31 @@ +require 'travis/notification' + +module Travis + module Addons + require 'travis/addons/archive' + require 'travis/addons/campfire' + require 'travis/addons/email' + require 'travis/addons/flowdock' + require 'travis/addons/github_status' + require 'travis/addons/hipchat' + require 'travis/addons/irc' + require 'travis/addons/pusher' + require 'travis/addons/states_cache' + require 'travis/addons/sqwiggle' + require 'travis/addons/webhook' + require 'travis/addons/slack' + require 'travis/addons/pushover' + + class << self + def register + constants(false).each do |name| + key = name.to_s.underscore + const = const_get(name) + handler = const.const_get(:EventHandler) rescue nil + Travis::Event::Subscription.register(key, handler) if handler + const.setup if const.respond_to?(:setup) + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/README.markdown b/vendor/travis-core/lib/travis/addons/README.markdown new file mode 100644 index 00000000..936031d5 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/README.markdown @@ -0,0 +1,17 @@ +# Travis Core Addons + +The Addons are event handlers that accepts events such as "build finished" and forwards them to different services. The different services are: + +- Campfire +- E-mail +- Flowdock +- GitHub Commit Statuses +- Hipchat +- IRC +- Pusher: Used to update our Web UI automatically. +- Sqwiggle +- States cache: Caches the state of each branch in Memcached for status images. +- Webhook +- Pushover + +To add a new notification service, an event handler and a task is needed. The event handler is run by [`travis-hub`](https://github.com/travis-ci/travis-hub) and has access to the database. This should check whether the event should be forwarded at all, and pull out any necessary configuration values. It should then asynchronously run the corresponding Task. The Task is run by [`travis-tasks`](https://github.com/travis-ci/travis-tasks) via Sidekiq and should do the actual API calls needed. The event handler should finish very quickly, while the task is allowed to take longer. diff --git a/vendor/travis-core/lib/travis/addons/archive.rb b/vendor/travis-core/lib/travis/addons/archive.rb new file mode 100644 index 00000000..cc66d4a9 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/archive.rb @@ -0,0 +1,8 @@ +module Travis + module Addons + module Archive + require 'travis/addons/archive/event_handler' + require 'travis/addons/archive/task' + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/archive/event_handler.rb b/vendor/travis-core/lib/travis/addons/archive/event_handler.rb new file mode 100644 index 00000000..725c251d --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/archive/event_handler.rb @@ -0,0 +1,36 @@ +require 'travis/addons/archive/task' +require 'travis/event/handler' +require 'travis/features' + +module Travis + module Addons + module Archive + class EventHandler < Event::Handler + EVENTS = /log:aggregated/ + + def handle? + Travis::Features.feature_active?(:log_archiving) + end + + def handle + Travis::Addons::Archive::Task.run(:archive, payload) + end + + def payload + @payload ||= { type: type, id: object.id, job_id: object.job_id } + end + + def type + @type ||= event.split(':').first + end + + class Instrument < Notification::Instrument::EventHandler + def notify_completed + publish(payload: handler.payload) + end + end + Instrument.attach_to(self) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/archive/task.rb b/vendor/travis-core/lib/travis/addons/archive/task.rb new file mode 100644 index 00000000..37b974ee --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/archive/task.rb @@ -0,0 +1,24 @@ +require 'travis/task' + +module Travis + module Addons + module Archive + class Task < Travis::Task + def process + Travis.run_service(:"archive_#{payload[:type]}", id: payload[:id], job_id: payload[:job_id]) + end + + class Instrument < Notification::Instrument::Task + def run_completed + publish( + :msg => "for #<#{target.payload[:type].camelize} id=#{target.payload[:id]}>", + :object_type => target.payload[:type].camelize, + :object_id => target.payload[:id] + ) + end + end + Instrument.attach_to(self) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/campfire.rb b/vendor/travis-core/lib/travis/addons/campfire.rb new file mode 100644 index 00000000..6aada6d5 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/campfire.rb @@ -0,0 +1,13 @@ +module Travis + module Addons + module Campfire + module Instruments + require 'travis/addons/campfire/instruments' + end + + require 'travis/addons/campfire/event_handler' + + class Task < ::Travis::Task; end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/campfire/event_handler.rb b/vendor/travis-core/lib/travis/addons/campfire/event_handler.rb new file mode 100644 index 00000000..0a312c81 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/campfire/event_handler.rb @@ -0,0 +1,33 @@ +require 'travis/addons/campfire/instruments' +require 'travis/event/handler' + +module Travis + module Addons + module Campfire + + # Publishes a build notification to campfire rooms as defined in the + # configuration (`.travis.yml`). + # + # Campfire credentials are encrypted using the repository's ssl key. + class EventHandler < Event::Handler + API_VERSION = 'v2' + + EVENTS = /build:finished/ + + def handle? + !pull_request? && targets.present? && config.send_on_finished_for?(:campfire) + end + + def handle + Travis::Addons::Campfire::Task.run(:campfire, payload, targets: targets) + end + + def targets + @targets ||= config.notification_values(:campfire, :rooms) + end + + Instruments::EventHandler.attach_to(self) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/campfire/instruments.rb b/vendor/travis-core/lib/travis/addons/campfire/instruments.rb new file mode 100644 index 00000000..c3b55de9 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/campfire/instruments.rb @@ -0,0 +1,30 @@ +require 'travis/notification/instrument/event_handler' +require 'travis/notification/instrument/task' + +module Travis + module Addons + module Campfire + module Instruments + class EventHandler < Notification::Instrument::EventHandler + def notify_completed + publish(:targets => handler.targets) + end + end + + class Task < Notification::Instrument::Task + def run_completed + publish( + :msg => "for #", + :repository => payload[:repository][:slug], + # :request_id => payload['request'][:id], # TODO + :object_type => 'Build', + :object_id => payload[:build][:id], + :targets => task.targets, + :message => task.message + ) + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/email.rb b/vendor/travis-core/lib/travis/addons/email.rb new file mode 100644 index 00000000..743d3f3f --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/email.rb @@ -0,0 +1,10 @@ +module Travis + module Addons + module Email + + require 'travis/addons/email/instruments' + require 'travis/addons/email/event_handler' + class Task < ::Travis::Task; end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/email/event_handler.rb b/vendor/travis-core/lib/travis/addons/email/event_handler.rb new file mode 100644 index 00000000..afdd4170 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/email/event_handler.rb @@ -0,0 +1,56 @@ +require 'travis/addons/email/instruments' +require 'travis/event/handler' +require 'travis/model/broadcast' + +module Travis + module Addons + module Email + + # Sends out build notification emails using ActionMailer. + class EventHandler < Event::Handler + API_VERSION = 'v2' + + EVENTS = ['build:finished', 'build:canceled'] + + def handle? + !pull_request? && config.enabled?(:email) && config.send_on_finished_for?(:email) && recipients.present? + end + + def handle + Travis::Addons::Email::Task.run(:email, payload, recipients: recipients, broadcasts: broadcasts) + end + + def recipients + @recipients ||= begin + recipients = config.notification_values(:email, :recipients) + recipients = config.notifications[:recipients] if recipients.blank? # TODO deprecate recipients + recipients = default_recipients if recipients.blank? + Array(recipients).join(',').split(',').map(&:strip).select(&:present?).uniq + end + end + + private + + def pull_request? + build['pull_request'] + end + + def broadcasts + Broadcast.by_repo(object.repository).map do |broadcast| + { message: broadcast.message } + end + end + + def default_recipients + recipients = object.repository.users.map {|u| u.emails.map(&:email)}.flatten + recipients.keep_if do |r| + r == object.commit.author_email or + r == object.commit.committer_email + end + end + + Instruments::EventHandler.attach_to(self) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/email/instruments.rb b/vendor/travis-core/lib/travis/addons/email/instruments.rb new file mode 100644 index 00000000..4cc5c3bd --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/email/instruments.rb @@ -0,0 +1,30 @@ +require 'travis/notification/instrument/event_handler' +require 'travis/notification/instrument/task' + +module Travis + module Addons + module Email + module Instruments + class EventHandler < Notification::Instrument::EventHandler + def notify_completed + publish(:recipients => handler.recipients) + end + end + + class Task < Notification::Instrument::Task + def run_completed + publish( + :msg => "for #", + :repository => payload[:repository][:slug], + # :request_id => payload['request_id'], # TODO + :object_type => 'Build', + :object_id => payload[:build][:id], + :email => task.type, + :recipients => task.recipients + ) + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/flowdock.rb b/vendor/travis-core/lib/travis/addons/flowdock.rb new file mode 100644 index 00000000..bbd62f1d --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/flowdock.rb @@ -0,0 +1,10 @@ +module Travis + module Addons + module Flowdock + require 'travis/addons/flowdock/instruments' + require 'travis/addons/flowdock/event_handler' + class Task < ::Travis::Task; end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/flowdock/event_handler.rb b/vendor/travis-core/lib/travis/addons/flowdock/event_handler.rb new file mode 100644 index 00000000..08167d63 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/flowdock/event_handler.rb @@ -0,0 +1,39 @@ +require 'travis/addons/flowdock/instruments' +require 'travis/event/handler' + +module Travis + module Addons + module Flowdock + + # Publishes a build notification to Flowdock rooms as defined in the + # configuration (`.travis.yml`). + # + # Flowdock credentials are encrypted using the repository's ssl key. + class EventHandler < Event::Handler + API_VERSION = 'v2' + + EVENTS = /build:finished/ + + def initialize(*) + super + @payload = Api.data(object, for: 'event', version: 'v0', params: data) + end + + def handle? + !pull_request? && targets.present? && config.send_on_finished_for?(:flowdock) + end + + def handle + Travis::Addons::Flowdock::Task.run(:flowdock, payload, targets: targets) + end + + def targets + @targets ||= config.notification_values(:flowdock, :rooms) + end + + Instruments::EventHandler.attach_to(self) + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/flowdock/instruments.rb b/vendor/travis-core/lib/travis/addons/flowdock/instruments.rb new file mode 100644 index 00000000..421132ed --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/flowdock/instruments.rb @@ -0,0 +1,31 @@ +require 'travis/notification/instrument/event_handler' +require 'travis/notification/instrument/task' + +module Travis + module Addons + module Flowdock + module Instruments + class EventHandler < Notification::Instrument::EventHandler + def notify_completed + publish(:targets => handler.targets) + end + end + + class Task < Notification::Instrument::Task + def run_completed + publish( + :msg => "for #", + :repository => payload[:repository][:slug], + # :request_id => payload['request'][:id], # TODO + :object_type => 'Build', + :object_id => payload[:build][:id], + :targets => task.targets, + :message => task.message + ) + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/github_status.rb b/vendor/travis-core/lib/travis/addons/github_status.rb new file mode 100644 index 00000000..d3886d74 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/github_status.rb @@ -0,0 +1,10 @@ +module Travis + module Addons + module GithubStatus + require 'travis/addons/github_status/instruments' + require 'travis/addons/github_status/event_handler' + class Task < ::Travis::Task; end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/github_status/event_handler.rb b/vendor/travis-core/lib/travis/addons/github_status/event_handler.rb new file mode 100644 index 00000000..2aae7fa5 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/github_status/event_handler.rb @@ -0,0 +1,74 @@ +require 'travis/addons/github_status/instruments' +require 'travis/event/handler' + +module Travis + module Addons + module GithubStatus + + # Adds a comment with a build notification to the pull-request the request + # belongs to. + class EventHandler < Event::Handler + API_VERSION = 'v2' + EVENTS = /build:(created|started|finished|canceled)/ + + def handle? + return token.present? unless multi_token? + + unless tokens.any? + error "No GitHub OAuth tokens found for #{object.repository.slug}" + end + + tokens.any? + end + + def handle + if multi_token? + Travis::Addons::GithubStatus::Task.run(:github_status, payload, tokens: tokens) + else + Travis::Addons::GithubStatus::Task.run(:github_status, payload, token: token) + end + end + + private + + def token + admin.try(:github_oauth_token) + end + + def tokens + @tokens ||= users.map { |user| { user.login => user.github_oauth_token } }.inject({}, :merge) + end + + def users + @users ||= [ + build_committer, + admin, + users_with_push_access, + ].flatten.compact + end + + def build_committer + user = User.with_email(object.commit.committer_email) + user if user && user.permission?(repository_id: object.repository.id, push: true) + end + + def admin + @admin ||= Travis.run_service(:find_admin, repository: object.repository) + rescue Travis::AdminMissing + nil + end + + def users_with_push_access + object.repository.users_with_permission(:push) + end + + def multi_token? + !Travis::Features.feature_deactivated?(:github_status_multi_tokens) + end + + Instruments::EventHandler.attach_to(self) + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/github_status/instruments.rb b/vendor/travis-core/lib/travis/addons/github_status/instruments.rb new file mode 100644 index 00000000..525742fa --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/github_status/instruments.rb @@ -0,0 +1,30 @@ +require 'travis/notification/instrument/event_handler' +require 'travis/notification/instrument/task' + +module Travis + module Addons + module GithubStatus + module Instruments + class EventHandler < Notification::Instrument::EventHandler + def notify_completed + publish + end + end + + class Task < Notification::Instrument::Task + def run_completed + publish( + :msg => "for #", + :repository => payload[:repository][:slug], + # :request_id => payload['request_id'], # TODO + :object_type => 'Build', + :object_id => payload[:build][:id], + :url => task.url.to_s + ) + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/hipchat.rb b/vendor/travis-core/lib/travis/addons/hipchat.rb new file mode 100644 index 00000000..b8664ff8 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/hipchat.rb @@ -0,0 +1,10 @@ +module Travis + module Addons + module Hipchat + require 'travis/addons/hipchat/instruments' + require 'travis/addons/hipchat/event_handler' + class Task < ::Travis::Task; end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/hipchat/event_handler.rb b/vendor/travis-core/lib/travis/addons/hipchat/event_handler.rb new file mode 100644 index 00000000..73a8657e --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/hipchat/event_handler.rb @@ -0,0 +1,40 @@ +require 'travis/addons/hipchat/instruments' +require 'travis/event/handler' + +module Travis + module Addons + module Hipchat + + # Publishes a build notification to hipchat rooms as defined in the + # configuration (`.travis.yml`). + # + # Hipchat credentials are encrypted using the repository's ssl key. + class EventHandler < Event::Handler + API_VERSION = 'v2' + + EVENTS = /build:finished/ + + def handle? + enabled? && targets.present? && config.send_on_finished_for?(:hipchat) + end + + def handle + Travis::Addons::Hipchat::Task.run(:hipchat, payload, targets: targets) + end + + def enabled? + enabled = config.notification_values(:hipchat, :on_pull_requests) + enabled = true if enabled.nil? + pull_request? ? enabled : true + end + + def targets + @targets ||= config.notification_values(:hipchat, :rooms) + end + + Instruments::EventHandler.attach_to(self) + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/hipchat/instruments.rb b/vendor/travis-core/lib/travis/addons/hipchat/instruments.rb new file mode 100644 index 00000000..b1fa6472 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/hipchat/instruments.rb @@ -0,0 +1,31 @@ +require 'travis/notification/instrument/event_handler' +require 'travis/notification/instrument/task' + +module Travis + module Addons + module Hipchat + module Instruments + class EventHandler < Notification::Instrument::EventHandler + def notify_completed + publish(:targets => handler.targets) + end + end + + class Task < Notification::Instrument::Task + def run_completed + publish( + :msg => "for #", + :repository => payload[:repository][:slug], + # :request_id => payload['request'][:id], # TODO + :object_type => 'Build', + :object_id => payload[:build][:id], + :targets => task.targets, + :message => task.message + ) + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/irc.rb b/vendor/travis-core/lib/travis/addons/irc.rb new file mode 100644 index 00000000..f5fc6133 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/irc.rb @@ -0,0 +1,10 @@ +module Travis + module Addons + module Irc + require 'travis/addons/irc/instruments' + require 'travis/addons/irc/event_handler' + class Task < ::Travis::Task; end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/irc/event_handler.rb b/vendor/travis-core/lib/travis/addons/irc/event_handler.rb new file mode 100644 index 00000000..dc50ae9b --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/irc/event_handler.rb @@ -0,0 +1,32 @@ +require 'travis/addons/irc/instruments' +require 'travis/event/handler' + +module Travis + module Addons + module Irc + + # Publishes a build notification to IRC channels as defined in the + # configuration (`.travis.yml`). + class EventHandler < Event::Handler + API_VERSION = 'v2' + + EVENTS = 'build:finished' + + def handle? + !pull_request? && channels.present? && config.send_on_finished_for?(:irc) + end + + def handle + Travis::Addons::Irc::Task.run(:irc, payload, channels: channels) + end + + def channels + @channels ||= config.notification_values(:irc, :channels) + end + + Instruments::EventHandler.attach_to(self) + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/irc/instruments.rb b/vendor/travis-core/lib/travis/addons/irc/instruments.rb new file mode 100644 index 00000000..ee013770 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/irc/instruments.rb @@ -0,0 +1,31 @@ +require 'travis/notification/instrument/event_handler' +require 'travis/notification/instrument/task' + +module Travis + module Addons + module Irc + module Instruments + class EventHandler < Notification::Instrument::EventHandler + def notify_completed + publish(:channels => handler.channels) + end + end + + class Task < Notification::Instrument::Task + def run_completed + publish( + :msg => "for #", + :repository => payload[:repository][:slug], + # :request_id => payload['request_id'], # TODO + :object_type => 'Build', + :object_id => payload[:build][:id], + :channels => task.channels, + :messages => task.messages + ) + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/pusher.rb b/vendor/travis-core/lib/travis/addons/pusher.rb new file mode 100644 index 00000000..90564fc3 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/pusher.rb @@ -0,0 +1,11 @@ +module Travis + module Addons + module Pusher + require 'travis/addons/pusher/instruments' + require 'travis/addons/pusher/event_handler' + + class Task < ::Travis::Task; end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/pusher/event_handler.rb b/vendor/travis-core/lib/travis/addons/pusher/event_handler.rb new file mode 100644 index 00000000..27c3d620 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/pusher/event_handler.rb @@ -0,0 +1,57 @@ +require 'travis/addons/pusher/instruments' +require 'travis/event/handler' + +module Travis + module Addons + module Pusher + + # Notifies registered clients about various state changes through Pusher. + class EventHandler < Event::Handler + EVENTS = [ + /^build:(created|received|started|finished|canceled)/, + /^job:test:(created|received|started|log|finished|canceled)/ + ] + + attr_reader :channels, :pusher_payload + + def initialize(*) + super + @pusher_payload = Api.data(object, :for => 'pusher', :type => type, :params => data) if handle? + end + + def handle? + true + end + + def handle + Travis::Addons::Pusher::Task.run(queue, pusher_payload, :event => event) + end + + private + + def type + event.sub('test:', '').sub(':', '/') + end + + def queue + if Travis::Features.enabled_for_all?(:"pusher-live") || + Travis::Features.repository_active?(:"pusher-live", repository_id) + :"pusher-live" + else + :pusher + end + end + + def repository_id + if payload && payload['repository'] && payload['repository']['id'] + payload['repository']['id'] + elsif object && object.repository && object.repository.id + object.repository.id + end + end + + Instruments::EventHandler.attach_to(self) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/pusher/instruments.rb b/vendor/travis-core/lib/travis/addons/pusher/instruments.rb new file mode 100644 index 00000000..97d5f5ff --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/pusher/instruments.rb @@ -0,0 +1,42 @@ +require 'travis/notification/instrument/event_handler' +require 'travis/notification/instrument/task' + +module Travis + module Addons + module Pusher + module Instruments + def self.publish?(event) + event.to_s != 'job:test:log' + end + + class EventHandler < Notification::Instrument::EventHandler + def notify_completed + publish if Instruments.publish?(handler.event) + end + end + + class Task < Notification::Instrument::Task + def run_completed + publish( + :msg => "for #<#{type.camelize} id=#{id}> (event: #{task.event}, channels: #{task.channels.join(', ')})", + :object_type => type.camelize, + :object_id => id, + :event => task.event, + :client_event => task.client_event, + :channels => task.channels + ) if Instruments.publish?(task.event) + end + + def type + @type ||= task.event.split(':').first + end + + def id + payload.key?(type.to_sym) ? payload[type.to_sym][:id] : payload[:id] + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/pushover.rb b/vendor/travis-core/lib/travis/addons/pushover.rb new file mode 100644 index 00000000..10cbd194 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/pushover.rb @@ -0,0 +1,13 @@ +module Travis + module Addons + module Pushover + module Instruments + require 'travis/addons/pushover/instruments' + end + + require 'travis/addons/pushover/event_handler' + + class Task < ::Travis::Task; end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/pushover/event_handler.rb b/vendor/travis-core/lib/travis/addons/pushover/event_handler.rb new file mode 100644 index 00000000..123e9495 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/pushover/event_handler.rb @@ -0,0 +1,37 @@ +require 'travis/addons/pushover/instruments' +require 'travis/event/handler' + +module Travis + module Addons + module Pushover + + # Publishes a build notification to pushover users as defined in the + # configuration (`.travis.yml`). + # + # Credentials are encrypted using the repository's ssl key. + class EventHandler < Event::Handler + API_VERSION = 'v2' + + EVENTS = /build:finished/ + + def handle? + !pull_request? && users.present? && api_key.present? && config.send_on_finished_for?(:pushover) + end + + def handle + Travis::Addons::Pushover::Task.run(:pushover, payload, users: users, api_key: api_key) + end + + def users + @users ||= config.notification_values(:pushover, :users) + end + + def api_key + @api_key ||= config.notifications[:pushover][:api_key] + end + + Instruments::EventHandler.attach_to(self) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/pushover/instruments.rb b/vendor/travis-core/lib/travis/addons/pushover/instruments.rb new file mode 100644 index 00000000..6ed31392 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/pushover/instruments.rb @@ -0,0 +1,30 @@ +require 'travis/notification/instrument/event_handler' +require 'travis/notification/instrument/task' + +module Travis + module Addons + module Pushover + module Instruments + class EventHandler < Notification::Instrument::EventHandler + def notify_completed + publish(:users => handler.users, :api_key => handler.api_key) + end + end + + class Task < Notification::Instrument::Task + def run_completed + publish( + :msg => "for #", + :repository => payload[:repository][:slug], + :object_type => 'Build', + :object_id => payload[:build][:id], + :users => task.users, + :message => task.message, + :api_key => task.api_key + ) + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/slack.rb b/vendor/travis-core/lib/travis/addons/slack.rb new file mode 100644 index 00000000..1d682235 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/slack.rb @@ -0,0 +1,11 @@ +module Travis + module Addons + module Slack + require 'travis/addons/slack/instruments' + require 'travis/addons/slack/event_handler' + + class Task < ::Travis::Task; end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/slack/event_handler.rb b/vendor/travis-core/lib/travis/addons/slack/event_handler.rb new file mode 100644 index 00000000..9cfa2c80 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/slack/event_handler.rb @@ -0,0 +1,30 @@ +module Travis + module Addons + module Slack + + # Publishes a build notification to Slack rooms as defined in the + # configuration (`.travis.yml`). + # + # Slack credentials are encrypted using the repository's ssl key. + class EventHandler < Event::Handler + API_VERSION = 'v2' + + EVENTS = /build:finished/ + + def handle? + targets.present? && config.send_on_finished_for?(:slack) + end + + def handle + Travis::Addons::Slack::Task.run(:slack, payload, targets: targets) + end + + def targets + @targets ||= config.notification_values(:slack, :rooms) + end + + Instruments::EventHandler.attach_to(self) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/slack/instruments.rb b/vendor/travis-core/lib/travis/addons/slack/instruments.rb new file mode 100644 index 00000000..ca3de728 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/slack/instruments.rb @@ -0,0 +1,25 @@ +module Travis + module Addons + module Slack + module Instruments + class EventHandler < Notification::Instrument::EventHandler + def notify_completed + publish(:targets => handler.targets) + end + end + + class Task < Notification::Instrument::Task + def run_completed + publish( + :msg => "for #", + :repository => payload[:repository][:slug], + :object_type => 'Build', + :object_id => payload[:build][:id], + :targets => task.targets + ) + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/sqwiggle.rb b/vendor/travis-core/lib/travis/addons/sqwiggle.rb new file mode 100644 index 00000000..17829b38 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/sqwiggle.rb @@ -0,0 +1,11 @@ +module Travis + module Addons + module Sqwiggle + require 'travis/addons/sqwiggle/instruments' + require 'travis/addons/sqwiggle/event_handler' + + class Task < ::Travis::Task; end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/sqwiggle/event_handler.rb b/vendor/travis-core/lib/travis/addons/sqwiggle/event_handler.rb new file mode 100644 index 00000000..a12ba383 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/sqwiggle/event_handler.rb @@ -0,0 +1,30 @@ +module Travis + module Addons + module Sqwiggle + + # Publishes a build notification to sqwiggle rooms as defined in the + # configuration (`.travis.yml`). + # + # sqwiggle credentials are encrypted using the repository's ssl key. + class EventHandler < Event::Handler + + EVENTS = /build:finished/ + + def handle? + !pull_request? && targets.present? && config.send_on_finished_for?(:sqwiggle) + end + + def handle + Travis::Addons::Sqwiggle::Task.run(:sqwiggle, payload, targets: targets) + end + + def targets + @targets ||= config.notification_values(:sqwiggle, :rooms) + end + + Instruments::EventHandler.attach_to(self) + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/sqwiggle/instruments.rb b/vendor/travis-core/lib/travis/addons/sqwiggle/instruments.rb new file mode 100644 index 00000000..6daa5d79 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/sqwiggle/instruments.rb @@ -0,0 +1,29 @@ +module Travis + module Addons + module Sqwiggle + module Instruments + class EventHandler < Notification::Instrument::EventHandler + def notify_completed + publish(:targets => handler.targets) + end + end + + class Task < Notification::Instrument::Task + def run_completed + publish( + :msg => "for #", + :repository => payload[:repository][:slug], + # :request_id => payload['request'][:id], # TODO + :object_type => 'Build', + :object_id => payload[:build][:id], + :targets => task.targets, + :message => task.message + ) + end + end + end + end + end +end + + diff --git a/vendor/travis-core/lib/travis/addons/states_cache.rb b/vendor/travis-core/lib/travis/addons/states_cache.rb new file mode 100644 index 00000000..c33a1ea2 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/states_cache.rb @@ -0,0 +1,7 @@ +module Travis + module Addons + module StatesCache + require 'travis/addons/states_cache/event_handler' + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/states_cache/event_handler.rb b/vendor/travis-core/lib/travis/addons/states_cache/event_handler.rb new file mode 100644 index 00000000..a786287c --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/states_cache/event_handler.rb @@ -0,0 +1,47 @@ +require 'travis/event/handler' + +module Travis + module Addons + module StatesCache + class EventHandler < Event::Handler + EVENTS = /build:finished/ + + def handle? + states_cache_enabled = Travis::Features.feature_active?(:states_cache) + result = !pull_request? && states_cache_enabled + Travis.logger.info("[states-cache] Checking if event handler should be run for " + + "repo_id=#{repository_id} branch=#{branch} build_id=#{build['id']}, result: #{result}, " + + "pull_request: #{pull_request?} states_cache_enabled: #{states_cache_enabled}") + result + end + + def handle + Travis.logger.info("[states-cache] Running event handler for repo_id=#{repository_id} build_id=#{build['id']} branch=#{branch}") + cache.write(repository_id, branch, data) + rescue Exception => e + Travis.logger.error("[states-cache] An error occurred while trying to handle states cache update: #{e.message}\n#{e.backtrace}") + raise + end + + def cache + Travis.states_cache + end + + def repository_id + build['repository_id'] + end + + def branch + commit['branch'] + end + + def data + { + 'id' => build['id'], + 'state' => build['state'] + } + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/util.rb b/vendor/travis-core/lib/travis/addons/util.rb new file mode 100644 index 00000000..3a0e1075 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/util.rb @@ -0,0 +1,7 @@ +module Travis + module Addons + module Util + require 'travis/addons/util/template' + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/webhook.rb b/vendor/travis-core/lib/travis/addons/webhook.rb new file mode 100644 index 00000000..3c03922c --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/webhook.rb @@ -0,0 +1,11 @@ +module Travis + module Addons + module Webhook + require 'travis/addons/webhook/instruments' + require 'travis/addons/webhook/event_handler' + + class Task < ::Travis::Task; end + end + end +end + diff --git a/vendor/travis-core/lib/travis/addons/webhook/event_handler.rb b/vendor/travis-core/lib/travis/addons/webhook/event_handler.rb new file mode 100644 index 00000000..129757d0 --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/webhook/event_handler.rb @@ -0,0 +1,39 @@ +require 'travis/addons/webhook/instruments' +require 'travis/event/handler' + +# TODO include_logs? has been removed. gotta be deprecated! +# +module Travis + module Addons + module Webhook + + # Sends build notifications to webhooks as defined in the configuration + # (`.travis.yml`). + class EventHandler < Event::Handler + EVENTS = /build:(started|finished)/ + + def initialize(*) + super + end + + def handle? + targets.present? && config.send_on?(:webhooks, event.split(':').last) + end + + def handle + Travis::Addons::Webhook::Task.run(:webhook, webhook_payload, targets: targets, token: request['token']) + end + + def webhook_payload + Api.data(object, :for => 'webhook', :type => 'build/finished', :version => 'v1') + end + + def targets + @targets ||= config.notification_values(:webhooks, :urls) + end + + Instruments::EventHandler.attach_to(self) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/addons/webhook/instruments.rb b/vendor/travis-core/lib/travis/addons/webhook/instruments.rb new file mode 100644 index 00000000..9552f04d --- /dev/null +++ b/vendor/travis-core/lib/travis/addons/webhook/instruments.rb @@ -0,0 +1,29 @@ +require 'travis/notification/instrument/event_handler' +require 'travis/notification/instrument/task' + +module Travis + module Addons + module Webhook + module Instruments + class EventHandler < Notification::Instrument::EventHandler + def notify_completed + publish(:targets => handler.targets) + end + end + + class Task < Notification::Instrument::Task + def run_completed + publish( + :msg => "for #", + :repository => payload[:repository].values_at(:owner_name, :name).join('/'), + :object_type => 'Build', + :object_id => payload[:id], + :targets => task.targets + ) + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/advisory_locks.rb b/vendor/travis-core/lib/travis/advisory_locks.rb new file mode 100644 index 00000000..4edae945 --- /dev/null +++ b/vendor/travis-core/lib/travis/advisory_locks.rb @@ -0,0 +1,59 @@ +require 'zlib' + +module Travis + # http://hashrocket.com/blog/posts/advisory-locks-in-postgres + # https://github.com/mceachen/with_advisory_lock + # 13.3.4. Advisory Locks : http://www.postgresql.org/docs/9.3/static/explicit-locking.html + # http://www.postgresql.org/docs/9.3/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS + class AdvisoryLocks + attr_reader :lock_name, :transactional + + def initialize(lock_name, transactional = false) + if lock_name.blank? + raise StandardError, "lock name cannot be blank" + end + @lock_name = lock_name + end + + # must be used within a transaction + def self.exclusive(lock_name, timeout = 30, transactional = false) + al = self.new(lock_name, transactional) + al.exclusive(timeout) { yield } + end + + # must be used within a transaction + def exclusive(timeout = 30) + give_up_at = Time.now + timeout if timeout + while timeout.nil? || Time.now < give_up_at do + if obtained_lock? + return yield + else + # Randomizing sleep time may help reduce contention. + sleep(rand(0.1..0.2)) + end + end + ensure + release_lock unless transactional + end + + private + + def obtained_lock? + xact = transactional ? "_xact" : nil + result = connection.select_value("select pg_try_advisory#{xact}_lock(#{lock_code});") + result == 't' || result == 'true' + end + + def release_lock + connection.execute("select pg_advisory_unlock(#{lock_code});") + end + + def connection + ActiveRecord::Base.connection + end + + def lock_code + Zlib.crc32(lock_name) + end + end +end \ No newline at end of file diff --git a/vendor/travis-core/lib/travis/api.rb b/vendor/travis-core/lib/travis/api.rb new file mode 100644 index 00000000..b3991d7b --- /dev/null +++ b/vendor/travis-core/lib/travis/api.rb @@ -0,0 +1,67 @@ +module Travis + module Api + require 'travis/api/formats' + require 'travis/api/v0' + require 'travis/api/v1' + require 'travis/api/v2' + + DEFAULT_VERSION = 'v2' + + class << self + def data(resource, options = {}) + new(resource, options).data + end + + def builder(resource, options = {}) + target = (options[:for] || 'http').to_s.camelize + version = (options[:version] || default_version(options)).to_s.camelize + type = (options[:type] || type_for(resource)).to_s.camelize.split('::') + ([version, target] + type).inject(Travis::Api) do |const, name| + begin + if const && const.const_defined?(name.to_s.camelize, false) + const.const_get(name, false) + else + nil + end + rescue NameError + nil + end + end + end + + def new(resource, options = {}) + builder = builder(resource, options) || raise(ArgumentError, "cannot serialize #{resource.inspect}, options: #{options.inspect}") + builder.new(resource, options[:params] || {}) + end + + private + + def type_for(resource) + if arel_relation?(resource) + type = resource.klass.name.pluralize + else + type = resource.class + type = type.base_class if active_record?(type) + type = type.name + end + type.split('::').last + end + + def arel_relation?(object) + object.respond_to?(:klass) + end + + def active_record?(object) + object.respond_to?(:base_class) + end + + def default_version(options) + if options[:for].to_s.downcase == "pusher" + "v0" + else + DEFAULT_VERSION + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/README.markdown b/vendor/travis-core/lib/travis/api/README.markdown new file mode 100644 index 00000000..0ca565b1 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/README.markdown @@ -0,0 +1,10 @@ +This directory contains serializers for events and models. + +- `v0/event`: Payloads used by [`Travis::Event::Handler`](../event/handler.rb). These are the payloads that the [addons](../addons) will get. +- `v0/pusher`: Payloads used to send events to the web UI using Pusher. +- `v0/worker`: Payloads sent to [travis-worker](https://github.com/travis-ci/travis-worker). + +- `v1/http`: Payloads for the v1 [API](https://github.com/travis-ci/travis-api). +- `v1/webhook`: Payloads for the webhook notifications. + +- `v2/http`: Payloads for the v2 [API](https://github.com/travis-ci/travis-api). \ No newline at end of file diff --git a/vendor/travis-core/lib/travis/api/formats.rb b/vendor/travis-core/lib/travis/api/formats.rb new file mode 100644 index 00000000..b218bda5 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/formats.rb @@ -0,0 +1,9 @@ +module Travis + module Api + module Formats + def format_date(date) + date && date.strftime('%Y-%m-%dT%H:%M:%SZ') + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0.rb b/vendor/travis-core/lib/travis/api/v0.rb new file mode 100644 index 00000000..d21ff344 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0.rb @@ -0,0 +1,11 @@ +module Travis + module Api + # V0 is an internal api that we can change at any time + module V0 + require 'travis/api/v0/event' + require 'travis/api/v0/notification' + require 'travis/api/v0/pusher' + require 'travis/api/v0/worker' + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/event.rb b/vendor/travis-core/lib/travis/api/v0/event.rb new file mode 100644 index 00000000..efd03a8d --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/event.rb @@ -0,0 +1,12 @@ +module Travis + module Api + module V0 + module Event + require 'travis/api/v0/event/build' + require 'travis/api/v0/event/job' + end + end + end +end + + diff --git a/vendor/travis-core/lib/travis/api/v0/event/build.rb b/vendor/travis-core/lib/travis/api/v0/event/build.rb new file mode 100644 index 00000000..797d399a --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/event/build.rb @@ -0,0 +1,95 @@ +module Travis + module Api + module V0 + module Event + class Build + include Formats + + attr_reader :build, :repository, :request, :commit, :options + + def initialize(build, options = {}) + @build = build + @repository = build.repository + @request = build.request + @commit = build.commit + # @options = options + end + + def data(extra = {}) + { + 'repository' => repository_data, + 'request' => request_data, + 'commit' => commit_data, + 'build' => build_data, + 'jobs' => build.matrix.map { |job| job_data(job) } + } + end + + private + + def build_data + { + 'id' => build.id, + 'repository_id' => build.repository_id, + 'commit_id' => build.commit_id, + 'number' => build.number, + 'pull_request' => build.pull_request?, + 'pull_request_number' => build.pull_request_number, + 'config' => build.config.try(:except, :source_key), + 'state' => build.state.to_s, + 'previous_state' => build.previous_state.to_s, + 'started_at' => format_date(build.started_at), + 'finished_at' => format_date(build.finished_at), + 'duration' => build.duration, + 'job_ids' => build.matrix_ids + } + end + + def repository_data + { + 'id' => repository.id, + 'key' => repository.key.try(:public_key), + 'slug' => repository.slug, + 'name' => repository.name, + 'owner_email' => repository.owner_email, + 'owner_avatar_url' => repository.owner.try(:avatar_url) + } + end + + def request_data + { + 'token' => request.token, + 'head_commit' => (request.head_commit || '') + } + end + + def commit_data + { + 'id' => commit.id, + 'sha' => commit.commit, + 'branch' => commit.branch, + 'message' => commit.message, + 'committed_at' => format_date(commit.committed_at), + 'author_name' => commit.author_name, + 'author_email' => commit.author_email, + 'committer_name' => commit.committer_name, + 'committer_email' => commit.committer_email, + 'compare_url' => commit.compare_url, + } + end + + def job_data(job) + { + 'id' => job.id, + 'number' => job.number, + 'state' => job.state.to_s, + 'tags' => job.tags + } + end + end + end + end + end +end + + diff --git a/vendor/travis-core/lib/travis/api/v0/event/job.rb b/vendor/travis-core/lib/travis/api/v0/event/job.rb new file mode 100644 index 00000000..ddbc58e8 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/event/job.rb @@ -0,0 +1,38 @@ +module Travis + module Api + module V0 + module Event + class Job + include Formats + + attr_reader :job + + def initialize(job, options = {}) + @job = job + # @options = options + end + + def data(extra = {}) + { + 'job' => job_data, + } + end + + private + + def job_data + { + 'queue' => job.queue, + 'created_at' => job.created_at, + 'started_at' => job.started_at, + 'finished_at' => job.finished_at, + } + end + end + end + end + end +end + + + diff --git a/vendor/travis-core/lib/travis/api/v0/notification.rb b/vendor/travis-core/lib/travis/api/v0/notification.rb new file mode 100644 index 00000000..45d37770 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/notification.rb @@ -0,0 +1,12 @@ +module Travis + module Api + module V0 + module Notification + require 'travis/api/v0/notification/build' + require 'travis/api/v0/notification/repository' + require 'travis/api/v0/notification/user' + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/api/v0/notification/build.rb b/vendor/travis-core/lib/travis/api/v0/notification/build.rb new file mode 100644 index 00000000..27b00504 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/notification/build.rb @@ -0,0 +1,28 @@ +module Travis + module Api + module V0 + module Notification + class Build + attr_reader :build + + def initialize(build, options = {}) + @build = build + end + + def data + { + 'build' => build_data + } + end + + def build_data + { + 'id' => build.id + } + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/api/v0/notification/repository.rb b/vendor/travis-core/lib/travis/api/v0/notification/repository.rb new file mode 100644 index 00000000..6b9861b8 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/notification/repository.rb @@ -0,0 +1,28 @@ +module Travis + module Api + module V0 + module Notification + class Repository + attr_reader :repository + + def initialize(repository, options = {}) + @repository = repository + end + + def data + { + 'repository' => repository_data + } + end + + def repository_data + { + 'id' => repository.id, + 'slug' => repository.slug + } + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/notification/user.rb b/vendor/travis-core/lib/travis/api/v0/notification/user.rb new file mode 100644 index 00000000..95ba9192 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/notification/user.rb @@ -0,0 +1,28 @@ +module Travis + module Api + module V0 + module Notification + class User + attr_reader :user + + def initialize(user, options = {}) + @user = user + end + + def data + { + 'user' => user_data + } + end + + def user_data + { + 'id' => user.id, + 'login' => user.login + } + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher.rb b/vendor/travis-core/lib/travis/api/v0/pusher.rb new file mode 100644 index 00000000..852c3045 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher.rb @@ -0,0 +1,11 @@ +module Travis + module Api + module V0 + module Pusher + require 'travis/api/v0/pusher/annotation' + require 'travis/api/v0/pusher/build' + require 'travis/api/v0/pusher/job' + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/annotation.rb b/vendor/travis-core/lib/travis/api/v0/pusher/annotation.rb new file mode 100644 index 00000000..f29f3d42 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/annotation.rb @@ -0,0 +1,33 @@ +module Travis + module Api + module V0 + module Pusher + class Annotation + require 'travis/api/v0/pusher/annotation/created' + require 'travis/api/v0/pusher/annotation/updated' + + include Formats + + attr_reader :annotation + + def initialize(annotation, options = {}) + @annotation = annotation + end + + def data + { + "annotation" => { + "id" => annotation.id, + "job_id" => annotation.job_id, + "description" => annotation.description, + "url" => annotation.url, + "status" => annotation.status, + "provider_name" => annotation.annotation_provider.name, + } + } + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/annotation/created.rb b/vendor/travis-core/lib/travis/api/v0/pusher/annotation/created.rb new file mode 100644 index 00000000..fb476fd1 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/annotation/created.rb @@ -0,0 +1,12 @@ +module Travis + module Api + module V0 + module Pusher + class Annotation + class Created < Annotation + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/annotation/updated.rb b/vendor/travis-core/lib/travis/api/v0/pusher/annotation/updated.rb new file mode 100644 index 00000000..99865c40 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/annotation/updated.rb @@ -0,0 +1,12 @@ +module Travis + module Api + module V0 + module Pusher + class Annotation + class Updated < Annotation + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/build.rb b/vendor/travis-core/lib/travis/api/v0/pusher/build.rb new file mode 100644 index 00000000..3762aa38 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/build.rb @@ -0,0 +1,111 @@ +module Travis + module Api + module V0 + module Pusher + class Build + require 'travis/api/v0/pusher/build/canceled' + require 'travis/api/v0/pusher/build/created' + require 'travis/api/v0/pusher/build/received' + require 'travis/api/v0/pusher/build/started' + require 'travis/api/v0/pusher/build/finished' + + include Formats + + attr_reader :build, :options + + def initialize(build, options = {}) + @build = build + @options = options + end + + def data + { + 'build' => build_data(build), + 'commit' => commit_data(build.commit), + 'repository' => repository_data(build.repository) + } + end + + private + + def build_data(build) + commit = build.commit + { + 'id' => build.id, + 'repository_id' => build.repository_id, + 'commit_id' => build.commit_id, + 'number' => build.number, + 'pull_request' => build.pull_request?, + 'pull_request_title' => build.pull_request_title, + 'pull_request_number' => build.pull_request_number, + 'state' => build.state.to_s, + 'started_at' => format_date(build.started_at), + 'finished_at' => format_date(build.finished_at), + 'duration' => build.duration, + 'job_ids' => build.matrix_ids, + 'event_type' => build.event_type, + + # this is a legacy thing, we should think about removing it + 'commit' => commit.commit, + 'branch' => commit.branch, + 'message' => commit.message, + 'compare_url' => commit.compare_url, + 'committed_at' => format_date(commit.committed_at), + 'author_name' => commit.author_name, + 'author_email' => commit.author_email, + 'committer_name' => commit.committer_name, + 'committer_email' => commit.committer_email + } + end + + def commit_data(commit) + { + 'id' => commit.id, + 'sha' => commit.commit, + 'branch' => commit.branch, + 'message' => commit.message, + 'committed_at' => format_date(commit.committed_at), + 'author_name' => commit.author_name, + 'author_email' => commit.author_email, + 'committer_name' => commit.committer_name, + 'committer_email' => commit.committer_email, + 'compare_url' => commit.compare_url, + } + end + + def repository_data(repository) + { + 'id' => repository.id, + 'slug' => repository.slug, + 'description' => repository.description, + 'private' => repository.private, + 'last_build_id' => repository.last_build_id, + 'last_build_number' => repository.last_build_number, + 'last_build_state' => repository.last_build_state.to_s, + 'last_build_duration' => repository.last_build_duration, + 'last_build_language' => nil, + 'last_build_started_at' => format_date(repository.last_build_started_at), + 'last_build_finished_at' => format_date(repository.last_build_finished_at), + 'github_language' => repository.github_language, + 'default_branch' => { + 'name' => repository.default_branch, + 'last_build_id' => last_build_on_default_branch_id(repository) + }, + 'active' => repository.active, + 'current_build_id' => repository.current_build_id + } + end + + def last_build_on_default_branch_id(repository) + default_branch = Branch.where(repository_id: repository.id, name: repository.default_branch).first + + if default_branch + default_branch.last_build_id + end + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/build/canceled.rb b/vendor/travis-core/lib/travis/api/v0/pusher/build/canceled.rb new file mode 100644 index 00000000..19ae8256 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/build/canceled.rb @@ -0,0 +1,12 @@ +module Travis + module Api + module V0 + module Pusher + class Build + class Canceled < Build + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/build/created.rb b/vendor/travis-core/lib/travis/api/v0/pusher/build/created.rb new file mode 100644 index 00000000..2a0d008e --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/build/created.rb @@ -0,0 +1,15 @@ +require 'travis/api/v1' + +module Travis + module Api + module V0 + module Pusher + class Build + class Created < Build + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/build/finished.rb b/vendor/travis-core/lib/travis/api/v0/pusher/build/finished.rb new file mode 100644 index 00000000..299df81d --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/build/finished.rb @@ -0,0 +1,14 @@ +module Travis + module Api + module V0 + module Pusher + class Build + class Finished < Build + end + end + end + end + end +end + + diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/build/received.rb b/vendor/travis-core/lib/travis/api/v0/pusher/build/received.rb new file mode 100644 index 00000000..005f2ab2 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/build/received.rb @@ -0,0 +1,12 @@ +module Travis + module Api + module V0 + module Pusher + class Build + class Received < Build + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/build/received/job.rb b/vendor/travis-core/lib/travis/api/v0/pusher/build/received/job.rb new file mode 100644 index 00000000..6786f598 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/build/received/job.rb @@ -0,0 +1,47 @@ +module Travis + module Api + module V0 + module Pusher + class Build + class Received < Build + class Job + include Formats, V1::Helpers::Legacy + + attr_reader :job, :commit + + def initialize(job) + @job = job + @commit = job.commit + end + + def data + { + 'id' => job.id, + 'repository_id' => job.repository_id, + 'repository_private' => repository.private, + 'parent_id' => job.source_id, + 'number' => job.number, + 'state' => job.state.to_s, + 'result' => legacy_job_result(job), + 'config' => job.obfuscated_config, + 'commit' => commit.commit, + 'branch' => commit.branch, + 'message' => commit.message, + 'compare_url' => commit.compare_url, + 'started_at' => format_date(job.started_at), + 'finished_at' => format_date(job.finished_at), + 'committed_at' => format_date(commit.committed_at), + 'author_name' => commit.author_name, + 'author_email' => commit.author_email, + 'committer_name' => commit.committer_name, + 'committer_email' => commit.committer_email, + 'allow_failure' => job.allow_failure + } + end + end + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/build/started.rb b/vendor/travis-core/lib/travis/api/v0/pusher/build/started.rb new file mode 100644 index 00000000..1ad598b0 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/build/started.rb @@ -0,0 +1,13 @@ +module Travis + module Api + module V0 + module Pusher + class Build + class Started < Build + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/build/started/job.rb b/vendor/travis-core/lib/travis/api/v0/pusher/build/started/job.rb new file mode 100644 index 00000000..4a94660b --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/build/started/job.rb @@ -0,0 +1,47 @@ +module Travis + module Api + module V0 + module Pusher + class Build + class Started < Build + class Job + include Formats, V1::Helpers::Legacy + + attr_reader :job, :commit + + def initialize(job) + @job = job + @commit = job.commit + end + + def data + { + 'id' => job.id, + 'repository_id' => job.repository_id, + 'repository_private' => repository.private, + 'parent_id' => job.source_id, + 'number' => job.number, + 'state' => job.state.to_s, + 'result' => legacy_job_result(job), + 'config' => job.obfuscated_config, + 'commit' => commit.commit, + 'branch' => commit.branch, + 'message' => commit.message, + 'compare_url' => commit.compare_url, + 'started_at' => format_date(job.started_at), + 'finished_at' => format_date(job.finished_at), + 'committed_at' => format_date(commit.committed_at), + 'author_name' => commit.author_name, + 'author_email' => commit.author_email, + 'committer_name' => commit.committer_name, + 'committer_email' => commit.committer_email, + 'allow_failure' => job.allow_failure + } + end + end + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/job.rb b/vendor/travis-core/lib/travis/api/v0/pusher/job.rb new file mode 100644 index 00000000..efd3cbc8 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/job.rb @@ -0,0 +1,67 @@ +module Travis + module Api + module V0 + module Pusher + class Job + require 'travis/api/v0/pusher/job/canceled' + require 'travis/api/v0/pusher/job/created' + require 'travis/api/v0/pusher/job/log' + require 'travis/api/v0/pusher/job/received' + require 'travis/api/v0/pusher/job/started' + require 'travis/api/v0/pusher/job/finished' + + include Formats + + attr_reader :job, :options + + def initialize(job, options = {}) + @job = job + @options = options + end + + def data + job_data(job).merge( + 'commit' => commit_data(job.commit) + ) + end + + private + + def job_data(job) + { + 'id' => job.id, + 'repository_id' => job.repository_id, + 'repository_slug' => job.repository.slug, + 'repository_private' => job.repository.private, + 'build_id' => job.source_id, + 'commit_id' => job.commit_id, + 'log_id' => job.log_id, + 'number' => job.number, + 'state' => job.state.to_s, + 'started_at' => format_date(job.started_at), + 'finished_at' => format_date(job.finished_at), + 'queue' => job.queue, + 'allow_failure' => job.allow_failure, + 'annotation_ids' => job.annotation_ids + } + end + + def commit_data(commit) + { + 'id' => commit.id, + 'sha' => commit.commit, + 'branch' => commit.branch, + 'message' => commit.message, + 'committed_at' => format_date(commit.committed_at), + 'author_name' => commit.author_name, + 'author_email' => commit.author_email, + 'committer_name' => commit.committer_name, + 'committer_email' => commit.committer_email, + 'compare_url' => commit.compare_url, + } + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/job/canceled.rb b/vendor/travis-core/lib/travis/api/v0/pusher/job/canceled.rb new file mode 100644 index 00000000..426d03f3 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/job/canceled.rb @@ -0,0 +1,12 @@ +module Travis + module Api + module V0 + module Pusher + class Job + class Canceled < Job + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/job/created.rb b/vendor/travis-core/lib/travis/api/v0/pusher/job/created.rb new file mode 100644 index 00000000..c2af620e --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/job/created.rb @@ -0,0 +1,12 @@ +module Travis + module Api + module V0 + module Pusher + class Job + class Created < Job + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/job/finished.rb b/vendor/travis-core/lib/travis/api/v0/pusher/job/finished.rb new file mode 100644 index 00000000..2214041c --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/job/finished.rb @@ -0,0 +1,12 @@ +module Travis + module Api + module V0 + module Pusher + class Job + class Finished < Job + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/job/log.rb b/vendor/travis-core/lib/travis/api/v0/pusher/job/log.rb new file mode 100644 index 00000000..54115ee6 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/job/log.rb @@ -0,0 +1,31 @@ +module Travis + module Api + module V0 + module Pusher + class Job + class Log + attr_reader :job, :options + + def initialize(job, options = {}) + @job = job + @options = options + end + + def data + { + 'id' => job.id, + 'build_id' => job.source_id, + 'repository_id' => job.repository_id, + 'repository_private' => repository.private, + '_log' => options[:_log], + 'number' => options[:number], + 'final' => options[:final] + } + end + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/job/received.rb b/vendor/travis-core/lib/travis/api/v0/pusher/job/received.rb new file mode 100644 index 00000000..0921280d --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/job/received.rb @@ -0,0 +1,12 @@ +module Travis + module Api + module V0 + module Pusher + class Job + class Received < Job + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/pusher/job/started.rb b/vendor/travis-core/lib/travis/api/v0/pusher/job/started.rb new file mode 100644 index 00000000..90ccc963 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/pusher/job/started.rb @@ -0,0 +1,12 @@ +module Travis + module Api + module V0 + module Pusher + class Job + class Started < Job + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/worker.rb b/vendor/travis-core/lib/travis/api/v0/worker.rb new file mode 100644 index 00000000..4826160e --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/worker.rb @@ -0,0 +1,9 @@ +module Travis + module Api + module V0 + module Worker + require 'travis/api/v0/worker/job' + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/worker/job.rb b/vendor/travis-core/lib/travis/api/v0/worker/job.rb new file mode 100644 index 00000000..796d0620 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/worker/job.rb @@ -0,0 +1,33 @@ +module Travis + module Api + module V0 + module Worker + class Job + require 'travis/api/v0/worker/job/test' + + attr_reader :job + + def initialize(job, options = {}) + @job = job + end + + def commit + job.commit + end + + def repository + job.repository + end + + def request + build.request + end + + def build + job.source + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v0/worker/job/test.rb b/vendor/travis-core/lib/travis/api/v0/worker/job/test.rb new file mode 100644 index 00000000..8462a9bc --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v0/worker/job/test.rb @@ -0,0 +1,118 @@ +module Travis + module Api + module V0 + module Worker + class Job + class Test < Job + include Formats + + def data + { + 'type' => 'test', + # TODO legacy. remove this once workers respond to a 'job' key + 'build' => job_data, + 'job' => job_data, + 'source' => build_data, + 'repository' => repository_data, + 'pull_request' => commit.pull_request? ? pull_request_data : false, + 'config' => job.decrypted_config, + 'queue' => job.queue, + 'uuid' => Travis.uuid, + 'ssh_key' => ssh_key, + 'env_vars' => env_vars, + 'timeouts' => timeouts + } + end + + def build_data + { + 'id' => build.id, + 'number' => build.number + } + end + + def job_data + data = { + 'id' => job.id, + 'number' => job.number, + 'commit' => commit.commit, + 'commit_range' => commit.range, + 'commit_message' => commit.message, + 'branch' => commit.branch, + 'ref' => commit.pull_request? ? commit.ref : nil, + 'state' => job.state.to_s, + 'secure_env_enabled' => job.secure_env_enabled? + } + data['tag'] = request.tag_name if include_tag_name? + data['pull_request'] = commit.pull_request? ? commit.pull_request_number : false + data + end + + def repository_data + { + 'id' => repository.id, + 'slug' => repository.slug, + 'github_id' => repository.github_id, + 'source_url' => repository.source_url, + 'api_url' => repository.api_url, + 'last_build_id' => repository.last_build_id, + 'last_build_number' => repository.last_build_number, + 'last_build_started_at' => format_date(repository.last_build_started_at), + 'last_build_finished_at' => format_date(repository.last_build_finished_at), + 'last_build_duration' => repository.last_build_duration, + 'last_build_state' => repository.last_build_state.to_s, + 'description' => repository.description, + 'default_branch' => repository.default_branch + } + end + + def pull_request_data + { + 'number' => commit.pull_request_number, + 'head_repo' => request.head_repo, + 'base_repo' => request.base_repo, + 'head_branch' => request.head_branch, + 'base_branch' => request.base_branch + } + end + + def ssh_key + nil + end + + def env_vars + vars = settings.env_vars + vars = vars.public unless job.secure_env_enabled? + + vars.map do |var| + { + 'name' => var.name, + 'value' => var.value.decrypt, + 'public' => var.public + } + end + end + + def timeouts + { 'hard_limit' => timeout(:hard_limit), 'log_silence' => timeout(:log_silence) } + end + + def timeout(type) + timeout = settings.send(:"timeout_#{type}") + timeout = timeout * 60 if timeout # worker handles timeouts in seconds + timeout + end + + def include_tag_name? + Travis.config.include_tag_name_in_worker_payload && request.tag_name.present? + end + + def settings + repository.settings + end + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1.rb b/vendor/travis-core/lib/travis/api/v1.rb new file mode 100644 index 00000000..bb29b348 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1.rb @@ -0,0 +1,10 @@ +module Travis + module Api + module V1 + require 'travis/api/v1/archive' + require 'travis/api/v1/http' + require 'travis/api/v1/helpers' + require 'travis/api/v1/webhook' + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/archive.rb b/vendor/travis-core/lib/travis/api/v1/archive.rb new file mode 100644 index 00000000..db343c81 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/archive.rb @@ -0,0 +1,9 @@ +module Travis + module Api + module V1 + module Archive + require 'travis/api/v1/archive/build' + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/archive/build.rb b/vendor/travis-core/lib/travis/api/v1/archive/build.rb new file mode 100644 index 00000000..9911cd0d --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/archive/build.rb @@ -0,0 +1,50 @@ +module Travis + module Api + module V1 + module Archive + class Build + autoload :Job, 'travis/api/v1/archive/build/job' + + include Formats + + attr_reader :build, :commit, :repository + + def initialize(build, options = {}) + @build = build + @commit = build.commit + @repository = build.repository + end + + def data + { + 'id' => build.id, + 'number' => build.number, + 'config' => build.obfuscated_config.stringify_keys, + 'result' => 0, + 'started_at' => format_date(build.started_at), + 'finished_at' => format_date(build.finished_at), + 'duration' => build.duration, + 'commit' => commit.commit, + 'branch' => commit.branch, + 'message' => commit.message, + 'committed_at' => format_date(commit.committed_at), + 'author_name' => commit.author_name, + 'author_email' => commit.author_email, + 'committer_name' => commit.committer_name, + 'committer_email' => commit.committer_email, + 'matrix' => build.matrix.map { |job| Job.new(job).data }, + 'repository' => repository_data + } + end + + def repository_data + { + 'id' => repository.id, + 'slug' => repository.slug + } + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/archive/build/job.rb b/vendor/travis-core/lib/travis/api/v1/archive/build/job.rb new file mode 100644 index 00000000..110d971a --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/archive/build/job.rb @@ -0,0 +1,31 @@ +module Travis + module Api + module V1 + module Archive + class Build + class Job + include Formats + + attr_reader :job, :commit + + def initialize(job) + @job = job + @commit = job.commit + end + + def data + { + 'id' => job.id, + 'number' => job.number, + 'config' => job.obfuscated_config, + 'started_at' => format_date(job.started_at), + 'finished_at' => format_date(job.finished_at), + 'log' => job.log_content + } + end + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/helpers.rb b/vendor/travis-core/lib/travis/api/v1/helpers.rb new file mode 100644 index 00000000..c4905ed9 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/helpers.rb @@ -0,0 +1,9 @@ +module Travis + module Api + module V1 + module Helpers + require 'travis/api/v1/helpers/legacy' + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/helpers/legacy.rb b/vendor/travis-core/lib/travis/api/v1/helpers/legacy.rb new file mode 100644 index 00000000..2a8d3a8b --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/helpers/legacy.rb @@ -0,0 +1,34 @@ +module Travis + module Api + module V1 + module Helpers + module Legacy + RESULTS = { + passed: 0, + failed: 1 + } + + def legacy_repository_last_build_result(repository) + RESULTS[repository.last_build_state.try(:to_sym)] + end + + def legacy_build_state(build) + build.finished? ? 'finished' : build.state.to_s + end + + def legacy_build_result(build) + RESULTS[build.state.try(:to_sym)] + end + + def legacy_job_state(job) + job.finished? ? 'finished' : job.state.to_s + end + + def legacy_job_result(job) + RESULTS[job.state.try(:to_sym)] + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/http.rb b/vendor/travis-core/lib/travis/api/v1/http.rb new file mode 100644 index 00000000..a7db9f95 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/http.rb @@ -0,0 +1,17 @@ +module Travis + module Api + module V1 + module Http + require 'travis/api/v1/http/branches' + require 'travis/api/v1/http/build' + require 'travis/api/v1/http/builds' + require 'travis/api/v1/http/hooks' + require 'travis/api/v1/http/job' + require 'travis/api/v1/http/jobs' + require 'travis/api/v1/http/repositories' + require 'travis/api/v1/http/repository' + require 'travis/api/v1/http/user' + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/http/branches.rb b/vendor/travis-core/lib/travis/api/v1/http/branches.rb new file mode 100644 index 00000000..30e0c01d --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/http/branches.rb @@ -0,0 +1,43 @@ +require 'travis/api/v1/helpers/legacy' + +module Travis + module Api + module V1 + module Http + class Branches + include Formats, Helpers::Legacy + + attr_reader :builds, :options + + def initialize(builds, options = {}) + builds = builds.last_finished_builds_by_branches if builds.is_a?(Repository) # TODO remove, bc + @builds = builds + end + + def cache_key + "branches-#{builds.map(&:id).join('-')}" + end + + def updated_at + builds.compact.map(&:finished_at).compact.sort.first + end + + def data + builds.compact.map do |build| + { + 'repository_id' => build.repository_id, + 'build_id' => build.id, + 'commit' => build.commit.commit, + 'branch' => build.commit.branch, + 'message' => build.commit.message, + 'result' => legacy_build_result(build), + 'finished_at' => format_date(build.finished_at), + 'started_at' => format_date(build.started_at) + } + end + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/http/build.rb b/vendor/travis-core/lib/travis/api/v1/http/build.rb new file mode 100644 index 00000000..e0431718 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/http/build.rb @@ -0,0 +1,47 @@ +module Travis + module Api + module V1 + module Http + class Build + require 'travis/api/v1/http/build/job' + + include Formats, Helpers::Legacy + + attr_reader :build, :commit, :request + + def initialize(build, options = {}) + @build = build + @commit = build.commit + @request = build.request + end + + def data + { + 'id' => build.id, + 'repository_id' => build.repository_id, + 'number' => build.number, + 'config' => build.obfuscated_config.stringify_keys, + 'state' => legacy_build_state(build), + 'result' => legacy_build_result(build), + 'status' => legacy_build_result(build), + 'started_at' => format_date(build.started_at), + 'finished_at' => format_date(build.finished_at), + 'duration' => build.duration, + 'commit' => commit.commit, + 'branch' => commit.branch, + 'message' => commit.message, + 'committed_at' => format_date(commit.committed_at), + 'author_name' => commit.author_name, + 'author_email' => commit.author_email, + 'committer_name' => commit.committer_name, + 'committer_email' => commit.committer_email, + 'compare_url' => commit.compare_url, + 'event_type' => build.event_type, + 'matrix' => build.matrix.map { |job| Job.new(job).data }, + } + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/http/build/job.rb b/vendor/travis-core/lib/travis/api/v1/http/build/job.rb new file mode 100644 index 00000000..e924041d --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/http/build/job.rb @@ -0,0 +1,32 @@ +module Travis + module Api + module V1 + module Http + class Build + class Job + include Formats, Helpers::Legacy + + attr_reader :job + + def initialize(job) + @job = job + end + + def data + { + 'id' => job.id, + 'repository_id' => job.repository_id, + 'number' => job.number, + 'config' => job.obfuscated_config.stringify_keys, + 'result' => legacy_job_result(job), + 'started_at' => format_date(job.started_at), + 'finished_at' => format_date(job.finished_at), + 'allow_failure' => job.allow_failure + } + end + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/http/builds.rb b/vendor/travis-core/lib/travis/api/v1/http/builds.rb new file mode 100644 index 00000000..74ca3e3d --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/http/builds.rb @@ -0,0 +1,38 @@ +module Travis + module Api + module V1 + module Http + class Builds + include Formats, Helpers::Legacy + + attr_reader :builds + + def initialize(builds, options = {}) + @builds = builds + end + + def data + builds.map { |build| build_data(build) } + end + + def build_data(build) + { + 'id' => build.id, + 'repository_id' => build.repository_id, + 'number' => build.number, + 'state' => legacy_build_state(build), + 'result' => legacy_build_result(build), + 'started_at' => format_date(build.started_at), + 'finished_at' => format_date(build.finished_at), + 'duration' => build.duration, + 'commit' => build.commit.commit, + 'branch' => build.commit.branch, + 'message' => build.commit.message, + 'event_type' => build.event_type, + } + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/http/hooks.rb b/vendor/travis-core/lib/travis/api/v1/http/hooks.rb new file mode 100644 index 00000000..a35412cb --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/http/hooks.rb @@ -0,0 +1,34 @@ +module Travis + module Api + module V1 + module Http + class Hooks + attr_reader :repos, :options + + def initialize(repos, options = {}) + @repos = repos + @options = options + end + + def data + repos.map { |repo| repo_data(repo) } + end + + private + + def repo_data(repo) + { + 'uid' => [repo.owner_name, repo.name].join(':'), + 'url' => "https://github.com/#{repo.owner_name}/#{repo.name}", + 'name' => repo.name, + 'owner_name' => repo.owner_name, + 'description' => repo.description, + 'active' => repo.active, + 'private' => repo.private + } + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/http/job.rb b/vendor/travis-core/lib/travis/api/v1/http/job.rb new file mode 100644 index 00000000..dd832083 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/http/job.rb @@ -0,0 +1,44 @@ +module Travis + module Api + module V1 + module Http + class Job + include Formats, Helpers::Legacy + + attr_reader :job, :commit + + def initialize(job, options = {}) + @job = job + @commit = job.commit + end + + def data + { + 'id' => job.id, + 'number' => job.number, + 'config' => job.obfuscated_config.stringify_keys, + 'repository_id' => job.repository_id, + 'build_id' => job.source_id, + 'state' => job.finished? ? 'finished' : job.state.to_s, + 'result' => legacy_job_result(job), + 'status' => legacy_job_result(job), + 'started_at' => format_date(job.started_at), + 'finished_at' => format_date(job.finished_at), + 'log' => job.log_content, + 'commit' => commit.commit, + 'branch' => commit.branch, + 'message' => commit.message, + 'committed_at' => format_date(commit.committed_at), + 'author_name' => commit.author_name, + 'author_email' => commit.author_email, + 'committer_name' => commit.committer_name, + 'committer_email' => commit.committer_email, + 'compare_url' => commit.compare_url, + 'worker' => job.worker + } + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/http/jobs.rb b/vendor/travis-core/lib/travis/api/v1/http/jobs.rb new file mode 100644 index 00000000..3279211b --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/http/jobs.rb @@ -0,0 +1,34 @@ +module Travis + module Api + module V1 + module Http + class Jobs + include Formats, Helpers::Legacy + + attr_reader :jobs + + def initialize(jobs, options = {}) + @jobs = jobs + end + + def data + jobs.map { |job| job_data(job) } + end + + def job_data(job) + commit = job.commit + { + 'id' => job.id, + 'repository_id' => job.repository_id, + 'number' => job.number, + 'state' => legacy_job_state(job), + 'queue' => job.queue, + 'allow_failure' => job.allow_failure + } + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/api/v1/http/repositories.rb b/vendor/travis-core/lib/travis/api/v1/http/repositories.rb new file mode 100644 index 00000000..efb00792 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/http/repositories.rb @@ -0,0 +1,37 @@ +module Travis + module Api + module V1 + module Http + class Repositories + include Formats, Helpers::Legacy + + attr_reader :repositories + + def initialize(repositories, options = {}) + @repositories = repositories + end + + def data + repositories.map { |repository| repository_data(repository) } + end + + def repository_data(repository) + { + 'id' => repository.id, + 'slug' => repository.slug, + 'description' => repository.description, + 'last_build_id' => repository.last_build_id, + 'last_build_number' => repository.last_build_number, + 'last_build_status' => legacy_repository_last_build_result(repository), + 'last_build_result' => legacy_repository_last_build_result(repository), + 'last_build_duration' => repository.last_build_duration, + 'last_build_language' => nil, + 'last_build_started_at' => format_date(repository.last_build_started_at), + 'last_build_finished_at' => format_date(repository.last_build_finished_at), + } + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/http/repository.rb b/vendor/travis-core/lib/travis/api/v1/http/repository.rb new file mode 100644 index 00000000..dfceebd0 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/http/repository.rb @@ -0,0 +1,34 @@ +module Travis + module Api + module V1 + module Http + class Repository + include Formats, Helpers::Legacy + + attr_reader :repository, :options + + def initialize(repository, options = {}) + @repository = repository + end + + def data + { + 'id' => repository.id, + 'slug' => repository.slug, + 'description' => repository.description, + 'public_key' => repository.key.public_key, + 'last_build_id' => repository.last_build_id, + 'last_build_number' => repository.last_build_number, + 'last_build_status' => legacy_repository_last_build_result(repository), + 'last_build_result' => legacy_repository_last_build_result(repository), + 'last_build_duration' => repository.last_build_duration, + 'last_build_language' => nil, + 'last_build_started_at' => format_date(repository.last_build_started_at), + 'last_build_finished_at' => format_date(repository.last_build_finished_at), + } + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/http/user.rb b/vendor/travis-core/lib/travis/api/v1/http/user.rb new file mode 100644 index 00000000..833f7533 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/http/user.rb @@ -0,0 +1,31 @@ +module Travis + module Api + module V1 + module Http + class User + include Formats + + attr_reader :user, :options + + def initialize(user, options = {}) + @user = user + @options = options + end + + def data + { + 'login' => user.login, + 'name' => user.name, + 'email' => user.email, + 'gravatar_id' => user.gravatar_id, + 'locale' => user.locale, + 'is_syncing' => user.is_syncing, + 'synced_at' => format_date(user.synced_at) + } + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/api/v1/webhook.rb b/vendor/travis-core/lib/travis/api/v1/webhook.rb new file mode 100644 index 00000000..2b38d28f --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/webhook.rb @@ -0,0 +1,9 @@ +module Travis + module Api + module V1 + module Webhook + require 'travis/api/v1/webhook/build' + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/webhook/build.rb b/vendor/travis-core/lib/travis/api/v1/webhook/build.rb new file mode 100644 index 00000000..ccea2076 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/webhook/build.rb @@ -0,0 +1,27 @@ +module Travis + module Api + module V1 + module Webhook + class Build + require 'travis/api/v1/webhook/build/finished' + + attr_reader :build, :commit, :request, :repository, :options + + def initialize(build, options = {}) + @build = build + @commit = build.commit + @request = build.request + @repository = build.repository + @options = options + end + + private + + def build_url + ["https://#{Travis.config.host}", repository.slug, 'builds', build.id].join('/') + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/webhook/build/finished.rb b/vendor/travis-core/lib/travis/api/v1/webhook/build/finished.rb new file mode 100644 index 00000000..7c49cd95 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/webhook/build/finished.rb @@ -0,0 +1,70 @@ +module Travis + module Api + module V1 + module Webhook + class Build + class Finished < Build + require 'travis/api/v1/webhook/build/finished/job' + + include Formats + + def data + data = { + 'id' => build.id, + 'repository' => repository_data, + 'number' => build.number, + 'config' => build.obfuscated_config.stringify_keys, + 'status' => build.result, + 'result' => build.result, + 'status_message' => result_message, + 'result_message' => result_message, + 'started_at' => format_date(build.started_at), + 'finished_at' => format_date(build.finished_at), + 'duration' => build.duration, + 'build_url' => build_url, + 'commit_id' => commit.id, + 'commit' => commit.commit, + 'base_commit' => request.base_commit, + 'head_commit' => request.head_commit, + 'branch' => commit.branch, + 'message' => commit.message, + 'compare_url' => commit.compare_url, + 'committed_at' => format_date(commit.committed_at), + 'author_name' => commit.author_name, + 'author_email' => commit.author_email, + 'committer_name' => commit.committer_name, + 'committer_email' => commit.committer_email, + 'matrix' => build.matrix.map { |job| Job.new(job, options).data }, + 'type' => build.event_type, + 'state' => build.state.to_s, + 'pull_request' => build.pull_request?, + 'pull_request_number' => build.pull_request_number, + 'pull_request_title' => build.pull_request_title, + 'tag' => request.tag_name + } + + if commit.pull_request? + data['pull_request_number'] = commit.pull_request_number + end + + data + end + + def repository_data + { + 'id' => repository.id, + 'name' => repository.name, + 'owner_name' => repository.owner_name, + 'url' => repository.url + } + end + + def result_message + @result_message ||= ::Build::ResultMessage.new(build).short + end + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v1/webhook/build/finished/job.rb b/vendor/travis-core/lib/travis/api/v1/webhook/build/finished/job.rb new file mode 100644 index 00000000..bf0493cc --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v1/webhook/build/finished/job.rb @@ -0,0 +1,50 @@ +module Travis + module Api + module V1 + module Webhook + class Build + class Finished < Build + class Job + include Formats + + attr_reader :job, :commit, :options + + def initialize(job, options = {}) + @job = job + @commit = job.commit + @options = options + end + + def data + data = { + 'id' => job.id, + 'repository_id' => job.repository_id, + 'parent_id' => job.source_id, + 'number' => job.number, + 'state' => job.finished? ? 'finished' : job.state.to_s, + 'config' => job.obfuscated_config, + 'status' => job.result, + 'result' => job.result, + 'commit' => commit.commit, + 'branch' => commit.branch, + 'message' => commit.message, + 'compare_url' => commit.compare_url, + 'committed_at' => format_date(commit.committed_at), + 'author_name' => commit.author_name, + 'author_email' => commit.author_email, + 'committer_name' => commit.committer_name, + 'committer_email' => commit.committer_email, + 'allow_failure' => job.allow_failure + } + data['log'] = job.log_content || '' if options[:include_logs] + data['started_at'] = format_date(job.started_at) if job.started? + data['finished_at'] = format_date(job.finished_at) if job.finished? + data + end + end + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/api/v2.rb b/vendor/travis-core/lib/travis/api/v2.rb new file mode 100644 index 00000000..e70b19f4 --- /dev/null +++ b/vendor/travis-core/lib/travis/api/v2.rb @@ -0,0 +1,7 @@ +module Travis + module Api + module V2 + end + end +end + diff --git a/vendor/travis-core/lib/travis/chunkifier.rb b/vendor/travis-core/lib/travis/chunkifier.rb new file mode 100644 index 00000000..39176887 --- /dev/null +++ b/vendor/travis-core/lib/travis/chunkifier.rb @@ -0,0 +1,59 @@ +require 'coder/cleaner/simple/encodings' + +module Travis + class Chunkifier < Struct.new(:content, :chunk_size, :options) + include Enumerable + include Coder::Cleaner::Simple::Encodings::UTF_8 + + def initialize(*) + super + + self.options ||= {} + end + + def json? + options[:json] + end + + def length + parts.length + end + + def each(&block) + parts.each(&block) + end + + def parts + @parts ||= split + end + + def split + parts = content.scan(/.{1,#{chunk_split_size}}/m) + chunks = [] + current_chunk = '' + + parts.each do |part| + if too_big?(current_chunk + part) + chunks << current_chunk + current_chunk = part + else + current_chunk << part + end + end + + chunks << current_chunk if current_chunk.length > 0 + + chunks + end + + def chunk_split_size + size = chunk_size / 10 + size == 0 ? 1 : size + end + + def too_big?(current_chunk) + current_chunk = current_chunk.to_json if json? + current_chunk.bytesize > chunk_size + end + end +end diff --git a/vendor/travis-core/lib/travis/commit_command.rb b/vendor/travis-core/lib/travis/commit_command.rb new file mode 100644 index 00000000..2331387f --- /dev/null +++ b/vendor/travis-core/lib/travis/commit_command.rb @@ -0,0 +1,23 @@ +module Travis + class CommitCommand + + def initialize(message) + @message = message.to_s + end + + def skip? + backwards_skip or command == 'skip' + end + + private + attr_reader :message + + def command + message =~ /\[ci(?: |:)([\w ]*)\]/i && $1.downcase + end + + def backwards_skip + message =~ /\[skip\s+ci\]/i && true + end + end +end diff --git a/vendor/travis-core/lib/travis/config/database.rb b/vendor/travis-core/lib/travis/config/database.rb new file mode 100644 index 00000000..6ee04efb --- /dev/null +++ b/vendor/travis-core/lib/travis/config/database.rb @@ -0,0 +1,40 @@ +module Travis + class Config + class Database < Struct.new(:options) + include Helpers + + VARIABLES = { application_name: ENV['DYNO'] || $0, statement_timeout: 10_000 } + DEFAULTS = { adapter: 'postgresql', encoding: 'unicode', variables: VARIABLES } + + def config + config = compact(Url.parse(url).to_h) + config = deep_merge(DEFAULTS, config) unless config.empty? + config[:pool] = pool.to_i if pool + config + end + + private + + def url + env('DATABASE_URL').compact.first + end + + def pool + env('DATABASE_POOL_SIZE', 'DB_POOL').compact.first + end + + def env(*keys) + ENV.values_at(*keys.map { |key| prefix(key) }.flatten) + end + + def prefix(key) + key = [options[:prefix], key].compact.join('_').upcase + ["TRAVIS_#{key}", key] + end + + def options + super || {} + end + end + end +end diff --git a/vendor/travis-core/lib/travis/config/defaults.rb b/vendor/travis-core/lib/travis/config/defaults.rb new file mode 100644 index 00000000..a322f94b --- /dev/null +++ b/vendor/travis-core/lib/travis/config/defaults.rb @@ -0,0 +1,72 @@ +require 'travis/config' + +module Travis + class Config < Hashr + require 'travis/config/database' + require 'travis/config/url' + + HOSTS = { + production: 'travis-ci.org', + staging: 'staging.travis-ci.org', + development: 'localhost:3000' + } + + define host: 'travis-ci.org', + shorten_host: 'trvs.io', + tokens: { internal: 'token' }, + auth: { target_origin: nil }, + assets: { host: HOSTS[Travis.env.to_sym] }, + amqp: { username: 'guest', password: 'guest', host: 'localhost', prefetch: 1 }, + database: { adapter: 'postgresql', database: "travis_#{Travis.env}", encoding: 'unicode', min_messages: 'warning', variables: { statement_timeout: 10_000 } }, + s3: { access_key_id: '', secret_access_key: '' }, + pusher: { app_id: 'app-id', key: 'key', secret: 'secret' }, + sidekiq: { namespace: 'sidekiq', pool_size: 1 }, + smtp: {}, + email: {}, + github: { api_url: 'https://api.github.com', token: 'travisbot-token' }, + async: {}, + notifications: [], # TODO rename to event.handlers + metrics: { reporter: 'librato' }, + logger: { thread_id: true }, + queues: [], + default_queue: 'builds.linux', + jobs: { retry: { after: 60 * 60 * 2, max_attempts: 1, interval: 60 * 5 } }, + queue: { limit: { default: 5, by_owner: {} }, interval: 3 }, + logs: { shards: 1, intervals: { vacuum: 10, regular: 180, force: 3 * 60 * 60 } }, + roles: {}, + archive: {}, + ssl: {}, + redis: { url: 'redis://localhost:6379' }, + repository: { ssl_key: { size: 4096 } }, + repository_filter: { include: [/^rails\/rails/], exclude: [/\/rails$/] }, + encryption: Travis.env == 'development' || Travis.env == 'test' ? { key: 'secret' * 10 } : {}, + sync: { organizations: { repositories_limit: 1000 } }, + states_cache: { memcached_servers: 'localhost:11211' }, + sentry: {}, + services: { find_requests: { max_limit: 100, default_limit: 25 } }, + settings: { timeouts: { defaults: { hard_limit: 50, log_silence: 10 }, maximums: { hard_limit: 180, log_silence: 60 } }, + rate_limit: { defaults: { api_builds: 10 }, maximums: { api_builds: 200 } } }, + endpoints: {}, + oauth2: {} + + default :_access => [:key] + + def initialize(*) + super + load_urls + end + + # Wild monkeypatch to backport travis-config v1.0.x resource url loading. + # Needed for enterprise. + class Docker + def load + config = compact( + database: Database.new.config, + logs_database: Database.new(prefix: :logs).config, + amqp: Url.parse(ENV['TRAVIS_RABBITMQ_URL'] || ENV['RABBITMQ_URL']).to_h.compact, + redis: { url: ENV['TRAVIS_REDIS_URL'] || ENV['REDIS_URL'] }.compact + ) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/config/url.rb b/vendor/travis-core/lib/travis/config/url.rb new file mode 100644 index 00000000..26e3cc6d --- /dev/null +++ b/vendor/travis-core/lib/travis/config/url.rb @@ -0,0 +1,36 @@ +module Travis + class Config + module Url + class Base < Struct.new(:username, :password, :host, :port, :database) + def to_h + Hash[each_pair.to_a] + end + end + + Generic = Class.new(Base) + Postgres = Class.new(Base) + Redis = Class.new(Base) + + class Amqp < Base + alias :vhost :database + + def to_h + super.reject { |key, value| key == :database }.merge(vhost: vhost) + end + end + + class << self + def parse(url) + return Generic.new if url.nil? || url.empty? + uri = URI.parse(url) + const = const_get(camelize(uri.scheme)) + const.new(uri.user, uri.password, uri.host, uri.port, uri.path[1..-1]) + end + + def camelize(string) + string.to_s.split('_').collect(&:capitalize).join + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/engine.rb b/vendor/travis-core/lib/travis/engine.rb new file mode 100644 index 00000000..875a4947 --- /dev/null +++ b/vendor/travis-core/lib/travis/engine.rb @@ -0,0 +1,21 @@ +require 'travis' +require 'rails/engine' + +module Travis + class Engine < Rails::Engine + initializer 'add migrations path' do |app| + # need to insert to both Rails.app.paths and Migrator.migration_paths + # because Rails' stupid rake tasks copy them over before loading the + # engines *unless* multiple rake db tasks are combined (as in rake + # db:create db:migrate). Happens in Rails <= 3.2.2 + paths = [ + Rails.application.paths['db/migrate'], + ActiveRecord::Migrator.migrations_paths + ] + paths.each do |paths| + path = root.join('db/migrate').to_s + paths << path unless paths.include?(path) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/enqueue.rb b/vendor/travis-core/lib/travis/enqueue.rb new file mode 100644 index 00000000..e5ac1d96 --- /dev/null +++ b/vendor/travis-core/lib/travis/enqueue.rb @@ -0,0 +1,6 @@ +module Travis + module Enqueue + require 'travis/enqueue/services' + end +end + diff --git a/vendor/travis-core/lib/travis/enqueue/services.rb b/vendor/travis-core/lib/travis/enqueue/services.rb new file mode 100644 index 00000000..9228aff5 --- /dev/null +++ b/vendor/travis-core/lib/travis/enqueue/services.rb @@ -0,0 +1,14 @@ +module Travis + module Enqueue + module Services + require 'travis/enqueue/services/enqueue_jobs' + + class << self + def register + constants(false).each { |name| const_get(name) } + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/enqueue/services/enqueue_jobs.rb b/vendor/travis-core/lib/travis/enqueue/services/enqueue_jobs.rb new file mode 100644 index 00000000..4e52275c --- /dev/null +++ b/vendor/travis-core/lib/travis/enqueue/services/enqueue_jobs.rb @@ -0,0 +1,127 @@ +require 'travis/services/base' +require 'travis/support/instrumentation' +require 'travis/support/exceptions/handling' + +module Travis + module Enqueue + module Services + # Finds owners that have queueable jobs and for each owner: + # + # * checks how many jobs can be enqueued + # * finds the oldest N queueable jobs and + # * enqueues them + class EnqueueJobs < Travis::Services::Base + TIMEOUT = 2 + + extend Travis::Instrumentation, Travis::Exceptions::Handling + + require 'travis/enqueue/services/enqueue_jobs/limit' + + register :enqueue_jobs + + def self.run + new.run + end + + def reports + @reports ||= {} + end + + def run + enqueue_all && reports unless disabled? + end + instrument :run + rescues :run, from: Exception, backtrace: false + + def disabled? + Timeout.timeout(TIMEOUT) do + Travis::Features.feature_deactivated?(:job_queueing) + end + rescue Timeout::Error, Redis::TimeoutError => e + Travis.logger.error("[enqueue] Timeout trying to check enqueuing feature flag.") + return false + end + + private + + def enqueue_all + grouped_jobs = jobs.group_by(&:owner) + + Metriks.timer('enqueue.total').time do + grouped_jobs.each do |owner, jobs| + next unless owner + Metriks.timer('enqueue.full_enqueue_per_owner').time do + limit = nil + queueable = nil + Metriks.timer('enqueue.limit_per_owner').time do + Travis.logger.info "About to evaluate jobs for: #{owner.login}." + limit = Limit.new(owner, jobs) + queueable = limit.queueable + end + + Metriks.timer('enqueue.enqueue_per_owner').time do + enqueue(queueable) + end + + Metriks.timer('enqueue.report_per_owner').time do + reports[owner.login] = limit.report + end + end + end + end + end + + def enqueue(jobs) + jobs.each do |job| + Travis.logger.info("enqueueing slug=#{job.repository.slug} job_id=#{job.id}") + Metriks.timer('enqueue.publish_job').time do + publish(job) + end + + Metriks.timer('enqueue.enqueue_job').time do + job.enqueue + end + end + end + + def publish(job) + Metriks.timer('enqueue.publish_job').time do + payload = Travis::Api.data(job, for: 'worker', type: 'Job::Test', version: 'v0') + publisher(job.queue).publish(payload, properties: { type: payload['type'], persistent: true }) + end + end + + def jobs + Metriks.timer('enqueue.fetch_jobs').time do + jobs = Job.includes(:owner).queueable.all + Travis.logger.info "Found #{jobs.size} jobs in total." if jobs.size > 0 + jobs + end + end + + def publisher(queue) + Travis::Amqp::Publisher.builds(queue) + end + + class Instrument < Notification::Instrument + def run_completed + publish(msg: format(target.reports), reports: target.reports) + end + + def format(reports) + reports = Array(reports) + if reports.any? + reports = reports.map do |repo, report| + " #{repo}: #{report.map { |key, value| "#{key}: #{value}" }.join(', ')}" + end + "enqueued:\n#{reports.join("\n")}" + else + 'nothing to enqueue.' + end + end + end + Instrument.attach_to(self) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/enqueue/services/enqueue_jobs/limit.rb b/vendor/travis-core/lib/travis/enqueue/services/enqueue_jobs/limit.rb new file mode 100644 index 00000000..3d23ef2f --- /dev/null +++ b/vendor/travis-core/lib/travis/enqueue/services/enqueue_jobs/limit.rb @@ -0,0 +1,85 @@ +require 'travis/services/base' +require 'travis/model/job' + +module Travis + module Enqueue + module Services + class EnqueueJobs < Travis::Services::Base + class Limit + attr_reader :owner, :jobs, :config + + def initialize(owner, jobs) + @owner = owner + @jobs = jobs + @config = Travis.config.queue.limit + end + + def queueable + @queueable ||= filter_by_repository(jobs)[0, max_queueable] + end + + def filter_by_repository(jobs) + return jobs unless Travis.config.limit_per_repo_enabled? + queueable_by_repository_id = {} + jobs.reject do |job| + if job.repository.settings.restricts_number_of_builds? + queueable?(job, queueable_by_repository_id, running_by_repository_id) + end + end + end + + def running_by_repository_id + @running_by_repository ||= Hash[running_jobs.group_by(&:repository_id).map {|repository_id, jobs| [repository_id, jobs.size]}] + end + + def queueable?(job, queueable, running) + repository = job.repository_id + queueable[repository] ||= 0 + + runnable_count = queueable[repository] + + (running[repository] || 0) + if runnable_count < job.repository.settings.maximum_number_of_builds + queueable[repository] += 1 + false + else + true + end + end + + def report + { total: jobs.size, running: running, max: max_jobs, queueable: queueable.size } + end + + private + + def running_jobs + @running_jobs ||= Job.owned_by(owner).running + end + + def running + @running ||= Job.owned_by(owner).running.count(:id) + end + + def max_queueable + return config.default if owner.login.nil? + + if unlimited? + 999 + else + queueable = max_jobs - running + queueable < 0 ? 0 : queueable + end + end + + def max_jobs + config.by_owner[owner.login] || config.default + end + + def unlimited? + config.by_owner[owner.login] == -1 + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/errors.rb b/vendor/travis-core/lib/travis/errors.rb new file mode 100644 index 00000000..939590c8 --- /dev/null +++ b/vendor/travis-core/lib/travis/errors.rb @@ -0,0 +1,20 @@ +module Travis + class RepositoryNotFoundError < StandardError + def initialize(params) + details = '' + + if id = params[:repository_id] || params[:id] + details = "with id=#{params[:repository_id] || params[:id]} " + elsif params[:github_id] + details = "with github_id=#{params[:github_id]} " + elsif params.key?(:slug) + details = "with slug=#{params[:slug]} " + elsif params.key?(:name) && params.key?(:owner_name) + details = "with slug=#{params[:name]}/#{params[:owner_name]} " + end + + + super("Repository #{details}could not be found") + end + end +end diff --git a/vendor/travis-core/lib/travis/event.rb b/vendor/travis-core/lib/travis/event.rb new file mode 100644 index 00000000..b4ae76d0 --- /dev/null +++ b/vendor/travis-core/lib/travis/event.rb @@ -0,0 +1,57 @@ +require 'core_ext/module/include' +require 'active_support/core_ext/string/inflections' + +module Travis + + # Event handlers register to events that are issued from state change + # events on the core domain models (such as Request, Build and Job::Test). + # + # Handler registrations are defined in Travis.config so they can be added or + # removed easily for different environments. + # + # Note that Travis::Event#notify accepts an internal event name like + # 'create' (coming from the simple_states implementation in the models) and + # turns it into a namespaced client event name like 'job:test:created'). + # Notification handlers register for and deal with these client event names. + module Event + require 'travis/event/config' + require 'travis/event/handler' + require 'travis/event/subscription' + + SUBSCRIBERS = %w(metrics) + + class << self + include Logging + + def subscriptions + @subscriptions ||= subscribers.map do |name| + name = 'github_status' if name == 'github_commit_status' # TODO compat, remove once configs have been updated + subscription = Subscription.new(name) + subscription if subscription.subscriber + end.compact + end + + def dispatch(event, *args) + subscriptions.each do |subscription| + subscription.notify(event, *args) + end + end + + def subscribers + (SUBSCRIBERS + Travis.config.notifications).uniq + end + end + + def notify(event, *args) + Travis::Event.dispatch(client_event(event, self), self, *args) + end + + protected + + def client_event(event, object) + event = "#{event}ed".gsub(/eded$|eed$/, 'ed') unless [:log, :ready].include?(event) + namespace = object.class.name.underscore.gsub('/', ':').gsub(/travis:model:/, '') + [namespace, event].join(':') + end + end +end diff --git a/vendor/travis-core/lib/travis/event/config.rb b/vendor/travis-core/lib/travis/event/config.rb new file mode 100644 index 00000000..67331a77 --- /dev/null +++ b/vendor/travis-core/lib/travis/event/config.rb @@ -0,0 +1,109 @@ +require 'travis/secure_config' + +module Travis + module Event + class Config + DEFAULTS = { + start: { email: false, webhooks: false, campfire: false, hipchat: false, irc: false, flowdock: false, sqwiggle: false, slack: false, pushover: false }, + success: { email: :change, webhooks: :always, campfire: :always, hipchat: :always, irc: :always, flowdock: :always, sqwiggle: :always, slack: :always, pushover: :always, }, + failure: { email: :always, webhooks: :always, campfire: :always, hipchat: :always, irc: :always, flowdock: :always, sqwiggle: :always, slack: :always, pushover: :always, } + } + + attr_reader :payload, :build, :secure_key, :config + + def initialize(payload, secure_key = nil) + @payload = payload + @build = payload['build'] + @config = build['config'] + @secure_key = secure_key + end + + def enabled?(key) + return !!notifications[key] if notifications.has_key?(key) # TODO this seems inconsistent. what if email: { disabled: true } + [:disabled, :disable].each { |key| return !notifications[key] if notifications.has_key?(key) } # TODO deprecate disabled and disable + true + end + + def send_on?(type, event) + send(:"send_on_#{event}_for?", type) + end + + def send_on_started_for?(type) + config = with_fallbacks(type, :on_start, DEFAULTS[:start][type]) + config == true || config == :always + end + + def send_on_finished_for?(type) + send_on_initial_build? || send_on_success_for?(type) || send_on_failure_for?(type) + end + + def send_on_initial_build? + build['previous_state'].nil? + end + + def send_on_success_for?(type) + !!if build_passed? + config = with_fallbacks(type, :on_success, DEFAULTS[:success][type]) + config == :always || (config == :change && !previous_build_passed?) + end + end + + def send_on_failure_for?(type) + !!if !build_passed? + config = with_fallbacks(type, :on_failure, DEFAULTS[:failure][type]) + config == :always || (config == :change && previous_build_passed?) + end + end + + def build_passed? + build['state'].try(:to_sym) == :passed + end + + def previous_build_passed? + build['previous_state'].try(:to_sym) == :passed + end + + # Fetches config with fallbacks. (notification type > global > default) + # Filters can be configured for each notification type. + # If no rules are configured for the given type, then fall back to the global rules, and then to the defaults. + def with_fallbacks(type, key, default) + config = if (notifications[type] && notifications[type].is_a?(Hash) && notifications[type].has_key?(key)) + # Returns the type config if key is present (notifications: email: [:on_success]) + notifications[type][key] + elsif notifications.has_key?(key) + # Returns the global config if key is present (notifications: [:on_success]) + notifications[key] + else + # Else, returns the given default + default + end + + config.respond_to?(:to_sym) ? config.to_sym : config + end + + # Returns (recipients, urls, channels) for (email, webhooks, irc) + # Notification type config can be nil, true/false, a string, an array of values, + # or a hash containing a key for these values. + def notification_values(type, key) + config = notifications[type] rescue {} + value = config.is_a?(Hash) ? config[key] : config + case value + when Array, String + normalize_array(value) + else + value + end + end + + def notifications + Travis::SecureConfig.decrypt(config.fetch(:notifications, {}), secure_key) + end + + def normalize_array(values) + values = Array(values).compact + values = values.map { |value| value.split(',') if value.is_a?(String) } + values.compact.flatten.map(&:strip).reject(&:blank?) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/event/handler.rb b/vendor/travis-core/lib/travis/event/handler.rb new file mode 100644 index 00000000..177c41ab --- /dev/null +++ b/vendor/travis-core/lib/travis/event/handler.rb @@ -0,0 +1,81 @@ +require 'active_support/core_ext/object/blank' +require 'travis/support/logging' +require 'travis/support/instrumentation' +require 'travis/support/exceptions/handling' + +require 'travis/api' +require 'travis/event/config' +require 'travis/model/build' + +module Travis + module Event + class Handler + require 'travis/event/handler/metrics' + require 'travis/event/handler/trail' + + include Logging + extend Instrumentation, Exceptions::Handling + + class << self + def notify(event, object, data = {}) + payload = Api.data(object, for: 'event', version: 'v0', params: data) if object.is_a?(Build) + handler = new(event, object, data, payload) + handler.notify if handler.handle? + end + end + + attr_reader :event, :object, :data, :payload + + def initialize(event, object, data = {}, payload = {}) + @event = event + @object = object + @data = data + @payload = payload + end + + def notify + handle + end + # TODO disable instrumentation in tests + instrument :notify + rescues :notify, from: Exception + + private + + def config + # TODO: we should decrypt things in tasks, not in event handler, + # secure_key should be passed to the task and then it should + # decrypt the values, which task needs + @config ||= Config.new(payload, secure_key) + end + + def repository + @repository ||= payload['repository'] + end + + def job + @job ||= payload['job'] + end + + def build + @build ||= payload['build'] + end + + def request + @request ||= payload['request'] + end + + def commit + @commit ||= payload['commit'] + end + + def secure_key + object.respond_to?(:repository) ? object.repository.key : nil + end + + def pull_request? + build['pull_request'] + end + end + end +end diff --git a/vendor/travis-core/lib/travis/event/handler/metrics.rb b/vendor/travis-core/lib/travis/event/handler/metrics.rb new file mode 100644 index 00000000..0548c7e8 --- /dev/null +++ b/vendor/travis-core/lib/travis/event/handler/metrics.rb @@ -0,0 +1,50 @@ +require 'travis/support/metrics' + +module Travis + module Event + class Handler + + # Stores metrics about domain events + class Metrics < Handler + EVENTS = /job:test:(started|finished)/ + + def initialize(*) + super + @payload = Api.data(object, type: 'job', for: 'event', version: 'v0', params: data) + end + + def handle? + true + end + + def handle + case event + when 'job:test:started' + events = %W(job.queue.wait_time job.queue.wait_time.#{queue}) + if job['created_at'] && job['started_at'] + meter(events, job['created_at'], job['started_at']) + end + when 'job:test:finished' + events = %W(job.duration job.duration.#{queue}) + if job['started_at'] && job['finished_at'] + meter(events, job['started_at'], job['finished_at']) + end + end + end + + private + + def queue + job['queue'].gsub('.', '-') + end + + def meter(events, started_at, finished_at) + events.each do |event| + Travis::Metrics.meter(event, started_at: started_at, finished_at: finished_at) + end + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/event/handler/trail.rb b/vendor/travis-core/lib/travis/event/handler/trail.rb new file mode 100644 index 00000000..a708f531 --- /dev/null +++ b/vendor/travis-core/lib/travis/event/handler/trail.rb @@ -0,0 +1,42 @@ +module Travis + module Event + class Handler + class Trail < Handler + # EVENTS = [/^((?!event|log|worker).)*$/] # i.e. does not include "log" + EVENTS = /--deactivated--/ + + # attr_reader :data + + # def initialize(*) + # super + # @data = build_data if handle? + # end + + def handle? + false + # Features.feature_active?(:event_trail) + end + + # def handle + # ::Event.create!(:source => object, :repository => repository, :event => event, :data => data) + # end + + # private + + # def repository + # object.is_a?(Repository) ? object : object.repository + # end + + # def build_data + # data = {} + # data[:commit] = object.commit.try(:commit) if object.respond_to?(:commit) + # data[:type] = object.request.try(:event_type) if object.respond_to?(:request) + # data[:number] = object.number if object.respond_to?(:number) + # data[:state] = object.result if object.respond_to?(:state) + # data[:message] = object.message if object.respond_to?(:message) + # data + # end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/event/subscription.rb b/vendor/travis-core/lib/travis/event/subscription.rb new file mode 100644 index 00000000..0897a2ce --- /dev/null +++ b/vendor/travis-core/lib/travis/event/subscription.rb @@ -0,0 +1,61 @@ +require 'active_support/inflector/inflections.rb' +require 'metriks' + +module Travis + module Event + + # Event handlers subscribe to events issued from core models (such + # as Build and Job::Test). + # + # Subscriptions are defined in Travis.config so they can easily be + # added/removed for an environment. + # + # Subscribing classes are supposed to define an EVENTS constant which holds + # a regular expression which will be matched against the event name. + class Subscription + class << self + def register(name, const) + handlers[name.to_sym] = const + end + + def handlers + @handlers ||= {} + end + end + + attr_reader :name + + def initialize(name) + @name = name + end + + def subscriber + self.class.handlers[name.to_sym] || Handler.const_get(name.to_s.camelize, false) + rescue NameError => e + Travis.logger.error "Could not find event handler #{name.inspect}, ignoring." + nil + end + + def patterns + subscriber ? Array(subscriber::EVENTS) : [] + end + + def notify(event, *args) + if matches?(event) + subscriber.notify(event, *args) + increment_counter(event) + end + end + + def matches?(event) + patterns.any? { |patterns| patterns.is_a?(Regexp) ? patterns.match(event) : patterns == event } + end + + def increment_counter(event) + # TODO ask mathias about this metric + metric = "travis.notifications.#{name}.#{event.gsub(/:/, '.')}" + Metriks.meter(metric).mark + end + end + end +end diff --git a/vendor/travis-core/lib/travis/features.rb b/vendor/travis-core/lib/travis/features.rb new file mode 100644 index 00000000..4ed075a1 --- /dev/null +++ b/vendor/travis-core/lib/travis/features.rb @@ -0,0 +1,146 @@ +require 'redis' +require 'rollout' +require 'active_support/deprecation' +require 'active_support/core_ext/module' + +module Travis + # Travis::Features contains methods to handle feature flags. + module Features + class << self + methods = (Rollout.public_instance_methods(false) - [:active?, "active?"]) << {:to => :rollout} + delegate(*methods) + end + + def redis + Travis.redis + end + + def rollout + @rollout ||= ::Rollout.new(redis) + end + + # Returns whether a given feature is enabled either globally or for a given + # repository. + # + # By default, this will return false. + def active?(feature, repository) + feature_active?(feature) or + (rollout.active?(feature, repository.owner) or + repository_active?(feature, repository)) + end + + def activate_repository(feature, repository) + redis.sadd(repository_key(feature), repository_id(repository)) + end + + def deactivate_repository(feature, repository) + redis.srem(repository_key(feature), repository_id(repository)) + end + + # Return whether a given feature is enabled for a repository. + # + # By default, this will return false. + def repository_active?(feature, repository) + redis.sismember(repository_key(feature), repository_id(repository)) + end + + # Return whether a given feature is enabled for a user. + # + # By default, this will return false. + def user_active?(feature, user) + rollout.active?(feature, user) + end + + def activate_all(feature) + redis.del(disabled_key(feature)) + end + + # Return whether a feature is enabled globally. + # + # By default, this will return false. + def feature_active?(feature) + enabled_for_all?(feature) and !feature_inactive?(feature) + end + + # Return whether a feature has been disabled. + # + # This is similar to feature_deactivated?, but with the opposite default. + # + # By default this will return true (ie. disabled). + def feature_inactive?(feature) + redis.get(disabled_key(feature)) != "1" + end + + # Return whether a feature has been disabled. + # + # This is similar to feature_inactive?, but with the opposite default. + # + # By default this will return false (ie not disabled). + def feature_deactivated?(feature) + redis.get(disabled_key(feature)) == '0' + end + + def deactivate_all(feature) + redis.set(disabled_key(feature), 0) + end + + # Return whether a feature has been enabled globally. + # + # By default this will return false. + def enabled_for_all?(feature) + redis.get(enabled_for_all_key(feature)) == '1' + end + + def enable_for_all(feature) + redis.set(enabled_for_all_key(feature), 1) + end + + def disable_for_all(feature) + redis.set(enabled_for_all_key(feature), 0) + end + + def activate_owner(feature, owner) + redis.sadd(owner_key(feature, owner), owner.id) + end + + def deactivate_owner(feature, owner) + redis.srem(owner_key(feature, owner), owner.id) + end + + # Return whether a feature has been enabled for a user. + # + # By default, this return false. + def owner_active?(feature, owner) + redis.sismember(owner_key(feature, owner), owner.id) + end + + extend self + + private + + def key(name) + "feature:#{name}" + end + + def owner_key(feature, owner) + suffix = owner.class.table_name + "#{key(feature)}:#{suffix}" + end + + def repository_key(feature) + "#{key(feature)}:repositories" + end + + def disabled_key(feature) + "#{key(feature)}:disabled" + end + + def enabled_for_all_key(feature) + "#{key(feature)}:disabled" + end + + def repository_id(repository) + repository.respond_to?(:id) ? repository.id : repository.to_i + end + end +end diff --git a/vendor/travis-core/lib/travis/github.rb b/vendor/travis-core/lib/travis/github.rb new file mode 100644 index 00000000..95b5a0ad --- /dev/null +++ b/vendor/travis-core/lib/travis/github.rb @@ -0,0 +1,36 @@ +require 'gh' +require 'core_ext/hash/compact' + +module Travis + module Github + require 'travis/github/services' + + class << self + def setup + GH.set( + client_id: Travis.config.oauth2.client_id, + client_secret: Travis.config.oauth2.client_secret, + user_agent: "Travis-CI/#{TravisCore::VERSION} GH/#{GH::VERSION}", + origin: Travis.config.host, + api_url: Travis.config.github.api_url, + ssl: Travis.config.ssl.to_h.merge(Travis.config.github.ssl || {}).to_h.compact + ) + end + + def authenticated(user, &block) + fail "we don't have a github token for #{user.inspect}" if user.github_oauth_token.blank? + GH.with(:token => user.github_oauth_token, &block) + end + + # TODO: Maybe this should move to gh? + def scopes_for(token) + token = token.github_oauth_token if token.respond_to? :github_oauth_token + scopes = GH.with(token: token.to_s) { GH.head('user') }.headers['x-oauth-scopes'] if token.present? + scopes &&= scopes.gsub(/\s/,'').split(',') + Array(scopes).sort + rescue GH::Error + [] + end + end + end +end diff --git a/vendor/travis-core/lib/travis/github/education.rb b/vendor/travis-core/lib/travis/github/education.rb new file mode 100644 index 00000000..765ecf30 --- /dev/null +++ b/vendor/travis-core/lib/travis/github/education.rb @@ -0,0 +1,45 @@ +require 'timeout' +require 'json' + +module Travis + module Github + class Education < Struct.new(:github_oauth_token) + def self.active?(owner) + if Travis::Features.feature_active?(:education) || Travis::Features.owner_active?(:education, owner) + owner.education? if owner.respond_to? :education? + end + end + + def self.education_queue?(owner) + # this method is here so it can be overridden with subscription logic + active?(owner) + end + + include Travis::Logging + + def student? + data['student'] + end + + def data + @data ||= fetch + end + + def fetch + Timeout::timeout(timeout) do + remote = GH::Remote.new + remote.setup('https://education.github.com/api', token: github_oauth_token) + response = remote.fetch_resource('/user') + JSON.parse(response.body) + end + rescue GH::Error, JSON::ParserError, Timeout::Error => e + log_exception(e) unless e.is_a? GH::Error and e.info[:response_status] == 401 + {} + end + + def timeout + Travis.config.education_endpoint_timeout || 2 + end + end + end +end diff --git a/vendor/travis-core/lib/travis/github/services.rb b/vendor/travis-core/lib/travis/github/services.rb new file mode 100644 index 00000000..fa408c79 --- /dev/null +++ b/vendor/travis-core/lib/travis/github/services.rb @@ -0,0 +1,18 @@ +module Travis + module Github + module Services + require 'travis/github/services/fetch_config' + require 'travis/github/services/find_or_create_org' + require 'travis/github/services/find_or_create_repo' + require 'travis/github/services/find_or_create_user' + require 'travis/github/services/set_hook' + require 'travis/github/services/sync_user' + + class << self + def register + constants(false).each { |name| const_get(name) } + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/github/services/fetch_config.rb b/vendor/travis-core/lib/travis/github/services/fetch_config.rb new file mode 100644 index 00000000..3d16aa15 --- /dev/null +++ b/vendor/travis-core/lib/travis/github/services/fetch_config.rb @@ -0,0 +1,90 @@ +require 'gh' +require 'yaml' +require 'active_support/core_ext/class/attribute' +require 'travis/support/logging' +require 'travis/support/instrumentation' +require 'travis/services/base' + +module Travis + module Github + module Services + # encapsulates fetching a .travis.yml from a given commit's config_url + class FetchConfig < Travis::Services::Base + include Logging + extend Instrumentation + + register :github_fetch_config + + def run + config = retrying(3) { filter(parse(fetch)) } + config || Travis.logger.warn("[request:fetch_config] Empty config for request id=#{request.id} config_url=#{config_url.inspect}") + rescue GH::Error => e + if e.info[:response_status] == 404 + { '.result' => 'not_found' } + else + { '.result' => 'server_error' } + end + end + instrument :run + + def request + params[:request] + end + + def config_url + request.config_url + end + + private + + def fetch + content = GH[config_url]['content'] + Travis.logger.warn("[request:fetch_config] Empty content for #{config_url}") if content.nil? + content = content.to_s.unpack('m').first + Travis.logger.warn("[request:fetch_config] Empty unpacked content for #{config_url}, content was #{content.inspect}") if content.nil? + nbsp = "\xC2\xA0".force_encoding("binary") + content = content.gsub(/^(#{nbsp})+/) { |match| match.gsub(nbsp, " ") } + + content + end + + def parse(yaml) + YAML.load(yaml).merge('.result' => 'configured') + rescue StandardError, Psych::SyntaxError => e + error "[request:fetch_config] Error parsing .travis.yml for #{config_url}: #{e.message}" + { + '.result' => 'parse_error', + '.result_message' => e.is_a?(Psych::SyntaxError) ? e.message.split(": ").last : e.message + } + end + + def filter(config) + unless Travis::Features.active?(:template_selection, request.repository) + config = config.to_h.except('dist').except('group') + end + + config + end + + def retrying(times) + count, result = 0, nil + until result || count > times + result = yield + count += 1 + Travis.logger.warn("[request:fetch_config] Retrying to fetch config for #{config_url}") unless result + end + result + end + + class Instrument < Notification::Instrument + def run_completed + # TODO exctract something like Url.strip_secrets + config_url = target.config_url.gsub(/(token|secret)=\w*/) { "#{$1}=[secure]" } + publish(msg: "#{config_url}", url: config_url, result: result) + end + end + Instrument.attach_to(self) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/github/services/find_or_create_org.rb b/vendor/travis-core/lib/travis/github/services/find_or_create_org.rb new file mode 100644 index 00000000..7bb4e748 --- /dev/null +++ b/vendor/travis-core/lib/travis/github/services/find_or_create_org.rb @@ -0,0 +1,80 @@ +require 'gh' +require 'travis/services/base' +require 'travis/model/organization' +require 'travis/model/repository' +require 'travis/model/user' + +module Travis + module Github + module Services + class FindOrCreateOrg < Travis::Services::Base + register :github_find_or_create_org + + def run + find || create + end + + private + + def find + ::Organization.where(:github_id => params[:github_id]).first.tap do |organization| + if organization + ActiveRecord::Base.transaction do + login = params[:login] || data['login'] + if organization.login != login + Repository.where(owner_name: organization.login). + update_all(owner_name: login) + organization.update_attributes(login: login) + end + + nullify_logins(organization.github_id, organization.login) + end + end + end + end + + def nullify_logins(github_id, login) + users = User.where(["login = ?", login]) + if users.exists? + Travis.logger.info("About to nullify login (#{login}) for users: #{users.map(&:id).join(', ')}") + users.update_all(login: nil) + end + + organizations = Organization.where(["github_id <> ? AND login = ?", github_id, login]) + if organizations.exists? + Travis.logger.info("About to nullify login (#{login}) for organizations: #{organizations.map(&:id).join(', ')}") + organizations.update_all(login: nil) + end + end + + def create + organization = Organization.create!( + :name => data['name'], + :login => data['login'], + :github_id => data['id'], + :email => data['email'], + :location => data['location'], + :avatar_url => data['_links'] && data['_links']['avatar'].try(:fetch, 'href'), + :company => data['company'], + :homepage => data['_links'] && data['_links']['blog'].try(:fetch, 'href') + ) + + nullify_logins(organization.github_id, organization.login) + + organization + rescue ActiveRecord::RecordNotUnique + find + end + + def avatar_url(github_data) + href = github_data.try(:fetch, 'href') + href ? href[/^(https:\/\/[\w\.\/]*)/, 1] : nil + end + + def data + @data ||= GH["organizations/#{params[:github_id]}"] || raise(Travis::GithubApiError) + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/github/services/find_or_create_repo.rb b/vendor/travis-core/lib/travis/github/services/find_or_create_repo.rb new file mode 100644 index 00000000..c85835cf --- /dev/null +++ b/vendor/travis-core/lib/travis/github/services/find_or_create_repo.rb @@ -0,0 +1,44 @@ +module Travis + module Github + module Services + class FindOrCreateRepo < Travis::Services::Base + register :github_find_or_create_repo + + def run + repo = find || create + repo.update_attributes(params) + repo + end + + private + + def find + unless params[:github_id] + message = "No github_id passed to FindOrCreateRepo#find, params: #{params.inspect}" + ActiveSupport::Deprecation.warn(message) + Travis.logger.info(message) + end + + query = if params[:github_id] + { github_id: params[:github_id] } + else + { owner_name: params[:owner_name], name: params[:name] } + end + + run_service(:find_repo, query) + end + + def create + unless params[:github_id] + message = "No github_id passed to FindOrCreateRepo#find, params: #{params.inspect}" + ActiveSupport::Deprecation.warn(message) + Travis.logger.info(message) + end + Repository.create!(:owner_name => params[:owner_name], :name => params[:name], github_id: params[:github_id]) + rescue ActiveRecord::RecordNotUnique + find + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/github/services/find_or_create_user.rb b/vendor/travis-core/lib/travis/github/services/find_or_create_user.rb new file mode 100644 index 00000000..839eb5e8 --- /dev/null +++ b/vendor/travis-core/lib/travis/github/services/find_or_create_user.rb @@ -0,0 +1,66 @@ +require 'gh' +require 'travis/model/repository' +require 'travis/model/user' +require 'travis/model/user/renaming' +require 'travis/services/base' + +module Travis + module Github + module Services + class FindOrCreateUser < Travis::Services::Base + register :github_find_or_create_user + + def run + find || create + end + + private + + include ::User::Renaming + + def find + ::User.where(github_id: params[:github_id]).first.tap do |user| + if user + ActiveRecord::Base.transaction do + login = params[:login] || data['login'] + if user.login != login + Travis.logger.info("Changing # login: current=\"#{user.login}\", new=\"#{login}\" (FindOrCreateUser), data: #{data.inspect}") + rename_repos_owner(user.login, login) + user.update_attributes(login: login) + end + end + + nullify_logins(user.github_id, user.login) + end + end + end + + def create + user = User.create!( + :name => data['name'], + :login => data['login'], + :email => data['email'], + :github_id => data['id'], + :gravatar_id => data['gravatar_id'] + ) + + nullify_logins(user.github_id, user.login) + + user + rescue ActiveRecord::RecordNotUnique + find + end + + def data + @data ||= fetch_data + end + + def fetch_data + data = GH["user/#{params[:github_id]}"] || raise(Travis::GithubApiError) + Travis.logger.info("Fetching data for github_id=#{params[:github_id]} (FindOrCreateUser), data: #{data.inspect}") + data + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/github/services/set_hook.rb b/vendor/travis-core/lib/travis/github/services/set_hook.rb new file mode 100644 index 00000000..338e01fc --- /dev/null +++ b/vendor/travis-core/lib/travis/github/services/set_hook.rb @@ -0,0 +1,67 @@ +require 'travis/github' +require 'travis/services/base' + +module Travis + module Github + module Services + class SetHook < Travis::Services::Base + EVENTS = [:push, :pull_request, :issue_comment, :public, :member] + + register :github_set_hook + + def run + Github.authenticated(current_user) do + update + end + end + + private + + def repo + @repo ||= run_service(:find_repo, id: params[:id]) + end + + def active? + params[:active] + end + + def hook + @hook ||= find || create + end + + def update + GH.patch(hook_url, payload) unless hook['active'] == active? + end + + def find + GH[hooks_url].detect { |hook| hook['name'] == 'travis' && hook['config']['domain'] == domain } + end + + def create + GH.post(hooks_url, payload) + end + + def payload + { + :name => 'travis', + :events => EVENTS, + :active => active?, + :config => { :user => current_user.login, :token => current_user.tokens.first.token, :domain => domain } + } + end + + def hooks_url + "repos/#{repo.slug}/hooks" + end + + def hook_url + hook['_links']['self']['href'] + end + + def domain + Travis.config.service_hook_url || '' + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/github/services/sync_user.rb b/vendor/travis-core/lib/travis/github/services/sync_user.rb new file mode 100644 index 00000000..1c5fda57 --- /dev/null +++ b/vendor/travis-core/lib/travis/github/services/sync_user.rb @@ -0,0 +1,74 @@ +require 'metriks' +require 'travis/mailer/user_mailer' +require 'travis/services/base' + +module Travis + module Github + module Services + class SyncUser < Travis::Services::Base + require 'travis/github/services/sync_user/organizations' + require 'travis/github/services/sync_user/repositories' + require 'travis/github/services/sync_user/repository' + require 'travis/github/services/sync_user/reset_token' + require 'travis/github/services/sync_user/user_info' + + register :github_sync_user + + def run + new_user? do + syncing do + # if Time.now.utc.tuesday? && Travis::Features.feature_active?("reset_token_in_sync") + # ResetToken.new(user).run + # end + UserInfo.new(user).run + Organizations.new(user).run + Repositories.new(user).run + end + end + ensure + user.update_column(:is_syncing, false) + end + + def user + # TODO check that clients are only passing the id + @user ||= current_user || User.find(params[:id]) + end + + def new_user? + new_user = user.synced_at.nil? && user.created_at > 48.hours.ago.utc + + yield if block_given? + + if new_user and Travis.config.welcome_email + send_welcome_email + end + end + + def send_welcome_email + return unless user.email.present? + UserMailer.welcome_email(user).deliver + logger.info("Sent welcome email to #{user.login}") + Metriks.meter('travis.welcome.email').mark + end + + private + + def syncing + unless user.github_oauth_token? + logger.warn "user sync for #{user.login} (id:#{user.id}) was cancelled as the user doesn't have a token" + return + end + user.update_column(:is_syncing, true) + result = yield + user.update_column(:synced_at, Time.now) + result + rescue GH::TokenInvalid => e + logger.warn "user sync for #{user.login} (id:#{user.id}) failed as the token was invalid, dropping the token" + user.update_column(:github_oauth_token, nil) + ensure + user.update_column(:is_syncing, false) + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/github/services/sync_user/organizations.rb b/vendor/travis-core/lib/travis/github/services/sync_user/organizations.rb new file mode 100644 index 00000000..f04caf33 --- /dev/null +++ b/vendor/travis-core/lib/travis/github/services/sync_user/organizations.rb @@ -0,0 +1,143 @@ +require 'gh' + +module Travis + module Github + module Services + class SyncUser < Travis::Services::Base + class Organizations + class Filter + attr_reader :data, :limit + def initialize(data, options = {}) + @data = data || {} + @limit = options[:repositories_limit] || 1000 + end + + def allow? + repositories_count < limit + end + + def repositories_count + # I was not sure how to handle the case where we don't get the + # sufficient amount of data here and this seems the best answer, + # that way we will not get orgs siltently ignored + data['public_repositories'] || 0 + end + end + + class << self + def cancel_memberships(user, orgs) + user.memberships.where(:organization_id => orgs.map(&:id)).delete_all + end + end + + extend Travis::Instrumentation + include Travis::Logging + + attr_reader :user, :data + + def initialize(user) + @user = user + end + + def run + with_github do + { :synced => create_or_update, :removed => remove } + end + end + instrument :run + + private + + def create_or_update + fetch_and_filter.map do |data| + org = create_or_update_org(data) + user.organizations << org unless user.organizations.include?(org) + org + end + end + + def remove + orgs = user.organizations.reject { |org| github_ids.include?(org.github_id) } + self.class.cancel_memberships(user, orgs) + orgs + end + + def fetch + @data ||= GH['user/orgs'].to_a + end + instrument :fetch, :level => :debug + + def github_ids + @github_ids ||= data.map { |org| org['id'] } + end + + def with_github(&block) + # TODO in_parallel should return the block's result in a future version + result = nil + GH.with(:token => user.github_oauth_token) do + # GH.in_parallel do + result = yield + # end + end + result + end + + def fetch_and_filter + fetch.map do |data| + fetch_resource("organizations/#{data['id']}") + end.find_all do |data| + options = Travis.config.sync.organizations || {} + Filter.new(data, options).allow? + end + end + + def fetch_resource(resource) + GH[resource] # TODO should be: ?type=#{self.class.type} but GitHub doesn't work as documented + rescue GH::Error => e + log_exception(e) + end + + def create_or_update_org(data) + org = Organization.find_or_create_by_github_id(data['id']) + org.update_attributes!({ + :name => data['name'], + :login => data['login'], + :email => data['email'], + :avatar_url => avatar_url(data['_links']['avatar']), + :location => data['location'], + :homepage => data['_links']['blog'].try(:fetch, 'href'), + :company => data['company'] + }) + org + end + + def avatar_url(github_data) + href = github_data.try(:fetch, 'href') + href ? href[/^(https:\/\/[\w\.\/]*)/, 1] : nil + end + + class Instrument < Notification::Instrument + def run_completed + format = lambda do |orgs| + orgs.map { |org| { id: org.id, login: org.login } } + end + + publish( + msg: %(for #), + result: { synced: format.call(result[:synced]), removed: format.call(result[:removed]) } + ) + end + + def fetch_completed + publish( + msg: %(for #), + result: result + ) + end + end + Instrument.attach_to(self) + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/github/services/sync_user/repositories.rb b/vendor/travis-core/lib/travis/github/services/sync_user/repositories.rb new file mode 100644 index 00000000..294f670e --- /dev/null +++ b/vendor/travis-core/lib/travis/github/services/sync_user/repositories.rb @@ -0,0 +1,140 @@ +require 'active_support/core_ext/class/attribute' + +module Travis + module Github + module Services + class SyncUser < Travis::Services::Base + # Fetches all repositories from Github which are in /user/repos or any of the user's + # orgs/[name]/repos. Creates or updates existing repositories on our side and adds + # it to the user's permissions. Also removes existing permissions for repositories + # which are not in the received Github data. NOTE that this does *not* delete any + # repositories because we do not know if the repository was deleted or renamed + # on Github's side. + class Repositories + extend Travis::Instrumentation + include Travis::Logging + + class_attribute :types + self.types = [:public] + + class << self + # TODO backwards compat, remove once all apps use `types=` + def type=(types) + self.types = Array.wrap(types).map(&:to_s).join(',').split(',').map(&:to_sym) + end + + def include?(type) + self.types.include?(type) + end + end + + attr_reader :user, :resources, :data + + def initialize(user) + @user = user + @resources = ['user/repos'] + user.organizations.map { |org| "orgs/#{org.login}/repos" } + end + + def run + with_github do + { :synced => create_or_update, :removed => remove } + end + end + instrument :run + + private + + def create_or_update + data.map do |repository| + Repository.new(user, repository).run + end + end + + def remove + repos = user.repositories.reject { |repo| slugs.include?(repo.slug) } + Repository.unpermit_all(user, repos) + repos + end + + # we have to filter these ourselves because the github api is broken for this + def data + @data ||= filter_duplicates(filter_based_on_repo_permission) + end + + def filter_based_on_repo_permission + fetch.select { |repo| self.class.include?(repo['private'] ? :private : :public) } + end + + def filter_duplicates(repositories) + repositories.each_with_object([]) do |repository, filtered_list| + unless in_filtered_list?(filtered_list, repository) + filtered_list.push(repository) + end + end + end + + def in_filtered_list?(filtered_list, other_repository) + filtered_list.any? do |existing_repository| + same_repository_with_admin?(existing_repository, other_repository) + end + end + + def same_repository_with_admin?(existing_repository, other_repository) + existing_repository['owner']['login'] == other_repository['owner']['login'] and + existing_repository['name'] == other_repository['name'] and + existing_repository['permissions']['admin'] == true + end + + def slugs + @slugs ||= data.map { |repo| "#{repo['owner']['login']}/#{repo['name']}" } + end + + def fetch + resources.map { |resource| fetch_resource(resource) }.map(&:to_a).flatten.compact + end + instrument :fetch, :level => :debug + + def fetch_resource(resource) + GH[resource] # TODO should be: ?type=#{self.class.type} but GitHub doesn't work as documented + rescue GH::Error => e + log_exception(e) + end + + def with_github(&block) + # TODO in_parallel should return the block's result in a future version + result = nil + GH.with(:token => user.github_oauth_token) do + # GH.in_parallel do + result = yield + # end + end + result + end + + class Instrument < Notification::Instrument + def run_completed + format = lambda do |repos| + repos.map { |repo| { id: repo.id, owner: repo.owner_name, name: repo.name } } + end + + publish( + msg: %(for #), + resources: target.resources, + result: { synced: format.call(result[:synced]), removed: format.call(result[:removed]) } + ) + end + + def fetch_completed + publish( + msg: %(for #), + resources: target.resources, + result: result + ) + end + end + Instrument.attach_to(self) + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/github/services/sync_user/repository.rb b/vendor/travis-core/lib/travis/github/services/sync_user/repository.rb new file mode 100644 index 00000000..1636d0f5 --- /dev/null +++ b/vendor/travis-core/lib/travis/github/services/sync_user/repository.rb @@ -0,0 +1,140 @@ +module Travis + module Github + module Services + class SyncUser < Travis::Services::Base + class Repository + class << self + def unpermit_all(user, repositories) + user.permissions.where(:repository_id => repositories.map(&:id)).delete_all unless repositories.empty? + end + end + + attr_reader :user, :data, :repo + + def initialize(user, data) + @user = user + @data = data + end + + def run + @repo = find || create + update + if permission + sync_permissions + elsif permit? + permit + end + repo + end + + private + + def find + ::Repository.where(:github_id => github_id).first + end + + def create + if Travis::Features.enabled_for_all?(:sync_repo_owner) + ::Repository.create!(:owner => owner, :owner_name => owner_name, :name => name, github_id: github_id) + else + ::Repository.create!(:owner_name => owner_name, :name => name, github_id: github_id) + end + end + # instrument :create, :level => :debug + + def permission + @permission ||= user.permissions.where(:repository_id => repo.id).first + end + + def sync_permissions + if permit? + permission.update_attributes!(permission_data) + else + permission.destroy + end + end + + def permit? + push_access? || admin_access? || repo.private? + end + + def permit + user.permissions.create!({ + :user => user, + :repository => repo + }.merge(permission_data)) + end + # instrument :permit, :level => :debug + + def update + if Travis::Features.enabled_for_all?(:sync_repo_owner) + repo.update_attributes!({ + owner: owner, + github_id: data['id'], + private: data['private'], + description: data['description'], + url: data['homepage'], + default_branch: data['default_branch'], + github_language: data['language'], + name: name, + owner_name: owner_name + }) + else + repo.update_attributes!({ + github_id: data['id'], + private: data['private'], + description: data['description'], + url: data['homepage'], + default_branch: data['default_branch'], + github_language: data['language'], + name: name, + owner_name: owner_name + }) + end + rescue ActiveRecord::RecordInvalid + # ignore for now. this seems to happen when multiple syncs (i.e. user sign + # in requests are running in parallel? + rescue GH::Error(response_status: 404) => e + Travis.logger.warn "[github][services][user_sync] GitHub info was not available for #{repo.owner_name}/#{repo.name}: #{e.inspect}" + end + + def owner + @owner ||= owner_type.constantize.find_by_github_id(owner_id) + end + + def owner_id + data['owner']['id'] + end + + def owner_type + data['owner']['type'] + end + + def owner_name + data['owner']['login'] + end + + def name + data['name'] + end + + def github_id + data['id'] + end + + def permission_data + data['permissions'] + end + + def push_access? + permission_data['push'] + end + + def admin_access? + permission_data['admin'] + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/github/services/sync_user/reset_token.rb b/vendor/travis-core/lib/travis/github/services/sync_user/reset_token.rb new file mode 100644 index 00000000..cc09ae26 --- /dev/null +++ b/vendor/travis-core/lib/travis/github/services/sync_user/reset_token.rb @@ -0,0 +1,36 @@ +require "gh" + +module Travis + module Github + module Services + class SyncUser < Travis::Services::Base + class ResetToken + def initialize(user, config = Travis.config.oauth2.to_h, gh = nil) + @user = user + @config = config + @gh = gh || GH.with(username: @config.client_id, password: @config.client_secret) + end + + def run + token = new_token + @user.update_attributes!(github_oauth_token: token) if token + end + + private + + def new_token + @new_token ||= @gh.post("/applications/#{client_id}/tokens/#{@user.github_oauth_token}", {})["token"] + end + + def client_id + @config.client_id + end + + def client_secret + @config.client_secret + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/github/services/sync_user/user_info.rb b/vendor/travis-core/lib/travis/github/services/sync_user/user_info.rb new file mode 100644 index 00000000..c3be5399 --- /dev/null +++ b/vendor/travis-core/lib/travis/github/services/sync_user/user_info.rb @@ -0,0 +1,90 @@ +require 'gh' +require 'travis/github/education' + +module Travis + module Github + module Services + class SyncUser < Travis::Services::Base + class UserInfo + attr_reader :user, :gh + + def initialize(user, gh = Github.authenticated(user)) + @user, @gh = user, gh + end + + def run + if user.github_id != user_info['id'].to_i + raise "Updating # failed, github_id differs. github_id on user: #{user.github_id}, github_id from data: #{user_info['id']}" + end + if user.login != login + Travis.logger.info("Changing # login: current=\"#{user.login}\", new=\"#{login}\" (UserInfo), data: #{user_info.inspect}") + end + if user.email != email + Travis.logger.info("Changing # email: current=\"#{user.email}\", new=\"#{email}\" (UserInfo)") + end + + user.update_attributes!(name: name, login: login, gravatar_id: gravatar_id, email: email, education: education) + emails = verified_emails + emails << email unless emails.include? email + emails.each { |e| user.emails.find_or_create_by_email!(e) } + end + + def education + if Travis::Features.feature_active?(:education_data_sync) || Travis::Features.owner_active?(:education_data_sync, user) + Education.new(user.github_oauth_token).student? + end + end + + def name + user_info['name'] + end + + def login + user_info.fetch('login') + end + + def gravatar_id + user_info['gravatar_id'] + end + + def email + user_info['email'].presence || primary_email || verified_email || user.email.presence || first_email + end + + def verified_emails + emails.select { |e| e["verified"] }.map { |e| e['email'] } + end + + private + + def emails + return [] unless user.github_scopes.include? 'user' or user.github_scopes.include? 'user:email' + @emails ||= gh['user/emails'].to_a + end + + def first_email + emails.first.try(:[], 'email') + end + + def primary_email + emails.detect { |e| e["primary"] }.try(:[], 'email') + end + + def verified_email + verified_emails.first + end + + def user_info + @user_info ||= begin + data = gh['user'].to_hash + if user.login != data['login'] + Travis.logger.info("Fetching data for github_id=#{user.github_id} (UserInfo), data: #{data.inspect}") + end + data + end + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/logs.rb b/vendor/travis-core/lib/travis/logs.rb new file mode 100644 index 00000000..89dfa53b --- /dev/null +++ b/vendor/travis-core/lib/travis/logs.rb @@ -0,0 +1,6 @@ +module Travis + module Logs + autoload :Services, 'travis/logs/services' + end +end + diff --git a/vendor/travis-core/lib/travis/logs/services.rb b/vendor/travis-core/lib/travis/logs/services.rb new file mode 100644 index 00000000..f235cf5a --- /dev/null +++ b/vendor/travis-core/lib/travis/logs/services.rb @@ -0,0 +1,17 @@ +module Travis + module Logs + module Services + autoload :Aggregate, 'travis/logs/services/aggregate' + autoload :Archive, 'travis/logs/services/archive' + autoload :Receive, 'travis/logs/services/receive' + + class << self + def register + constants(false).each { |name| const_get(name) } + end + end + end + end +end + + diff --git a/vendor/travis-core/lib/travis/logs/services/aggregate.rb b/vendor/travis-core/lib/travis/logs/services/aggregate.rb new file mode 100644 index 00000000..944f2f9d --- /dev/null +++ b/vendor/travis-core/lib/travis/logs/services/aggregate.rb @@ -0,0 +1,96 @@ +require 'active_support/core_ext/string/filters' +require 'travis/features' +require 'travis/model/log' +require 'travis/model/log/part' +require 'travis/services/base' + +module Travis + module Logs + module Services + class Aggregate < Travis::Services::Base + register :logs_aggregate + + AGGREGATE_UPDATE_SQL = <<-sql.squish + UPDATE logs + SET aggregated_at = ?, + content = (COALESCE(content, '') || (#{Log::AGGREGATE_PARTS_SELECT_SQL})) + WHERE logs.id = ? + sql + + AGGREGATEABLE_SELECT_SQL = <<-sql.squish + SELECT DISTINCT log_id + FROM log_parts + WHERE created_at <= NOW() - interval '? seconds' AND final = ? + OR created_at <= NOW() - interval '? seconds' + sql + + def run + return unless active? + aggregateable_ids.each do |id| + transaction do + aggregate(id) + vacuum(id) + notify(id) + end + end + end + + private + + def active? + Travis::Features.feature_active?(:log_aggregation) + end + + def aggregate(id) + meter('logs.aggregate') do + connection.execute(sanitize_sql([AGGREGATE_UPDATE_SQL, Time.now, id, id])) + end + end + + def vacuum(id) + meter('logs.vacuum') do + Log::Part.delete_all(log_id: id) + end + end + + def notify(id) + Log.find(id).notify('aggregated') + rescue ActiveRecord::RecordNotFound + puts "[warn] could not find a log with the id #{id}" + end + + def aggregateable_ids + Log::Part.connection.select_values(query).map { |id| id.nil? ? id : id.to_i } + end + + def query + Log::Part.send(:sanitize_sql, [AGGREGATEABLE_SELECT_SQL, intervals[:regular], true, intervals[:force]]) + end + + def intervals + Travis.config.logs.intervals + end + + def transaction(&block) + ActiveRecord::Base.transaction(&block) + rescue ActiveRecord::ActiveRecordError => e + # puts e.message, e.backtrace + Travis::Exceptions.handle(e) + end + + def meter(name, &block) + Metriks.timer(name).time(&block) + end + + def connection + Log::Part.connection + end + + def sanitize_sql(*args) + Log::Part.send(:sanitize_sql, *args) + end + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/logs/services/archive.rb b/vendor/travis-core/lib/travis/logs/services/archive.rb new file mode 100644 index 00000000..98890b31 --- /dev/null +++ b/vendor/travis-core/lib/travis/logs/services/archive.rb @@ -0,0 +1,167 @@ +begin + require 'aws/s3' +rescue LoadError => e +end +require 'uri' +require 'active_support/core_ext/hash/slice' +require 'faraday' +require 'travis/support/instrumentation' +require 'travis/notification/instrument' +require 'travis/services/base' + +module Travis + class S3 + class << self + def setup + AWS.config(Travis.config.s3.to_h.slice(:access_key_id, :secret_access_key)) + end + end + + attr_reader :s3, :url + + def initialize(url) + @s3 = AWS::S3.new + @url = url + end + + def store(data) + object.write(data, content_type: 'text/plain', acl: :public_read) + end + + def object + @object ||= bucket.objects[URI.parse(url).path[1..-1]] + end + + def bucket + @bucket ||= s3.buckets[URI.parse(url).host] + end + end + + module Logs + module Services + class Archive < Travis::Services::Base + class FetchFailed < StandardError + def initialize(source_url, status, message) + super("Could not retrieve #{source_url}. Response status: #{status}, message: #{message}") + end + end + + class VerificationFailed < StandardError + def initialize(source_url, target_url, expected, actual) + super("Expected #{target_url} (from: #{source_url}) to have the content length #{expected.inspect}, but had #{actual.inspect}") + end + end + + extend Travis::Instrumentation + + register :archive_log + + attr_reader :log + + def run + fetch + store + verify + report + end + instrument :run + + def source_url + "https://#{hostname('api')}/logs/#{params[:id]}.txt" + end + + def report_url + "https://#{hostname('api')}/logs/#{params[:id]}" + end + + def target_url + "http://#{hostname('archive')}/jobs/#{params[:job_id]}/log.txt" + end + + private + + def fetch + retrying(:fetch) do + response = request(:get, source_url) + if response.status == 200 + @log = response.body.to_s + else + raise(FetchFailed.new(source_url, response.status, response.body.to_s)) + end + end + end + + def store + retrying(:store) do + S3.setup + s3.store(log) + end + end + + def verify + retrying(:verify) do + expected = log.bytesize + actual = request(:head, target_url).headers['content-length'].try(:to_i) + raise VerificationFailed.new(target_url, source_url, expected, actual) unless expected == actual + end + end + + def report + retrying(:report) do + request(:put, report_url, { archived_at: Time.now, archive_verified: true }, token: Travis.config.tokens.internal) + end + end + + def request(method, url, params = nil, headers = nil, &block) + http.send(*[method, url, params, headers].compact, &block) + rescue Faraday::Error => e + puts "Exception while trying to #{method.inspect}: #{source_url}:" + puts e.message, e.backtrace + raise e + end + + def http + Faraday.new(ssl: Travis.config.ssl.to_h.compact) do |f| + f.request :url_encoded + f.adapter :net_http + end + end + + def s3 + S3.new(target_url) + end + + def hostname(name) + "#{name}#{'-staging' if Travis.env == 'staging'}.#{Travis.config.host.split('.')[-2, 2].join('.')}" + end + + def retrying(header, times = 5) + yield + rescue => e + count ||= 0 + if times > (count += 1) + puts "[#{header}] retry #{count} because: #{e.message}" + Travis::Instrumentation.meter("#{self.class.name.underscore.gsub("/", ".")}.retries.#{header}") + sleep count * 3 unless params[:no_sleep] + retry + else + raise + end + end + + class Instrument < Notification::Instrument + def run_completed + publish( + msg: "for (to: #{target.target_url})", + source_url: target.source_url, + target_url: target.target_url, + object_type: 'Log', + object_id: target.params[:id] + ) + end + end + Instrument.attach_to(self) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/logs/services/receive.rb b/vendor/travis-core/lib/travis/logs/services/receive.rb new file mode 100644 index 00000000..8f56cc80 --- /dev/null +++ b/vendor/travis-core/lib/travis/logs/services/receive.rb @@ -0,0 +1,79 @@ +require 'coder' +require 'metriks' +require 'travis/model/log/part' +require 'travis/model/job/test' +require 'travis/services/base' + +module Travis + module Logs + module Services + class Receive < Travis::Services::Base + # TODO remove this once we know aggregation works fine and the worker passes a :final flag + FINAL = 'Done. Build script exited with:' + + register :logs_receive + + def run + create_part + notify + end + + private + + def create_part + measure('logs.update') do + Travis.logger.warn "[warn] log.id is #{log.id.inspect} in :logs_append! job_id: #{data[:id]}" if log.id.to_i == 0 + Log::Part.create!(log_id: log.id, content: chars, number: number, final: final?) + end + rescue ActiveRecord::ActiveRecordError => e + Travis.logger.warn "[warn] could not save log in :logs_append job_id: #{data[:id]}: #{e.message}" + Travis.logger.warn e.backtrace + end + + def notify + job.notify(:log, _log: chars, number: number, final: final?) + rescue => e + Metriks.meter('travis.logs.update.notify.errors').mark + Travis.logger.error("Error notifying of log update: #{e.message} (from #{e.backtrace.first})") + end + + def log + @log ||= Log.where(job_id: job.id).select(:id).first || create_log + end + + def create_log + Travis.logger.warn "[warn] Had to create a log for job_id: #{job.id}!" + job.create_log! + end + + def job + @job ||= Job::Test.find(data[:id]) + end + + def chars + @chars ||= filter(data[:log]) + end + + def number + data[:number] + end + + def final? + !!data[:final] || chars.include?(FINAL) + end + + def data + @data ||= params[:data].symbolize_keys + end + + def filter(chars) + Coder.clean!(chars.to_s.gsub("\0", '')) # postgres seems to have issues with null chars + end + + def measure(name, &block) + Metriks.timer(name).time(&block) + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/mailer.rb b/vendor/travis-core/lib/travis/mailer.rb new file mode 100644 index 00000000..8a19cf01 --- /dev/null +++ b/vendor/travis-core/lib/travis/mailer.rb @@ -0,0 +1,26 @@ +require 'action_mailer' +require 'i18n' + +module Travis + module Mailer + class << self + def config + config = Travis.config.smtp + config ? config.to_h : {} + end + + def setup + if config.present? + mailer = ActionMailer::Base + mailer[:delivery_method] = :smtp + mailer[:smtp_settings] = config + @setup = true + end + end + + def setup? + !!@setup + end + end + end +end diff --git a/vendor/travis-core/lib/travis/mailer/user_mailer.rb b/vendor/travis-core/lib/travis/mailer/user_mailer.rb new file mode 100644 index 00000000..3a352648 --- /dev/null +++ b/vendor/travis-core/lib/travis/mailer/user_mailer.rb @@ -0,0 +1,18 @@ +require 'action_mailer' + +class UserMailer < ActionMailer::Base + ActionMailer::Base.append_view_path("#{File.dirname(__FILE__)}/views") + + layout 'contact_email' + + def welcome_email(user) + @user = user + mail(subject: "Welcome to Travis CI!", from: from, to: user.email) do |format| + format.html + end + end + + def from + Travis.config.email.from + end +end diff --git a/vendor/travis-core/lib/travis/mailer/views/layouts/contact_email.html.erb b/vendor/travis-core/lib/travis/mailer/views/layouts/contact_email.html.erb new file mode 100644 index 00000000..ceef3ba3 --- /dev/null +++ b/vendor/travis-core/lib/travis/mailer/views/layouts/contact_email.html.erb @@ -0,0 +1,170 @@ + + + + + + +
+ + +
+ +
+ <%= yield %> +
+ + + + diff --git a/vendor/travis-core/lib/travis/mailer/views/user_mailer/welcome_email.html.erb b/vendor/travis-core/lib/travis/mailer/views/user_mailer/welcome_email.html.erb new file mode 100644 index 00000000..3f210a32 --- /dev/null +++ b/vendor/travis-core/lib/travis/mailer/views/user_mailer/welcome_email.html.erb @@ -0,0 +1,54 @@ +
+

Welcome to Travis CI!

+ +

+ Hey <%= @user.name.blank? ? @user.login : @user.name %>, +

+ +

+ We'd like to extend a warm welcome to you and provide with some links to help you get started. +

+ +

+ If you haven't set up your first project yet, head to your + accounts page and enable the project you'd like to test on Travis CI. Push some code, and your repository + will appear on <%= Travis.config.host -%>. +

+ +

+ Are you part of an organization that's already running builds on Travis CI? Great news, we've just finished + synchronizing your permissions from GitHub. You can see the projects you have access to and that have already + been built on Travis CI at <%= Travis.config.host %> so you can + dive in right away. +

+ +

+ Remember to add a .travis.yml file to your project to tell us what steps we should execute to set up your build + environment and run your build. We have sensible defaults, but you're free to customize everything to your + liking. +

+ +

+ You can find all details on how to setup specific languages, the + available configuration options for your + builds, and our build environment in our documentation. Don't forget to setup notifications if + you'd like to be kept up-to-date about your builds on Campfire, HipChat, IRC, and others. +

+ +

+ If you have any questions or issues, shoot us an email. +

+ +

+ Have an awesome day! +

+ +

+ Cheers, +

+ +

+ The Travis CI Team +

+
diff --git a/vendor/travis-core/lib/travis/model.rb b/vendor/travis-core/lib/travis/model.rb new file mode 100644 index 00000000..5c0aa565 --- /dev/null +++ b/vendor/travis-core/lib/travis/model.rb @@ -0,0 +1,60 @@ +# This module is required for preloading classes on JRuby, see +# https://github.com/travis-ci/travis-support/blob/master/lib/core_ext/module/load_constants.rb +# which is used in +# https://github.com/travis-ci/travis-hub/blob/master/lib/travis/hub/cli.rb#L15 +require 'active_record' +require 'core_ext/active_record/base' + +module Travis + class Model < ActiveRecord::Base + require 'travis/model/logs_model' + require 'travis/model/account' + require 'travis/model/annotation' + require 'travis/model/annotation_provider' + require 'travis/model/branch' + require 'travis/model/broadcast' + require 'travis/model/build' + require 'travis/model/commit' + require 'travis/model/email' + require 'travis/model/env_helpers' + require 'travis/model/job' + require 'travis/model/log' + require 'travis/model/membership' + require 'travis/model/organization' + require 'travis/model/permission' + require 'travis/model/repository' + require 'travis/model/request' + require 'travis/model/ssl_key' + require 'travis/model/token' + require 'travis/model/user' + require 'travis/model/url' + + self.abstract_class = true + + cattr_accessor :follower_connection_handler + + class << self + def connection_handler + if Thread.current['Travis.with_follower_connection_handler'] + follower_connection_handler + else + super + end + end + + def establish_follower_connection(spec) + self.follower_connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new unless self.follower_connection_handler + using_follower do + self.establish_connection(spec) + end + end + + def using_follower + Thread.current['Travis.with_follower_connection_handler'] = true + yield + ensure + Thread.current['Travis.with_follower_connection_handler'] = false + end + end + end +end diff --git a/vendor/travis-core/lib/travis/model/account.rb b/vendor/travis-core/lib/travis/model/account.rb new file mode 100644 index 00000000..ae471ea4 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/account.rb @@ -0,0 +1,22 @@ +class Account + class << self + def from(record, attrs = {}) + new(record.attributes.merge(:type => record.class.name).merge(attrs)) + end + end + + ATTR_NAMES = [:id, :type, :name, :login, :repos_count, :avatar_url] + + attr_accessor *ATTR_NAMES + + def initialize(attrs) + attrs = attrs.symbolize_keys + ATTR_NAMES.each do |name| + self.send(:"#{name}=", attrs[name]) + end + end + + def ==(other) + id == other.id && type == other.type + end +end diff --git a/vendor/travis-core/lib/travis/model/annotation.rb b/vendor/travis-core/lib/travis/model/annotation.rb new file mode 100644 index 00000000..7a5ee4fc --- /dev/null +++ b/vendor/travis-core/lib/travis/model/annotation.rb @@ -0,0 +1,28 @@ +require "active_record" +require "addressable/uri" +require 'travis/event' + +class Annotation < ActiveRecord::Base + include Travis::Event + + belongs_to :job + belongs_to :annotation_provider + + attr_accessible :description, :url, :job_id, :status + + validates :job_id, presence: true + validates :description, presence: true + validate :validate_url_scheme + + private + def validate_url_scheme + return unless self.url + + uri = Addressable::URI.parse(self.url) + unless %w[http https].include?(uri.scheme) + errors.add(:url, 'URL must use http or https scheme') + end + rescue Addressable::URI::InvalidURIError + errors.add(:url, 'URL is invalid') + end +end diff --git a/vendor/travis-core/lib/travis/model/annotation_provider.rb b/vendor/travis-core/lib/travis/model/annotation_provider.rb new file mode 100644 index 00000000..c5193662 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/annotation_provider.rb @@ -0,0 +1,20 @@ +require 'active_record' +require 'travis/model/encrypted_column' + +class AnnotationProvider < ActiveRecord::Base + has_many :annotations + + serialize :api_key, Travis::Model::EncryptedColumn.new + + def self.authenticate_provider(username, key) + provider = where(api_username: username).first + + return unless provider && provider.api_key == key + + provider + end + + def annotation_for_job(job_id) + annotations.where(job_id: job_id).first || annotations.build(job_id: job_id) + end +end diff --git a/vendor/travis-core/lib/travis/model/branch.rb b/vendor/travis-core/lib/travis/model/branch.rb new file mode 100644 index 00000000..ad8a21d6 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/branch.rb @@ -0,0 +1,6 @@ +require 'travis/model' + +class Branch < Travis::Model + belongs_to :repository + belongs_to :last_build, class_name: 'Build' +end diff --git a/vendor/travis-core/lib/travis/model/broadcast.rb b/vendor/travis-core/lib/travis/model/broadcast.rb new file mode 100644 index 00000000..cf5214f0 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/broadcast.rb @@ -0,0 +1,30 @@ +require 'travis/model' + +class Broadcast < Travis::Model + belongs_to :recipient, polymorphic: true + + class << self + def by_user(user) + sql = %( + recipient_type IS NULL OR + recipient_type = ? AND recipient_id IN(?) OR + recipient_type = ? AND recipient_id = ? OR + recipient_type = ? AND recipient_id IN (?) + ) + active.where(sql, 'Organization', user.organization_ids, 'User', user.id, 'Repository', user.repository_ids) + end + + def by_repo(repository) + sql = %( + recipient_type IS NULL OR + recipient_type = ? AND recipient_id = ? OR + recipient_type = ? AND recipient_id = ? + ) + active.where(sql, 'Repository', repository.id, repository.owner_type, repository.owner_id) + end + + def active + where('created_at >= ? AND (expired IS NULL OR expired <> ?)', 14.days.ago, true).order('id DESC') + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build.rb b/vendor/travis-core/lib/travis/model/build.rb new file mode 100644 index 00000000..636648af --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build.rb @@ -0,0 +1,228 @@ +require 'core_ext/hash/deep_symbolize_keys' +require 'simple_states' +require 'travis/model' +require 'travis/services/next_build_number' + +# Build currently models a central but rather abstract domain entity: the thing +# that is triggered by a Github request (service hook ping). +# +# Build groups a matrix of Job::Test instances, and belongs to a Request (and +# thus Commit as well as a Repository). +# +# A Build is created when its Request was configured (by fetching .travis.yml) +# and approved (e.g. not excluded by the configuration). Once a Build is +# created it will expand its matrix according to the given configuration and +# create the according Job::Test instances. Each Job::Test instance will +# trigger a test run remotely (on the worker). Once all Job::Test instances +# have finished the Build will be finished as well. +# +# Each of these state changes (build:created, job:started, job:finished, ...) +# will issue events that are listened for by the event handlers contained in +# travis/notification. These event handlers then send out various notifications +# of various types through email, pusher and irc, archive builds and queue +# jobs for the workers. +# +# Build is split up to several modules: +# +# * Build - ActiveRecord structure, validations and scopes +# * States - state definitions and events +# * Denormalize - some state changes denormalize attributes to the build's +# repository (e.g. Build#started_at gets propagated to +# Repository#last_started_at) +# * Matrix - logic related to expanding the build matrix, normalizing +# configuration for Job::Test instances, evaluating the +# final build result etc. +# * Messages - helpers for evaluating human readable result messages +# (e.g. "Still Failing") +# * Events - helpers that are used by notification handlers (and that +# TODO probably should be cleaned up and moved to +# travis/notification) +class Build < Travis::Model + require 'travis/model/build/config' + require 'travis/model/build/denormalize' + require 'travis/model/build/update_branch' + require 'travis/model/build/matrix' + require 'travis/model/build/metrics' + require 'travis/model/build/result_message' + require 'travis/model/build/states' + require 'travis/model/env_helpers' + + include Matrix, States, SimpleStates + + belongs_to :commit + belongs_to :request + belongs_to :repository, autosave: true + belongs_to :owner, polymorphic: true + has_many :matrix, as: :source, order: :id, class_name: 'Job::Test', dependent: :destroy + has_many :events, as: :source + + validates :repository_id, :commit_id, :request_id, presence: true + + serialize :config + + delegate :same_repo_pull_request?, :to => :request + + class << self + def recent + where(state: ['failed', 'passed']).order('id DESC').limit(25) + end + + def running + where(state: ['started']).order('started_at DESC') + end + + def was_started + where('state <> ?', :created) + end + + def finished + where(state: [:finished, :passed, :failed, :errored, :canceled]) # TODO extract + end + + def on_state(state) + where(state.present? ? ['builds.state IN (?)', state] : []) + end + + def on_branch(branch) + api_and_pushes.where(branch.present? ? ['branch IN (?)', normalize_to_array(branch)] : []) + end + + def by_event_type(event_types) + event_types = Array(event_types).flatten + event_types << 'push' if event_types.empty? + where(event_type: event_types) + end + + def pushes + where(event_type: 'push') + end + + def pull_requests + where(event_type: 'pull_request') + end + + def api_and_pushes + by_event_type(['api', 'push']) + end + + def previous(build) + where('builds.repository_id = ? AND builds.id < ?', build.repository_id, build.id).finished.descending.limit(1).first + end + + def descending + order(arel_table[:id].desc) + end + + def paged(options) + page = (options[:page] || 1).to_i + limit(per_page).offset(per_page * (page - 1)) + end + + def last_build_on(options) + scope = descending + scope = scope.on_state(options[:state]) if options[:state] + scope = scope.on_branch(options[:branch]) if options[:branch] + scope.first + end + + def last_state_on(options) + last_build_on(options).try(:state).try(:to_sym) + end + + def older_than(build = nil) + scope = order('number::integer DESC').paged({}) # TODO in which case we'd call older_than without an argument? + scope = scope.where('number::integer < ?', (build.is_a?(Build) ? build.number : build).to_i) if build + scope + end + + protected + + def normalize_to_array(object) + Array(object).compact.join(',').split(',') + end + + def per_page + 25 + end + end + + # set the build number and expand the matrix; downcase language + before_create do + next_build_number = Travis::Services::NextBuildNumber.new(repository_id: repository.id).run + self.number = next_build_number + self.previous_state = last_finished_state_on_branch + self.event_type = request.event_type + self.pull_request_title = request.pull_request_title + self.pull_request_number = request.pull_request_number + self.branch = commit.branch + expand_matrix + end + + after_create do + UpdateBranch.new(self).update_last_build unless pull_request? + end + + after_save do + unless cached_matrix_ids + update_column(:cached_matrix_ids, to_postgres_array(matrix_ids)) + end + end + + # AR 3.2 does not handle pg arrays and the plugins supporting them + # do not work well with jdbc drivers + # TODO: remove this once we're on >= 4.0 + def cached_matrix_ids + if (value = super) && value =~ /^{/ + value.gsub(/^{|}$/, '').split(',').map(&:to_i) + end + end + + def matrix_ids + matrix.map(&:id) + end + + def secure_env_enabled? + !pull_request? || same_repo_pull_request? + end + alias addons_enabled? secure_env_enabled? + + def config=(config) + super((config || {}).deep_symbolize_keys) + end + + def config + @config ||= Config.new(super, multi_os: repository.multi_os_enabled?).normalize + end + + def obfuscated_config + Config.new(config, key_fetcher: lambda { self.repository.key }).obfuscate + end + + def cancelable? + matrix.any? { |job| job.cancelable? } + end + + def pull_request? + event_type == 'pull_request' + end + + # COMPAT: used in http api v1, deprecate as soon as v1 gets retired + def result + state.try(:to_sym) == :passed ? 0 : 1 + end + + def on_default_branch? + branch == repository.default_branch + end + + private + + def last_finished_state_on_branch + repository.builds.finished.last_state_on(branch: commit.branch) + end + + def to_postgres_array(ids) + ids = ids.compact.uniq + "{#{ids.map { |id| id.to_i.to_s }.join(',')}}" unless ids.empty? + end +end diff --git a/vendor/travis-core/lib/travis/model/build/config.rb b/vendor/travis-core/lib/travis/model/build/config.rb new file mode 100644 index 00000000..80ac807c --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/config.rb @@ -0,0 +1,119 @@ +require 'travis/model/build/config/dist' +require 'travis/model/build/config/env' +require 'travis/model/build/config/features' +require 'travis/model/build/config/group' +require 'travis/model/build/config/language' +require 'travis/model/build/config/matrix' +require 'travis/model/build/config/obfuscate' +require 'travis/model/build/config/os' +require 'travis/model/build/config/yaml' + +class Build + class Config + NORMALIZERS = [Features, Yaml, Env, Language, Group, Dist] + + DEFAULT_LANG = 'ruby' + + ENV_KEYS = [ + :compiler, + :crystal, + :csharp, + :d, + :dart, + :elixir, + :env, + :fsharp, + :gemfile, + :ghc, + :go, + :haxe, + :jdk, + :julia, + :mono, + :node_js, + :otp_release, + :perl, + :perl6, + :php, + :python, + :ruby, + :rust, + :rvm, + :r, + :scala, + :smalltalk, + :smalltalk_config, + :visualbasic, + :xcode_scheme, + :xcode_sdk + ] + + EXPANSION_KEYS_FEATURE = [:os] + + EXPANSION_KEYS_LANGUAGE = { + 'c' => [:compiler], + 'c++' => [:compiler], + 'clojure' => [:lein, :jdk], + 'cpp' => [:compiler], + 'crystal' => [:crystal], + 'csharp' => [:csharp, :mono], + 'd' => [:d], + 'dart' => [:dart], + 'elixir' => [:elixir, :otp_release], + 'erlang' => [:otp_release], + 'fsharp' => [:fsharp, :mono], + 'go' => [:go], + 'groovy' => [:jdk], + 'haskell' => [:ghc], + 'haxe' => [:haxe], + 'java' => [:jdk], + 'julia' => [:julia], + 'node_js' => [:node_js], + 'objective-c' => [:rvm, :gemfile, :xcode_sdk, :xcode_scheme], + 'perl' => [:perl], + 'perl6' => [:perl6], + 'php' => [:php], + 'python' => [:python], + 'ruby' => [:rvm, :gemfile, :jdk, :ruby], + 'rust' => [:rust], + 'r' => [:r], + 'scala' => [:scala, :jdk], + 'smalltalk' => [:smalltalk, :smalltalk_config], + 'visualbasic' => [:visualbasic, :mono] + } + + EXPANSION_KEYS_UNIVERSAL = [:env, :branch] + + def self.matrix_keys_for(config, options = {}) + keys = matrix_keys(config, options) + keys & config.keys.map(&:to_sym) + end + + def self.matrix_keys(config, options = {}) + lang = Array(config.symbolize_keys[:language]).first + keys = ENV_KEYS + keys &= EXPANSION_KEYS_LANGUAGE.fetch(lang, EXPANSION_KEYS_LANGUAGE[DEFAULT_LANG]) + keys << :os if options[:multi_os] + keys += [:dist, :group] if options[:dist_group_expansion] + keys | EXPANSION_KEYS_UNIVERSAL + end + + attr_reader :config, :options + + def initialize(config, options = {}) + @config = (config || {}).deep_symbolize_keys + @options = options + end + + def normalize + normalizers = options[:multi_os] ? NORMALIZERS : NORMALIZERS + [OS] + normalizers.inject(config) do |config, normalizer| + normalizer.new(config, options).run + end + end + + def obfuscate + Obfuscate.new(config, options).run + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/config/dist.rb b/vendor/travis-core/lib/travis/model/build/config/dist.rb new file mode 100644 index 00000000..0b4d87f6 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/config/dist.rb @@ -0,0 +1,59 @@ +class Build + class Config + class Dist + DIST_LANGUAGE_MAP = { + 'objective-c' => 'osx' + }.freeze + DIST_OS_MAP = { + 'osx' => 'osx' + }.freeze + DIST_SERVICES_MAP = { + 'docker' => 'trusty' + }.freeze + DEFAULT_DIST = 'precise' + + attr_reader :config, :options + + def initialize(config, options) + @config = config + @options = options + end + + def run + return config unless config_hashy? + return config if config.key?(:dist) || config.key?('dist') + + config.dup.tap do |c| + c.merge!(dist: dist_for_config) + + matrix = c.fetch(:matrix, {}) + return c unless config_hashy?(matrix) + + included = matrix.fetch(:include, []) || [] + + included.each do |inc| + next unless config_hashy?(inc) + next if inc.key?(:dist) || inc.key?('dist') + inc.merge!(dist: dist_for_config(inc)) + end + end + end + + private + + def config_hashy?(h = config) + %w(key? dup merge! fetch).all? { |m| h.respond_to?(m) } + end + + def dist_for_config(h = config) + return DIST_LANGUAGE_MAP[h[:language]] if + DIST_LANGUAGE_MAP.key?(h[:language]) + (Array(h[:services]) || []).each do |service| + return DIST_SERVICES_MAP[service] if DIST_SERVICES_MAP.key?(service) + end + return DEFAULT_DIST if options[:multi_os] + DIST_OS_MAP.fetch(Array(h[:os]).first, DEFAULT_DIST) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/config/env.rb b/vendor/travis-core/lib/travis/model/build/config/env.rb new file mode 100644 index 00000000..ba4c02e6 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/config/env.rb @@ -0,0 +1,50 @@ +require 'core_ext/hash/compact' + +class Build + class Config + class Env < Struct.new(:config, :options) + def run + case config[:env] + when Hash + config.merge(normalize_hash(config[:env])).compact + when Array + config.merge(env: config[:env].map { |value| normalize_value(value) }) + else + config + end + end + + def normalize_hash(env) + if env[:global] || env[:matrix] + { global_env: normalize_values(env[:global]), env: normalize_values(env[:matrix]) } + else + { env: normalize_values(env) } + end + end + + def normalize_values(values) + values = [values].compact unless values.is_a?(Array) + values.map { |value| normalize_value(value) } unless values.empty? + end + + def normalize_value(value) + case value + when Hash + to_env_var(value) + when Array + value.map { |value| to_env_var(value) } + else + value + end + end + + def to_env_var(hash) + if hash.is_a?(Hash) && !hash.key?(:secure) + hash.map { |name, value| "#{name}=#{value}" }.join(' ') + else + hash + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/config/features.rb b/vendor/travis-core/lib/travis/model/build/config/features.rb new file mode 100644 index 00000000..5c08ca8f --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/config/features.rb @@ -0,0 +1,22 @@ +require 'core_ext/hash/compact' + +class Build + class Config + class Features < Struct.new(:config, :options) + def run + config = self.config + config = remove_multi_os(config) unless options[:multi_os] + config + end + + def remove_multi_os(config) + config.delete(:os) + includes = config[:matrix].is_a?(Hash) && config[:matrix][:include] + return config unless includes.is_a?(Array) + includes = includes.each { |c| c.delete(:os) if c.is_a?(Hash) }.uniq + config[:matrix][:include] = includes + config + end + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/config/group.rb b/vendor/travis-core/lib/travis/model/build/config/group.rb new file mode 100644 index 00000000..bac46c36 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/config/group.rb @@ -0,0 +1,18 @@ +class Build + class Config + class Group + DEFAULT_GROUP = 'stable' + + attr_reader :config + + def initialize(config, *) + @config = config + end + + def run + return config if config.key?(:group) || config.key?('group') + config.merge(group: DEFAULT_GROUP) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/config/language.rb b/vendor/travis-core/lib/travis/model/build/config/language.rb new file mode 100644 index 00000000..b6108de3 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/config/language.rb @@ -0,0 +1,27 @@ +require 'active_support/core_ext/array/wrap' + +class Build + class Config + class Language < Struct.new(:config, :options) + def run + config[:language] = Array.wrap(config[:language]).first.to_s.downcase + config[:language] = DEFAULT_LANG if config[:language].empty? + config.select { |key, _| include_key?(key) } + end + + private + + def include_key?(key) + matrix_keys.include?(key) || !known_env_key?(key) + end + + def matrix_keys + Config.matrix_keys(config, options) + end + + def known_env_key?(key) + (ENV_KEYS | EXPANSION_KEYS_FEATURE).include?(key) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/config/matrix.rb b/vendor/travis-core/lib/travis/model/build/config/matrix.rb new file mode 100644 index 00000000..8f5bcd3b --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/config/matrix.rb @@ -0,0 +1,108 @@ +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/array/wrap' + +class Build + class Config + class Matrix + attr_reader :config, :options + + def initialize(config, options = {}) + @config = config || {} + @options = options + end + + def expand + configs = expand_matrix + configs = include_matrix_configs(exclude_matrix_configs(configs)) + configs = configs.map { |config| cleanup_config(merge_config(Hash[config])) } + configs.map { |config| Build::Config::OS.new(config, options).run } + end + + def allow_failure_configs + Array(settings[:allow_failures] || []).select do |config| + # TODO check with @drogus how/when this might happen + config = config.to_hash.symbolize_keys if config.respond_to?(:to_hash) + end + end + + def fast_finish? + settings[:fast_finish] + end + + private + + def settings + @_settings ||= config[:matrix] || {} + @_settings = {} if @_settings.is_a?(Array) + @_settings + end + + def expand_matrix + rows = config.slice(*expand_keys).values.select { |value| value.is_a?(Array) } + max_size = rows.max_by(&:size).try(:size) || 1 + + array = expand_keys.inject([]) do |result, key| + values = Array.wrap(config[key]) + values += [values.last] * (max_size - values.size) + result << values.map { |value| [key, value] } + end + + permutations(array).uniq + end + + # recursively builds up permutations of values in the rows of a nested array + def permutations(base, result = []) + base = base.dup + base.empty? ? [result] : base.shift.map { |value| permutations(base, result + [value]) }.flatten(1) + end + + def expand_keys + @expand_keys ||= config.keys.map(&:to_sym) & Config.matrix_keys_for(config, options) + end + + def exclude_matrix_configs(configs) + configs.reject { |config| exclude_config?(config) } + end + + def exclude_config?(config) + exclude_configs = normalize_matrix_filter_configs(settings[:exclude] || []) + config = config.map { |config| [config[0].to_s, *config[1..-1]] }.sort + exclude_configs.any? do |excluded| + excluded.all? { |matrix_key| config.include? matrix_key } + end + end + + def include_matrix_configs(configs) + include_configs = normalize_matrix_filter_configs(settings[:include] || []) + if configs.flatten.empty? && settings.has_key?(:include) + include_configs + else + configs + include_configs + end + end + + def normalize_matrix_filter_configs(configs) + configs = configs.select { |c| c.is_a?(Hash) } + configs = configs.compact.map(&:stringify_keys) + configs.map(&:to_a).map(&:sort) + end + + def merge_config(row) + config.select { |key, value| include_key?(key) }.merge(row) + end + + def cleanup_config(config) + config.delete(:matrix) + config + end + + def include_key?(key) + Config.matrix_keys_for(config, options).include?(key.to_sym) || !known_env_key?(key.to_sym) + end + + def known_env_key?(key) + (ENV_KEYS | EXPANSION_KEYS_FEATURE).include?(key) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/config/obfuscate.rb b/vendor/travis-core/lib/travis/model/build/config/obfuscate.rb new file mode 100644 index 00000000..b1f8a39e --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/config/obfuscate.rb @@ -0,0 +1,53 @@ +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/array/wrap' +require 'travis/secure_config' + +class Build + class Config + class Obfuscate < Struct.new(:config, :options) + ENV_VAR_PATTERN = /(?<=\=)(?:(?['"]).*?[^\\]\k|(.*?)(?= \w+=|$))/ + + def run + config = self.config.except(:source_key) + config[:env] = obfuscate(config[:env]) if config[:env] + config + end + + private + + def obfuscate(env) + Array.wrap(env).map do |value| + obfuscate_values(value).join(' ') + end + end + + def obfuscate_values(values) + Array.wrap(values).compact.map do |value| + obfuscate_value(value) + end + end + + def obfuscate_value(value) + secure.decrypt(value) do |decrypted| + obfuscate_env_vars(decrypted) + end + end + + def obfuscate_env_vars(line) + if line.respond_to?(:gsub) + line.gsub(ENV_VAR_PATTERN) { |val| '[secure]' } + else + '[One of the secure variables in your .travis.yml has an invalid format.]' + end + end + + def secure + @secure ||= Travis::SecureConfig.new(key) + end + + def key + @key ||= options[:key_fetcher].call + end + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/config/os.rb b/vendor/travis-core/lib/travis/model/build/config/os.rb new file mode 100644 index 00000000..e37fde19 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/config/os.rb @@ -0,0 +1,27 @@ +class Build + class Config + class OS + OS_LANGUAGE_MAP = { + 'objective-c' => 'osx', + } + DEFAULT_OS = 'linux' + + attr_reader :config + + def initialize(config, _) + @config = config + end + + def run + return config if config.key?(:os) || config.key?('os') + config.merge(os: os_for_language) + end + + private + + def os_for_language + OS_LANGUAGE_MAP.fetch(config[:language], DEFAULT_OS) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/config/yaml.rb b/vendor/travis-core/lib/travis/model/build/config/yaml.rb new file mode 100644 index 00000000..1afe86ff --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/config/yaml.rb @@ -0,0 +1,14 @@ +class Build + class Config + class Yaml < Struct.new(:config, :options) + def run + normalize(config) + end + + def normalize(hash) + Hash[hash.map { |key, value| [key == true ? :on : key, value] }] + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/model/build/denormalize.rb b/vendor/travis-core/lib/travis/model/build/denormalize.rb new file mode 100644 index 00000000..d01a5127 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/denormalize.rb @@ -0,0 +1,34 @@ +class Build + + # The build's start and finish events (state changes) trigger denormalization + # of certain attributes to the repository in order to disburden the db a bit. + # + # E.g. on `start` the `started_at` attribute of a build gets set to its + # repository's `last_started_at` attribute. Likewise on `finish` the + # `finished_at` and `state` attributes are set to `last_build_finished_at` and + # `last_build_state` on the repository. + # + # These attributes are used in the repositories list and thus read frequently. + module Denormalize + def denormalize(event, *args) + repository.update_attributes!(denormalize_attributes_for(event)) if denormalize?(event) + end + + DENORMALIZE = { + start: %w(id number state duration started_at finished_at), + finish: %w(state duration finished_at), + reset: %w(state duration started_at finished_at), + cancel: %w(state duration finished_at) + } + + def denormalize?(event) + DENORMALIZE.key?(event) + end + + def denormalize_attributes_for(event) + DENORMALIZE[event].inject({}) do |result, key| + result.merge(:"last_build_#{key}" => send(key)) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/matrix.rb b/vendor/travis-core/lib/travis/model/build/matrix.rb new file mode 100644 index 00000000..25bf7b67 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/matrix.rb @@ -0,0 +1,114 @@ +require 'active_support/concern' +require 'active_support/core_ext/hash/keys' +require 'core_ext/hash/deep_symbolize_keys' + +class Build + + # A Build contains a number of Job::Test instances that make up the build + # matrix. + # + # The matrix is defined in the build configuration (`.travis.yml`) and + # expanded (evaluated and instantiated) when the Build is created. + # + # A build matrix has 1 to 3 dimensions and can be defined by specifying + # multiple values for either of: + # + # * a language/vm variant (e.g. 1.9.2, rbx, jruby for a Ruby build) + # * a dependency definition (e.g. a Gemfile for a Ruby build) + # * an arbitrary env key that can be used from within the test suite in + # order to branch out specific variations of the test run + module Matrix + extend ActiveSupport::Concern + + def matrix_finished? + if matrix_config.fast_finish? + required_jobs.all?(&:finished?) || required_jobs.any?(&:finished_unsuccessfully?) + else + matrix.all?(&:finished?) + end + end + + def matrix_duration + matrix_finished? ? matrix.inject(0) { |duration, job| duration + job.duration.to_i } : nil + end + + def matrix_state + if required_jobs.blank? + :passed + elsif required_jobs.any?(&:canceled?) + :canceled + elsif required_jobs.any?(&:errored?) + :errored + elsif required_jobs.any?(&:failed?) + :failed + elsif required_jobs.all?(&:passed?) + :passed + else + raise InvalidMatrixStateException.new(matrix) + end + end + + # expand the matrix (i.e. create test jobs) and update the config for each job + def expand_matrix + matrix_config.expand.each_with_index do |row, ix| + attributes = self.attributes.slice(*Job.column_names - ['status', 'result']).symbolize_keys + attributes.merge!( + owner: owner, + number: "#{number}.#{ix + 1}", + config: row, + log: Log.new + ) + matrix.build(attributes) + end + matrix_allow_failures # TODO should be able to join this with the loop above + matrix + end + + def expand_matrix! + expand_matrix + save! + end + + # Return only the child builds whose config matches against as passed hash + # e.g. build.filter_matrix(rvm: '1.8.7', env: 'DB=postgresql') + def filter_matrix(config) + config.blank? ? matrix : matrix.select { |job| job.matches_config?(config) } + end + + private + + def matrix_config + @matrix_config ||= Config::Matrix.new(config, multi_os: repository.multi_os_enabled?, dist_group_expansion: repository.dist_group_expansion_enabled?) + end + + def matrix_allow_failures + configs = matrix_config.allow_failure_configs + jobs = configs.map { |config| filter_matrix(config) }.flatten + jobs.each { |job| job.allow_failure = true } + end + + def required_jobs + @required_jobs ||= matrix.reject { |test| test.allow_failure? } + end + end + + class InvalidMatrixStateException < StandardError + attr_reader :matrix + + def initialize(matrix) + @matrix = matrix + end + + def to_s + sanitized = matrix.map do |job| + "\n\tid: #{job.id}, repository: #{job.repository.slug}, state: #{job.state}, " + + "allow_failure: #{job.allow_failure}, " + + "created_at: #{job.created_at.inspect}, queued_at: #{job.queued_at.inspect}, " + + "started_at: #{job.started_at.inspect}, finished_at: #{job.finished_at.inspect}, " + + "canceled_at: #{job.canceled_at.inspect}, updated_at: #{job.updated_at.inspect}" + end.join + + "Invalid build matrix state detected.\nMatrix: #{sanitized}" + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/metrics.rb b/vendor/travis-core/lib/travis/model/build/metrics.rb new file mode 100644 index 00000000..897e05ef --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/metrics.rb @@ -0,0 +1,15 @@ +require 'travis/model/build' +class Build + module Metrics + def start(data = {}) + super + meter 'travis.builds.start.delay', started_at - request.created_at + end + + private + + def meter(name, time) + Metriks.timer(name).update(time) + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/result_message.rb b/vendor/travis-core/lib/travis/model/build/result_message.rb new file mode 100644 index 00000000..669c0492 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/result_message.rb @@ -0,0 +1,79 @@ +require 'hashr' + +class Build + class ResultMessage + + # TODO extract to I18n + SHORT = { + pending: 'Pending', + passed: 'Passed', + failed: 'Failed', + broken: 'Broken', + fixed: 'Fixed', + failing: 'Still Failing', + errored: 'Errored', + canceled: 'Canceled' + } + + FULL = { + pending: 'The build is pending.', + passed: 'The build passed.', + failed: 'The build failed.', + broken: 'The build was broken.', + fixed: 'The build was fixed.', + failing: 'The build is still failing.', + errored: 'The build has errored.', + canceled: 'The build was canceled.' + } + + EMAIL = { + pending: 'Build #%d is pending.', + passed: 'Build #%d passed.', + failed: 'Build #%d failed.', + broken: 'Build #%d was broken.', + fixed: 'Build #%d was fixed.', + failing: 'Build #%d is still failing.', + errored: 'Build #%d has errored.', + canceled: 'Build #%d was canceled.' + } + + + attr_reader :build + + def initialize(build) + build = Hashr.new(build) if build.is_a?(Hash) + @build = build + end + + def short + SHORT[result_key] + end + + def full + FULL[result_key] + end + + def email + EMAIL[result_key] % build.number.to_i + end + + private + + def result_key + current = build.state.try(:to_sym) + previous = build.previous_state.try(:to_sym) + + if [:created, :queued, :received, :started].include?(current) + :pending + elsif previous == :passed && current == :failed + :broken + elsif previous == :failed && current == :passed + :fixed + elsif previous == :failed && current == :failed + :failing + else + current + end + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/states.rb b/vendor/travis-core/lib/travis/model/build/states.rb new file mode 100644 index 00000000..25b1193f --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/states.rb @@ -0,0 +1,109 @@ +require 'active_support/concern' +require 'simple_states' + +class Build + + # A Build goes through the following lifecycle: + # + # * A newly created Build is in the `created` state. + # * When started it sets its `started_at` attribute from the given + # (worker) payload. + # * A build won't be restarted if it already is started (each matrix job + # will try to start it). + # * A build will be finished only if all matrix jobs are finished (each + # matrix job will try to finish it). + # * After both `start` and `finish` events the build will denormalize + # attributes to its repository and notify event listeners. + module States + extend ActiveSupport::Concern + include Denormalize, Travis::Event + + included do + include SimpleStates + + states :created, :received, :started, :passed, :failed, :errored, :canceled + + event :receive, to: :received, unless: [:received?, :started?, :failed?, :errored?] + event :start, to: :started, unless: [:started?, :failed?, :errored?] + event :finish, to: :finished, if: :should_finish? + event :reset, to: :created + event :cancel, to: :canceled, if: :cancelable? + event :all, after: [:denormalize, :notify] + end + + def should_finish? + matrix_finished? && !finished? + end + + def receive(data = {}) + self.received_at = data[:received_at] + end + + def start(data = {}) + self.started_at = data[:started_at] + end + + def finish(data = {}) + self.state = matrix_state + self.duration = matrix_duration + self.finished_at = data[:finished_at] + + save! + end + + def cancel(options = {}) + matrix.each do |job| + job.cancel! + end + + finalize_cancel + end + + def finalize_cancel + self.state = matrix_state + self.duration = matrix_duration + self.canceled_at = Time.now + self.finished_at = Time.now + + save! + end + + def cancel_job + if matrix_finished? + finalize_cancel + denormalize(:cancel) + end + end + + def reset(options = {}) + self.state = :created unless matrix.any? { |job| job.state == :started } + %w(duration started_at finished_at).each { |attr| write_attribute(attr, nil) } + matrix.each(&:reset!) if options[:reset_matrix] + end + + def resetable? + finished? && !invalid_config? + end + + def invalid_config? + config[:".result"] == "parse_error" + end + + def pending? + created? || started? + end + + def finished? + passed? || failed? || errored? || canceled? + end + + def color + pending? ? 'yellow' : passed? ? 'green' : 'red' + end + + def notify(event, *args) + event = :create if event == :reset + super + end + end +end diff --git a/vendor/travis-core/lib/travis/model/build/update_branch.rb b/vendor/travis-core/lib/travis/model/build/update_branch.rb new file mode 100644 index 00000000..9ed8e936 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/build/update_branch.rb @@ -0,0 +1,37 @@ +class Build + class UpdateBranch < Struct.new(:build) + MSGS = { + update: 'Setting last_build_id to %s on branch %s, repo %s', + kaputt: 'Inconsistent branch.last_build_id=%s on branch: %s (repo: %s). Should be: %s.' + } + + def update_last_build + logger.info MSGS[:update] % [build.id, branch_name, repository.slug] + branch.update_attributes!(last_build_id: build.id) + validate_branch_last_build_id # TODO double check and remove after a few days + end + + private + + def validate_branch_last_build_id + return if branch.reload.last_build_id == build.id + logger.warn MSGS[:kaputt] % [branch.last_build_id, branch_name, repository.slug, build.id] + end + + def branch + Branch.where(repository_id: repository.id, name: branch_name).first_or_create + end + + def branch_name + build.branch || 'master' + end + + def repository + build.repository + end + + def logger + Travis.logger + end + end +end diff --git a/vendor/travis-core/lib/travis/model/commit.rb b/vendor/travis-core/lib/travis/model/commit.rb new file mode 100644 index 00000000..f21aeb26 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/commit.rb @@ -0,0 +1,28 @@ +require 'travis/model' + +# Encapsulates a commit that a Build belongs to (and that a Github Request +# referred to). +class Commit < Travis::Model + has_one :request + belongs_to :repository + + validates :commit, :branch, :committed_at, :presence => true + + def pull_request? + ref =~ %r(^refs/pull/\d+/merge$) + end + + def pull_request_number + if pull_request? && (num = ref.scan(%r(^refs/pull/(\d+)/merge$)).flatten.first) + num.to_i + end + end + + def range + if pull_request? + "#{request.base_commit}...#{request.head_commit}" + elsif compare_url && compare_url =~ /\/([0-9a-f]+\^*\.\.\.[0-9a-f]+\^*$)/ + $1 + end + end +end diff --git a/vendor/travis-core/lib/travis/model/email.rb b/vendor/travis-core/lib/travis/model/email.rb new file mode 100644 index 00000000..b652893f --- /dev/null +++ b/vendor/travis-core/lib/travis/model/email.rb @@ -0,0 +1,5 @@ +require 'travis/model' + +class Email < Travis::Model + belongs_to :user +end diff --git a/vendor/travis-core/lib/travis/model/encrypted_column.rb b/vendor/travis-core/lib/travis/model/encrypted_column.rb new file mode 100644 index 00000000..67045051 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/encrypted_column.rb @@ -0,0 +1,105 @@ +require 'securerandom' +require 'base64' + +class Travis::Model::EncryptedColumn + attr_reader :disable, :options + alias disabled? disable + + def initialize(options = {}) + @options = options || {} + @disable = self.options[:disable] + @key = self.options[:key] + end + + def enabled? + !disabled? + end + + def load(data) + return nil unless data + + data = data.to_s + + decrypt?(data) ? decrypt(data) : data + end + + def dump(data) + encrypt?(data) ? encrypt(data.to_s) : data + end + + def key + @key || config.key + end + + def iv + SecureRandom.hex(8) + end + + def prefix + '--ENCR--' + end + + def decrypt?(data) + data.present? && (!use_prefix? || prefix_used?(data)) + end + + def encrypt?(data) + data.present? && enabled? + end + + def prefix_used?(data) + data[0..7] == prefix + end + + def decrypt(data) + data = data[8..-1] if prefix_used?(data) + + data = decode data + + iv = data[-16..-1] + data = data[0..-17] + + aes = create_aes :decrypt, key.to_s, iv + + result = aes.update(data) + aes.final + end + + def encrypt(data) + iv = self.iv + + aes = create_aes :encrypt, key.to_s, iv + + encrypted = aes.update(data) + aes.final + + encrypted = "#{encrypted}#{iv}" + encrypted = encode encrypted + encrypted = "#{prefix}#{encrypted}" if use_prefix? + encrypted + end + + def use_prefix? + options.has_key?(:use_prefix) ? options[:use_prefix] : Travis::Features.feature_inactive?(:db_encryption_prefix) + end + + def create_aes(mode = :encrypt, key, iv) + aes = OpenSSL::Cipher::AES.new(256, :CBC) + + aes.send(mode) + aes.key = key + aes.iv = iv + + aes + end + + def config + Travis.config.encryption + end + + def decode(str) + Base64.strict_decode64 str + end + + def encode(str) + Base64.strict_encode64 str + end +end diff --git a/vendor/travis-core/lib/travis/model/env_helpers.rb b/vendor/travis-core/lib/travis/model/env_helpers.rb new file mode 100644 index 00000000..5ba5a6c3 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/env_helpers.rb @@ -0,0 +1,10 @@ +module Travis::Model::EnvHelpers + def obfuscate_env(vars) + vars = [vars] unless vars.is_a?(Array) + vars.compact.map do |var| + repository.key.secure.decrypt(var) do |decrypted| + Travis::Helpers.obfuscate_env_vars(decrypted) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/model/job.rb b/vendor/travis-core/lib/travis/model/job.rb new file mode 100644 index 00000000..1d86e278 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/job.rb @@ -0,0 +1,235 @@ +require 'travis/model' +require 'active_support/core_ext/hash/deep_dup' +require 'travis/model/build/config/language' + +# Job models a unit of work that is run on a remote worker. +# +# There currently only one job type: +# +# * Job::Test belongs to a Build (one or many Job::Test instances make up a +# build matrix) and executes a test suite with parameters defined in the +# configuration. +class Job < Travis::Model + require 'travis/model/job/queue' + require 'travis/model/job/test' + require 'travis/model/env_helpers' + + WHITELISTED_ADDONS = %w( + apt + apt_packages + apt_sources + firefox + hosts + mariadb + postgresql + ssh_known_hosts + ).freeze + + class << self + # what we return from the json api + def queued(queue = nil) + scope = where(state: [:created, :queued]) + scope = scope.where(queue: queue) if queue + scope + end + + # what needs to be queued up + def queueable(queue = nil) + scope = where(state: :created).order('jobs.id') + scope = scope.where(queue: queue) if queue + scope + end + + # what already is queued or started + def running(queue = nil) + scope = where(state: [:queued, :received, :started]).order('jobs.id') + scope = scope.where(queue: queue) if queue + scope + end + + def unfinished + # TODO conflate Job and Job::Test and use States::FINISHED_STATES + where('state NOT IN (?)', [:finished, :passed, :failed, :errored, :canceled]) + end + + def owned_by(owner) + where(owner_id: owner.id, owner_type: owner.class.to_s) + end + end + + include Travis::Model::EnvHelpers + + has_one :log, dependent: :destroy + has_many :events, as: :source + has_many :annotations, dependent: :destroy + + belongs_to :repository + belongs_to :commit + belongs_to :source, polymorphic: true, autosave: true + belongs_to :owner, polymorphic: true + + validates :repository_id, :commit_id, :source_id, :source_type, :owner_id, :owner_type, presence: true + + serialize :config + + delegate :request_id, to: :source # TODO denormalize + delegate :pull_request?, to: :commit + delegate :secure_env_enabled?, :addons_enabled?, to: :source + + after_initialize do + self.config = {} if config.nil? rescue nil + end + + before_create do + build_log + self.state = :created if self.state.nil? + self.queue = Queue.for(self).name + end + + after_commit on: :create do + notify(:create) + end + + def propagate(name, *args) + # if we propagate cancel, we can't send it as "cancel", because + # it would trigger cancelling the entire matrix + if name == :cancel + name = :cancel_job + end + Metriks.timer("job.propagate.#{name}").time do + source.send(name, *args) + end + true + end + + def duration + started_at && finished_at ? finished_at - started_at : nil + end + + def ssh_key + config[:source_key] + end + + def config=(config) + super normalize_config(config) + end + + def obfuscated_config + normalize_config(config).deep_dup.tap do |config| + delete_addons(config) + config.delete(:source_key) + if config[:env] + obfuscated_env = process_env(config[:env]) { |env| obfuscate_env(env) } + config[:env] = obfuscated_env ? obfuscated_env.join(' ') : nil + end + if config[:global_env] + obfuscated_env = process_env(config[:global_env]) { |env| obfuscate_env(env) } + config[:global_env] = obfuscated_env ? obfuscated_env.join(' ') : nil + end + end + end + + def decrypted_config + normalize_config(self.config).deep_dup.tap do |config| + config[:env] = process_env(config[:env]) { |env| decrypt_env(env) } if config[:env] + config[:global_env] = process_env(config[:global_env]) { |env| decrypt_env(env) } if config[:global_env] + if config[:addons] + if addons_enabled? + config[:addons] = decrypt_addons(config[:addons]) + else + delete_addons(config) + end + end + end + rescue => e + logger.warn "[job id:#{id}] Config could not be decrypted due to #{e.message}" + {} + end + + def matches_config?(other) + config = self.config.slice(*other.keys) + config = config.merge(branch: commit.branch) if other.key?(:branch) # TODO test this + return false if config.size == 0 + config.all? { |key, value| value == other[key] || commit.branch == other[key] } + end + + def log_content=(content) + create_log! unless log + log.update_attributes!(content: content, aggregated_at: Time.now) + end + + # compatibility, we still use result in webhooks + def result + state.try(:to_sym) == :passed ? 0 : 1 + end + + private + + def delete_addons(config) + if config[:addons].is_a?(Hash) + config[:addons].keep_if { |key, _| WHITELISTED_ADDONS.include? key.to_s } + else + config.delete(:addons) + end + end + + def normalize_config(config) + config = config ? config.deep_symbolize_keys : {} + + if config[:deploy] + if config[:addons].is_a? Hash + config[:addons][:deploy] = config.delete(:deploy) + else + config.delete(:addons) + config[:addons] = { deploy: config.delete(:deploy) } + end + end + + config + end + + def process_env(env) + env = [env] unless env.is_a?(Array) + env = normalize_env(env) + env = if secure_env_enabled? + yield(env) + else + remove_encrypted_env_vars(env) + end + env.compact.presence + end + + def remove_encrypted_env_vars(env) + env.reject do |var| + var.is_a?(Hash) && var.has_key?(:secure) + end + end + + def normalize_env(env) + env.map do |line| + if line.is_a?(Hash) && !line.has_key?(:secure) + line.map { |k, v| "#{k}=#{v}" }.join(' ') + else + line + end + end + end + + def decrypt_addons(addons) + decrypt(addons) + end + + def decrypt_env(env) + env.map do |var| + decrypt(var) do |var| + var.dup.insert(0, 'SECURE ') unless var.include?('SECURE ') + end + end + rescue + {} + end + + def decrypt(v, &block) + repository.key.secure.decrypt(v, &block) + end +end diff --git a/vendor/travis-core/lib/travis/model/job/cleanup.rb b/vendor/travis-core/lib/travis/model/job/cleanup.rb new file mode 100644 index 00000000..609aaa36 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/job/cleanup.rb @@ -0,0 +1,41 @@ +# require 'active_support/concern' +# +# class Job +# +# # Cleanup contains logic that is supposed to re-enqueue stalled jobs +# # and finally finish them forcefully. +# # +# # This stuff is currently not used except when we occasionally +# # re-enqueue jobs manually from the console. +# module Cleanup +# extend ActiveSupport::Concern +# +# FORCE_FINISH_MESSAGE = <<-msg.strip +# This job could not be processed and was forcefully finished. +# msg +# +# included do +# class << self +# def cleanup +# stalled.each do |job| +# job.requeueable? ? job.enqueue : job.force_finish +# end +# end +# +# def stalled +# unfinished.where('created_at < ?', Time.now.utc - Travis.config.jobs.retry.after) +# end +# end +# end +# +# def force_finish +# append_log!("\n#{FORCE_FINISH_MESSAGE}") if respond_to?(:append_log!) +# finish!(state: :errored, finished_at: Time.now.utc) +# end +# +# def requeueable? +# false +# # retries < Travis.config.jobs.retry.max_attempts +# end +# end +# end diff --git a/vendor/travis-core/lib/travis/model/job/queue.rb b/vendor/travis-core/lib/travis/model/job/queue.rb new file mode 100644 index 00000000..ff7b8197 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/job/queue.rb @@ -0,0 +1,121 @@ +class Job + + # Encapsulates logic for figuring out which queue a given job needs to go + # into. + # + # Queue names for Job::Test instances are configured in `Travis.config` and + # are determined based on the repository slug (e.g. 'rails/rails' has its own + # queue) or the language given in the configuration (`.travis.yml`) and + # default to 'builds.linux'. + class Queue + SUDO_REQUIRED_EXECUTABLES = %w( + docker + ping + sudo + ) + + SUDO_DETECTION_REGEXP = /^[^#]*\b(#{SUDO_REQUIRED_EXECUTABLES.join('|')})\b/ + + CUSTOM_STAGES = %w( + before_install + install + before_script + script + before_cache + after_success + after_failure + after_script + before_deploy + ).map(&:to_sym) + + class << self + def for(job) + queues.find(-> { ifnone }) { |queue| queue.matches?(job) } + end + + def queues + @queues ||= Array(Travis.config.queues).compact.map do |queue| + Queue.new(queue[:queue], queue.reject { |key, value| key == :queue }) + end + end + + def default + @default ||= Queue.new(Travis.config.default_queue, {}) + end + + def sudo_detected?(config) + config.values_at(*CUSTOM_STAGES).compact.flatten.any? do |s| + SUDO_DETECTION_REGEXP =~ s.to_s + end + end + + private + + def ifnone + Travis.logger.info("job matches queue #{default.name} via ifnone proc") + default + end + end + + attr_reader :name, :attrs + + def initialize(name, attrs) + @name = name + @attrs = attrs + end + + def matches?(job) + matchers = matchers_for(job) + + unknown_matchers = @attrs.keys - matchers.keys + if unknown_matchers.length > 0 + warn "unknown matchers used for queue #{name}: #{unknown_matchers.join(", ")}" + end + + known_matchers = @attrs.keys & matchers.keys + + all_match = known_matchers.all? do |key| + matchers[key.to_sym] === @attrs[key] + end + + if known_matchers.length > 0 && all_match + logger.info("job matches queue #{name} via matchers #{matchers.inspect}") + return true + end + + false + end + + private + + def matchers_for(job) + { + slug: "#{job.repository.try(:owner_name)}/#{job.repository.try(:name)}", + owner: job.repository.try(:owner_name), + os: job.config[:os], + language: Array(job.config[:language]).flatten.compact.first, + sudo: job.config.fetch(:sudo) { !repo_is_default_docker?(job) }, + dist: job.config[:dist], + group: job.config[:group], + osx_image: job.config[:osx_image], + percentage: lambda { |percentage| rand(100) < percentage }, + services: lambda { |other| !(Array(job.config[:services]) & other).empty? }, + } + end + + def repo_is_default_docker?(job) + return true if Travis::Github::Education.education_queue?(job.repository.try(:owner)) + return false unless Travis::Features.feature_active?(:docker_default_queue) + !self.class.sudo_detected?(job.config) && repo_created_after_docker_cutoff?(job.repository) + end + + def repo_created_after_docker_cutoff?(repository) + return true if repository.created_at.nil? + repository.created_at > Time.parse(Travis.config.docker_default_queue_cutoff) + end + + def logger + Travis.logger + end + end +end diff --git a/vendor/travis-core/lib/travis/model/job/test.rb b/vendor/travis-core/lib/travis/model/job/test.rb new file mode 100644 index 00000000..6977b598 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/job/test.rb @@ -0,0 +1,107 @@ +require 'active_support/core_ext/hash/slice' +require 'simple_states' + +class Job + + # Executes a test job (i.e. runs a test suite) remotely and keeps tabs about + # state changes throughout its lifecycle in the database. + # + # Job::Test belongs to a Build as part of the build matrix and will be + # created with the Build. + class Test < Job + FINISHED_STATES = [:passed, :failed, :errored, :canceled] + FAILED_STATES = [:failed, :errored, :canceled] + + include SimpleStates, Travis::Event + + states :created, :queued, :received, :started, :passed, :failed, :errored, :canceled + + event :receive, to: :received + event :start, to: :started + event :finish, to: :finished + event :reset, to: :created, unless: :created? + event :cancel, to: :canceled, if: :cancelable? + event :all, after: [:propagate, :notify] + + def enqueue # TODO rename to queue and make it an event, simple_states should support that now + update_attributes!(state: :queued, queued_at: Time.now.utc) + notify(:queue) + end + + def receive(data = {}) + log.update_attributes!(content: '', removed_at: nil, removed_by: nil) # TODO this should be in a restart method, right? + data = data.symbolize_keys.slice(:received_at, :worker) + data.each { |key, value| send(:"#{key}=", value) } + end + + def start(data = {}) + data = data.symbolize_keys.slice(:started_at) + data.each { |key, value| send(:"#{key}=", value) } + end + + def finish(data = {}) + data = data.symbolize_keys.slice(:state, :finished_at) + data.each { |key, value| send(:"#{key}=", value) } + end + + def reset(*) + self.state = :created + attrs = %w(started_at queued_at finished_at worker) + attrs.each { |attr| write_attribute(attr, nil) } + if log + log.clear! + else + build_log + end + annotations.destroy_all + end + + def cancel + self.canceled_at = Time.now + self.finished_at = Time.now + + save! + end + + def cancelable? + !finished? + end + + def resetable? + finished? && !invalid_config? + end + + def invalid_config? + config[:".result"] == "parse_error" + end + + def finished? + FINISHED_STATES.include?(state.to_sym) + end + + def finished_unsuccessfully? + FAILED_STATES.include?(state.to_sym) + end + + def passed? + state.to_s == "passed" + end + + def failed? + state.to_s == "failed" + end + + def unknown? + state == nil + end + + def notify(event, *args) + Metriks.timer("job.notify.#{event}").time do + event = :create if event == :reset + super + end + end + + delegate :id, :content, :to => :log, :prefix => true, :allow_nil => true + end +end diff --git a/vendor/travis-core/lib/travis/model/log.rb b/vendor/travis-core/lib/travis/model/log.rb new file mode 100644 index 00000000..66672026 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/log.rb @@ -0,0 +1,54 @@ +require 'metriks' +require 'active_support/core_ext/string/filters' +require 'travis/model' + +class Log < Travis::LogsModel + require 'travis/model/log/part' + + AGGREGATE_PARTS_SELECT_SQL = <<-sql.squish + SELECT array_to_string(array_agg(log_parts.content ORDER BY number, id), '') + FROM log_parts + WHERE log_id = ? + sql + + class << self + def aggregated_content(id) + Metriks.timer('logs.read_aggregated').time do + connection.select_value(sanitize_sql([AGGREGATE_PARTS_SELECT_SQL, id])) || '' + end + end + end + + include Travis::Event + + belongs_to :job + belongs_to :removed_by, class_name: 'User', foreign_key: :removed_by + has_many :parts, class_name: 'Log::Part', foreign_key: :log_id, :dependent => :destroy + + def content + content = read_attribute(:content) || '' + content = [content, self.class.aggregated_content(id)].join unless aggregated? + content + end + + def aggregated? + !!aggregated_at + end + + def clear! + update_column(:content, '') # TODO why in the world does update_attributes not set content to '' + update_column(:aggregated_at, nil) # TODO why in the world does update_attributes not set aggregated_at to nil? + update_column(:archived_at, nil) + update_column(:archive_verified, nil) + Log::Part.where(log_id: id).delete_all + parts.reload + end + + def archived? + archived_at && archive_verified? + end + + def to_json + { 'log' => attributes.slice(*%w(id content created_at job_id updated_at)) }.to_json + end +end diff --git a/vendor/travis-core/lib/travis/model/log/part.rb b/vendor/travis-core/lib/travis/model/log/part.rb new file mode 100644 index 00000000..dfb9ffdd --- /dev/null +++ b/vendor/travis-core/lib/travis/model/log/part.rb @@ -0,0 +1,5 @@ +class Log::Part < Travis::LogsModel + self.table_name = 'log_parts' + + validates :log_id, presence: true, numericality: { greater_than: 0 } +end diff --git a/vendor/travis-core/lib/travis/model/logs_model.rb b/vendor/travis-core/lib/travis/model/logs_model.rb new file mode 100644 index 00000000..f32209b8 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/logs_model.rb @@ -0,0 +1,3 @@ +class Travis::LogsModel < ActiveRecord::Base + self.abstract_class = true +end diff --git a/vendor/travis-core/lib/travis/model/membership.rb b/vendor/travis-core/lib/travis/model/membership.rb new file mode 100644 index 00000000..5a1900be --- /dev/null +++ b/vendor/travis-core/lib/travis/model/membership.rb @@ -0,0 +1,7 @@ +require 'travis/model' + +class Membership < Travis::Model + belongs_to :user + belongs_to :organization +end + diff --git a/vendor/travis-core/lib/travis/model/organization.rb b/vendor/travis-core/lib/travis/model/organization.rb new file mode 100644 index 00000000..42a4c430 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/organization.rb @@ -0,0 +1,14 @@ +require 'gh' +require 'travis/model' + +class Organization < Travis::Model + has_many :memberships + has_many :users, :through => :memberships + has_many :repositories, :as => :owner + + def education? + Travis::Features.owner_active?(:educational_org, self) + end + alias education education? +end + diff --git a/vendor/travis-core/lib/travis/model/permission.rb b/vendor/travis-core/lib/travis/model/permission.rb new file mode 100644 index 00000000..53ec486e --- /dev/null +++ b/vendor/travis-core/lib/travis/model/permission.rb @@ -0,0 +1,26 @@ +require 'core_ext/active_record/none_scope' +require 'travis/model' + +class Permission < Travis::Model + ROLES = %w(admin push pull) + + class << self + def by_roles(roles) + roles = Array(roles).select { |role| ROLES.include?(role.to_s) } + roles.empty? ? none : where(has_roles(roles)) + end + + def has_roles(roles) + roles.inject(has_role(roles.shift)) do |sql, role| + sql.or(has_role(role)) + end + end + + def has_role(role) + arel_table[role].eq(true) + end + end + + belongs_to :user + belongs_to :repository +end diff --git a/vendor/travis-core/lib/travis/model/repository.rb b/vendor/travis-core/lib/travis/model/repository.rb new file mode 100644 index 00000000..cc70423d --- /dev/null +++ b/vendor/travis-core/lib/travis/model/repository.rb @@ -0,0 +1,206 @@ +require 'uri' +require 'core_ext/hash/compact' +require 'travis/model' + +# Models a repository that has many builds and requests. +# +# A repository has an ssl key pair that is used to encrypt and decrypt +# sensitive data contained in the public `.travis.yml` file, such as Campfire +# authentication data. +# +# A repository also has a ServiceHook that can be used to de/activate service +# hooks on Github. +class Repository < Travis::Model + require 'travis/model/repository/status_image' + require 'travis/model/repository/settings' + + has_many :commits, dependent: :delete_all + has_many :requests, dependent: :delete_all + has_many :builds, dependent: :delete_all + has_many :events + has_many :permissions, dependent: :delete_all + has_many :users, through: :permissions + + has_one :last_build, class_name: 'Build', order: 'id DESC' + has_one :key, class_name: 'SslKey' + belongs_to :owner, polymorphic: true + + validates :name, presence: true + validates :owner_name, presence: true + + before_create do + build_key + end + + delegate :public_key, to: :key + + class << self + def timeline + active.order('last_build_finished_at IS NULL AND last_build_started_at IS NOT NULL DESC, last_build_started_at DESC NULLS LAST, id DESC') + end + + def with_builds + where(arel_table[:last_build_id].not_eq(nil)) + end + + def administratable + includes(:permissions).where('permissions.admin = ?', true) + end + + def recent + limit(25) + end + + def by_owner_name(owner_name) + without_invalidated.where(owner_name: owner_name) + end + + def by_member(login) + without_invalidated.joins(:users).where(users: { login: login }) + end + + def by_slug(slug) + without_invalidated.where(owner_name: slug.split('/').first, name: slug.split('/').last).order('id DESC') + end + + def search(query) + query = query.gsub('\\', '/') + without_invalidated.where("(repositories.owner_name || chr(47) || repositories.name) ILIKE ?", "%#{query}%") + end + + def active + without_invalidated.where(active: true) + end + + def without_invalidated + where(invalidated_at: nil) + end + + def find_by(params) + if id = params[:repository_id] || params[:id] + find_by_id(id) + elsif params[:github_id] + find_by_github_id(params[:github_id]) + elsif params.key?(:slug) + by_slug(params[:slug]).first + elsif params.key?(:name) && params.key?(:owner_name) + by_slug("#{params[:owner_name]}/#{params[:name]}").first + end + end + + def by_name + Hash[*all.map { |repository| [repository.name, repository] }.flatten] + end + + def counts_by_owner_names(owner_names) + query = %(SELECT owner_name, count(*) FROM repositories WHERE owner_name IN (?) AND invalidated_at IS NULL GROUP BY owner_name) + query = sanitize_sql([query, owner_names]) + rows = connection.select_all(query, owner_names) + Hash[*rows.map { |row| [row['owner_name'], row['count'].to_i] }.flatten] + end + end + + delegate :builds_only_with_travis_yml?, to: :settings + + def admin + @admin ||= Travis.run_service(:find_admin, repository: self) # TODO check who's using this + end + + def slug + @slug ||= [owner_name, name].join('/') + end + + def api_url + "#{Travis.config.github.api_url}/repos/#{slug}" + end + + def source_url + (private? || private_mode?) ? "git@#{source_host}:#{slug}.git": "git://#{source_host}/#{slug}.git" + end + + def private_mode? + source_host != 'github.com' + end + + def source_host + Travis.config.github.source_host || 'github.com' + end + + def branches + self.class.connection.select_values %( + SELECT DISTINCT ON (branch) branch + FROM builds + WHERE builds.repository_id = #{id} + ORDER BY branch DESC + LIMIT 25 + ) + end + + def last_completed_build(branch = nil) + builds.api_and_pushes.last_build_on(state: [:passed, :failed, :errored, :canceled], branch: branch) + end + + def last_build_on(branch) + builds.api_and_pushes.last_build_on(branch: branch) + end + + def build_status(branch) + builds.api_and_pushes.last_state_on(state: [:passed, :failed, :errored, :canceled], branch: branch) + end + + def last_finished_builds_by_branches(limit = 50) + Build.joins(%( + inner join ( + select distinct on (branch) builds.id + from builds + where builds.repository_id = #{id} and builds.event_type = 'push' + order by branch, finished_at desc + ) as last_builds on builds.id = last_builds.id + )).limit(limit).order('finished_at DESC') + end + + def regenerate_key! + ActiveRecord::Base.transaction do + key.destroy unless key.nil? + build_key + save! + end + end + + def settings + @settings ||= begin + instance = Repository::Settings.load(super, repository_id: id) + instance.on_save do + self.settings = instance.to_json + self.save! + end + instance + end + end + + def settings=(value) + if value.is_a?(String) || value.nil? + super(value) + else + super(value.to_json) + end + end + + def users_with_permission(permission) + users.includes(:permissions).where(permissions: { permission => true }).limit(10).all + end + + def reload(*) + @settings = nil + super + end + + def multi_os_enabled? + Travis::Features.enabled_for_all?(:multi_os) || Travis::Features.active?(:multi_os, self) + end + + def dist_group_expansion_enabled? + Travis::Features.enabled_for_all?(:dist_group_expansion) || Travis::Features.active?(:dist_group_expansion, self) + end + +end diff --git a/vendor/travis-core/lib/travis/model/repository/settings.rb b/vendor/travis-core/lib/travis/model/repository/settings.rb new file mode 100644 index 00000000..86060fd7 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/repository/settings.rb @@ -0,0 +1,135 @@ +# encoding: utf-8 +require 'coercible' +require 'travis/settings' +require 'travis/overwritable_method_definitions' +require 'travis/settings/encrypted_value' +require 'openssl' + +class Repository::Settings < Travis::Settings + class EnvVar < Travis::Settings::Model + attribute :id, String + attribute :name, String + attribute :value, Travis::Settings::EncryptedValue + attribute :public, Boolean, default: false + attribute :repository_id, Integer + + validates :name, presence: true + end + + class SshKey < Travis::Settings::Model + class NotAPrivateKeyError < StandardError; end + + attribute :description, String + attribute :value, Travis::Settings::EncryptedValue + attribute :repository_id, Integer + + validates :value, presence: true + validate :validate_correctness + + def validate_correctness + return unless value.decrypt + key = OpenSSL::PKey::RSA.new(value.decrypt, '') + raise NotAPrivateKeyError unless key.private? + rescue OpenSSL::PKey::RSAError, NotAPrivateKeyError + # it seems there is no easy way to check if key + # needs a pass phrase with ruby's openssl bindings, + # that's why we need to manually check that + if value.decrypt.to_s =~ /ENCRYPTED/ + errors.add(:value, :key_with_a_passphrase) + else + errors.add(:value, :not_a_private_key) + end + end + end + + class EnvVars < Collection + model EnvVar + + def public + find_all { |var| var.public? } + end + end + + class TimeoutsValidator < ActiveModel::Validator + def validate(settings) + [:hard_limit, :log_silence].each do |type| + next if valid_timeout?(settings, type) + msg = "Invalid #{type} timout value (allowed: 0 - #{max_value(settings, type)})" + settings.errors.add :"timeout_#{type}", msg + end + end + + private + + def valid_timeout?(settings, type) + value = settings.send(:"timeout_#{type}") + value.nil? || value.to_i > 0 && value.to_i <= max_value(settings, type) + end + + def max_value(settings, type) + config = Travis.config.settings.timeouts + values = config.send(custom_timeouts?(settings) ? :maximums : :defaults) || {} + values[type] + end + + def custom_timeouts?(settings) + Travis::Features.repository_active?(:custom_timeouts, settings.repository_id) + end + end + + attribute :env_vars, EnvVars.for_virtus + + attribute :builds_only_with_travis_yml, Boolean, default: false + attribute :build_pushes, Boolean, default: true + attribute :build_pull_requests, Boolean, default: true + attribute :maximum_number_of_builds, Integer + attribute :ssh_key, SshKey + attribute :timeout_hard_limit + attribute :timeout_log_silence + attribute :api_builds_rate_limit, Integer + + validates :maximum_number_of_builds, numericality: true + + validate :api_builds_rate_limit_restriction + + validates_with TimeoutsValidator + + def maximum_number_of_builds + super || 0 + end + + def restricts_number_of_builds? + maximum_number_of_builds > 0 + rescue => e + false + end + + def timeout_hard_limit + value = super + value == 0 ? nil : value + end + + def timeout_log_silence + value = super + value == 0 ? nil : value + end + + def api_builds_rate_limit + super || nil + end + + def api_builds_rate_limit_restriction + if api_builds_rate_limit.to_i > Travis.config.settings.rate_limit.maximums.api_builds + errors.add(:api_builds_rate_limit, "can't be more than 200") + end + end + + + def repository_id + additional_attributes[:repository_id] + end +end + +class Repository::DefaultSettings < Repository::Settings + include Travis::DefaultSettings +end diff --git a/vendor/travis-core/lib/travis/model/repository/status_image.rb b/vendor/travis-core/lib/travis/model/repository/status_image.rb new file mode 100644 index 00000000..94212590 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/repository/status_image.rb @@ -0,0 +1,61 @@ +class Repository + class StatusImage + RESULTS = { + passed: :passing, + failed: :failing, + errored: :error, + canceled: :canceled + } + + attr_reader :repo, :branch + + def initialize(repo, branch = nil) + @repo = repo + @branch = branch + end + + def result + last_state ? RESULTS[last_state] : :unknown + end + + private + + def cache_enabled? + defined?(@cache_enabled) ? @cache_enabled : @cache_enabled = Travis::Features.feature_active?(:states_cache) + end + + def last_state + @last_state ||= (state_from_cache || state_from_database) + end + + def state_from_cache + return unless repo + return unless cache_enabled? + + cache.fetch_state(repo.id, branch).tap do |result| + if result + Metriks.meter('status-image.cache-hit').mark + else + Metriks.meter('status-image.cache-miss').mark + end + end + rescue Travis::StatesCache::CacheError => e + Travis.logger.error(e.message) + return nil + end + + def state_from_database + return unless repo + + build = repo.last_completed_build(branch) + if build + cache.write(repo.id, build.branch, build) if cache_enabled? + build.state.to_sym + end + end + + def cache + Travis.states_cache + end + end +end diff --git a/vendor/travis-core/lib/travis/model/request.rb b/vendor/travis-core/lib/travis/model/request.rb new file mode 100644 index 00000000..25bed946 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/request.rb @@ -0,0 +1,115 @@ +require 'active_record' +require 'simple_states' +require 'travis/model/encrypted_column' + +# Models an incoming request. The only supported source for requests currently is Github. +# +# The Request will be configured by fetching `.travis.yml` from the Github API +# and needs to be approved based on the configuration. Once approved the +# Request creates a Build. +class Request < Travis::Model + require 'travis/model/request/approval' + require 'travis/model/request/branches' + require 'travis/model/request/pull_request' + require 'travis/model/request/states' + + include States, SimpleStates + + serialize :token, Travis::Model::EncryptedColumn.new(disable: true) + + class << self + def last_by_head_commit(head_commit) + where(head_commit: head_commit).order(:id).last + end + + def older_than(id) + recent.where('id < ?', id) + end + + def recent(limit = 25) + order('id DESC').limit(limit) + end + end + + belongs_to :commit + belongs_to :repository + belongs_to :owner, polymorphic: true + has_many :builds + has_many :events, as: :source + + validates :repository_id, presence: true + + serialize :config + serialize :payload + + def event_type + read_attribute(:event_type) || 'push' + end + + def ref + payload['ref'] if payload + end + + def branch_name + ref.scan(%r{refs/heads/(.*?)$}).flatten.first if ref + end + + def tag_name + ref.scan(%r{refs/tags/(.*?)$}).flatten.first if ref + end + + def api_request? + event_type == 'api' + end + + def pull_request? + event_type == 'pull_request' + end + + def pull_request + @pull_request ||= PullRequest.new(payload && payload['pull_request']) + end + + def pull_request_title + pull_request.title if pull_request? + end + + def pull_request_number + pull_request.number if pull_request? + end + + def head_repo + pull_request.head_repo + end + + def base_repo + pull_request.base_repo + end + + def head_branch + pull_request.head_branch + end + + def base_branch + pull_request.base_branch + end + + def config_url + GH.full_url("repos/#{repository.slug}/contents/.travis.yml?ref=#{commit.commit}").to_s + end + + def same_repo_pull_request? + begin + head_repo && base_repo && head_repo == base_repo + rescue => e + Travis.logger.error("[request:#{id}] Couldn't determine whether pull request is from the same repository: #{e.message}") + false + end + end + + def creates_jobs? + Build::Config::Matrix.new( + Build::Config.new(config).normalize, multi_os: repository.multi_os_enabled?, dist_group_expansion: repository.dist_group_expansion_enabled? + ).expand.size > 0 + end +end diff --git a/vendor/travis-core/lib/travis/model/request/approval.rb b/vendor/travis-core/lib/travis/model/request/approval.rb new file mode 100644 index 00000000..cf47b469 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/request/approval.rb @@ -0,0 +1,130 @@ +class Request + class Approval + attr_reader :request, :repository, :commit + + def initialize(request) + @request = request + @repository = request.repository + @commit = request.commit + end + + def settings + repository.settings + end + + delegate :build_pushes?, :build_pull_requests?, to: :settings + + def accepted? + commit.present? && + !repository.private? && + (!excluded_repository? || included_repository?) && + !skipped? && + !compare_url_too_long? && + enabled_in_settings? + end + + def enabled_in_settings? + request.api_request? || (request.pull_request? ? build_pull_requests? : build_pushes?) + end + + def disabled_in_settings? + !enabled_in_settings? + end + + def branch_accepted? + github_pages_explicitly_enabled? || !github_pages? + end + + def config_accepted? + (travis_yml_present? || allow_builds_without_travis_yml?) + end + + def travis_yml_present? + request.config && request.config['.result'] == 'configured' + end + + def allow_builds_without_travis_yml? + !repository.builds_only_with_travis_yml? + end + + def compare_url_too_long? + commit.compare_url.length > 255 + end + + def approved? + accepted? && request.config.present? && branch_approved? && request.creates_jobs? + end + + def result + approved? ? :accepted : :rejected + end + + def message + if !commit.present? + 'missing commit' + elsif excluded_repository? + 'excluded repository' + elsif skipped? + 'skipped through commit message' + elsif disabled_in_settings? + request.pull_request? ? 'pull requests disabled' : 'pushes disabled' + elsif github_pages? + 'github pages branch' + elsif !branch_approved? || !branch_accepted? + 'branch not included or excluded' + elsif !config_accepted? + '.travis.yml is missing and builds without .travis.yml are disabled' + elsif repository.private? + 'private repository' + elsif !request.creates_jobs? + 'matrix created no jobs' + elsif compare_url_too_long? + 'compare URL too long; branch/tag names may be too long' + elsif request.config.blank? + 'config is missing or contains YAML syntax error' + end + end + + private + + def skipped? + Travis::CommitCommand.new(commit.message).skip? + end + + def github_pages_explicitly_enabled? + request.config && + request.config['branches'] && + request.config['branches'].is_a?(Hash) && + request.config['branches']['only'] && + Array(request.config['branches']['only']).grep(/gh[-_]pages/i) + end + + def github_pages? + commit.branch =~ /gh[-_]pages/i + end + + def excluded_repository? + exclude_rules.any? { |rule| repository.slug =~ rule } + end + + def included_repository? + include_rules.any? { |rule| repository.slug =~ rule } + end + + def include_rules + Travis.config.repository_filter.include.map { |rule| rule.is_a?(Regexp) ? rule : Regexp.new(rule) } + end + + def exclude_rules + Travis.config.repository_filter.exclude.map { |rule| rule.is_a?(Regexp) ? rule : Regexp.new(rule) } + end + + def branch_approved? + branches.included?(commit.branch) && !branches.excluded?(commit.branch) + end + + def branches + @branches ||= Branches.new(request) + end + end +end diff --git a/vendor/travis-core/lib/travis/model/request/branches.rb b/vendor/travis-core/lib/travis/model/request/branches.rb new file mode 100644 index 00000000..87e74de2 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/request/branches.rb @@ -0,0 +1,57 @@ +class Request + + # Logic that figures out whether a branch is in- or excluded (white- or + # blacklisted) by the configuration (`.travis.yml`) + class Branches + attr_reader :request, :commit + + def initialize(request) + @request = request + @commit = request.commit + end + + def included?(branch) + !included || includes?(included, branch) + end + + def excluded?(branch) + excluded && includes?(excluded, branch) + end + + private + + def included + config['only'] + end + + def excluded + config['except'] + end + + def includes?(branches, branch) + branches.any? { |pattern| matches?(pattern, branch) } + end + + def matches?(pattern, branch) + pattern = pattern =~ %r{^/(.*)/$} ? Regexp.new($1) : pattern + pattern === branch + end + + def config + @config ||= case branches = request.config.try(:[], 'branches') + when Array + { :only => branches } + when String + { :only => split(branches) } + when Hash + branches.each_with_object({}) { |(k, v), result| result[k] = split(v) } + else + {} + end + end + + def split(branches) + branches.is_a?(String) ? branches.split(',').map(&:strip) : branches + end + end +end diff --git a/vendor/travis-core/lib/travis/model/request/pull_request.rb b/vendor/travis-core/lib/travis/model/request/pull_request.rb new file mode 100644 index 00000000..2e60d600 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/request/pull_request.rb @@ -0,0 +1,39 @@ +class Request + class PullRequest + Try = Struct.new(:value) do + def method_missing(*args, &block) + Try.new(value.nil? ? nil : value.public_send(*args, &block)) + end + end + + attr_reader :payload + + def initialize(payload) + @payload = Try.new(Hashr.new(payload || {})) + end + + def title + payload.title.value + end + + def number + payload.number.value + end + + def head_repo + payload.head.repo.full_name.value + end + + def base_repo + payload.base.repo.full_name.value + end + + def head_branch + payload.head.ref.value + end + + def base_branch + payload.base.ref.value + end + end +end diff --git a/vendor/travis-core/lib/travis/model/request/states.rb b/vendor/travis-core/lib/travis/model/request/states.rb new file mode 100644 index 00000000..344d9ce0 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/request/states.rb @@ -0,0 +1,119 @@ +require 'active_support/concern' +require 'simple_states' + +class Request + module States + extend ActiveSupport::Concern + include Travis::Event + + included do + include SimpleStates + + states :created, :started, :finished + event :start, :to => :started, :after => :configure + event :configure, :to => :configured, :after => :finish + event :finish, :to => :finished + event :all, :after => :notify + end + + def configure + if !accepted? + Travis.logger.warn("[request:configure] Request not accepted: event_type=#{event_type.inspect} commit=#{commit.try(:commit).inspect} message=#{approval.message.inspect}") + else + self.config = fetch_config.merge(config || {}) + + if branch_accepted? && config_accepted? + Travis.logger.info("[request:configure] Request successfully configured commit=#{commit.commit.inspect}") + else + self.config = nil + Travis.logger.warn("[request:configure] Request not accepted: event_type=#{event_type.inspect} commit=#{commit.try(:commit).inspect} message=#{approval.message.inspect}") + end + end + save! + end + + def finish + if config.blank? + Travis.logger.warn("[request:finish] Request not creating a build: config is blank or contains YAML syntax error, config=#{config.inspect} commit=#{commit.try(:commit).inspect}") + elsif !approved? + Travis.logger.warn("[request:finish] Request not creating a build: not approved commit=#{commit.try(:commit).inspect} message=#{approval.message.inspect}") + elsif parse_error? + Travis.logger.info("[request:finish] Request created but Build and Job automatically errored due to a config parsing error. commit=#{commit.try(:commit).inspect}") + add_parse_error_build + elsif server_error? + Travis.logger.info("[request:finish] Request created but Build and Job automatically errored due to a config server error. commit=#{commit.try(:commit).inspect}") + add_server_error_build + else + add_build_and_notify + Travis.logger.info("[request:finish] Request created a build. commit=#{commit.try(:commit).inspect}") + end + self.result = approval.result + self.message = approval.message + Travis.logger.info("[request:finish] Request finished. result=#{result.inspect} message=#{message.inspect} commit=#{commit.try(:commit).inspect}") + end + + def add_build + builds.create!(:repository => repository, :commit => commit, :config => config, :owner => owner) + end + + def add_build_and_notify + add_build.tap do |build| + build.notify(:created) if Travis.config.notify_on_build_created + end + end + + protected + + delegate :accepted?, :approved?, :branch_accepted?, :config_accepted?, :to => :approval + + def approval + @approval ||= Approval.new(self) + end + + def fetch_config + Travis.run_service(:github_fetch_config, request: self) # TODO move to a service, have it pass the config to configure + end + + def add_parse_error_build + Build.transaction do + build = add_build + job = build.matrix.first + job.start!(started_at: Time.now.utc) + job.log_content = < true, :uniqueness => true + validates :public_key, :presence => true + validates :private_key, :presence => true + + before_validation :generate_keys, :on => :create + + serialize :private_key, Travis::Model::EncryptedColumn.new + + def encode(string) + Base64.encode64(encrypt(string)).strip + end + + def encrypt(string) + build_key.public_encrypt(string) + end + + def decrypt(string) + build_key.private_decrypt(string) + end + + def generate_keys! + self.public_key = self.private_key = nil + generate_keys + end + + def generate_keys + unless public_key && private_key + keys = OpenSSL::PKey::RSA.generate(Travis.config.repository.ssl_key.size) + self.public_key = keys.public_key.to_s + self.private_key = keys.to_pem + end + end + + def encoded_public_key + key = build_key.public_key + ['ssh-rsa ', "\0\0\0\assh-rsa#{sized_bytes(key.e)}#{sized_bytes(key.n)}"].pack('a*m').gsub("\n", '') + end + + def encoded_private_key + [private_key].pack('m').strip + end + + def secure + Travis::SecureConfig.new(self) + end + + private + + def build_key + @build_key ||= OpenSSL::PKey::RSA.new(private_key) + end + + def sized_bytes(value) + bytes = to_byte_array(value.to_i) + [bytes.size, *bytes].pack('NC*') + end + + def to_byte_array(num, *significant) + return significant if num.between?(-1, 0) and significant[0][7] == num[7] + to_byte_array(*num.divmod(256)) + significant + end +end diff --git a/vendor/travis-core/lib/travis/model/token.rb b/vendor/travis-core/lib/travis/model/token.rb new file mode 100644 index 00000000..384e1901 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/token.rb @@ -0,0 +1,25 @@ +require 'securerandom' +require 'travis/model' + +# Tokens used for authenticating requests from Github. +# +# Users can have one or many tokens (even though the current UI only allows for +# one) that they need use on their service hooks. This gives us some security +# that people cannot throw random repositories at Travis CI. +class Token < Travis::Model + belongs_to :user + + validate :token, :presence => true + + before_validation :generate_token, on: :create + + attr_accessible # nothing is changable + + serialize :token, Travis::Model::EncryptedColumn.new(disable: true) + + protected + + def generate_token + self.token = SecureRandom.base64(15).tr('+/=lIO0', 'pqrsxyz') + end +end diff --git a/vendor/travis-core/lib/travis/model/url.rb b/vendor/travis-core/lib/travis/model/url.rb new file mode 100644 index 00000000..6d29dfad --- /dev/null +++ b/vendor/travis-core/lib/travis/model/url.rb @@ -0,0 +1,23 @@ +require 'digest/sha1' +require 'travis/model' + +class Url < Travis::Model + validates :url, :presence => true, :uniqueness => true + validates :code, :presence => true, :uniqueness => true + + before_validation :set_code, :on => :create + + def self.shorten(url) + find_or_create_by_url(url) + end + + def short_url + ["http://#{Travis.config.shorten_host}", code].join('/') + end + + private + + def set_code + self.code = Digest::SHA1.hexdigest(url)[0..9] + end +end diff --git a/vendor/travis-core/lib/travis/model/user.rb b/vendor/travis-core/lib/travis/model/user.rb new file mode 100644 index 00000000..f356632b --- /dev/null +++ b/vendor/travis-core/lib/travis/model/user.rb @@ -0,0 +1,150 @@ +require 'gh' +require 'travis/model' + +class User < Travis::Model + require 'travis/model/user/oauth' + + has_many :tokens, dependent: :destroy + has_many :memberships, dependent: :destroy + has_many :organizations, through: :memberships + has_many :permissions, dependent: :destroy + has_many :repositories, through: :permissions + has_many :emails, dependent: :destroy + + attr_accessible :name, :login, :email, :github_id, :github_oauth_token, :gravatar_id, :locale, :education, :first_logged_in_at + + before_create :set_as_recent + after_create :create_a_token + after_commit :sync, on: :create + + serialize :github_scopes + before_save :track_github_scopes + + serialize :github_oauth_token, Travis::Model::EncryptedColumn.new + + class << self + def with_permissions(permissions) + where(:permissions => permissions).includes(:permissions) + end + + def authenticate_by(options) + options = options.symbolize_keys + + if user = User.find_by_login(options[:login]) + user if user.tokens.any? { |t| t.token == options[:token] } + end + end + + def find_or_create_for_oauth(payload) + Oauth.find_or_create_by(payload) + end + + def with_github_token + where("github_oauth_token IS NOT NULL and github_oauth_token != ''") + end + + def with_email(email_address) + Email.where(email: email_address).first.try(:user) + end + end + + def to_json + keys = %w/id login email name locale github_id gravatar_id is_syncing synced_at updated_at created_at/ + { 'user' => attributes.slice(*keys) }.to_json + end + + def permission?(roles, options = {}) + roles, options = nil, roles if roles.is_a?(Hash) + scope = permissions.where(options) + scope = scope.by_roles(roles) if roles + scope.any? + end + + def first_sync? + synced_at.nil? + end + + def sync + Travis.run_service(:sync_user, self) # TODO remove once apps use the service + end + + def syncing? + is_syncing? + end + + def service_hook(options = {}) + service_hooks(options).first + end + + def service_hooks(options = {}) + hooks = repositories + unless options[:all] + hooks = hooks.administratable + end + hooks = hooks.includes(:permissions). + select('repositories.*, permissions.admin as admin, permissions.push as push'). + order('owner_name, name') + # TODO remove owner_name/name once we're on api everywhere + if options.key?(:id) + hooks = hooks.where(options.slice(:id)) + elsif options.key?(:owner_name) || options.key?(:name) + hooks = hooks.where(options.slice(:id, :owner_name, :name)) + end + hooks + end + + def organization_ids + @organization_ids ||= memberships.map(&:organization_id) + end + + def repository_ids + @repository_ids ||= permissions.map(&:repository_id) + end + + def recently_signed_up? + @recently_signed_up || false + end + + def profile_image_hash + # TODO: + # If Github always sends valid gravatar_id in oauth payload (need to check that) + # then these fallbacks (email hash and zeros) are superfluous and can be removed. + gravatar_id.presence || (email? && Digest::MD5.hexdigest(email)) || '0' * 32 + end + + def github_scopes + return [] unless github_oauth_token + read_attribute(:github_scopes) || [] + end + + def correct_scopes? + missing = Oauth.wanted_scopes - github_scopes + missing.empty? + end + + def avatar_url + "https://0.gravatar.com/avatar/#{profile_image_hash}" + end + + def inspect + if github_oauth_token + super.gsub(github_oauth_token, '[REDACTED]') + else + super + end + end + + protected + + def track_github_scopes + self.github_scopes = Travis::Github.scopes_for(self) if github_oauth_token_changed? or github_scopes.blank? + end + + def set_as_recent + @recently_signed_up = true + end + + def create_a_token + self.tokens.create! + end +end diff --git a/vendor/travis-core/lib/travis/model/user/oauth.rb b/vendor/travis-core/lib/travis/model/user/oauth.rb new file mode 100644 index 00000000..17ccf6d9 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/user/oauth.rb @@ -0,0 +1,28 @@ +class User + module Oauth + class << self + def wanted_scopes + return [] unless Travis.config.oauth2.scope + @wanted_scopes ||= Travis.config.oauth2.scope.split(',').sort + end + + def find_or_create_by(payload) + attrs = attributes_from(payload) + user = User.find_by_github_id(attrs['github_id']) + user ? user.update_attributes(attrs) : user = User.create!(attrs) + user + end + + def attributes_from(payload) + { + 'name' => payload['info']['name'], + 'email' => payload['info']['email'], + 'login' => payload['info']['nickname'], + 'github_id' => payload['uid'].to_i, + 'github_oauth_token' => payload['credentials']['token'], + 'gravatar_id' => payload['extra']['raw_info']['gravatar_id'] + } + end + end + end +end diff --git a/vendor/travis-core/lib/travis/model/user/renaming.rb b/vendor/travis-core/lib/travis/model/user/renaming.rb new file mode 100644 index 00000000..c4d8d680 --- /dev/null +++ b/vendor/travis-core/lib/travis/model/user/renaming.rb @@ -0,0 +1,21 @@ +module User::Renaming + def nullify_logins(github_id, login) + users = User.where(["github_id <> ? AND login = ?", github_id, login]) + if users.exists? + Travis.logger.info("About to nullify login (#{login}) for users: #{users.map(&:id).join(', ')}") + users.update_all(login: nil) + end + + organizations = Organization.where(["login = ?", login]) + if organizations.exists? + Travis.logger.info("About to nullify login (#{login}) for organizations: #{organizations.map(&:id).join(', ')}") + organizations.update_all(login: nil) + end + end + + def rename_repos_owner(old_login, new_login) + return if old_login == new_login + Repository.where(owner_name: old_login). + update_all(owner_name: new_login) + end +end diff --git a/vendor/travis-core/lib/travis/notification.rb b/vendor/travis-core/lib/travis/notification.rb new file mode 100644 index 00000000..e5ed94eb --- /dev/null +++ b/vendor/travis-core/lib/travis/notification.rb @@ -0,0 +1,24 @@ +require 'active_support/core_ext/hash/reverse_merge' + +module Travis + module Notification + require 'travis/notification/instrument' + require 'travis/notification/publisher' + + class << self + attr_accessor :publishers + + def setup(options = { instrumentation: true }) + Travis::Instrumentation.setup if options[:instrumentation] && Travis.config.metrics.reporter + publishers << Publisher::Log.new + publishers << Publisher::Redis.new if Travis::Features.feature_active?(:notifications_publisher_redis) + end + + def publish(event) + publishers.each { |publisher| publisher.publish(event) } + end + end + + self.publishers ||= [] + end +end diff --git a/vendor/travis-core/lib/travis/notification/instrument.rb b/vendor/travis-core/lib/travis/notification/instrument.rb new file mode 100644 index 00000000..72160d34 --- /dev/null +++ b/vendor/travis-core/lib/travis/notification/instrument.rb @@ -0,0 +1,58 @@ +require 'active_support/core_ext/object/try' +require 'core_ext/hash/compact' + +module Travis + module Notification + class Instrument + require 'travis/notification/instrument/event_handler' + require 'travis/notification/instrument/task' + + class << self + def attach_to(const) + statuses = %w(received completed failed) + instrumented_methods(const).product(statuses).each do |method, status| + ActiveSupport::Notifications.subscribe(/^#{const.instrumentation_key}(\..+)?.#{method}:#{status}/) do |message, args| + publish(message, method, status, args) + end + end + end + + def instrumented_methods(const) + consts = ancestors.select { |const| (const.name || '')[0..5] == 'Travis' } + methods = consts.map { |const| const.public_instance_methods(false) }.flatten.uniq + methods = methods.map { |method| method.to_s =~ /^(.*)_(received|completed|failed)$/ && $1 } + methods.compact.uniq + end + + def publish(event, method, status, payload) + instrument = new(event, method, status, payload) + callback = :"#{method}_#{status}" + instrument.respond_to?(callback) ? instrument.send(callback) : instrument.publish + end + end + + attr_reader :target, :method, :status, :result, :exception, :meta + + def initialize(event, method, status, payload) + @method, @status = method, status + @target, @result, @exception = payload.values_at(:target, :result, :exception) + started_at, finished_at = payload.values_at(:started_at, :finished_at) + @meta = { + uuid: Travis.uuid, + event: event, + started_at: started_at, + finished_at: finished_at, + duration: finished_at ? finished_at - started_at : nil, + }.compact + end + + def publish(data = {}) + message = "#{target.class.name}##{method}:#{status} #{data.delete(:msg)}".strip + payload = meta.merge(message: message, data: data) + payload[:result] = data.delete(:result) if data.key?(:result) + payload[:exception] = exception if exception + Notification.publish(payload) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/notification/instrument/event_handler.rb b/vendor/travis-core/lib/travis/notification/instrument/event_handler.rb new file mode 100644 index 00000000..512964b0 --- /dev/null +++ b/vendor/travis-core/lib/travis/notification/instrument/event_handler.rb @@ -0,0 +1,35 @@ +require 'travis/notification/instrument' + +module Travis + module Notification + class Instrument + class EventHandler < Instrument + attr_reader :handler, :object, :args, :result + + def initialize(message, method, status, payload) + @handler, @args, @result = payload.values_at(:target, :args, :result) + @object = handler.object + super + end + + def notify_completed + publish + end + + def publish(event = {}) + event = event.reverse_merge( + :msg => "(#{handler.event}) for #<#{object.class.name} id=#{object.id}>", + :object_type => object.class.name, + :object_id => object.id, + :event => handler.event + ) + + event[:payload] = handler.payload + event[:request_id] = object.request_id if object.respond_to?(:request_id) + event[:repository] = object.repository.slug if object.respond_to?(:repository) + super(event) + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/notification/instrument/task.rb b/vendor/travis-core/lib/travis/notification/instrument/task.rb new file mode 100644 index 00000000..aa769b09 --- /dev/null +++ b/vendor/travis-core/lib/travis/notification/instrument/task.rb @@ -0,0 +1,34 @@ +module Travis + module Notification + class Instrument + class Task < Instrument + attr_reader :task, :payload + + def initialize(message, method, status, payload) + @task = payload[:target] + @payload = task.payload + super + end + + def run_completed + publish + end + + def publish(event = {}) + event[:msg] = "#{event[:msg]} #{queue_info}" if Travis::Async.enabled? && Travis::Task.run_local? + super(event.merge(:payload => self.payload)) + end + + private + + def queue_info + "(queue size: #{queue.items.size})" if queue + end + + def queue + Travis::Async::Threaded.queues[task.class.name] + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/notification/publisher.rb b/vendor/travis-core/lib/travis/notification/publisher.rb new file mode 100644 index 00000000..4464ca9e --- /dev/null +++ b/vendor/travis-core/lib/travis/notification/publisher.rb @@ -0,0 +1,9 @@ +module Travis + module Notification + module Publisher + require 'travis/notification/publisher/log' + require 'travis/notification/publisher/redis' + require 'travis/notification/publisher/memory' + end + end +end diff --git a/vendor/travis-core/lib/travis/notification/publisher/log.rb b/vendor/travis-core/lib/travis/notification/publisher/log.rb new file mode 100644 index 00000000..a011d344 --- /dev/null +++ b/vendor/travis-core/lib/travis/notification/publisher/log.rb @@ -0,0 +1,42 @@ +module Travis + module Notification + module Publisher + class Log + def publish(event) + return if ignore?(event) + + level = event.key?(:exception) ? :error : :info + message = event[:message] + message = "#{message} (#{'%.5f' % event[:duration]}s)" if event[:duration] + log(level, message) + + if level == :error || Travis.logger.level == ::Logger::DEBUG + event.each do |key, value| + next if key == :message + level = event.key?(:exception) ? :error : :debug + log(level, " #{key}: #{value.inspect}") + end + end + end + + def log(level, msg) + Travis.logger.send(level, msg) + end + + def ignore?(event) + event_received?(event) && !sync_or_request_handler?(event) + end + + def event_received?(event) + event[:event].end_with?("received") + end + + # TODO why do ignore these again? + def sync_or_request_handler?(event) + msg = event[:message] + msg && msg =~ /Travis::Hub::Handler::(Sync|Request)#handle/ + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/notification/publisher/memory.rb b/vendor/travis-core/lib/travis/notification/publisher/memory.rb new file mode 100644 index 00000000..9cb1834c --- /dev/null +++ b/vendor/travis-core/lib/travis/notification/publisher/memory.rb @@ -0,0 +1,15 @@ +module Travis + module Notification + module Publisher + class Memory + def publish(event) + events << event + end + + def events + @events ||= [] + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/notification/publisher/redis.rb b/vendor/travis-core/lib/travis/notification/publisher/redis.rb new file mode 100644 index 00000000..1f53bb2e --- /dev/null +++ b/vendor/travis-core/lib/travis/notification/publisher/redis.rb @@ -0,0 +1,50 @@ +require 'redis' +require 'multi_json' + +module Travis + module Notification + module Publisher + class Redis + extend Exceptions::Handling + attr_accessor :redis, :ttl + + def initialize(options = {}) + @redis = options[:redis] || ::Redis.connect(url: Travis.config.redis.url) + @ttl = options[:ttl] || 10 + end + + def publish(event) + event = filter(event) + payload = MultiJson.encode(event) + # list = 'events:' << event[:uuid] + list = 'events' + + redis.publish list, payload + + # redis.pipelined do + # redis.publish list, payload + # redis.multi do + # redis.persist(list) + # redis.rpush(list, payload) + # redis.expire(list, ttl) + # end + # end + end + rescues :publish, from: Exception + + def filter(value) + case value + when Array + value.map { |value| filter(value) } + when Hash + value.inject({}) { |hash, (key, value)| hash.merge(key => filter(value)) } + when String, Numeric, TrueClass, FalseClass, NilClass + value + else + nil + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/overwritable_method_definitions.rb b/vendor/travis-core/lib/travis/overwritable_method_definitions.rb new file mode 100644 index 00000000..9225e9ed --- /dev/null +++ b/vendor/travis-core/lib/travis/overwritable_method_definitions.rb @@ -0,0 +1,39 @@ +module Travis + # OverwritableMethodDefinitions module allows to easily define methods which will be + # overwritable in the same class. For example, given such a class: + # + # class Foo + # include Travis::OverwritableMethodDefinitions + # + # define_overwritable_method :foo do + # 'foo' + # end + # + # def foo + # super + '!' + # end + # end + # + # Foo.new.foo #=> foo! + module OverwritableMethodDefinitions + def self.included(base) + base.extend(ClassMethods) + base.initialize_overwritable_methods_module + end + + module ClassMethods + def inherited(child) + child.initialize_overwritable_methods_module + end + + def initialize_overwritable_methods_module + @generated_overwritable_methods = Module.new + include @generated_overwritable_methods + end + + def define_overwritable_method(*args, &block) + @generated_overwritable_methods.send :define_method, *args, &block + end + end + end +end diff --git a/vendor/travis-core/lib/travis/redis_pool.rb b/vendor/travis-core/lib/travis/redis_pool.rb new file mode 100644 index 00000000..2db49ff9 --- /dev/null +++ b/vendor/travis-core/lib/travis/redis_pool.rb @@ -0,0 +1,32 @@ +require 'connection_pool' +require 'redis' +require 'metriks' + +module Travis + class RedisPool + attr_reader :pool + + def initialize(options = {}) + pool_options = options.delete(:pool) || {} + pool_options[:size] ||= 10 + pool_options[:timeout] ||= 10 + @pool = ConnectionPool.new(pool_options) do + ::Redis.new(options) + end + end + + def method_missing(name, *args, &block) + timer = Metriks.timer('redis.pool.wait').time + @pool.with do |redis| + timer.stop + if redis.respond_to?(name) + Metriks.timer("redis.operations").time do + redis.send(name, *args, &block) + end + else + super + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/requests.rb b/vendor/travis-core/lib/travis/requests.rb new file mode 100644 index 00000000..2ff1043b --- /dev/null +++ b/vendor/travis-core/lib/travis/requests.rb @@ -0,0 +1,6 @@ +module Travis + module Requests + require 'travis/requests/services' + end +end + diff --git a/vendor/travis-core/lib/travis/requests/services.rb b/vendor/travis-core/lib/travis/requests/services.rb new file mode 100644 index 00000000..8ea5f674 --- /dev/null +++ b/vendor/travis-core/lib/travis/requests/services.rb @@ -0,0 +1,13 @@ +module Travis + module Requests + module Services + require 'travis/requests/services/receive' + + class << self + def register + constants(false).each { |name| const_get(name) } + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/requests/services/receive.rb b/vendor/travis-core/lib/travis/requests/services/receive.rb new file mode 100644 index 00000000..b0f55dab --- /dev/null +++ b/vendor/travis-core/lib/travis/requests/services/receive.rb @@ -0,0 +1,207 @@ +require 'gh' +require 'travis/services/base' +require 'travis/model/request/approval' +require 'travis/notification/instrument' +require 'travis/advisory_locks' +require 'travis/travis_yml_stats' + +module Travis + module Requests + module Services + class Receive < Travis::Services::Base + require 'travis/requests/services/receive/api' + require 'travis/requests/services/receive/cron' + require 'travis/requests/services/receive/pull_request' + require 'travis/requests/services/receive/push' + + extend Travis::Instrumentation + + class PayloadValidationError < StandardError; end + + register :receive_request + + class << self + def payload_for(type, data) + data = GH.load(data) + const_get(type.camelize).new(data) + end + end + + attr_reader :request, :accepted + + def run + with_transactional_advisory_lock do + if accept? + create && start + store_config_info if verify + else + rejected + end + request + end + rescue GH::Error => e + Travis.logger.error "payload for #{slug} could not be received as GitHub returned a #{e.info[:response_status]}: #{e.info}, github-guid=#{github_guid}, event-type=#{event_type}" + end + instrument :run + + def accept? + payload.validate! + validate! + @accepted = payload.accept? + rescue PayloadValidationError => e + Travis.logger.error "#{e.message}, github-guid=#{github_guid}, event-type=#{event_type}" + @accepted = false + end + + private + + def with_transactional_advisory_lock + return yield unless payload.repository + result = nil + Travis::AdvisoryLocks.exclusive("receive-repo:#{payload.repository[:github_id]}", 300) do + ActiveRecord::Base.connection.transaction do + result = yield + end + end + result + rescue => e + ActiveRecord::Base.connection.rollback_db_transaction + raise + end + + def validate! + repo_not_found! unless repo + verify_owner + end + + def verify_owner + owner = owner_by_payload + owner_not_found! unless owner + update_owner(owner) if owner.id != repo.owner_id && !api_request? + end + + def create + @request = repo.requests.create!(payload.request.merge( + :payload => params[:payload], + :event_type => event_type, + :state => :created, + :commit => commit, + :owner => repo.owner, + :token => params[:token] + )) + end + + def start + request.start! + end + + def verify + request.reload + if request.builds.count == 0 + approval = Request::Approval.new(request) + Travis.logger.warn("[request:receive] Request #{request.id} commit=#{request.commit.try(:commit).inspect} didn't create any builds: #{approval.result}/#{approval.message}") + false + elsif !request.creates_jobs? + approval = Request::Approval.new(request) + Travis.logger.warn("[request:receive] Request #{request.id} commit=#{request.commit.try(:commit).inspect} didn't create any job: #{approval.result}/#{approval.message}") + false + else + Travis.logger.info("[request:receive] Request #{request.id} commit=#{request.commit.try(:commit).inspect} created #{request.builds.count} builds") + true + end + end + + def update_owner(owner) + repo.update_attributes!(owner: owner, owner_name: owner.login) + owner_updated + end + + def owner_by_payload + if id = payload.repository[:owner_id] + lookup_owner(payload.repository[:owner_type], id: id) + elsif github_id = payload.repository[:owner_github_id] + lookup_owner(payload.repository[:owner_type], github_id: github_id) + elsif login = payload.repository[:owner_name] + lookup_owner(%w(User Organization), login: login) + end + end + + def lookup_owner(types, attrs) + Array(types).map(&:constantize).each do |type| + owner = type.where(attrs).first + return owner if owner + end + nil + end + + def repo_not_found! + Travis::Metrics.meter('request.receive.repository_not_found') + raise PayloadValidationError, "Repository not found: #{payload.repository.slice(:id, :github_id, :owner_name, :name)}" + end + + def owner_not_found! + Travis::Metrics.meter('request.receive.repository_owner_not_found') + raise PayloadValidationError, "The given repository owner could not be found: #{payload.repository.slice(:owner_id, :owner_github_id, :owner_type, :owner_name).inspect}" + end + + def owner_updated + Travis::Metrics.meter('request.receive.update_owner') + Travis.logger.warn("[request:receive] Repository owner updated for #{slug}: #{repo.owner_type}##{repo.owner_id} (#{repo.owner_name})") + end + + def rejected + commit = payload.commit['commit'].inspect if payload.commit rescue nil + Travis.logger.info("[request:receive] Github event rejected: event_type=#{event_type.inspect} repo=\"#{slug}\" commit=#{commit} action=#{payload.action.inspect}") + end + + def payload + @payload ||= self.class.payload_for(event_type, params[:payload]) + end + + def github_guid + params[:github_guid] + end + + def event_type + @event_type ||= (params[:event_type] || 'push').gsub('-', '_') + end + + def api_request? + event_type == 'api' + end + + def repo + @repo ||= run_service(:find_repo, payload.repository) + end + + def slug + payload.repository ? payload.repository.values_at(:owner_name, :name).join('/') : '?' + end + + def commit + @commit ||= repo.commits.create!(payload.commit) if payload.commit + end + + def store_config_info + Travis::TravisYmlStats.store_stats(request) + rescue => e + Travis.logger.warn("[request:receive] Couldn't store .travis.yml stats: #{e.message}") + Travis::Exceptions.handle(e) + end + + class Instrument < Notification::Instrument + def run_completed + params = target.params + publish( + :msg => "type=#{params[:event_type].inspect}", + :type => params[:event_type], + :accept? => target.accepted, + :payload => params[:payload] + ) + end + end + Instrument.attach_to(self) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/requests/services/receive/api.rb b/vendor/travis-core/lib/travis/requests/services/receive/api.rb new file mode 100644 index 00000000..1711b622 --- /dev/null +++ b/vendor/travis-core/lib/travis/requests/services/receive/api.rb @@ -0,0 +1,106 @@ +module Travis + module Requests + module Services + class Receive < Travis::Services::Base + class Api + VALIDATION_ERRORS = { + repo: 'Repository data is not present in payload', + } + attr_reader :event + + def initialize(event) + @event = event + end + + def accept? + true + end + + def validate! + error(:repo) if repo_data.nil? + end + + def action + nil + end + + def repository + @repository ||= { + owner_id: repo_data['owner_id'], + owner_type: repo_data['owner_type'], + owner_name: repo_data['owner_name'], + name: repo_data['name'] + } + end + + def request + @request ||= { + :config => event['config'] + } + end + + def commit + @commit ||= { + commit: commit_data['sha'], + message: message, + branch: branch, + ref: nil, # TODO verify that we do not need this + committed_at: commit_data['commit']['committer']['date'], # TODO in case of API requests we'd want to display the timestamp of the incoming request + committer_name: commit_data['commit']['committer']['name'], + committer_email: commit_data['commit']['committer']['email'], + author_name: commit_data['commit']['author']['name'], + author_email: commit_data['commit']['author']['email'], + compare_url: commit_data['_links']['self']['href'] + } + end + + private + + def gh + Github.authenticated(user) + end + + def user + @user ||= User.find(event['user']['id']) + end + + def repo_data + event['repository'] || {} + end + + def message + event['message'] || commit_data['commit']['message'] + end + + def slug + repo_data.values_at('owner_name', 'name').join('/') + end + + def branch + event['branch'] || 'master' + end + + def repo_github_id + repo.try(:github_id) || raise(ActiveRecord::RecordNotFound) + end + + def repo + if id = repo_data['id'] + Repository.find(id) + else + Repository.by_slug(slug).first + end + end + + def commit_data + @commit_data ||= gh["repos/#{slug}/commits?sha=#{branch}&per_page=1"].first # TODO I guess Api would protect against GH errors? + end + + def error(type) + raise PayloadValidationError, VALIDATION_ERRORS[type] + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/requests/services/receive/cron.rb b/vendor/travis-core/lib/travis/requests/services/receive/cron.rb new file mode 100644 index 00000000..dfc3c743 --- /dev/null +++ b/vendor/travis-core/lib/travis/requests/services/receive/cron.rb @@ -0,0 +1,10 @@ +module Travis + module Requests + module Services + class Receive < Travis::Services::Base + class Cron < Api + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/requests/services/receive/pull_request.rb b/vendor/travis-core/lib/travis/requests/services/receive/pull_request.rb new file mode 100644 index 00000000..d09c5c0c --- /dev/null +++ b/vendor/travis-core/lib/travis/requests/services/receive/pull_request.rb @@ -0,0 +1,117 @@ +module Travis + module Requests + module Services + class Receive < Travis::Services::Base + class PullRequest + attr_reader :event + + def initialize(event) + @event = event + end + + def accept? + return false if disabled? || closed? + case action + when :opened, :reopened then !!merge_commit + when :synchronize then head_change? + else false + end + end + + def validate! + if event['repository'].nil? + raise PayloadValidationError, "Repository data is not present in payload" + end + end + + def disabled? + Travis::Features.feature_deactivated?(:pull_requests) + end + + def closed? + pull_request['state'] == 'closed' + end + + def head_change? + head_commit && ::Request.last_by_head_commit(head_commit['sha']).nil? + end + + def repository + @repository ||= repo && { + name: repo['name'], + description: repo['description'], + url: repo['_links']['html']['href'], + owner_github_id: repo['owner']['id'], + owner_type: repo['owner']['type'], + owner_name: repo['owner']['login'], + owner_email: repo['owner']['email'], + private: !!repo['private'], + github_id: repo['id'] + } + end + + def request + @request ||= { + comments_url: comments_url, + base_commit: base_commit['sha'], + head_commit: head_commit['sha'] + } + end + + def commit + @commit ||= if merge_commit + { + commit: merge_commit['sha'], + message: head_commit['message'], + branch: pull_request['base']['ref'], + ref: merge_commit['ref'], + committed_at: committer['date'], + committer_name: committer['name'], + committer_email: committer['email'], + author_name: author['name'], + author_email: author['email'], + compare_url: pull_request['_links']['html']['href'] + } + end + end + + def pull_request + event['pull_request'] || {} + end + + def action + event['action'].try(:to_sym) + end + + def comments_url + pull_request.fetch('_links', {}).fetch('comments', {}).fetch('href', '') + end + + def base_commit + pull_request['base_commit'] || { 'sha' => '' } + end + + def head_commit + pull_request['head_commit'] + end + + def merge_commit + pull_request['merge_commit'] + end + + def repo + event['repository'] + end + + def committer + head_commit.fetch('committer', {}) + end + + def author + head_commit.fetch('author', {}) + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/requests/services/receive/push.rb b/vendor/travis-core/lib/travis/requests/services/receive/push.rb new file mode 100644 index 00000000..bab6b2df --- /dev/null +++ b/vendor/travis-core/lib/travis/requests/services/receive/push.rb @@ -0,0 +1,84 @@ +module Travis + module Requests + module Services + class Receive < Travis::Services::Base + class Push + attr_reader :event + + def initialize(event) + @event = event + end + + def accept? + true + end + + def validate! + if event['repository'].nil? + raise PayloadValidationError, "Repository data is not present in payload" + end + end + + def action + nil + end + + def repository + @repository ||= repo_data && { + name: repo_data['name'], + description: repo_data['description'], + url: repo_data['_links']['html']['href'], + owner_github_id: repo_data['owner']['id'], + owner_type: repo_data['owner']['type'], + owner_name: repo_data['owner']['login'], + owner_email: repo_data['owner']['email'], + private: !!repo_data['private'], + github_id: repo_data['id'] + } + end + + def request + @request ||= {} + end + + def commit + @commit ||= commit_data && { + commit: commit_data['sha'], + message: commit_data['message'], + branch: event['ref'].split('/', 3).last, + ref: event['ref'], + committed_at: commit_data['date'], + committer_name: commit_data['committer']['name'], + committer_email: commit_data['committer']['email'], + author_name: commit_data['author']['name'], + author_email: commit_data['author']['email'], + compare_url: event['compare'] + } + end + + private + + def repo_data + event['repository'] + end + + def commit_data + last_unskipped_commit || commits.last || event['head_commit'] + end + + def last_unskipped_commit + commits.reverse.find { |commit| !skip_commit?(commit) } + end + + def commits + event['commits'] || [] + end + + def skip_commit?(commit) + Travis::CommitCommand.new(commit['message']).skip? + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/secure_config.rb b/vendor/travis-core/lib/travis/secure_config.rb new file mode 100644 index 00000000..232b21ce --- /dev/null +++ b/vendor/travis-core/lib/travis/secure_config.rb @@ -0,0 +1,71 @@ +require 'base64' + +module Travis + # Decrypts a single configuration value from a configuration file using the + # repository's SSL key. + # + # This is used so people can add encrypted sensitive data to their + # `.travis.yml` file. + class SecureConfig < Struct.new(:key) + def self.decrypt(config, key) + self.new(key).decrypt(config) + end + + def self.encrypt(config, key) + self.new(key).encrypt(config) + end + + def decrypt(config) + return config if config.is_a?(String) + + config.inject(config.class.new) do |result, element| + key, element = element if result.is_a?(Hash) + value = process(result, key, decrypt_element(key, element)) + block_given? ? yield(value) : value + end + end + + def encrypt(config) + { 'secure' => key.encode(config) } + end + + private + + def decrypt_element(key, element) + if element.is_a?(Array) || element.is_a?(Hash) + decrypt(element) + elsif secure_key?(key) && element + decrypt_value(element) + else + element + end + end + + def process(result, key, value) + if result.is_a?(Array) + result << value + elsif result.is_a?(Hash) && !secure_key?(key) + result[key] = value + result + else + value + end + end + + def decrypt_value(value) + decoded = Base64.decode64(value) + # TODO should probably be checked earlier + if key.respond_to?(:decrypt) + key.decrypt(decoded) + else + Travis.logger.error("Can not decrypt secure config value: #{value.inspect[0..10]} using key: #{key.inspect[0..10]}") + end + rescue OpenSSL::PKey::RSAError => e + value + end + + def secure_key?(key) + key && (key == :secure || key == 'secure') + end + end +end diff --git a/vendor/travis-core/lib/travis/services.rb b/vendor/travis-core/lib/travis/services.rb new file mode 100644 index 00000000..b555482e --- /dev/null +++ b/vendor/travis-core/lib/travis/services.rb @@ -0,0 +1,74 @@ +module Travis + module Services + module Registry + def add(key, const = nil) + if key.is_a?(Hash) + key.each { |key, const| add(key, const) } + else + services[key.to_sym] = const + end + end + + def [](key) + services[key.to_sym] || raise("can not use unregistered service #{key}. known services are: #{services.keys.inspect}") + end + + private + + def services + @services ||= {} + end + end + + extend Registry + + class << self + def register + constants(false).each { |name| const_get(name) } + end + end + end +end + +require 'travis/services/helpers' + +module Travis + extend Services::Helpers +end + +require 'travis/services/base' +require 'travis/services/cancel_job' +require 'travis/services/cancel_build' +require 'travis/services/delete_caches' +require 'travis/services/find_admin' +require 'travis/services/find_annotations' +require 'travis/services/find_branch' +require 'travis/services/find_branches' +require 'travis/services/find_build' +require 'travis/services/find_builds' +require 'travis/services/find_daily_repos_stats' +require 'travis/services/find_daily_tests_stats' +require 'travis/services/find_caches' +require 'travis/services/find_hooks' +require 'travis/services/find_job' +require 'travis/services/find_jobs' +require 'travis/services/find_log' +require 'travis/services/find_repo' +require 'travis/services/find_repos' +require 'travis/services/find_repo_key' +require 'travis/services/find_requests' +require 'travis/services/find_request' +require 'travis/services/find_repo_settings' +require 'travis/services/find_user_accounts' +require 'travis/services/find_user_broadcasts' +require 'travis/services/find_user_permissions' +require 'travis/services/next_build_number' +require 'travis/services/regenerate_repo_key' +require 'travis/services/remove_log' +require 'travis/services/reset_model' +require 'travis/services/sync_user' +require 'travis/services/update_annotation' +require 'travis/services/update_hook' +require 'travis/services/update_job' +require 'travis/services/update_log' +require 'travis/services/update_user' diff --git a/vendor/travis-core/lib/travis/services/base.rb b/vendor/travis-core/lib/travis/services/base.rb new file mode 100644 index 00000000..0f2c21e1 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/base.rb @@ -0,0 +1,28 @@ +require 'travis/services/helpers' + +module Travis + module Services + class Base + def self.register(key) + Travis.services.add(key, self) + end + + include Helpers + + attr_reader :current_user, :params + + def initialize(*args) + @params = args.last.is_a?(Hash) ? args.pop.symbolize_keys : {} + @current_user = args.last + end + + def scope(key) + key.to_s.camelize.constantize + end + + def logger + Travis.logger + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/cancel_build.rb b/vendor/travis-core/lib/travis/services/cancel_build.rb new file mode 100644 index 00000000..26f2a968 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/cancel_build.rb @@ -0,0 +1,78 @@ +require 'travis/services/base' + +module Travis + module Services + class CancelBuild < Base + extend Travis::Instrumentation + + register :cancel_build + + attr_reader :source + + def initialize(*) + super + + @source = params.delete(:source) || 'unknown' + end + + def run + cancel if can_cancel? + end + instrument :run + + def messages + messages = [] + messages << { :notice => 'The build was successfully cancelled.' } if can_cancel? + messages << { :error => 'You are not authorized to cancel this build.' } unless authorized? + messages << { :error => "The build could not be cancelled." } unless build.cancelable? + messages + end + + def cancel + # build may have been retrieved with a :join query, so we need to reset the readonly status + build.send(:instance_variable_set, :@readonly, false) + build.cancel! + publish! + end + + def publish! + # TODO: I think that instead of keeping publish logic in both cancel build + # and cancel job services, we could call cancel_job service for each job + # in the matrix, which would put build in canceled state, even without calling + # cancel! on build explicitly. This may be a better way to handle cancelling + # build + build.matrix.each do |job| + Travis.logger.info("Publishing cancel_job message to worker.commands queue for Job##{job.id}") + publisher.publish(type: 'cancel_job', job_id: job.id, source: source) + end + + end + + def can_cancel? + authorized? && build.cancelable? + end + + def authorized? + current_user.permission?(:pull, :repository_id => build.repository_id) + end + + def build + @build ||= run_service(:find_build, params) + end + + def publisher + Travis::Amqp::FanoutPublisher.new('worker.commands') + end + + class Instrument < Notification::Instrument + def run_completed + publish( + :msg => "for (#{target.current_user.login})", + :result => result + ) + end + end + Instrument.attach_to(self) + end + end +end diff --git a/vendor/travis-core/lib/travis/services/cancel_job.rb b/vendor/travis-core/lib/travis/services/cancel_job.rb new file mode 100644 index 00000000..5be04197 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/cancel_job.rb @@ -0,0 +1,73 @@ +require 'travis/notification' +require 'travis/services/base' + +module Travis + module Services + class CancelJob < Base + extend Travis::Instrumentation + + register :cancel_job + + attr_reader :source + + def initialize(*) + super + + @source = params.delete(:source) || 'unknown' + end + + def run + cancel if can_cancel? + end + instrument :run + + def messages + messages = [] + messages << { :notice => 'The job was successfully cancelled.' } if can_cancel? + messages << { :error => 'You are not authorized to cancel this job.' } unless authorized? + messages << { :error => "The job could not be cancelled because it is currently #{job.state}." } unless job.cancelable? + messages + end + + def cancel + # job may have been retrieved with a :join query, so we need to reset the readonly status + job.send(:instance_variable_set, :@readonly, false) + publish! + job.cancel! + end + + def can_cancel? + authorized? && job.cancelable? + end + + def authorized? + current_user.permission?(:pull, :repository_id => job.repository_id) + end + + def job + @job ||= run_service(:find_job, params) + end + + def publish! + Travis.logger.info("Publishing cancel_job message to worker.commands queue for Job##{job.id}") + publisher.publish(type: 'cancel_job', job_id: job.id, source: source) + end + + private + + def publisher + Travis::Amqp::FanoutPublisher.new('worker.commands') + end + + class Instrument < Notification::Instrument + def run_completed + publish( + :msg => "for (#{target.current_user.login})", + :result => result + ) + end + end + Instrument.attach_to(self) + end + end +end diff --git a/vendor/travis-core/lib/travis/services/delete_caches.rb b/vendor/travis-core/lib/travis/services/delete_caches.rb new file mode 100644 index 00000000..fd4526dc --- /dev/null +++ b/vendor/travis-core/lib/travis/services/delete_caches.rb @@ -0,0 +1,15 @@ +require 'travis/services/base' + +module Travis + module Services + class DeleteCaches < Base + register :delete_caches + + def run + caches = run_service(:find_caches, params) + caches.each { |c| c.destroy } + caches + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_admin.rb b/vendor/travis-core/lib/travis/services/find_admin.rb new file mode 100644 index 00000000..ea4eac4f --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_admin.rb @@ -0,0 +1,94 @@ +require 'faraday/error' +require 'travis/services/base' + +# TODO extract github specific stuff to a separate service + +module Travis + module Services + class FindAdmin < Base + extend Travis::Instrumentation + include Travis::Logging + + register :find_admin + + def run + if repository + admin = candidates.first + admin || raise_admin_missing + else + error "[github-admin] repository is nil: #{params.inspect}" + raise Travis::RepositoryMissing, "no repository given" + end + end + instrument :run + + def repository + params[:repository] + end + + private + + def candidates + User.with_github_token.with_permissions(:repository_id => repository.id, :admin => true) + end + + def validate(user) + Timeout.timeout(2) do + data = Github.authenticated(user) { repository_data } + if data['permissions'] && data['permissions']['admin'] + user + else + info "[github-admin] #{user.login} no longer has admin access to #{repository.slug}" + update(user, data['permissions']) + false + end + end + rescue Timeout::Error => error + handle_error(user, error) + false + rescue GH::Error => error + handle_error(user, error) + false + end + + def handle_error(user, error) + status = error.info[:response_status] + case status + when 401 + error "[github-admin] token for #{user.login} no longer valid" + user.update_attributes!(:github_oauth_token => "") + when 404 + info "[github-admin] #{user.login} no longer has any access to #{repository.slug}" + update(user, {}) + else + error "[github-admin] error retrieving repository info for #{repository.slug} for #{user.login}: #{error.message}" + end + end + + # TODO should this not be memoized? + def repository_data + data = GH["repos/#{repository.slug}"] + info "[github-admin] could not retrieve data for #{repository.slug}" unless data + data || { 'permissions' => {} } + end + + def update(user, permissions) + user.update_attributes!(:permissions => permissions) + end + + def raise_admin_missing + raise Travis::AdminMissing.new("no admin available for #{repository.slug}") + end + + class Instrument < Notification::Instrument + def run_completed + publish( + msg: "for #{target.repository.slug}: #{result.login}", + result: result + ) + end + end + Instrument.attach_to(self) + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_annotations.rb b/vendor/travis-core/lib/travis/services/find_annotations.rb new file mode 100644 index 00000000..bdcd3db1 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_annotations.rb @@ -0,0 +1,17 @@ +module Travis + module Services + class FindAnnotations < Base + register :find_annotations + + def run + if params[:ids] + scope(:annotation).where(id: params[:ids]) + elsif params[:job_id] + scope(:annotation).where(job_id: params[:job_id]) + else + [] + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_branch.rb b/vendor/travis-core/lib/travis/services/find_branch.rb new file mode 100644 index 00000000..ab7fdf4b --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_branch.rb @@ -0,0 +1,36 @@ +require 'core_ext/active_record/none_scope' +require 'travis/services/base' + +module Travis + module Services + class FindBranch < Base + register :find_branch + + def run + result + end + + def updated_at + result.updated_at if result + end + + private + + def result + @result ||= params[:id] ? by_id : by_params + end + + def by_id + scope(:build).find(params[:id]) + end + + def by_params + repo.last_build_on params[:branch] if repo and params[:branch] + end + + def repo + @repo ||= run_service(:find_repo, params) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_branches.rb b/vendor/travis-core/lib/travis/services/find_branches.rb new file mode 100644 index 00000000..95916f33 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_branches.rb @@ -0,0 +1,32 @@ +require 'core_ext/active_record/none_scope' +require 'travis/services/base' + +module Travis + module Services + class FindBranches < Base + register :find_branches + + def run + result + end + + private + + def result + @result ||= params[:ids] ? by_ids : by_params + end + + def by_ids + scope(:build).where(:id => params[:ids]) + end + + def by_params + repo ? repo.last_finished_builds_by_branches : scope(:build).none + end + + def repo + @repo ||= run_service(:find_repo, params) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_build.rb b/vendor/travis-core/lib/travis/services/find_build.rb new file mode 100644 index 00000000..da009e7f --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_build.rb @@ -0,0 +1,55 @@ +require 'travis/services/base' + +module Travis + module Services + class FindBuild < Base + register :find_build + + def run + preload(result) if result + end + + def final? + # TODO builds can be requeued, so finished builds are no more final + # result.try(:finished?) + false + end + + def updated_at + max = all_resources.max_by(&:updated_at) + max.updated_at if max.respond_to?(:updated_at) + end + + private + + def all_resources + if result + all = [result, result.commit, result.request, result.matrix.to_a, result.matrix.map(&:annotations)] + all.flatten.find_all { |r| r.updated_at } + else + [] + end + end + + def result + @result ||= load_result + end + + def load_result + columns = scope(:build).column_names + columns -= %w(config) if params[:exclude_config] + columns = columns.map { |c| %Q{"builds"."#{c}"} } + scope(:build).select(columns).find_by_id(params[:id]).tap do |res| + res.config = {} if params[:exclude_config] + end + end + + def preload(build) + ActiveRecord::Associations::Preloader.new(build, [:matrix, :commit, :request]).run + ActiveRecord::Associations::Preloader.new(build.matrix, :log, select: [:id, :job_id, :updated_at]).run + ActiveRecord::Associations::Preloader.new(build.matrix, :annotations).run + build + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_builds.rb b/vendor/travis-core/lib/travis/services/find_builds.rb new file mode 100644 index 00000000..4cf408b8 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_builds.rb @@ -0,0 +1,54 @@ +require 'core_ext/active_record/none_scope' +require 'travis/services/base' + +# v2 builds.all +# build => commit, request, matrix.id + +module Travis + module Services + class FindBuilds < Base + register :find_builds + + def run + preload(result) + end + + private + + def result + @result ||= params[:ids] ? by_ids : by_params + end + + def by_ids + scope(:build).where(:id => params[:ids]) + end + + def by_params + if repo + # TODO :after_number seems like a bizarre api why not just pass an id? pagination style? + builds = repo.builds + builds = builds.by_event_type(params[:event_type]) if params[:event_type] + if params[:number] + builds.where(:number => params[:number].to_s) + else + builds.older_than(params[:after_number]) + end + elsif params[:running] + scope(:build).running.limit(25) + elsif params.nil? || params == {} + scope(:build).recent + else + scope(:build).none + end + end + + def preload(builds) + builds.includes(:commit) + end + + def repo + @repo ||= run_service(:find_repo, params) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_caches.rb b/vendor/travis-core/lib/travis/services/find_caches.rb new file mode 100644 index 00000000..e218ccb5 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_caches.rb @@ -0,0 +1,191 @@ +require 's3' +require 'travis/services/base' +require 'google/apis/storage_v1' + +module Travis + module Services + class FindCaches < Base + register :find_caches + + class S3Wrapper + attr_reader :repository, :s3_object + + def initialize(repository, s3_object) + @repository = repository + @s3_object = s3_object + end + + def source + 'S3' + end + + def last_modified + s3_object.last_modified + end + + def size + Integer(s3_object.size) + end + + def slug + File.basename(s3_object.key, '.tbz') + end + + def branch + s3_object.key[%r{^\d+/(.*)/[^/]+$}, 1] + end + + def destroy + Travis.logger.info "action=delete backend=s3 s3_object=#{s3_object.key}" + s3_object.destroy + end + + def temporary_url + s3_object.temporary_url + end + + def content + s3_object.content + end + end + + class GcsWrapper + attr_reader :storage, :bucket_name, :repository, :cache_object + + def initialize(storage, bucket_name, repository, cache_object) + @storage = storage + @bucket_name = bucket_name + @repository = repository + @cache_object = cache_object + end + + def source + 'GCS' + end + + def last_modified + cache_object.updated + end + + def size + Integer(cache_object.size) + end + + def slug + File.basename(cache_object.name, '.tbz') + end + + def branch + cache_object.name[%r{^\d+/(.*)/[^/]+$}, 1] + end + + def destroy + Travis.logger.info "action=delete backend=gcs bucket_name=#{bucket_name} cache_name=#{cache_object.name}" + storage.delete_object(bucket_name, cache_object.name) + rescue Google::Apis::ClientError + end + + def content + io = StringIO.new + storage.get_object(bucket_name, cache_object.name, download_dest: io) + io.rewind + io.read + end + end + + def run + return [] unless setup? && permission? + c = caches(prefix: prefix) + c.select! { |o| o.slug.include?(params[:match]) } if params[:match] + c + end + + private + + def setup? + return true if entries.any? { |entry| valid?(entry) } + + logger.warn "[services:find-caches] cache settings incomplete" + false + end + + def permission? + current_user.permission?(required_role, repository_id: repo.id) + end + + def required_role + Travis.config.roles.find_cache || "push" + end + + def repo + @repo ||= run_service(:find_repo, params) + end + + def branch + params[:branch].presence + end + + def prefix + prefix = "#{repo.github_id}/" + prefix << branch << '/' if branch + prefix + end + + def caches(options = {}) + if @caches + return @caches + end + + c = [] + + entries.map do |entry| + if config = entry[:s3] + svc = ::S3::Service.new(config.to_h.slice(:secret_access_key, :access_key_id)) + bucket = svc.buckets.find(config.fetch(:bucket_name)) + + next unless bucket + + c += bucket.objects(options).map { |object| S3Wrapper.new(repo, object) } + elsif config = entry[:gcs] + storage = ::Google::Apis::StorageV1::StorageService.new + json_key_io = StringIO.new(config.to_h[:json_key]) + bucket_name = config[:bucket_name] + + storage.authorization = ::Google::Auth::ServiceAccountCredentials.make_creds( + json_key_io: json_key_io, + scope: [ + 'https://www.googleapis.com/auth/devstorage.read_write' + ] + ) + + next unless items = storage.list_objects(bucket_name, prefix: prefix).items + + items.map do |object| + c << GcsWrapper.new(storage, bucket_name, repo, object) + end + end + end + + @caches = c.compact + end + + def entries + collection = Travis.config.to_h.fetch(:cache_options) { [] } + collection = [collection] unless collection.is_a? Array + collection + end + + def valid?(entry) + valid_s3?(entry) or valid_gcs?(entry) + end + + def valid_s3?(entry) + (s3_config = entry[:s3]) && s3_config[:access_key_id] && s3_config[:secret_access_key] && s3_config[:bucket_name] + end + + def valid_gcs?(entry) + (gcs_config = entry[:gcs]) && gcs_config[:json_key] && gcs_config[:bucket_name] + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_daily_repos_stats.rb b/vendor/travis-core/lib/travis/services/find_daily_repos_stats.rb new file mode 100644 index 00000000..4d9232a8 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_daily_repos_stats.rb @@ -0,0 +1,23 @@ +require 'travis/services/base' + +module Travis + module Services + class FindDailyReposStats < Base + register :find_daily_repos_stats + + def run + select scope(:repository). + select(['date(created_at) AS date', 'count(created_at) AS count']). + where('last_build_id IS NOT NULL'). + group('date'). + order('date').to_sql + end + + private + + def select(sql) + ActiveRecord::Base.connection.select_all(sql) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_daily_tests_stats.rb b/vendor/travis-core/lib/travis/services/find_daily_tests_stats.rb new file mode 100644 index 00000000..d1f1adbf --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_daily_tests_stats.rb @@ -0,0 +1,23 @@ +require 'travis/services/base' + +module Travis + module Services + class FindDailyTestsStats < Base + register :find_daily_tests_stats + + def run + select scope(:job). + select(['date(created_at) AS date', 'count(created_at) AS count']). + group('date'). + order('date'). + where(['created_at > ?', 28.days.ago]).to_sql + end + + private + + def select(sql) + ActiveRecord::Base.connection.select_all(sql) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_hooks.rb b/vendor/travis-core/lib/travis/services/find_hooks.rb new file mode 100644 index 00000000..b4ed9763 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_hooks.rb @@ -0,0 +1,13 @@ +require 'travis/services/base' + +module Travis + module Services + class FindHooks < Base + register :find_hooks + + def run + current_user.service_hooks(params) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_job.rb b/vendor/travis-core/lib/travis/services/find_job.rb new file mode 100644 index 00000000..c7a11a5a --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_job.rb @@ -0,0 +1,48 @@ +require 'travis/services/base' + +module Travis + module Services + class FindJob < Base + register :find_job + + def run + preload(result) if result + end + + def final? + # TODO jobs can be requeued, so finished jobs are no more final + # result.try(:finished?) + false + end + + def updated_at + [result].concat(result.annotations).map(&:updated_at).max if result + end + + private + + def result + @result ||= load_result + rescue ActiveRecord::SubclassNotFound => e + Travis.logger.warn "[services:find-job] #{e.message}" + raise ActiveRecord::RecordNotFound + end + + def load_result + columns = scope(:job).column_names + columns -= %w(config) if params[:exclude_config] + columns = columns.map { |c| %Q{"jobs"."#{c}"} } + scope(:job).select(columns).find_by_id(params[:id]).tap do |res| + res.config = {} if params[:exclude_config] + end + end + + def preload(job) + ActiveRecord::Associations::Preloader.new(job, :log).run + ActiveRecord::Associations::Preloader.new(job, :commit).run + ActiveRecord::Associations::Preloader.new(job, :annotations).run + job + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_jobs.rb b/vendor/travis-core/lib/travis/services/find_jobs.rb new file mode 100644 index 00000000..36eea22c --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_jobs.rb @@ -0,0 +1,43 @@ +require 'travis/services/base' + +module Travis + module Services + class FindJobs < Base + register :find_jobs + + def run + preload(result) + end + + private + + def result + @result ||= params[:ids] ? by_ids : by_params + end + + def by_ids + scope(:job).where(:id => params[:ids]) + end + + def by_params + jobs = scope(:job) + if params[:state] + jobs = jobs.where(state: params[:state]) + else + jobs = jobs.where(state: [:created, :queued, :received, :started]) + # we don't use it anymore, but just for backwards compat + jobs = jobs.where(queue: params[:queue]) if params[:queue] + jobs + end + jobs.limit(250) + end + + def preload(jobs) + jobs = jobs.includes(:commit) + ActiveRecord::Associations::Preloader.new(jobs, :log, :select => [:id, :job_id]).run + ActiveRecord::Associations::Preloader.new(jobs, :repository, :select => [:id, :owner_name, :name]).run + jobs + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_log.rb b/vendor/travis-core/lib/travis/services/find_log.rb new file mode 100644 index 00000000..6264e287 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_log.rb @@ -0,0 +1,33 @@ +require 'travis/services/base' + +module Travis + module Services + class FindLog < Base + register :find_log + + def run(options = {}) + result if result + end + + def final? + # TODO jobs can be requeued, so finished jobs are no more final + # result && result.job && result.job.finished? + false + end + + # def updated_at + # result.updated_at + # end + + private + + def result + @result ||= if params[:id] + scope(:log).find_by_id(params[:id]) + elsif params[:job_id] + scope(:log).where(job_id: params[:job_id]).first + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_repo.rb b/vendor/travis-core/lib/travis/services/find_repo.rb new file mode 100644 index 00000000..26cb0fe7 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_repo.rb @@ -0,0 +1,23 @@ +require 'travis/services/base' + +module Travis + module Services + class FindRepo < Base + register :find_repo + + def run(options = {}) + result + end + + def updated_at + result.try(:updated_at) + end + + private + + def result + @result ||= scope(:repository).find_by(params) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_repo_key.rb b/vendor/travis-core/lib/travis/services/find_repo_key.rb new file mode 100644 index 00000000..1c60e555 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_repo_key.rb @@ -0,0 +1,23 @@ +require 'travis/services/base' + +module Travis + module Services + class FindRepoKey < FindRepo + register :find_repo_key + + def run(options = {}) + result + end + + def updated_at + result.try(:updated_at) + end + + private + + def result + @result ||= (repo = super) ? repo.key : nil + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_repo_settings.rb b/vendor/travis-core/lib/travis/services/find_repo_settings.rb new file mode 100644 index 00000000..a7ec4ce6 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_repo_settings.rb @@ -0,0 +1,31 @@ +require 'travis/services/base' + +module Travis + module Services + class FindRepoSettings < Base + register :find_repo_settings + + def run(options = {}) + result if repo && authorized? + end + + def updated_at + result.try(:updated_at) + end + + def authorized? + current_user && current_user.permission?(:push, :repository_id => repo.id) + end + + private + + def repo + @repo ||= run_service(:find_repo, params) + end + + def result + repo.settings if repo + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_repos.rb b/vendor/travis-core/lib/travis/services/find_repos.rb new file mode 100644 index 00000000..3a6bfa4f --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_repos.rb @@ -0,0 +1,53 @@ +require 'travis/services/base' + +module Travis + module Services + class FindRepos < Base + register :find_repos + + def run + result + end + + private + + def result + @result ||= params[:ids] ? by_ids : by_params + end + + def by_ids + scope(:repository).where(:id => params[:ids]) + end + + def by_params + scope = self.scope(:repository).without_invalidated + scope = scope.timeline.recent if timeline? + scope = scope.by_member(params[:member]) if params[:member] + scope = scope.by_owner_name(params[:owner_name]) if params[:owner_name] + scope = scope.by_slug(params[:slug]) if params[:slug] + if params[:search].present? + scope = scope.search(params[:search]).order('last_build_started_at DESC NULLS LAST') + end + scope = scope.limit(limit) if limit + scope + end + + def limit + limit = params[:limit].to_i + + return 25 if limit == 0 + + if limit > 50 + 50 + else + limit + end + end + + def timeline? + # :member is passed for the left sidebar on pro/enterprise + not [:owner_name, :slug, :search].any? { |key| params[key] } + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_request.rb b/vendor/travis-core/lib/travis/services/find_request.rb new file mode 100644 index 00000000..2a9d4cb0 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_request.rb @@ -0,0 +1,25 @@ +module Travis + module Services + class FindRequest < Base + register :find_request + + def run(options = {}) + result + end + + def final? + true + end + + def updated_at + result.updated_at if result.respond_to?(:updated_at) + end + + private + + def result + @result ||= scope(:request).find_by_id(params[:id]) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_requests.rb b/vendor/travis-core/lib/travis/services/find_requests.rb new file mode 100644 index 00000000..eb575767 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_requests.rb @@ -0,0 +1,50 @@ +require 'core_ext/active_record/none_scope' + +module Travis + module Services + class FindRequests < Base + register :find_requests + + def run + preload(result) + end + + private + + def preload(requests) + requests.includes(:commit, :builds) + end + + def result + if repo + columns = %w/id repository_id commit_id created_at owner_id owner_type + event_type base_commit head_commit result message payload state/ + requests = repo.requests.select(columns.map { |c| %Q["requests"."#{c}"] }) + if params[:older_than] + requests.older_than(params[:older_than]) + else + requests.recent(requests_limit) + end + else + raise Travis::RepositoryNotFoundError.new(params) + end + end + + def repo + @repo ||= run_service(:find_repo, params) + end + + def requests_limit + max_limit = Travis.config.services.find_requests.max_limit + default_limit = Travis.config.services.find_requests.default_limit + if !params[:limit] + default_limit + elsif params[:limit] > max_limit + max_limit + else + params[:limit] + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_user_accounts.rb b/vendor/travis-core/lib/travis/services/find_user_accounts.rb new file mode 100644 index 00000000..3f75cd33 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_user_accounts.rb @@ -0,0 +1,33 @@ +require 'travis/services/base' + +module Travis + module Services + class FindUserAccounts < Base + register :find_user_accounts + + def run + ([current_user] + orgs).map do |record| + ::Account.from(record, :repos_count => repos_counts[record.login]) + end + end + + private + + def orgs + Organization.where(:login => account_names) + end + + def repos_counts + @repos_counts ||= Repository.counts_by_owner_names(account_names) + end + + def account_names + repos = current_user.repositories + unless params[:all] + repos = repos.administratable + end + repos.select(:owner_name).map(&:owner_name).uniq + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_user_broadcasts.rb b/vendor/travis-core/lib/travis/services/find_user_broadcasts.rb new file mode 100644 index 00000000..df9c43e1 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_user_broadcasts.rb @@ -0,0 +1,13 @@ +require 'travis/services/base' + +module Travis + module Services + class FindUserBroadcasts < Base + register :find_user_broadcasts + + def run + Broadcast.by_user(current_user) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/find_user_permissions.rb b/vendor/travis-core/lib/travis/services/find_user_permissions.rb new file mode 100644 index 00000000..0e1cf6a0 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/find_user_permissions.rb @@ -0,0 +1,15 @@ +require 'travis/services/base' + +module Travis + module Services + class FindUserPermissions < Base + register :find_user_permissions + + def run + scope = current_user.permissions + scope = scope.by_roles(params[:roles].to_s.split(',')) if params[:roles] + scope + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/helpers.rb b/vendor/travis-core/lib/travis/services/helpers.rb new file mode 100644 index 00000000..65f41a4f --- /dev/null +++ b/vendor/travis-core/lib/travis/services/helpers.rb @@ -0,0 +1,20 @@ +require 'travis/services/registry' + +module Travis + module Services + module Helpers + def run_service(key, *args) + service(key, *args).run + end + + def service(key, *args) + params = args.last.is_a?(Hash) ? args.pop : {} + user = args.last + user ||= current_user if respond_to?(:current_user) + Travis.services[key].new(user, params) + end + end + end + + extend Services::Helpers +end diff --git a/vendor/travis-core/lib/travis/services/next_build_number.rb b/vendor/travis-core/lib/travis/services/next_build_number.rb new file mode 100644 index 00000000..480c984a --- /dev/null +++ b/vendor/travis-core/lib/travis/services/next_build_number.rb @@ -0,0 +1,40 @@ +require 'travis/services/base' +require 'travis/notification' + +module Travis + module Services + class NextBuildNumber < Base + extend Travis::Instrumentation + + register :next_build_number + + def run + number = repository.next_build_number + if number.nil? + number = repository.builds.maximum('number::int4').to_i + 1 + repository.next_build_number = number + 1 + else + repository.next_build_number += 1 + end + repository.save!(validate: false) + number + end + instrument :run + + def repository + @repository ||= Repository.find(params[:repository_id]) + end + + class Instrument < Notification::Instrument + def run_completed + params = target.params + publish( + msg: "for repository_id=#{params[:repository_id]}", + repository_id: params[:repository_id] + ) + end + end + Instrument.attach_to(self) + end + end +end diff --git a/vendor/travis-core/lib/travis/services/regenerate_repo_key.rb b/vendor/travis-core/lib/travis/services/regenerate_repo_key.rb new file mode 100644 index 00000000..f58e119a --- /dev/null +++ b/vendor/travis-core/lib/travis/services/regenerate_repo_key.rb @@ -0,0 +1,35 @@ +require 'travis/services/base' + +module Travis + module Services + class RegenerateRepoKey < Base + register :regenerate_repo_key + + def run(options = {}) + if repo && accept? + regenerate + repo.key + end + end + + def accept? + has_permission? + end + + private + + def regenerate + repo.regenerate_key! + end + + def repo + @repo ||= service(:find_repo, params).run + end + + def has_permission? + current_user && current_user.permission?(:admin, :repository_id => repo.id) + end + + end + end +end diff --git a/vendor/travis-core/lib/travis/services/registry.rb b/vendor/travis-core/lib/travis/services/registry.rb new file mode 100644 index 00000000..1632b061 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/registry.rb @@ -0,0 +1,31 @@ +module Travis + module Services + module Registry + def add(key, const = nil) + if key.is_a?(Hash) + key.each { |key, const| add(key, const) } + else + services[key.to_sym] = const + end + end + + def [](key) + services[key.to_sym] || raise("can not use unregistered service #{key}. known services are: #{services.keys.inspect}") + end + + private + + def services + @services ||= {} + end + end + + extend Registry + + class << self + def register + constants(false).each { |name| const_get(name) } + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/remove_log.rb b/vendor/travis-core/lib/travis/services/remove_log.rb new file mode 100644 index 00000000..67aaf426 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/remove_log.rb @@ -0,0 +1,79 @@ +module Travis + module Services + class RemoveLog < Base + extend Travis::Instrumentation + include Travis::Logging + + register :remove_log + + FORMAT = "Log removed by %s at %s" + + def run + return nil unless job + + if log.removed_at || log.removed_by + error "Log for job #{job.id} has already been removed by #{log.removed_by} at #{log.removed_at}" + raise LogAlreadyRemoved, "Log for job #{job.id} has already been removed" + end + + unless authorized? + error "Current user #{current_user} is unauthorized to remove log for job #{job.id}" + raise AuthorizationDenied, "insufficient permission to remove logs" + end + + unless job.finished? + error " is not finished" + raise JobUnfinished, "Job #{job.id} is not finished" + end + + removed_at = Time.now + + message = FORMAT % [current_user.name, removed_at.utc] + if params[:reason] + message << "\n\n#{params[:reason]}" + end + + log.clear! + log.update_attributes!( + :content => nil, + :aggregated_at => nil, + :removed_at => removed_at, + :removed_by => current_user + ) + log.parts.create(content: message, number: 1, final: true) + log + end + + instrument :run + + def log + @log ||= job.log + end + + def can_remove? + authorized? && job.finished? + end + + def authorized? + current_user && current_user.permission?(:push, repository_id: job.repository.id) + end + + def job + @job ||= scope(:job).find_by_id(params[:id]) + rescue ActiveRecord::SubclassNotFound => e + Travis.logger.warn "[services:remove-log] #{e.message}" + raise ActiveRecord::RecordNotFound + end + + class Instrument < Notification::Instrument + def run_completed + publish( + :msg => "for (#{target.current_user.login})", + :result => result + ) + end + end + Instrument.attach_to(self) + end + end +end diff --git a/vendor/travis-core/lib/travis/services/reset_model.rb b/vendor/travis-core/lib/travis/services/reset_model.rb new file mode 100644 index 00000000..f8a2a0db --- /dev/null +++ b/vendor/travis-core/lib/travis/services/reset_model.rb @@ -0,0 +1,74 @@ +require 'travis/support/instrumentation' +require 'travis/services/base' + +module Travis + module Services + class ResetModel < Base + extend Travis::Instrumentation + + register :reset_model + + def run + reset if current_user && target && accept? + true + end + instrument :run + + def accept? + current_user && permission? && resetable? + end + + def messages + messages = [] + messages << { notice: "The #{type} was successfully restarted." } if accept? + messages << { error: 'You do not seem to have sufficient permissions.' } unless permission? + messages << { error: "This #{type} currently can not be restarted." } unless resetable? + messages + end + + def type + @type ||= params[:build_id] ? :build : :job + end + + def id + @id ||= params[:"#{type}_id"] + end + + private + + def reset + # target may have been retrieved with a :join query, so we need to reset the readonly status + target.send(:instance_variable_set, :@readonly, false) + target.reset!(reset_matrix: type == :build) + end + + def permission? + current_user.permission?(required_role, repository_id: target.repository_id) + end + + def resetable? + defined?(@resetable) ? @resetable : @resetable = target.resetable? + end + + def required_role + Travis.config.roles.reset_model + end + + def target + @target ||= service(:"find_#{type}", id: id).run + end + + class Instrument < Notification::Instrument + def run_completed + publish( + msg: "build_id=#{target.id} #{target.accept? ? 'accepted' : 'not accepted'}", + type: target.type, + id: target.id, + accept?: target.accept? + ) + end + end + Instrument.attach_to(self) + end + end +end diff --git a/vendor/travis-core/lib/travis/services/sync_user.rb b/vendor/travis-core/lib/travis/services/sync_user.rb new file mode 100644 index 00000000..98849f2e --- /dev/null +++ b/vendor/travis-core/lib/travis/services/sync_user.rb @@ -0,0 +1,26 @@ +require 'travis/sidekiq/synchronize_user' +require 'travis/services/base' + +module Travis + module Services + class SyncUser < Base + register :sync_user + + def run + trigger_sync unless user.syncing? + end + + def trigger_sync + logger.info("Synchronizing via Sidekiq for user: #{user.login}") + Travis::Sidekiq::SynchronizeUser.perform_async(user.id) + user.update_column(:is_syncing, true) + true + end + + def user + # TODO check that clients are only passing the id + @user ||= current_user || User.find(params[:id]) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/update_annotation.rb b/vendor/travis-core/lib/travis/services/update_annotation.rb new file mode 100644 index 00000000..94a9c03c --- /dev/null +++ b/vendor/travis-core/lib/travis/services/update_annotation.rb @@ -0,0 +1,36 @@ +module Travis + module Services + class UpdateAnnotation < Base + register :update_annotation + + def run + if annotations_enabled? && annotation_provider + cached_annotation = annotation + cached_annotation.update_attributes!(attributes) + + cached_annotation + end + end + + private + + def annotations_enabled? + job = Job.find(params[:job_id]) + repo = job.repository + Travis::Features.enabled_for_all?(:annotations) || Travis::Features.active?(:annotations, repo) + end + + def annotation + annotation_provider.annotation_for_job(params[:job_id]) + end + + def annotation_provider + @annotation_provider ||= AnnotationProvider.authenticate_provider(params[:username], params[:key]) + end + + def attributes + params.slice(:description, :status, :url) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/services/update_hook.rb b/vendor/travis-core/lib/travis/services/update_hook.rb new file mode 100644 index 00000000..bae79049 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/update_hook.rb @@ -0,0 +1,47 @@ +require 'travis/support/instrumentation' +require 'travis/services/base' + +module Travis + module Services + class UpdateHook < Base + extend Travis::Instrumentation + + register :update_hook + + def run + run_service(:github_set_hook, id: repo.id, active: active?) + repo.update_column(:active, active?) + true + end + instrument :run + + # TODO + # def messages + # messages = [] + # messages << { :notice => "The service hook was successfully #{active? ? 'enabled' : 'disabled'}." } if what? + # messages << { :error => 'The service hook could not be set.' } unless what? + # messages + # end + + def repo + @repo ||= current_user.service_hook(params.slice(:id, :owner_name, :name)) + end + + def active? + active = params[:active] + active = { 'true' => true, 'false' => false }[active] if active.is_a?(String) + !!active + end + + class Instrument < Notification::Instrument + def run_completed + publish( + :msg => "for #{target.repo.slug} active=#{target.active?.inspect} (#{target.current_user.login})", + :result => result + ) + end + end + Instrument.attach_to(self) + end + end +end diff --git a/vendor/travis-core/lib/travis/services/update_job.rb b/vendor/travis-core/lib/travis/services/update_job.rb new file mode 100644 index 00000000..30be4ca1 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/update_job.rb @@ -0,0 +1,71 @@ +require 'active_support/core_ext/hash/except' +require 'travis/support/instrumentation' +require 'travis/services/base' + +module Travis + module Services + class UpdateJob < Base + extend Travis::Instrumentation + + register :update_job + + EVENT = [:receive, :start, :finish, :reset] + + def run + if job.canceled? && event != :reset + # job is canceled, so we ignore events other than reset + # and we send cancel event to the worker, it may not get + # the first one + cancel_job_in_worker + else + Metriks.timer("update_job.#{event}").time do + job.send(:"#{event}!", data.except(:id)) + end + end + end + instrument :run + + def job + @job ||= Job::Test.find(data[:id]) + end + + def data + @data ||= begin + data = params[:data].symbolize_keys + # TODO remove once workers send the state + data[:state] = { 0 => :passed, 1 => :failed }[data.delete(:result)] if data.key?(:result) + data + end + end + + def event + @event ||= EVENT.detect { |event| event == params[:event].try(:to_sym) } || raise_unknown_event + end + + def raise_unknown_event + raise ArgumentError, "Unknown event: #{params[:event]}, data: #{data}" + end + + def cancel_job_in_worker + publisher.publish(type: 'cancel_job', job_id: job.id, source: 'update_job_service') + end + + def publisher + Travis::Amqp::FanoutPublisher.new('worker.commands') + end + + class Instrument < Notification::Instrument + def run_completed + publish( + msg: "event: #{target.event} for data=#{target.data.inspect}", + job_id: target.data[:id], + event: target.event, + data: target.data + ) + end + end + Instrument.attach_to(self) + end + end +end + diff --git a/vendor/travis-core/lib/travis/services/update_log.rb b/vendor/travis-core/lib/travis/services/update_log.rb new file mode 100644 index 00000000..530a5253 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/update_log.rb @@ -0,0 +1,31 @@ +require 'travis/support/instrumentation' +require 'travis/services/base' + +module Travis + module Services + class UpdateLog < Base + extend Travis::Instrumentation + + register :update_log + + def run + log = run_service(:find_log, id: params[:id]) + log.update_attributes(archived_at: params[:archived_at], archive_verified: params[:archive_verified]) if log + end + instrument :run + + class Instrument < Notification::Instrument + def run_completed + publish( + msg: "for # params=#{target.params.inspect}", + object_type: 'Log', + object_id: target.params[:id], + params: target.params, + result: result + ) + end + end + Instrument.attach_to(self) + end + end +end diff --git a/vendor/travis-core/lib/travis/services/update_user.rb b/vendor/travis-core/lib/travis/services/update_user.rb new file mode 100644 index 00000000..c0a12ac5 --- /dev/null +++ b/vendor/travis-core/lib/travis/services/update_user.rb @@ -0,0 +1,38 @@ +require 'travis/services/base' + +module Travis + module Services + class UpdateUser < Base + register :update_user + + LOCALES = %w(en es fr ja nb nl pl pt-BR ru de) # TODO how to figure these out + + attr_reader :result + + def run + @result = current_user.update_attributes!(attributes) if valid_locale? + true + end + + def messages + messages = [] + if result + messages << { :notice => "Your profile was successfully updated." } + else + messages << { :error => 'Your profile could not be updated.' } + end + messages + end + + private + + def attributes + params.slice(:locale) + end + + def valid_locale? + LOCALES.include?(params[:locale]) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/settings.rb b/vendor/travis-core/lib/travis/settings.rb new file mode 100644 index 00000000..dcaf9eb9 --- /dev/null +++ b/vendor/travis-core/lib/travis/settings.rb @@ -0,0 +1,67 @@ +require 'coercible' +require 'travis/overwritable_method_definitions' +require 'travis/settings/collection' +require 'travis/settings/model' +require 'travis/settings/model_extensions' + +module Travis + class Settings + include Travis::OverwritableMethodDefinitions + include Virtus.model + include ActiveModel::Validations + include Travis::Settings::ModelExtensions + + def on_save(&block) + @on_save = block + self + end + + def merge(hash) + hash.each { |k, v| + set(k, v) unless collection?(k) || model?(k) + } + end + + def obfuscated + to_hash + end + + def save + if valid? + @on_save.call + end + end + + def to_json + to_hash.to_json + end + + def to_hash + result = super + + result.each do |key, value| + if value.respond_to?(:to_hash) + result[key] = value.to_hash + end + end + + result + end + end + + module DefaultSettings + def initialize(*) + super + + freeze + end + + def merge(*) + raise "merge is not supported on default settings" + end + + def set(key, value) + raise "setting values is not supported on default settings" + end + end +end diff --git a/vendor/travis-core/lib/travis/settings/collection.rb b/vendor/travis-core/lib/travis/settings/collection.rb new file mode 100644 index 00000000..8d378756 --- /dev/null +++ b/vendor/travis-core/lib/travis/settings/collection.rb @@ -0,0 +1,79 @@ +class Travis::Settings + class Collection + include Enumerable + + delegate :each, :<<, :push, :delete, :length, :first, :last, to: '@collection' + attr_accessor :additional_attributes + + class << self + # This feels a bit weird, but I don't know how to do it better. + # Virtus checks for collection type by checking an array member, + # so if you pass Array[String], a collection type will be set to String. + # Here, we already specify what is a model class for a collection. + # In order to not have to specify class twice, I created this method + # which creates just what Virtus needs. + def for_virtus + self[model_class] + end + + def [](*args) + new(*args) + end + + def model(model_name_or_class = nil) + if model_name_or_class + klass = if model_name_or_class.is_a?(String) || model_name_or_class.is_a?(Symbol) + name = model_name_or_class.to_s.classify + self.const_defined?(name, false) ? self.const_get(name, false) : Travis::Settings.const_get(name, false) + else + model_name_or_class + end + + @model_class = klass + else + @model_class + end + end + attr_reader :model_class + end + + delegate :model_class, to: 'self.class' + + def initialize(*args) + @collection = Array[*args] + end + + def create(attributes) + model = model_class.new(attributes) + model.load({}, additional_attributes) + model.id = SecureRandom.uuid unless model.id + push model + model + end + + def find(id) + detect { |model| model.id == id.to_s } + end + + def destroy(id) + record = find(id) + if record + delete record + record + end + end + + def to_hash + @collection.map(&:to_hash) + end + + def load(collection, additional_attributes = {}) + self.additional_attributes = additional_attributes + return unless collection.respond_to?(:each) + + collection.each do |element| + self.push model_class.load(element, additional_attributes) + end + end + end +end diff --git a/vendor/travis-core/lib/travis/settings/encrypted_value.rb b/vendor/travis-core/lib/travis/settings/encrypted_value.rb new file mode 100644 index 00000000..a9b852e6 --- /dev/null +++ b/vendor/travis-core/lib/travis/settings/encrypted_value.rb @@ -0,0 +1,62 @@ +require 'virtus' + +class Travis::Settings + class EncryptedValue + include Virtus.value_object + attr_reader :value, :key + + values do + attribute :value, String + end + + def initialize(value) + if value.is_a? String + # a value is set through the accessor, not loaded in jason, we + # need to encrypt it and put into hash form + value = { value: encrypt(value) } + end + + super value + end + + def to_s + value + end + + def to_str + value + end + + def to_json(*) + as_json.to_json + end + + def as_json(*) + value + end + + def to_hash + value + end + + def inspect + "" + end + + def key + Travis.config.encryption.key + end + + def encrypt(value) + Travis::Model::EncryptedColumn.new(key: key, use_prefix: false).dump(value) + end + + def decrypt + Travis::Model::EncryptedColumn.new(key: key, use_prefix: false).load(value) + end + + def load(value, additional_attributes = nil) + self.instance_variable_set('@value', value) + end + end +end diff --git a/vendor/travis-core/lib/travis/settings/model.rb b/vendor/travis-core/lib/travis/settings/model.rb new file mode 100644 index 00000000..37b97f7c --- /dev/null +++ b/vendor/travis-core/lib/travis/settings/model.rb @@ -0,0 +1,39 @@ +require 'virtus' +require 'travis/settings/encrypted_value' +require 'travis/settings/model_extensions' + +class Travis::Settings + class Model + include Virtus.model + include ActiveModel::Validations + include ModelExtensions + include ActiveModel::Serialization + + def attribute?(name) + attributes.keys.include?(name.to_sym) + end + + def read_attribute_for_serialization(name) + self.send(name) if attribute?(name) + end + + def read_attribute_for_validation(name) + return unless attribute?(name) + + value = self.send(name) + value.is_a?(EncryptedValue) ? value.to_s : value + end + + def update(attributes) + self.attributes = attributes + end + + def key + Travis.config.encryption.key + end + + def to_json + to_hash.to_json + end + end +end diff --git a/vendor/travis-core/lib/travis/settings/model_extensions.rb b/vendor/travis-core/lib/travis/settings/model_extensions.rb new file mode 100644 index 00000000..46f19a47 --- /dev/null +++ b/vendor/travis-core/lib/travis/settings/model_extensions.rb @@ -0,0 +1,177 @@ +module Travis + class Settings + module AccessorExtensions + def set(instance, value) + if instance.frozen? + raise 'setting values is not supported on default settings' + end + + super + end + + def get(instance) + if type.primitive <= Travis::Settings::EncryptedValue + unless instance.instance_variable_get(instance_variable_name) + value = Travis::Settings::EncryptedValue.new(nil) + if instance.frozen? + return value + else + set(instance, value) + end + end + end + + super instance + end + end + + module ModelExtensions + class Errors < ActiveModel::Errors + # Default behavior of Errors in Active Model is to + # translate symbolized message into full text message, + # using i18n if available. I don't want such a behavior, + # as I want to return error "codes" like :blank, not + # full text like "can't be blank" + def normalize_message(attribute, message, options) + message || :invalid + end + end + + module ClassMethods + def attribute(name, type = nil, options = {}) + options[:finalize] = false + + super name, type, options + + attribute = attribute_set[name] + attribute.extend(AccessorExtensions) + attribute.finalize + attribute.define_accessor_methods(attribute_set) + + self + end + + def load(json, additional_attributes = {}) + instance = new() + + json = JSON.parse(json) if json.is_a?(String) + instance.load json, additional_attributes + instance + end + end + + def self.included(base) + base.extend ClassMethods + end + + def additional_attributes + @additional_attributes || {} + end + + def additional_attributes=(hash = {}) + attribute_set.each do |attribute| + value = get(attribute.name) + if value.respond_to?(:additional_attributes=) + value.additional_attributes = hash + end + end + @additional_attributes = hash + end + + def errors + @errors ||= Errors.new(self) + end + + def attribute?(key) + attributes.keys.include? key.to_sym + end + + def to_hash + attributes.each_with_object({}) do |(name, value), hash| + hash[name] = value.respond_to?(:to_hash) ? value.to_hash : value + end + end + + def collection?(name) + # TODO: I don't like this type of class checking, it will be better to work + # based on an API contract, but it should do for now + if attribute = attribute_set[name.to_sym] + attribute.type.primitive <= Travis::Settings::Collection + end + end + + def encrypted?(name) + if attribute = attribute_set[name.to_sym] + attribute.type.primitive <= Travis::Settings::EncryptedValue + end + end + + def model?(name) + if attribute = attribute_set[name.to_sym] + attribute.type.primitive <= Travis::Settings::Model + end + end + + def primitive(name) + attribute_set[name.to_sym].type.primitive + end + + def get(key) + if attribute?(key) + self.send(key) + end + end + + def set(key, value) + if attribute?(key) + self.send("#{key}=", value) + end + end + + def simple_attributes + attributes.select { |k, v| simple_attribute?(k) } + end + + def simple_attribute?(key) + !(collection?(key) || encrypted?(key) || model?(key)) + end + + def load(hash = {}, additional_attributes = {}) + hash ||= {} + self.additional_attributes = additional_attributes || {} + + hash.merge(self.additional_attributes).each do |key, value| + if collection?(key) || encrypted?(key) || model?(key) + thing = get(key) + thing = set(key, primitive(key).new) if !thing && value + thing.load(value, self.additional_attributes) if thing + elsif attribute?(key) + set(key, value) + end + end + end + + def create(key, attributes) + attributes = (attributes || {}).merge(additional_attributes || {}) + set(key, primitive(key).new(attributes)) + get(key) + end + + def delete(key) + model = get(key) + set(key, nil) + model + end + + def update(key, attributes) + attributes = (attributes || {}).merge(additional_attributes || {}) + if model = get(key) + model.update(attributes) + model + else + create(key, attributes) + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/states_cache.rb b/vendor/travis-core/lib/travis/states_cache.rb new file mode 100644 index 00000000..8703b486 --- /dev/null +++ b/vendor/travis-core/lib/travis/states_cache.rb @@ -0,0 +1,158 @@ +require 'dalli' +require 'connection_pool' +require 'active_support/core_ext/module/delegation' +require 'travis/api' + +module Travis + class StatesCache + class CacheError < StandardError; end + + include Travis::Api::Formats + + attr_reader :adapter + + delegate :fetch, :to => :adapter + + def initialize(options = {}) + @adapter = options[:adapter] || MemcachedAdapter.new + end + + def write(id, branch, data) + if data.respond_to?(:id) + data = { + 'id' => data.id, + 'state' => data.state.to_s + } + end + + adapter.write(id, branch, data) + end + + def fetch_state(id, branch) + data = fetch(id, branch) + data['state'].to_sym if data && data['state'] + end + + class TestAdapter + attr_reader :calls + def initialize + @calls = [] + end + + def fetch(id, branch) + calls << [:fetch, id, branch] + end + + def write(id, branch, data) + calls << [:write, id, branch, data] + end + + def clear + calls.clear + end + end + + class MemcachedAdapter + attr_reader :pool + attr_accessor :jitter + attr_accessor :ttl + + def initialize(options = {}) + @pool = ConnectionPool.new(:size => 10, :timeout => 3) do + if options[:client] + options[:client] + else + new_dalli_connection + end + end + @jitter = 0.5 + @ttl = 7.days + end + + def fetch(id, branch = nil) + data = get(key(id, branch)) + data ? JSON.parse(data) : nil + end + + def write(id, branch, data) + build_id = data['id'] + data = data.to_json + + Travis.logger.info("[states-cache] Writing states cache for repo_id=#{id} branch=#{branch} build_id=#{build_id}") + set(key(id), data) if update?(id, nil, build_id) + set(key(id, branch), data) if update?(id, branch, build_id) + end + + def update?(id, branch, build_id) + current_data = fetch(id, branch) + return true unless current_data + + current_id = current_data['id'].to_i + new_id = build_id.to_i + + update = new_id >= current_id + message = "[states-cache] Checking if cache is stale for repo_id=#{id} branch=#{branch}. " + if update + message << "The cache is going to get an update, " + else + message << "The cache is fresh, " + end + message << "last cached build id=#{current_id}, we're checking build with id=#{new_id}" + Travis.logger.info(message) + + return update + end + + def key(id, branch = nil) + key = "state:#{id}" + if branch + key << "-#{branch}" + end + key + end + + private + + def new_dalli_connection + Dalli::Client.new(Travis.config.states_cache.memcached_servers, Travis.config.states_cache.memcached_options) + end + + def get(key) + retry_ringerror do + pool.with { |client| client.get(key) } + end + rescue Dalli::RingError => e + Metriks.meter("memcached.connect-errors").mark + raise CacheError, "Couldn't connect to a memcached server: #{e.message}" + end + + def set(key, data) + retry_ringerror do + pool.with { |client| client.set(key, data) } + Travis.logger.info("[states-cache] Setting cache for key=#{key} data=#{data}") + end + rescue Dalli::RingError => e + Metriks.meter("memcached.connect-errors").mark + Travis.logger.info("[states-cache] Writing cache key failed key=#{key} data=#{data}") + raise CacheError, "Couldn't connect to a memcached server: #{e.message}" + end + + def retry_ringerror + retries = 0 + begin + yield + rescue Dalli::RingError + retries += 1 + if retries <= 3 + # Sleep for up to 1/2 * (2^retries - 1) seconds + # For retries <= 3, this means up to 3.5 seconds + sleep(jitter * (rand(2 ** retries - 1) + 1)) + retry + else + raise + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/task.rb b/vendor/travis-core/lib/travis/task.rb new file mode 100644 index 00000000..dad3b98f --- /dev/null +++ b/vendor/travis-core/lib/travis/task.rb @@ -0,0 +1,93 @@ +require 'faraday' +require 'core_ext/hash/compact' +require 'core_ext/hash/deep_symbolize_keys' +require 'active_support/core_ext/string' +require 'active_support/core_ext/class/attribute' + +module Travis + class Task + include Logging + extend Instrumentation + + class_attribute :run_local + + class << self + extend Exceptions::Handling + + def run(queue, *args) + Travis::Async.run(self, :perform, async_options(queue), *args) + end + + def run_local? + !!run_local || Travis::Features.feature_deactivated?(:travis_tasks) + end + + def inline_or_sidekiq + run_local? ? :inline : :sidekiq + end + + def async_options(queue) + { queue: queue, use: inline_or_sidekiq, retries: 8, backtrace: true } + end + + def perform(*args) + new(*args).run + end + end + + attr_reader :payload, :params + + def initialize(payload, params = {}) + @payload = payload.deep_symbolize_keys + @params = params.deep_symbolize_keys + end + + def run + timeout after: params[:timeout] || 60 do + process + end + end + instrument :run + + private + + def repository + @repository ||= payload[:repository] + end + + def job + @job ||= payload[:job] + end + + def build + @build ||= payload[:build] + end + + def request + @request ||= payload[:request] + end + + def commit + @commit ||= payload[:commit] + end + + def pull_request? + build[:pull_request] + end + + def http + @http ||= Faraday.new(http_options) do |f| + f.request :url_encoded + f.adapter :net_http + end + end + + def http_options + { ssl: Travis.config.ssl.compact } + end + + def timeout(options = { after: 60 }, &block) + Timeout::timeout(options[:after], &block) + end + end +end diff --git a/vendor/travis-core/lib/travis/testing.rb b/vendor/travis-core/lib/travis/testing.rb new file mode 100644 index 00000000..445795fd --- /dev/null +++ b/vendor/travis-core/lib/travis/testing.rb @@ -0,0 +1,9 @@ +require 'faraday' +require 'core_ext/hash/compact' + +module Travis + module Testing + require 'travis/testing/stubs' + end +end + diff --git a/vendor/travis-core/lib/travis/testing/factories.rb b/vendor/travis-core/lib/travis/testing/factories.rb new file mode 100644 index 00000000..5490e84b --- /dev/null +++ b/vendor/travis-core/lib/travis/testing/factories.rb @@ -0,0 +1,135 @@ +require 'factory_girl' + +FactoryGirl.define do + factory :build do + owner { User.first || Factory(:user) } + repository { Repository.first || Factory(:repository) } + association :request + association :commit + started_at { Time.now.utc } + finished_at { Time.now.utc } + number 1 + state :passed + end + + factory :commit do + commit '62aae5f70ceee39123ef' + branch 'master' + message 'the commit message' + committed_at '2011-11-11T11:11:11Z' + committer_name 'Sven Fuchs' + committer_email 'svenfuchs@artweb-design.de' + author_name 'Sven Fuchs' + author_email 'svenfuchs@artweb-design.de' + compare_url 'https://github.com/svenfuchs/minimal/compare/master...develop' + end + + factory :test, :class => 'Job::Test' do + owner { User.first || Factory(:user) } + repository { Repository.first || Factory(:repository) } + commit { Factory(:commit) } + source { Factory(:build) } + log { Factory(:log) } + config { { 'rvm' => '1.8.7', 'gemfile' => 'test/Gemfile.rails-2.3.x' } } + number '2.1' + tags "" + end + + factory :log do + content '$ bundle install --pa' + end + + factory :request do + repository { Repository.first || Factory(:repository) } + association :commit + token 'the-token' + event_type 'push' + end + + factory :repository do + owner { User.find_by_login('svenfuchs') || Factory(:user) } + name 'minimal' + owner_name 'svenfuchs' + owner_email 'svenfuchs@artweb-design.de' + active true + url { |r| "http://github.com/#{r.owner_name}/#{r.name}" } + created_at { |r| Time.utc(2011, 01, 30, 5, 25) } + updated_at { |r| r.created_at + 5.minutes } + last_build_state :passed + last_build_number '2' + last_build_id 2 + last_build_started_at { Time.now.utc } + last_build_finished_at { Time.now.utc } + sequence(:github_id) {|n| n } + end + + factory :minimal, :parent => :repository do + end + + factory :enginex, :parent => :repository do + name 'enginex' + owner_name 'josevalim' + owner_email 'josevalim@email.com' + owner { User.find_by_login('josevalim') || Factory(:user, :login => 'josevalim') } + end + + factory :event do + repository { Repository.first || Factory(:repository) } + source { Build.first || Factory(:build) } + event 'build:started' + end + + factory :user do + name 'Sven Fuchs' + login 'svenfuchs' + email 'sven@fuchs.com' + tokens { [Token.new] } + github_oauth_token 'github_oauth_token' + end + + factory :org, :class => 'Organization' do + name 'travis-ci' + end + + factory :running_build, :parent => :build do + repository { Factory(:repository, :name => 'running_build') } + state :started + end + + factory :successful_build, :parent => :build do + repository { |b| Factory(:repository, :name => 'successful_build') } + state :passed + started_at { Time.now.utc } + finished_at { Time.now.utc } + end + + factory :broken_build, :parent => :build do + repository { Factory(:repository, :name => 'broken_build', :last_build_state => :failed) } + state :failed + started_at { Time.now.utc } + finished_at { Time.now.utc } + end + + factory :broken_build_with_tags, :parent => :build do + repository { Factory(:repository, :name => 'broken_build_with_tags', :last_build_state => :errored) } + matrix {[Factory(:test, :tags => "database_missing,rake_not_bundled", :number => "1.1"), + Factory(:test, :tags => "database_missing,log_limit_exceeded", :number => "1.2")]} + state :failed + started_at { Time.now.utc } + finished_at { Time.now.utc } + end + + factory :annotation do + url "https://travis-ci.org/travis-ci/travis-ci/jobs/12345" + description "Job passed" + job { Factory(:test) } + annotation_provider { Factory(:annotation_provider) } + end + + factory :annotation_provider do + name "Travis CI" + api_username "travis-ci" + api_key "0123456789abcdef" + end +end + diff --git a/vendor/travis-core/lib/travis/testing/matchers.rb b/vendor/travis-core/lib/travis/testing/matchers.rb new file mode 100644 index 00000000..d768d54c --- /dev/null +++ b/vendor/travis-core/lib/travis/testing/matchers.rb @@ -0,0 +1,50 @@ +# RSpec::Matchers.define :serve_result_image do |result| +# match do |request| +# path = "#{Rails.root}/public/images/result/#{result}.png" +# controller.expects(:send_file).with(path, { :type => 'image/png', :disposition => 'inline' }).once +# request.call +# end +# end + +RSpec::Matchers.define :issue_queries do |count| + match do |code| + queries = call(code) + + failure_message_for_should do + (["expected #{count} queries to be issued, but got #{queries.size}:"] + queries).join("\n\n") + end + + queries.size == count + end + + def call(code) + queries = [] + ActiveSupport::Notifications.subscribe 'sql.active_record' do |name, start, finish, id, payload| + queries << payload[:sql] unless payload[:sql] =~ /(?:ROLLBACK|pg_|BEGIN|COMMIT)/ + end + code.call + queries + end +end + +RSpec::Matchers.define :publish_instrumentation_event do |data| + match do |event| + non_matching = data.map { |key, value| [key, value, event[key]] unless event[key] == value }.compact + expected_keys = [:uuid, :event, :started_at] + missing_keys = expected_keys.select { |key| !event.key?(key) } + + failure_message_for_should do + message = "Expected a notification event to be published:\n\n\t#{event.inspect}\n\n" + message << "Including:\n\n\t#{data.inspect}\n\n" + + non_matching.each do |key, expected, actual| + message << "#{key.inspect} expected to be\n\n\t#{expected.inspect}\n\nbut was\n\n\t#{actual.inspect}\n\n" + end + + message << "Expected #{missing_keys.map(&:inspect).join(', ')} to be present." if missing_keys.present? + message + end + + non_matching.empty? && missing_keys.empty? + end +end diff --git a/vendor/travis-core/lib/travis/testing/payloads.rb b/vendor/travis-core/lib/travis/testing/payloads.rb new file mode 100644 index 00000000..e2c42578 --- /dev/null +++ b/vendor/travis-core/lib/travis/testing/payloads.rb @@ -0,0 +1,248 @@ +PAYLOADS = {} + +PAYLOADS[:github] = { + "private-repo" => %({ + "repository": { + "url": "http://github.com/svenfuchs/gem-release", + "name": "gem-release", + "private":true, + "owner": { + "email": "svenfuchs@artweb-design.de", + "name": "svenfuchs" + } + }, + "commits": [{ + "id": "9854592", + "message": "Bump to 0.0.15", + "timestamp": "2010-10-27 04:32:37", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Christopher Floess", + "email": "chris@flooose.de" + } + }], + "ref": "refs/heads/master" + }), + + "gem-release" => %({ + "repository": { + "url": "http://github.com/svenfuchs/gem-release", + "name": "gem-release", + "owner": { + "email": "svenfuchs@artweb-design.de", + "name": "svenfuchs" + } + }, + "commits": [{ + "id": "46ebe012ef3c0be5542a2e2faafd48047127e4be", + "message": "Bump to 0.0.15", + "timestamp": "2010-10-27 04:32:37", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Christopher Floess", + "email": "chris@flooose.de" + } + }], + "ref": "refs/heads/master", + "compare": "https://github.com/svenfuchs/gem-release/compare/af674bd...9854592" + }), + + "gh-pages-update" => %({ + "repository": { + "url": "http://github.com/svenfuchs/gem-release", + "name": "gem-release", + "owner": { + "email": "svenfuchs@artweb-design.de", + "name": "svenfuchs" + } + }, + "commits": [{ + "id": "9854592", + "message": "Bump to 0.0.15", + "timestamp": "2010-10-27 04:32:37", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Christopher Floess", + "email": "chris@flooose.de" + } + }], + "ref": "refs/heads/gh-pages" + }), + + "gh_pages-update" => %({ + "repository": { + "url": "http://github.com/svenfuchs/gem-release", + "name": "gem-release", + "owner": { + "email": "svenfuchs@artweb-design.de", + "name": "svenfuchs" + } + }, + "commits": [{ + "id": "9854592", + "message": "Bump to 0.0.15", + "timestamp": "2010-10-27 04:32:37", + "committer": { + "name": "Sven Fuchs", + "email": "svenfuchs@artweb-design.de" + }, + "author": { + "name": "Christopher Floess", + "email": "chris@flooose.de" + } + }], + "ref": "refs/heads/gh_pages" + }), + + # it is unclear why this payload was send but it happened quite often. the force option + # seems to indicate something like $ git push --force + "force-no-commit" => %({ + "pusher": { "name": "LTe", "email":"lite.88@gmail.com" }, + "repository":{ + "name":"acts-as-messageable", + "created_at":"2010/08/02 07:41:30 -0700", + "has_wiki":true, + "size":200, + "private":false, + "watchers":13, + "fork":false, + "url":"https://github.com/LTe/acts-as-messageable", + "language":"Ruby", + "pushed_at":"2011/05/31 04:16:01 -0700", + "open_issues":0, + "has_downloads":true, + "homepage":"http://github.com/LTe/acts-as-messageable", + "has_issues":true, + "forks":5, + "description":"ActsAsMessageable", + "owner": { "name":"LTe", "email":"lite.88@gmail.com" } + }, + "ref_name":"v0.3.0", + "forced":true, + "after":"b842078c2f0084bb36cea76da3dad09129b3c26b", + "deleted":false, + "ref":"refs/tags/v0.3.0", + "commits":[], + "base_ref":"refs/heads/master", + "before":"0000000000000000000000000000000000000000", + "compare":"https://github.com/LTe/acts-as-messageable/compare/v0.3.0", + "created":true + }), + + + :hook_inactive => %({ + "last_response": { + "status": "ok", + "message": "", + "code": 200 + }, + "config": { + "domain": "staging.travis-ci.org", + "user": "svenfuchs", + "token": "token" + }, + "created_at": "2011-09-18T10:49:06Z", + "events": [ + "push", + "pull_request", + "issue_comment", + "public", + "member" + ], + "active": false, + "updated_at": "2012-08-09T09:32:42Z", + "name": "travis", + "_links": { + "self": { + "href": "https://api.github.com/repos/svenfuchs/minimal/hooks/77103" + } + }, + "id": 77103 + }), + + :hook_active => %({ + "last_response": { + "status": "ok", + "message": "", + "code": 200 + }, + "config": { + "domain": "staging.travis-ci.org", + "user": "svenfuchs", + "token": "token" + }, + "created_at": "2011-09-18T10:49:06Z", + "events": [ + "push", + "pull_request", + "issue_comment", + "public", + "member" + ], + "active": true, + "updated_at": "2012-08-09T09:32:42Z", + "name": "travis", + "_links": { + "self": { + "href": "https://api.github.com/repos/svenfuchs/minimal/hooks/77103" + } + }, + "id": 77103 + }), + + :oauth => { + "uid" => "234423", + "user_info" => { + "name" => "John", + "nickname" => "john", + "email" => "john@email.com" + }, + "credentials" => { + "token" => "1234567890abcdefg" + } + }, + + :oauth_updated => { + "uid" => "234423", + "user_info" => { + "name" => "Johnathan", + "nickname" => "johnathan", + "email" => "johnathan@email.com" + }, + "credentials" => { + "token" => "1234567890abcdefg" + } + } +} + +PAYLOADS[:worker] = { + 'job:test:started' => { 'id' => 1, 'state' => 'started', 'started_at' => '2011-01-01 00:02:00 +0200', 'worker' => 'ruby3.worker.travis-ci.org:travis-ruby-4' }, + 'job:test:log' => { 'id' => 1, 'log' => '... appended' }, + 'job:test:log:1' => { 'id' => 1, 'log' => 'the ' }, + 'job:test:log:2' => { 'id' => 1, 'log' => 'full ' }, + 'job:test:log:3' => { 'id' => 1, 'log' => 'log' }, + 'job:test:finished' => { 'id' => 1, 'state' => 'finished', 'finished_at' => '2011-01-01 00:03:00 +0200', 'result' => 0, 'log' => 'the full log' } +} + +PAYLOADS[:queue] = { + 'job:test:1' => { + :build => { :id => 2, :number => '1.1', :commit => '9854592', :branch => 'master', :config => { :rvm => '1.8.7' } }, + :repository => { :id => 1, :slug => 'svenfuchs/gem-release' }, + :queue => 'builds' + }, + 'job:test:2' => { + :build => { :id => 3, :number => '1.2', :commit => '9854592', :branch => 'master', :config => { :rvm => '1.9.2' } }, + :repository => { :id => 1, :slug => 'svenfuchs/gem-release' }, + :queue => 'builds' + } +} + diff --git a/vendor/travis-core/lib/travis/testing/scenario.rb b/vendor/travis-core/lib/travis/testing/scenario.rb new file mode 100644 index 00000000..6a2daa78 --- /dev/null +++ b/vendor/travis-core/lib/travis/testing/scenario.rb @@ -0,0 +1,115 @@ +module Scenario + class << self + def default + minimal, enginex = repositories :minimal, :enginex + + build :repository => minimal, + :owner => minimal.owner, + :number => 1, + :config => { 'rvm' => ['1.8.7', '1.9.2'], 'gemfile' => ['test/Gemfile.rails-2.3.x', 'test/Gemfile.rails-3.0.x'] }, + :state => 'failed', + :started_at => '2010-11-12 12:00:00', + :finished_at => '2010-11-12 12:00:10', + :commit => { + :commit => '1a738d9d6f297c105ae2', + :ref => 'refs/heads/develop', + :branch => 'master', + :message => 'add Gemfile', + :committer_name => 'Sven Fuchs', + :committer_email => 'svenfuchs@artweb-design.de', + :committed_at => '2010-11-12 11:50:00', + }, + :jobs => [ + { :owner => minimal.owner, :log => Log.new(:content => 'minimal log 1'), :worker => 'ruby3.worker.travis-ci.org:travis-ruby-4' } + ] + + build :repository => minimal, + :owner => minimal.owner, + :number => 2, + :config => { 'rvm' => ['1.8.7', '1.9.2'], 'gemfile' => ['test/Gemfile.rails-2.3.x', 'test/Gemfile.rails-3.0.x'] }, + :state => 'passed', + :started_at => '2010-11-12 12:30:00', + :finished_at => '2010-11-12 12:30:20', + :commit => { + :commit => '91d1b7b2a310131fe3f8', + :ref => 'refs/heads/master', + :branch => 'master', + :message => 'Bump to 0.0.22', + :committer_name => 'Sven Fuchs', + :committer_email => 'svenfuchs@artweb-design.de', + :committed_at => '2010-11-12 12:25:00', + }, + :jobs => [ + { :owner => minimal.owner, :log => Log.new(:content => 'minimal log 2'), :worker => 'ruby3.worker.travis-ci.org:travis-ruby-4' } + ] + + build :repository => minimal, + :owner => minimal.owner, + :number => '3', + :config => { 'rvm' => ['1.8.7', '1.9.2'], 'gemfile' => ['test/Gemfile.rails-2.3.x', 'test/Gemfile.rails-3.0.x'] }, + :state => 'configured', + :started_at => '2010-11-12 13:00:00', + :finished_at => nil, + :commit => { + :commit => 'add057e66c3e1d59ef1f', + :ref => 'refs/heads/master', + :branch => 'master', + :message => 'unignore Gemfile.lock', + :committed_at => '2010-11-12 12:55:00', + :committer_name => 'Sven Fuchs', + :committer_email => 'svenfuchs@artweb-design.de', + }, + :jobs => [ + { :owner => minimal.owner, :log => Log.new(:content => 'minimal log 3.1'), :worker => 'ruby3.worker.travis-ci.org:travis-ruby-4' }, + { :owner => minimal.owner, :log => Log.new(:content => 'minimal log 3.2'), :worker => 'ruby3.worker.travis-ci.org:travis-ruby-4' }, + { :owner => minimal.owner, :log => Log.new(:content => 'minimal log 3.3'), :worker => 'ruby3.worker.travis-ci.org:travis-ruby-4' }, + { :owner => minimal.owner, :log => Log.new(:content => 'minimal log 3.4'), :worker => 'ruby3.worker.travis-ci.org:travis-ruby-4' } + ] + + build :repository => enginex, + :owner => enginex.owner, + :number => 1, + :state => 'failes', + :started_at => '2010-11-11 12:00:00', + :finished_at => '2010-11-11 12:00:05', + :commit => { + :commit => '565294c05913cfc23230', + :branch => 'master', + :ref => 'refs/heads/master', + :message => 'Update Capybara', + :author_name => 'Jose Valim', + :author_email => 'jose@email.com', + :committer_name => 'Jose Valim', + :committer_email => 'jose@email.com', + :committed_at => '2010-11-11 11:55:00', + }, + :jobs => [ + { :owner => enginex.owner, :log => Log.new(:content => 'enginex log 1'), :worker => 'ruby3.worker.travis-ci.org:travis-ruby-4' } + ] + + [minimal, enginex] + end + + def repositories(*names) + names.map { |name| Factory(name) } + end + + def build(attributes) + commit = attributes.delete(:commit) + jobs = attributes.delete(:jobs) + commit = Factory(:commit, commit) + + build = Factory(:build, attributes.merge(:commit => commit)) + build.matrix.each_with_index do |job, ix| + job.update_attributes!(jobs[ix] || {}) + end + + if build.finished? + keys = %w(id number state finished_at started_at) + attributes = keys.inject({}) { |result, key| result.merge(:"last_build_#{key}" => build.send(key)) } + build.repository.update_attributes!(attributes) + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/testing/stubs.rb b/vendor/travis-core/lib/travis/testing/stubs.rb new file mode 100644 index 00000000..f4b08611 --- /dev/null +++ b/vendor/travis-core/lib/travis/testing/stubs.rb @@ -0,0 +1,356 @@ +require 'active_support/core_ext/numeric/time' + +module Travis + module Testing + module Stubs + require 'travis/testing/stubs/stub' + + class << self + include Stub + + def included(base) + base.send(:instance_eval) do + let(:repository) { stub_repo } + let(:repo) { stub_repo } + let(:request) { stub_request } + let(:commit) { stub_commit } + let(:build) { stub_build } + let(:test) { stub_test } + let(:log) { stub_log } + let(:annotation) { stub_annotation } + let(:annotation_provider) { stub_annotation_provider } + let(:event) { stub_event } + let(:worker) { stub_worker } + let(:user) { stub_user } + let(:org) { stub_org } + let(:url) { stub_url } + let(:broadcast) { stub_broadcast } + let(:travis_token) { stub_travis_token } + let(:cache) { stub_cache } + end + end + end + + def stub_repo(attributes = {}) + Stubs.stub 'repository', attributes.reverse_merge( + id: 1, + owner: stub_user(id: 1, login: 'svenfuchs'), + owner_type: 'User', + owner_id: 1, + owner_name: 'svenfuchs', + owner_email: 'svenfuchs@artweb-design.de', + name: 'minimal', + slug: 'svenfuchs/minimal', + description: 'the repo description', + url: 'http://github.com/svenfuchs/minimal', + source_url: 'git://github.com/svenfuchs/minimal.git', + api_url: 'https://api.github.com/repos/svenfuchs/minimal', + key: stub_key, + admin: stub_user, + active: true, + private: false, + private?: false, + last_build_id: 1, + last_build_number: 2, + last_build_started_at: Time.now.utc - 60, + last_build_finished_at: Time.now.utc, + last_build_state: :passed, + last_build_duration: 60, + github_language: 'ruby', + github_id: 549743, + builds_only_with_travis_yml?: false, + settings: stub_settings, + users_with_permission: [], + default_branch: 'master', + current_build_id: nil + ) + end + alias stub_repository stub_repo + + def stub_settings + Stubs.stub 'settings', 'ssh_keys' => [], 'env_vars' => [] + end + + def stub_key(attributes = {}) + Stubs.stub 'key', attributes.reverse_merge( + id: 1, + public_key: '-----BEGIN PUBLIC KEY-----' + ) + end + + def stub_request(attributes = {}) + repo = stub_repository + commit = stub_commit + Stubs.stub 'request', attributes.reverse_merge( + id: 1, + repository: repo, + repository_id: repo.id, + commit: commit, + commit_id: commit.id, + config: {}, + event_type: 'push', + head_commit: 'head-commit', + base_commit: 'base-commit', + token: 'token', + api_request?: false, + pull_request?: false, + comments_url: 'http://github.com/path/to/comments', + config_url: 'https://api.github.com/repos/svenfuchs/minimal/contents/.travis.yml?ref=62aae5f70ceee39123ef', + result: :accepted, + created_at: DateTime.new(2013, 01, 01, 0, 0, 0), + owner_type: 'User', + owner_id: 1, + owner_name: 'svenfuchs', + owner_email: 'svenfuchs@artweb-design.de', + message: 'a message', + branch_name: 'master', + tag_name: '', + pull_request: false, + pull_request_title: nil, + pull_request_number: nil, + head_repo: 'BanzaiMan/travis-core', + head_branch: 'feature-branch', + base_repo: 'travis-ci/travis-core', + base_branch: 'master', + ) + end + + def stub_commit(attributes = {}) + Stubs.stub 'commit', attributes.reverse_merge( + id: 1, + commit: '62aae5f70ceee39123ef', + range: '0cd9ffaab2c4ffee...62aae5f70ceee39123ef', + branch: 'master', + ref: 'refs/master', + message: 'the commit message', + author_name: 'Sven Fuchs', + author_email: 'svenfuchs@artweb-design.de', + committer_name: 'Sven Fuchs', + committer_email: 'svenfuchs@artweb-design.de', + committed_at: Time.now.utc - 3600, + compare_url: 'https://github.com/svenfuchs/minimal/compare/master...develop', + pull_request?: false, + pull_request_number: nil + ) + end + + def stub_build(attributes = {}) + Stubs.stub 'build', attributes.reverse_merge( + id: 1, + repository_id: repository.id, + repository: stub_repository(owner: attributes.delete(:owner)), + request_id: request.id, + request: request, + commit_id: commit.id, + commit: commit, + matrix: attributes[:matrix] || [stub_test(id: 1, number: '2.1'), stub_test(id: 2, number: '2.2')], + matrix_ids: [1, 2], + cached_matrix_ids: [1, 2], + number: 2, + config: { 'rvm' => ['1.8.7', '1.9.2'], 'gemfile' => ['test/Gemfile.rails-2.3.x', 'test/Gemfile.rails-3.0.x'] }, + obfuscated_config: { 'rvm' => ['1.8.7', '1.9.2'], 'gemfile' => ['test/Gemfile.rails-2.3.x', 'test/Gemfile.rails-3.0.x'] }, + state: 'passed', + result: 0, # see build/compat.rb + passed?: true, + failed?: false, + finished?: true, + previous_state: 'passed', + started_at: Time.now.utc - 60, + finished_at: Time.now.utc, + duration: 60, + pull_request?: false, + queue: 'builds.linux', + pull_request_title: nil, + pull_request_number: nil, + secure_env_enabled?: true, + event_type: 'push' + ) + end + + def stub_test(attributes = {}) + log = self.log + annotation = stub_annotation(job_id: 1) + test = Stubs.stub 'test', attributes.reverse_merge( + id: 1, + owner: stub_user, + repository_id: 1, + repository: repository, + source_id: 1, + request_id: 1, + commit_id: commit.id, + commit: commit, + log: log, + log_id: log.id, + annotations: [annotation], + annotation_ids: [annotation.id], + number: '2.1', + config: { 'rvm' => '1.8.7', 'gemfile' => 'test/Gemfile.rails-2.3.x' }, + decrypted_config: { 'rvm' => '1.8.7', 'gemfile' => 'test/Gemfile.rails-2.3.x' }, + obfuscated_config: { 'rvm' => '1.8.7', 'gemfile' => 'test/Gemfile.rails-2.3.x' }, + state: :passed, + result: 0, # see job/compat.rb + started?: true, + finished?: true, + queue: 'builds.linux', + allow_failure: false, + started_at: Time.now.utc - 60, + finished_at: Time.now.utc, + worker: 'ruby3.worker.travis-ci.org:travis-ruby-4', + tags: 'tag-a,tag-b', + log_content: log.content, + ssh_key: nil, + secure_env_enabled?: true + ) + + source = stub_build(:matrix => [test]) + test.define_singleton_method(:source) { source } + test + end + + def stub_log(attributes = {}) + Stubs.stub 'log', attributes.reverse_merge( + class: Stubs.stub('class', name: 'Log'), + id: 1, + job_id: 1, + content: 'the test log' + ) + end + + def stub_log_part(attributes = {}) + Stubs.stub 'log_part', attributes.reverse_merge( + id: 1, + log_id: 1, + content: 'the test log', + number: 1, + final: false + ) + end + + def stub_annotation(attributes = {}) + Stubs.stub 'annotation', attributes.reverse_merge( + class: Stubs.stub('class', name: 'Annotation'), + id: 1, + job_id: attributes[:job_id] || test.id, # Needed to break the infinite loop in stub_test + annotation_provider_id: annotation_provider.id, + annotation_provider: annotation_provider, + description: "The job passed.", + url: "https://travis-ci.org/travis-ci/travis-ci/12345", + status: "" + ) + end + + def stub_annotation_provider(attributes = {}) + Stubs.stub 'annotation_provider', attributes.reverse_merge( + class: Stubs.stub('class', name: 'AnnotationProvider'), + id: 1, + name: "Travis CI", + api_username: "travis-ci", + api_key: "0123456789abcdef", + ) + end + + def stub_event(attributes = {}) + Stubs.stub 'event', attributes.reverse_merge( + id: 1, + repository_id: 1, + source: stub_request, + source_id: stub_request.id, + source_type: 'Request', + event: 'request:finished', + data: { 'result' => 'accepted' }, + created_at: Time.now + ) + end + + def stub_worker(attributes = {}) + Stubs.stub 'worker', attributes.reverse_merge( + id: 1, + name: 'ruby-1', + host: 'ruby-1.worker.travis-ci.org', + queue: 'builds.linux', + state: 'created', + last_seen_at: Time.now.utc, + payload: nil, + ) + end + + def stub_user(attributes = {}) + Stubs.stub 'user', attributes.reverse_merge( + id: 1, + github_id: 1, + organizations: [org], + name: 'Sven Fuchs', + login: 'svenfuchs', + email: 'svenfuchs@artweb-design.de', + gravatar_id: '402602a60e500e85f2f5dc1ff3648ecb', + avatar_url: 'https://0.gravatar.com/avatar/402602a60e500e85f2f5dc1ff3648ecb', + locale: 'de', + github_oauth_token: 'token', + syncing?: false, + is_syncing: false, + synced_at: Time.now.utc - 3600, + tokens: [stub('token', token: 'token')], + github_scopes: Travis.config.oauth2.to_h[:scopes].to_s.split(','), + correct_scopes?: true, + created_at: Time.now.utc - 7200 + ) + end + + def stub_org(attributes = {}) + Stubs.stub 'org', attributes.reverse_merge( + id: 1, + login: 'travis-ci', + name: 'Travis CI', + email: 'contact@travis-ci.org' + ) + end + + def stub_url(attributes = {}) + Stubs.stub 'url', attributes.reverse_merge( + id: 1, + short_url: 'http://trvs.io/short' + ) + end + + def stub_broadcast(attributes = {}) + Stubs.stub 'broadcast', attributes.reverse_merge( + id: 1, + message: 'message' + ) + end + + def stub_travis_token(attributes = {}) + Stubs.stub 'travis_token', attributes.reverse_merge( + id: 1, + user: stub_user, + token: 'super secret' + ) + end + + def stub_cache(attributes = {}) + Stubs.stub 'cache', attributes.reverse_merge( + repository: stub_repository, + size: 1000, + slug: 'cache', + branch: 'master', + last_modified: Time.at(0).utc + ) + end + + def stub_email(attributes = {}) + Stubs.stub 'email', attributes.reverse_merge( + email: 'email' + ) + end + + def stub_job(attributes = {}) + Stubs.stub 'job', attributes.reverse_merge( + repository: stub_repository, + id: '42.1', + enqueue: true + ) + end + end + end +end + diff --git a/vendor/travis-core/lib/travis/testing/stubs/stub.rb b/vendor/travis-core/lib/travis/testing/stubs/stub.rb new file mode 100644 index 00000000..fcad2c9e --- /dev/null +++ b/vendor/travis-core/lib/travis/testing/stubs/stub.rb @@ -0,0 +1,45 @@ +module Travis + module Testing + module Stubs + module Stub + def stub(name, attributes) + Object.new.tap do |object| + meta_class = (class << object; self; end) + class_stub = stub_class(name.camelize) + + attributes.each do |name, value| + meta_class.send(:define_method, name) { |*| value } + end + + meta_class.send(:define_method, :class) do + class_stub + end + + meta_class.send(:define_method, :is_a?) do |const| + const.name.to_s == name.to_s.camelize + end + + meta_class.send(:define_method, :inspect) do + attrs = attributes.map { |name, value| [name, value.inspect].join('=') }.join(' ') + "#<#{name.camelize}:#{object.object_id} #{attrs}>" + end + end + end + + # TODO needs to take care of nested namespaces, so we can pass 'job/test' + def stub_class(name) + if const_defined?(*method(:const_defined?).arity == 1 ? [name] : [name, false]) + const_get(name) + else + Class.new.tap do |const| + const_set(name, const) + meta_class = (class << const; self; end) + meta_class.send(:define_method, :name) { name } + meta_class.send(:define_method, :inspect) { name } + end + end + end + end + end + end +end diff --git a/vendor/travis-core/lib/travis/travis_yml_stats.rb b/vendor/travis-core/lib/travis/travis_yml_stats.rb new file mode 100644 index 00000000..776e9318 --- /dev/null +++ b/vendor/travis-core/lib/travis/travis_yml_stats.rb @@ -0,0 +1,195 @@ +require "sidekiq" + +begin + require "keen" +rescue LoadError +end + +module Travis + class TravisYmlStats + class KeenPublisher + include ::Sidekiq::Worker + + sidekiq_options queue: :keen_events + + def perform(payload, deployment_payload = nil, notification_payload = nil) + if defined?(Keen) && ENV["KEEN_PROJECT_ID"] + payload = { :requests => [payload] } + payload[:deployments] = deployment_payload if deployment_payload.to_a.size > 0 + payload[:notifications] = notification_payload if notification_payload.to_a.size > 0 + Keen.publish_batch(payload) + end + end + end + + LANGUAGE_VERSION_KEYS = %w[ + ghc + go + jdk + node_js + otp_release + perl + php + python + ruby + rvm + scala + ] + + def self.store_stats(request, publisher=KeenPublisher) + new(request, publisher).store_stats + end + + def initialize(request, publisher) + @request = request + @publisher = publisher + @keen_payload = {} + @keen_payload_deployment = [] + @keen_payload_notification = [] + end + + def store_stats + set_basic_info + set_language + set_language_version + set_uses_sudo + set_uses_apt_get + set_dist + set_group + set_deployment_provider_count + set_notification + + @publisher.perform_async(keen_payload, keen_payload_deployment, keen_payload_notification) + end + + private + + attr_reader :request, :keen_payload + attr_accessor :keen_payload_deployment + attr_accessor :keen_payload_notification + + def set(path, value, collection = keen_payload) + path = Array(path) + hsh = collection + path[0..-2].each do |key| + hsh[key.to_sym] ||= {} + hsh = hsh[key.to_sym] + end + + hsh[path.last.to_sym] = value + end + + def set_basic_info + set :event_type, request.event_type + set :matrix_size, request.builds.map { |build| build.matrix.size }.reduce(:+) + set :repository_id, request.repository_id + set :owner_id, request.owner_id + set :owner_type, request.owner_type + # The owner_type, owner_id tuple is there so we can do unique counts on it + set :owner, [request.owner_type, request.owner_id] + end + + def set_language + set :language, travis_yml_language + set :github_language, github_language + end + + def set_language_version + LANGUAGE_VERSION_KEYS.each do |key| + if config.key?(key) + case config[key] + when String, Array + set [:language_version, key], Array(config[key]).map(&:to_s).sort + else + set [:language_version, key], ["invalid"] + end + end + end + end + + def set_uses_sudo + set :uses_sudo, commands.any? { |command| command =~ /\bsudo\b/ } + end + + def set_uses_apt_get + set :uses_apt_get, commands.any? { |command| command =~ /\bapt-get\b/ } + end + + def set_dist + set :dist_name, dist_name + end + + def set_group + set :group_name, group_name + end + + def set_deployment_provider_count + deploy = config["deploy"] || return + # Hash#to_a is not what we want here + deployments = deploy.is_a?(Hash) ? [deploy] : Array(deploy) + deployments.map {|d| d["provider"] }.uniq.each do |provider| + keen_payload_deployment << { provider: provider.downcase, repository_id: request.repository_id } + end + rescue + nil + end + + def set_notification + notifications = config["notifications"] || return + notifications.keys.each do |notifier| + keen_payload_notification << { notifier: notifier.downcase, repository_id: request.repository_id } + end + rescue + nil + end + + def config + request.config + end + + def payload + request.payload.is_a?(String) ? MultiJson.decode(request.payload) : request.payload + end + + def commands + [ + config["before_install"], + config["install"], + config["before_script"], + config["script"], + config["after_success"], + config["after_failure"], + config["before_deploy"], + config["after_deploy"], + ].flatten.compact + end + + def travis_yml_language + language = config["language"] + case language + when String + language + when nil + "default" + else + "invalid" + end + end + + def github_language + payload.fetch("repository", {})["language"] + end + + def normalize_string(str) + str.downcase.gsub("#", "-sharp").gsub(/[^A-Za-z0-9.:\-_]/, "") + end + + def dist_name + config.fetch('dist', 'default') + end + + def group_name + config.fetch('group', 'default') + end + end +end diff --git a/vendor/travis-core/lib/travis_core/version.rb b/vendor/travis-core/lib/travis_core/version.rb new file mode 100644 index 00000000..f8fc62f6 --- /dev/null +++ b/vendor/travis-core/lib/travis_core/version.rb @@ -0,0 +1,3 @@ +module TravisCore + VERSION = "0.0.1" +end diff --git a/vendor/travis-core/travis-core.gemspec b/vendor/travis-core/travis-core.gemspec new file mode 100644 index 00000000..d34586ec --- /dev/null +++ b/vendor/travis-core/travis-core.gemspec @@ -0,0 +1,49 @@ +# encoding: utf-8 + +$:.unshift File.expand_path('../lib', __FILE__) +require 'travis_core/version' + +Gem::Specification.new do |s| + s.name = "travis-core" + s.version = TravisCore::VERSION + s.authors = ["Travis CI"] + s.email = "contact@travis-ci.org" + s.homepage = "https://github.com/travis-ci/travis-core" + s.summary = "The heart of Travis" + + s.files = Dir['{lib/**/*,spec/**/*,[A-Z]*}'] + s.platform = Gem::Platform::RUBY + s.require_path = 'lib' + s.rubyforge_project = '[none]' + + s.add_dependency 'rake' + s.add_dependency 'thor' + s.add_dependency 'activerecord', '~> 3.2.19' + s.add_dependency 'actionmailer', '~> 3.2.19' + s.add_dependency 'railties', '~> 3.2.19' + s.add_dependency 'rollout', '~> 1.1.0' + s.add_dependency 'coder', '~> 0.4.0' + s.add_dependency 'virtus', '~> 1.0.0' + + # travis + s.add_dependency 'travis-config', '~> 0.1.0' + + # db + s.add_dependency 'data_migrations', '~> 0.0.1' + s.add_dependency 'redis', '~> 3.0' + + + # structures + s.add_dependency 'hashr' + s.add_dependency 'metriks', '~> 0.9.7' + + # app + s.add_dependency 'simple_states', '~> 1.0.0' + + # apis + s.add_dependency 'pusher', '~> 0.14.0' + s.add_dependency 's3', '~> 0.3' + s.add_dependency 'gh' + s.add_dependency 'multi_json' + s.add_dependency 'google-api-client', '~> 0.9.4' +end