From 37d6d1e46130f44f2fe05171b814b5682696839c Mon Sep 17 00:00:00 2001
From: Shinya Maeda <gitlab.shinyamaeda@gmail.com>
Date: Fri, 24 Mar 2017 00:18:13 +0900
Subject: [PATCH] basic components

---
 app/models/ci/scheduled_trigger.rb            | 10 +-
 app/services/ci/create_pipeline_service.rb    | 10 +-
 app/workers/scheduled_trigger_worker.rb       |  8 +-
 spec/factories/ci/scheduled_triggers.rb       | 18 +++-
 spec/lib/ci/cron_parser_spec.rb               | 91 +++++++------------
 spec/models/ci/scheduled_trigger_spec.rb      | 31 +++----
 .../ci/create_pipeline_service_spec.rb        |  4 +
 spec/workers/scheduled_trigger_worker_spec.rb | 54 ++++++++++-
 8 files changed, 127 insertions(+), 99 deletions(-)

diff --git a/app/models/ci/scheduled_trigger.rb b/app/models/ci/scheduled_trigger.rb
index 5b1ff7bd7a4..9af274243a5 100644
--- a/app/models/ci/scheduled_trigger.rb
+++ b/app/models/ci/scheduled_trigger.rb
@@ -9,15 +9,13 @@ module Ci
 
     def schedule_next_run!
       next_time = Ci::CronParser.new(cron, cron_time_zone).next_time_from_now
-      update(:next_run_at => next_time) if next_time.present?
-    end
-
-    def valid_ref?
-      true #TODO:
+      if next_time.present?
+        update_attributes(next_run_at: next_time)
+      end
     end
 
     def update_last_run!
-      update(:last_run_at => Time.now)
+      update_attributes(last_run_at: Time.now)
     end
   end
 end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 38a85e9fc42..6e3880e1e63 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -2,14 +2,14 @@ module Ci
   class CreatePipelineService < BaseService
     attr_reader :pipeline
 
-    def execute(ignore_skip_ci: false, save_on_errors: true, trigger_request: nil)
+    def execute(ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, scheduled_trigger: false)
       @pipeline = Ci::Pipeline.new(
         project: project,
         ref: ref,
         sha: sha,
         before_sha: before_sha,
         tag: tag?,
-        trigger_requests: Array(trigger_request),
+        trigger_requests: (scheduled_trigger) ? [] : Array(trigger_request),
         user: current_user
       )
 
@@ -17,8 +17,10 @@ module Ci
         return error('Pipeline is disabled')
       end
 
-      unless trigger_request || can?(current_user, :create_pipeline, project)
-        return error('Insufficient permissions to create a new pipeline')
+      unless scheduled_trigger
+        unless trigger_request || can?(current_user, :create_pipeline, project)
+          return error('Insufficient permissions to create a new pipeline')
+        end
       end
 
       unless branch? || tag?
diff --git a/app/workers/scheduled_trigger_worker.rb b/app/workers/scheduled_trigger_worker.rb
index 7dc17aa4332..5c2f03dee79 100644
--- a/app/workers/scheduled_trigger_worker.rb
+++ b/app/workers/scheduled_trigger_worker.rb
@@ -3,15 +3,15 @@ class ScheduledTriggerWorker
   include CronjobQueue
 
   def perform
-    # TODO: Update next_run_at
-
-    Ci::ScheduledTriggers.where("next_run_at < ?", Time.now).find_each do |trigger|
+    Ci::ScheduledTrigger.where("next_run_at < ?", Time.now).find_each do |trigger|
       begin
-        Ci::CreateTriggerRequestService.new.execute(trigger.project, trigger, trigger.ref)
+        Ci::CreatePipelineService.new(trigger.project, trigger.owner, ref: trigger.ref).
+          execute(ignore_skip_ci: true, scheduled_trigger: true)
       rescue => e
         Rails.logger.error "#{trigger.id}: Failed to trigger job: #{e.message}"
       ensure
         trigger.schedule_next_run!
+        trigger.update_last_run!
       end
     end
   end
diff --git a/spec/factories/ci/scheduled_triggers.rb b/spec/factories/ci/scheduled_triggers.rb
index 9d45f4b4962..c97b2d14bd1 100644
--- a/spec/factories/ci/scheduled_triggers.rb
+++ b/spec/factories/ci/scheduled_triggers.rb
@@ -1,42 +1,58 @@
 FactoryGirl.define do
   factory :ci_scheduled_trigger, class: Ci::ScheduledTrigger do
-    project factory: :empty_project
+    project factory: :project
     owner factory: :user
     ref 'master'
 
+    trait :force_triggable do
+      next_run_at Time.now - 1.month
+    end
+
     trait :cron_nightly_build do
       cron '0 1 * * *'
       cron_time_zone 'Europe/Istanbul'
+      next_run_at do # TODO: Use CronParser
+        time = Time.now.in_time_zone(cron_time_zone)
+        time = time + 1.day if time.hour > 1
+        time = time.change(sec: 0, min: 0, hour: 1)
+        time
+      end
     end
 
     trait :cron_weekly_build do
       cron '0 1 * * 5'
       cron_time_zone 'Europe/Istanbul'
+      # TODO: next_run_at
     end
 
     trait :cron_monthly_build do
       cron '0 1 22 * *'
       cron_time_zone 'Europe/Istanbul'
+      # TODO: next_run_at
     end
 
     trait :cron_every_5_minutes do
       cron '*/5 * * * *'
       cron_time_zone 'Europe/Istanbul'
+      # TODO: next_run_at
     end
 
     trait :cron_every_5_hours do
       cron '* */5 * * *'
       cron_time_zone 'Europe/Istanbul'
+      # TODO: next_run_at
     end
 
     trait :cron_every_5_days do
       cron '* * */5 * *'
       cron_time_zone 'Europe/Istanbul'
+      # TODO: next_run_at
     end
 
     trait :cron_every_5_months do
       cron '* * * */5 *'
       cron_time_zone 'Europe/Istanbul'
+      # TODO: next_run_at
     end
   end
 end
diff --git a/spec/lib/ci/cron_parser_spec.rb b/spec/lib/ci/cron_parser_spec.rb
index 58eb26c9421..f8c7e88edb3 100644
--- a/spec/lib/ci/cron_parser_spec.rb
+++ b/spec/lib/ci/cron_parser_spec.rb
@@ -6,91 +6,62 @@ module Ci
       subject { described_class.new(cron, cron_time_zone).next_time_from_now }
 
       context 'when cron and cron_time_zone are valid' do
-        context 'at 00:00, 00:10, 00:20, 00:30, 00:40, 00:50' do
-          let(:cron) { '*/10 * * * *' }
-          let(:cron_time_zone) { 'US/Pacific' }
+        context 'when specific time' do
+          let(:cron) { '3 4 5 6 *' }
+          let(:cron_time_zone) { 'Europe/London' }
 
-          it 'returns next time from now' do
-            time = Time.now.in_time_zone(cron_time_zone)
-            time = time + 10.minutes
-            time = time.change(sec: 0, min: time.min-time.min%10)
-            is_expected.to eq(time)
+          it 'returns exact time in the future' do
+            expect(subject).to be > Time.now.in_time_zone(cron_time_zone)
+            expect(subject.min).to eq(3)
+            expect(subject.hour).to eq(4)
+            expect(subject.day).to eq(5)
+            expect(subject.month).to eq(6)
           end
         end
 
-        context 'at 10:00, 20:00' do
-          let(:cron) { '0 */10 * * *' }
-          let(:cron_time_zone) { 'US/Pacific' }
+        context 'when specific day of week' do
+          let(:cron) { '* * * * 0' }
+          let(:cron_time_zone) { 'Europe/London' }
 
-          it 'returns next time from now' do
-            time = Time.now.in_time_zone(cron_time_zone)
-            time = time + 10.hours
-            time = time.change(sec: 0, min: 0, hour: time.hour-time.hour%10)
-            is_expected.to eq(time)
+          it 'returns exact day of week in the future' do
+            expect(subject).to be > Time.now.in_time_zone(cron_time_zone)
+            expect(subject.wday).to eq(0)
           end
         end
 
-        context 'when cron is every 10 days' do
-          let(:cron) { '0 0 */10 * *' }
+        context 'when slash used' do
+          let(:cron) { '*/10 */6 */10 */10 *' }
           let(:cron_time_zone) { 'US/Pacific' }
 
-          it 'returns next time from now' do
-            time = Time.now.in_time_zone(cron_time_zone)
-            time = time + 10.days
-            time = time.change(sec: 0, min: 0, hour: 0, day: time.day-time.day%10)
-            is_expected.to eq(time)
+          it 'returns exact minute' do
+            expect(subject).to be > Time.now.in_time_zone(cron_time_zone)
+            expect(subject.min).to be_in([0, 10, 20, 30, 40, 50])
+            expect(subject.hour).to be_in([0, 6, 12, 18])
+            expect(subject.day).to be_in([1, 11, 21, 31])
+            expect(subject.month).to be_in([1, 11])
           end
         end
 
-        context 'when cron is every week 2:00 AM' do
-          let(:cron) { '0 2 * * *' }
+        context 'when range used' do
+          let(:cron) { '0,20,40 * 1-5 * *' }
           let(:cron_time_zone) { 'US/Pacific' }
 
           it 'returns next time from now' do
-            time = Time.now.in_time_zone(cron_time_zone)
-            is_expected.to eq(time.change(sec: 0, min: 0, hour: 2, day: time.day+1))
+            expect(subject).to be > Time.now.in_time_zone(cron_time_zone)
+            expect(subject.min).to be_in([0, 20, 40])
+            expect(subject.day).to be_in((1..5).to_a)
           end
         end
 
         context 'when cron_time_zone is US/Pacific' do
-          let(:cron) { '0 1 * * *' }
+          let(:cron) { '* * * * *' }
           let(:cron_time_zone) { 'US/Pacific' }
 
           it 'returns next time from now' do
-            time = Time.now.in_time_zone(cron_time_zone)
-            is_expected.to eq(time.change(sec: 0, min: 0, hour: 1, day: time.day+1))
-          end
-        end
-
-        context 'when cron_time_zone is Europe/London' do
-          let(:cron) { '0 1 * * *' }
-          let(:cron_time_zone) { 'Europe/London' }
-
-          it 'returns next time from now' do
-            time = Time.now.in_time_zone(cron_time_zone)
-            is_expected.to eq(time.change(sec: 0, min: 0, hour: 1, day: time.day+1))
+            expect(subject).to be > Time.now.in_time_zone(cron_time_zone)
+            expect(subject.utc_offset/60/60).to eq(-7)
           end
         end
-
-        context 'when cron_time_zone is Asia/Tokyo' do
-          let(:cron) { '0 1 * * *' }
-          let(:cron_time_zone) { 'Asia/Tokyo' }
-
-          it 'returns next time from now' do
-            time = Time.now.in_time_zone(cron_time_zone)
-            is_expected.to eq(time.change(sec: 0, min: 0, hour: 1, day: time.day+1))
-          end
-        end
-      end
-
-      context 'when cron is given and cron_time_zone is not given' do
-        let(:cron) { '0 1 * * *' }
-
-        it 'returns next time from now in utc' do
-          obj = described_class.new(cron).next_time_from_now
-          time = Time.now.in_time_zone('UTC')
-          expect(obj).to eq(time.change(sec: 0, min: 0, hour: 1, day: time.day+1))
-        end 
       end
 
       context 'when cron and cron_time_zone are invalid' do
diff --git a/spec/models/ci/scheduled_trigger_spec.rb b/spec/models/ci/scheduled_trigger_spec.rb
index 68ba9c379b8..bb5e969fa44 100644
--- a/spec/models/ci/scheduled_trigger_spec.rb
+++ b/spec/models/ci/scheduled_trigger_spec.rb
@@ -1,5 +1,4 @@
 require 'spec_helper'
-require 'rufus-scheduler' # Included in sidekiq-cron
 
 describe Ci::ScheduledTrigger, models: true do
 
@@ -9,30 +8,22 @@ describe Ci::ScheduledTrigger, models: true do
   end
 
   describe '#schedule_next_run!' do
-    context 'when cron and cron_time_zone are vaild' do
-      context 'when nightly build' do
-        it 'schedules next run' do
-          scheduled_trigger = create(:ci_scheduled_trigger, :cron_nightly_build)
-          scheduled_trigger.schedule_next_run!
-          puts "scheduled_trigger: #{scheduled_trigger.inspect}"
+    subject { scheduled_trigger.schedule_next_run! }
 
-          expect(scheduled_trigger.cron).to be_nil
-        end
-      end
+    let(:scheduled_trigger) { create(:ci_scheduled_trigger, :cron_nightly_build, next_run_at: nil) }
 
-      context 'when weekly build' do
-
-      end
-
-      context 'when monthly build' do
-
-      end
+    it 'updates next_run_at' do
+      is_expected.not_to be_nil
     end
+  end
+
+  describe '#update_last_run!' do
+    subject { scheduled_trigger.update_last_run! }
 
-    context 'when cron and cron_time_zone are invaild' do
-      it 'schedules nothing' do
+    let(:scheduled_trigger) { create(:ci_scheduled_trigger, :cron_nightly_build, last_run_at: nil) }
 
-      end
+    it 'updates last_run_at' do
+      is_expected.not_to be_nil
     end
   end
 end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index d2f0337c260..4e34acc3585 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -214,5 +214,9 @@ describe Ci::CreatePipelineService, services: true do
         expect(Environment.find_by(name: "review/master")).not_to be_nil
       end
     end
+
+    context 'when scheduled_trigger' do
+      # TODO: spec if approved
+    end
   end
 end
diff --git a/spec/workers/scheduled_trigger_worker_spec.rb b/spec/workers/scheduled_trigger_worker_spec.rb
index c17536720a4..ffcb27602a1 100644
--- a/spec/workers/scheduled_trigger_worker_spec.rb
+++ b/spec/workers/scheduled_trigger_worker_spec.rb
@@ -1,11 +1,57 @@
 require 'spec_helper'
 
 describe ScheduledTriggerWorker do
-  subject { described_class.new.perform }
+  let(:worker) { described_class.new }
 
-  context '#perform' do # TODO:
-    it 'does' do
-      is_expected.to be_nil
+  before do
+    stub_ci_pipeline_to_return_yaml_file
+  end
+
+  context 'when there is a scheduled trigger within next_run_at' do
+    before do
+      create(:ci_scheduled_trigger, :cron_nightly_build, :force_triggable)
+      worker.perform
+    end
+
+    it 'creates a new pipeline' do
+      expect(Ci::Pipeline.last.status).to eq('pending')
+    end
+
+    it 'schedules next_run_at' do
+      scheduled_trigger2 = create(:ci_scheduled_trigger, :cron_nightly_build)
+      expect(Ci::ScheduledTrigger.last.next_run_at).to eq(scheduled_trigger2.next_run_at)
+    end
+  end
+
+  context 'when there are no scheduled triggers within next_run_at' do
+    let!(:scheduled_trigger) { create(:ci_scheduled_trigger, :cron_nightly_build) }
+
+    before do
+      worker.perform
+    end
+
+    it 'do not create a new pipeline' do
+      expect(Ci::Pipeline.all).to be_empty
+    end
+
+    it 'do not reschedule next_run_at' do
+      expect(Ci::ScheduledTrigger.last.next_run_at).to eq(scheduled_trigger.next_run_at)
+    end
+  end
+
+  context 'when next_run_at is nil' do
+    let!(:scheduled_trigger) { create(:ci_scheduled_trigger, :cron_nightly_build, next_run_at: nil) }
+
+    before do
+      worker.perform
+    end
+
+    it 'do not create a new pipeline' do
+      expect(Ci::Pipeline.all).to be_empty
+    end
+
+    it 'do not reschedule next_run_at' do
+      expect(Ci::ScheduledTrigger.last.next_run_at).to eq(scheduled_trigger.next_run_at)
     end
   end
 end
-- 
2.30.9