
#
# Specifying rufus-scheduler
#
# Wed Apr 17 06:00:59 JST 2013
#

require 'spec_helper'


describe Rufus::Scheduler do

  describe '#initialize' do

    it 'starts the scheduler thread' do

      scheduler = Rufus::Scheduler.new

      t = Thread.list.find { |t|
        t[:name] == "rufus_scheduler_#{scheduler.object_id}_scheduler"
      }

      expect(t[:rufus_scheduler]).to eq(scheduler)
    end

    it 'sets :name and :rufus_scheduler in the scheduler thread local vars' do

      scheduler = Rufus::Scheduler.new

      expect(scheduler.thread[:name]).to eq(
        "rufus_scheduler_#{scheduler.object_id}_scheduler"
      )
      expect(scheduler.thread[:rufus_scheduler]).to eq(
        scheduler
      )
    end

    it 'accepts a :frequency => integer option' do

      scheduler = Rufus::Scheduler.new(:frequency => 2)

      expect(scheduler.frequency).to eq(2)
    end

    it 'accepts a :frequency => "2h1m" option' do

      scheduler = Rufus::Scheduler.new(:frequency => '2h1m')

      expect(scheduler.frequency).to eq(3600 * 2 + 60)
    end

    it 'accepts a :thread_name option' do

      scheduler = Rufus::Scheduler.new(:thread_name => 'oliphant')

      t = Thread.list.find { |t| t[:name] == 'oliphant' }

      expect(t[:rufus_scheduler]).to eq(scheduler)
    end

    #it 'accepts a :min_work_threads option' do
    #  scheduler = Rufus::Scheduler.new(:min_work_threads => 9)
    #  scheduler.min_work_threads.should == 9
    #end

    it 'accepts a :max_work_threads option' do

      scheduler = Rufus::Scheduler.new(:max_work_threads => 9)

      expect(scheduler.max_work_threads).to eq(9)
    end
  end

  before :each do
    @scheduler = Rufus::Scheduler.new
  end
  after :each do
    @scheduler.shutdown
  end

  describe 'a schedule method' do

    it 'passes the job to its block when it triggers' do

      j = nil
      job = @scheduler.schedule_in('0s') { |jj| j = jj }

      sleep 0.4

      expect(j).to eq(job)
    end

    it 'passes the trigger time as second block argument' do

      t = nil
      @scheduler.schedule_in('0s') { |jj, tt| t = tt }

      sleep 0.4

      expect(t.class).to eq(EtOrbi::EoTime)
    end

    class MyHandler
      attr_reader :counter
      def initialize
        @counter = 0
      end
      def call(job, time)
        @counter = @counter + 1
      end
    end

    it 'accepts a callable object instead of a block' do

      mh = MyHandler.new

      @scheduler.schedule_in('0s', mh)

      sleep 0.4

      expect(mh.counter).to eq(1)
    end

    class MyOtherHandler
      attr_reader :counter
      def initialize
        @counter = 0
      end
      def call
        @counter = @counter + 1
      end
    end

    it 'accepts a callable obj instead of a block (#call with no args)' do

      job = @scheduler.schedule_in('0s', MyOtherHandler.new)

      sleep 0.4

      expect(job.handler.counter).to eq(1)
    end

    it 'accepts a class as callable' do

      job =
        @scheduler.schedule_in('0s', Class.new do
          attr_reader :value
          def call
            @value = 7
          end
        end)

      sleep 0.4

      expect(job.handler.value).to eq(7)
    end

    it 'raises if the scheduler is shutting down' do

      @scheduler.shutdown

      expect {
        @scheduler.in('0s') { puts 'hhhhhhhhhhhello!!' }
      }.to raise_error(Rufus::Scheduler::NotRunningError)
    end
  end

  describe '#in / #at' do

    # scheduler.in(2.hours.from_now) { ... }

    it 'accepts point in time and duration indifferently (#in)' do

      seen = false

      t = Time.now + 1

      @scheduler.in(t) { seen = true }

      sleep 0.1 while seen != true
    end

    it 'accepts point in time and duration indifferently (#at)' do

      seen = false

      t = 1

      @scheduler.at(t) { seen = true }

      sleep 0.1 while seen != true
    end
  end

  describe '#schedule' do

    it 'accepts a duration and schedules an InJob' do

      j = @scheduler.schedule '1s' do; end

      expect(j.class).to eq(Rufus::Scheduler::InJob)
      expect(j.original).to eq('1s')
    end

    it 'accepts a point in time and schedules an AtJob' do

      j = @scheduler.schedule '2070/12/24 23:00' do; end

      expect(j.class).to eq(Rufus::Scheduler::AtJob)
      expect(j.next_time.strftime('%Y %m %d')).to eq('2070 12 24')
    end

    it 'accepts a cron string and schedules a CronJob' do

      j = @scheduler.schedule '* * * * *' do; end

      expect(j.class).to eq(Rufus::Scheduler::CronJob)
    end
  end

  describe '#repeat' do

    it 'accepts a duration and schedules an EveryJob' do

      j = @scheduler.repeat '1s' do; end

      expect(j.class).to eq(Rufus::Scheduler::EveryJob)
    end

    it 'accepts a cron string and schedules a CronJob' do

      j = @scheduler.repeat '* * * * *' do; end

      expect(j.class).to eq(Rufus::Scheduler::CronJob)
    end
  end

  describe '#unschedule(job_or_work_id)' do

    it 'accepts job ids' do

      job = @scheduler.schedule_in '10d' do; end

      expect(job.unscheduled_at).to eq(nil)

      @scheduler.unschedule(job.id)

      expect(job.unscheduled_at).not_to eq(nil)
    end

    it 'accepts jobs' do

      job = @scheduler.schedule_in '10d' do; end

      expect(job.unscheduled_at).to eq(nil)

      @scheduler.unschedule(job)

      expect(job.unscheduled_at).not_to eq(nil)
    end

    it 'carefully unschedules repeat jobs' do

      counter = 0

      job =
        @scheduler.schedule_every '0.5s' do
          counter = counter + 1
        end

      sleep 1.5
      c = counter

      @scheduler.unschedule(job)

      sleep 1.5
      expect(counter).to eq(c)
    end
  end

  describe '#uptime' do

    it 'returns the uptime as a float' do

      expect(@scheduler.uptime).to be >= 0.0
    end
  end

  describe '#uptime_s' do

    it 'returns the uptime as a human readable string' do

      sleep 1

      expect(@scheduler.uptime_s).to match(/^[12]s\d+$/)
    end
  end

  describe '#join' do

    it 'joins the scheduler thread' do

      t = Thread.new { @scheduler.join; Thread.current['a'] = 'over' }

      expect(t['a']).to eq(nil)

      @scheduler.shutdown

      sleep(1)

      expect(t['a']).to eq('over')
    end
  end

  describe '#job(job_id)' do

    it 'returns nil if there is no corresponding Job instance' do

      expect(@scheduler.job('nada')).to eq(nil)
    end

    it 'returns the corresponding Job instance' do

      job_id = @scheduler.in '10d' do; end

      sleep(1) # give it some time to get scheduled

      expect(@scheduler.job(job_id).job_id).to eq(job_id)
    end
  end

#    describe '#find_by_tag(t)' do
#
#      it 'returns an empty list when there are no jobs with the given tag' do
#
#        @scheduler.find_by_tag('nada').should == []
#      end
#
#      it 'returns all the jobs with the given tag' do
#
#        @scheduler.in '10d', :tag => 't0' do; end
#        @scheduler.every '2h', :tag => %w[ t0 t1 ] do; end
#        @scheduler.every '3h' do; end
#
#        @scheduler.find_by_tag('t0').map(&:original).should ==
#          %w[ 2h 10d ]
#        @scheduler.find_by_tag('t1').map(&:original).should ==
#          %w[ 2h ]
#        @scheduler.find_by_tag('t1', 't0').map(&:original).sort.should ==
#          %w[ 2h ]
#      end
#    end

  describe '#threads' do

    it 'just lists the main thread (scheduler thread) when no job is scheduled' do

      expect(@scheduler.threads).to eq([ @scheduler.thread ])
    end

    it 'lists all the threads a scheduler uses' do

      @scheduler.in '0s' do
        sleep(2)
      end

      sleep 0.4

      expect(@scheduler.threads.size).to eq(2)
    end
  end

  describe '#work_threads(:all | :vacant)' do

    it 'returns an empty array when the scheduler has not yet done anything' do

      expect(@scheduler.work_threads).to eq([])
      expect(@scheduler.work_threads(:all)).to eq([])
      expect(@scheduler.work_threads(:vacant)).to eq([])
    end

    it 'lists the [vacant] work threads in the pool' do

      @scheduler.in '0s' do
        sleep(0.2)
      end
      @scheduler.in '0s' do
        sleep(2.0)
      end

      sleep 0.7

      if @scheduler.work_threads.size == 1
        expect(@scheduler.work_threads.size).to eq(1)
        expect(@scheduler.work_threads(:all).size).to eq(1)
        expect(@scheduler.work_threads(:vacant).size).to eq(0)
      else
        expect(@scheduler.work_threads.size).to eq(2)
        expect(@scheduler.work_threads(:all).size).to eq(2)
        expect(@scheduler.work_threads(:vacant).size).to eq(1)
      end
    end
  end

  describe '#work_threads(:active)' do

    it 'returns [] when there are no jobs running' do

      expect(@scheduler.work_threads(:active)).to eq([])
    end

    it 'returns the list of threads of the running jobs' do

      job =
        @scheduler.schedule_in('0s') do
          sleep 1
        end

      sleep 0.49

      expect(@scheduler.work_threads(:active).size).to eq(1)

      t = @scheduler.work_threads(:active).first

      expect(t.class).to eq(Thread)
      expect(t[@scheduler.thread_key]).to eq(true)
      expect(t[:rufus_scheduler_job]).to eq(job)
      expect(t[:rufus_scheduler_time]).not_to eq(nil)
    end

    it 'does not return threads from other schedulers' do

      scheduler = Rufus::Scheduler.new

      job =
        @scheduler.schedule_in('0s') do
          sleep(1)
        end

      sleep 0.4

      expect(scheduler.work_threads(:active)).to eq([])

      scheduler.shutdown
    end
  end

  #describe '#min_work_threads' do
  #  it 'returns the min job thread count' do
  #    @scheduler.min_work_threads.should == 3
  #  end
  #end
  #describe '#min_work_threads=' do
  #  it 'sets the min job thread count' do
  #    @scheduler.min_work_threads = 1
  #    @scheduler.min_work_threads.should == 1
  #  end
  #end

  describe '#max_work_threads' do

    it 'returns the max job thread count' do

      expect(@scheduler.max_work_threads).to eq(28)
    end
  end

  describe '#max_work_threads=' do

    it 'sets the max job thread count' do

      @scheduler.max_work_threads = 14

      expect(@scheduler.max_work_threads).to eq(14)
    end
  end

  #describe '#kill_all_work_threads' do
  #
  #  it 'kills all the work threads' do
  #
  #    @scheduler.in '0s' do; sleep(5); end
  #    @scheduler.in '0s' do; sleep(5); end
  #    @scheduler.in '0s' do; sleep(5); end
  #
  #    sleep 0.5
  #
  #    @scheduler.work_threads.size.should == 3
  #
  #    @scheduler.send(:kill_all_work_threads)
  #
  #    sleep 0.5
  #
  #    @scheduler.work_threads.size.should == 0
  #  end
  #end

  describe '#running_jobs' do

    it 'returns [] when there are no running jobs' do

      expect(@scheduler.running_jobs).to eq([])
    end

    it 'returns a list of running Job instances' do

      job =
        @scheduler.schedule_in('0s') do
          sleep(1)
        end

      sleep 0.4

      expect(job.running?).to eq(true)
      expect(@scheduler.running_jobs).to eq([ job ])
    end

    it 'does not return twice the same job' do

      job =
        @scheduler.schedule_every('0.3s') do
          sleep(5)
        end

      sleep 1.5

      expect(job.running?).to eq(true)
      expect(@scheduler.running_jobs).to eq([ job ])
    end
  end

  describe '#running_jobs(:tag/:tags => x)' do

    it 'returns a list of running jobs filtered by tag' do

      @scheduler.in '0.1s', :tag => 't0' do
        sleep 3
      end
      @scheduler.in '0.2s', :tag => 't1' do
        sleep 3
      end

      sleep 1

      expect(@scheduler.running_jobs(:tag => 't0').map(&:original)).to eq(
        %w[ 0.1s ]
      )
      expect(@scheduler.running_jobs(:tag => 't1').map(&:original)).to eq(
        %w[ 0.2s ]
      )
      expect(@scheduler.running_jobs(:tags => %w[ t0 t1 ]).map(&:original)).to eq(
        []
      )
    end
  end

  describe '#occurrences(time0, time1)' do

    it 'returns a { job => [ times ] } of job occurrences' do

      j0 = @scheduler.schedule_in '7m' do; end
      j1 = @scheduler.schedule_at '10m' do; end
      j2 = @scheduler.schedule_every '5m' do; end
      j3 = @scheduler.schedule_interval '5m' do; end
      j4 = @scheduler.schedule_cron '* * * * *' do; end

      h = @scheduler.occurrences(Time.now + 4 * 60, Time.now + 11 * 60)

      expect(h.size).to eq(5)
      expect(h[j0]).to eq([ j0.next_time ])
      expect(h[j1]).to eq([ j1.next_time ])
      expect(h[j2].size).to eq(2)
      expect(h[j3].size).to eq(2)
      expect(h[j4].size).to eq(7)
    end

    it 'returns a [ [ time, job ], ... ] of job occurrences when :timeline' do

      j0 = @scheduler.schedule_in '5m' do; end
      j1 = @scheduler.schedule_in '10m' do; end

      a =
        @scheduler.occurrences(Time.now + 4 * 60, Time.now + 11 * 60, :timeline)

      expect(a[0][0]).to be_within_1s_of(Time.now + 5 * 60)
      expect(a[0][1]).to eq(j0)
      expect(a[1][0]).to be_within_1s_of(Time.now + 10 * 60)
      expect(a[1][1]).to eq(j1)
    end

    it 'respects :first_at for repeat jobs' do

      j0 = @scheduler.schedule_every '5m', :first_in => '10m' do; end

      h = @scheduler.occurrences(Time.now + 4 * 60, Time.now + 16 * 60)

      expect(h[j0][0]).to be_within_1s_of(Time.now + 10 * 60)
      expect(h[j0][1]).to be_within_1s_of(Time.now + 15 * 60)
    end

    it 'respects :times for repeat jobs' do

      j0 = @scheduler.schedule_every '1m', :times => 10 do; end

      t0 = Time.parse((Time.now + 5 * 60).strftime('%Y-%m-%d %H:%M:01'))
      t1 = t0 + 12 * 60 - 1

      h = @scheduler.occurrences(t0, t1)

      expect(h[j0].size).to eq(6)
    end
  end

  describe '#timeline' do

    it 'returns a [ [ time, job ], ... ] of job occurrences' do

      j0 = @scheduler.schedule_in '5m' do; end
      j1 = @scheduler.schedule_in '10m' do; end

      a = @scheduler.timeline(Time.now + 4 * 60, Time.now + 11 * 60)

      expect(a[0][0]).to be_within_1s_of(Time.now + 5 * 60)
      expect(a[0][1]).to eq(j0)
      expect(a[1][0]).to be_within_1s_of(Time.now + 10 * 60)
      expect(a[1][1]).to eq(j1)
    end

    it 'should not lock when running timeline with a :first_at specified' do

      now = Time.now

      # scheduling a cron job with a first_at and running #timeline used
      # to result in an infinite loop.

      @scheduler.cron('* * * * * *', :first_at => now + 3) {}

      jobs = @scheduler.timeline(now, now + 4)
      expect(jobs.size).to be 2
      expect(jobs[0][0]).to be_within_1s_of now + 3
      expect(jobs[1][0]).to be_within_1s_of now + 4
    end
  end

  #--
  # management methods
  #++

  describe '#shutdown' do

    it 'blanks the uptime' do

      @scheduler.shutdown

      expect(@scheduler.uptime).to eq(nil)
    end

    it 'shuts the scheduler down' do

      @scheduler.shutdown

      sleep 0.100
      sleep 0.400 if RUBY_VERSION < '1.9.0'

      t = Thread.list.find { |t|
        t[:name] == "rufus_scheduler_#{@scheduler.object_id}"
      }

      expect(t).to eq(nil)
    end

    it 'has a #stop alias' do

      @scheduler.stop

      expect(@scheduler.uptime).to eq(nil)
    end

    #it 'has a #close alias'
  end

  describe '#shutdown(:wait)' do

    it 'shuts down and blocks until all the jobs ended their current runs' do

      counter = 0

      @scheduler.in '0s' do
        sleep 1
        counter = counter + 1
      end

      sleep 0.4

      @scheduler.shutdown(:wait)

      expect(counter).to eq(1)
      expect(@scheduler.uptime).to eq(nil)
      expect(@scheduler.running_jobs).to eq([])
      expect(@scheduler.threads).to eq([])
    end
  end

  describe '#shutdown(:kill)' do

    it 'kills all the jobs and then shuts down' do

      counter = 0

      @scheduler.in '0s' do
        sleep 1
        counter = counter + 1
      end
      @scheduler.at Time.now + 0.3 do
        sleep 1
        counter = counter + 1
      end

      sleep 0.4

      @scheduler.shutdown(:kill)

      sleep 1.4

      expect(counter).to eq(0)
      expect(@scheduler.uptime).to eq(nil)
      expect(@scheduler.running_jobs).to eq([])
      expect(@scheduler.threads).to eq([])
    end
  end

  describe '#pause' do

    it 'pauses the scheduler' do

      job = @scheduler.schedule_in '1s' do; end

      @scheduler.pause

      sleep(3)

      expect(job.last_time).to eq(nil)
    end
  end

  describe '#resume' do

    it 'works' do

      job = @scheduler.schedule_in '2s' do; end

      @scheduler.pause
      sleep(1)
      @scheduler.resume
      sleep(2)

      expect(job.last_time).not_to eq(nil)
    end
  end

  describe '#paused?' do

    it 'returns true if the scheduler is paused' do

      @scheduler.pause
      expect(@scheduler.paused?).to eq(true)
    end

    it 'returns false if the scheduler is not paused' do

      expect(@scheduler.paused?).to eq(false)

      @scheduler.pause
      @scheduler.resume

      expect(@scheduler.paused?).to eq(false)
    end
  end

  describe '#down?' do

    it 'returns true when the scheduler is down' do

      @scheduler.shutdown

      expect(@scheduler.down?).to eq(true)
    end

    it 'returns false when the scheduler is up' do

      expect(@scheduler.down?).to eq(false)
    end
  end

  describe '#up?' do

    it 'returns true when the scheduler is up' do

      expect(@scheduler.up?).to eq(true)
    end
  end

  #--
  # job methods
  #++

  describe '#jobs' do

    it 'is empty at the beginning' do

      expect(@scheduler.jobs).to eq([])
    end

    it 'returns the list of scheduled jobs' do

      @scheduler.in '10d' do; end
      @scheduler.in '1w' do; end

      sleep(1)

      jobs = @scheduler.jobs

      expect(jobs.collect { |j| j.original }.sort).to eq(%w[ 10d 1w ])
    end

    it 'returns all the jobs (even those pending reschedule)' do

      @scheduler.in '0s', :blocking => true do
        sleep 2
      end

      sleep 0.4

      expect(@scheduler.jobs.size).to eq(1)
    end

    it 'does not return unscheduled jobs' do

      job =
        @scheduler.schedule_in '0s', :blocking => true do
          sleep 2
        end

      sleep 0.4

      job.unschedule

      expect(@scheduler.jobs.size).to eq(0)
    end
  end

  describe '#jobs(:tag / :tags => x)' do

    it 'returns [] when there are no jobs with the corresponding tag' do

      expect(@scheduler.jobs(:tag => 'nada')).to eq([])
      expect(@scheduler.jobs(:tags => %w[ nada hello ])).to eq([])
    end

    it 'returns the jobs with the corresponding tag' do

      @scheduler.in '10d', :tag => 't0' do; end
      @scheduler.every '2h', :tag => %w[ t0 t1 ] do; end
      @scheduler.every '3h' do; end

      expect(@scheduler.jobs(:tags => 't0').map(&:original).sort).to eq(
        %w[ 10d 2h ]
      )
      expect(@scheduler.jobs(:tags => 't1').map(&:original).sort).to eq(
        %w[ 2h ]
      )
      expect(@scheduler.jobs(:tags => [ 't1', 't0' ]).map(&:original).sort).to eq(
        %w[ 2h ]
      )
    end
  end

  describe '#every_jobs' do

    it 'returns EveryJob instances' do

      @scheduler.at '2030/12/12 12:10:00' do; end
      @scheduler.in '10d' do; end
      @scheduler.every '5m' do; end

      jobs = @scheduler.every_jobs

      expect(jobs.collect { |j| j.original }.sort).to eq(%w[ 5m ])
    end
  end

  describe '#at_jobs' do

    it 'returns AtJob instances' do

      @scheduler.at '2030/12/12 12:10:00' do; end
      @scheduler.in '10d' do; end
      @scheduler.every '5m' do; end

      jobs = @scheduler.at_jobs

      expect(jobs.collect { |j| j.original }.sort).to eq([ '2030/12/12 12:10:00' ])
    end
  end

  describe '#in_jobs' do

    it 'returns InJob instances' do

      @scheduler.at '2030/12/12 12:10:00' do; end
      @scheduler.in '10d' do; end
      @scheduler.every '5m' do; end

      jobs = @scheduler.in_jobs

      expect(jobs.collect { |j| j.original }.sort).to eq(%w[ 10d ])
    end
  end

  describe '#cron_jobs' do

    it 'returns CronJob instances' do

      @scheduler.at '2030/12/12 12:10:00' do; end
      @scheduler.in '10d' do; end
      @scheduler.every '5m' do; end
      @scheduler.cron '* * * * *' do; end

      jobs = @scheduler.cron_jobs

      expect(jobs.collect { |j| j.original }.sort).to eq([ '* * * * *' ])
    end
  end

  describe '#interval_jobs' do

    it 'returns IntervalJob instances' do

      @scheduler.at '2030/12/12 12:10:00' do; end
      @scheduler.in '10d' do; end
      @scheduler.every '5m' do; end
      @scheduler.cron '* * * * *' do; end
      @scheduler.interval '7m' do; end

      jobs = @scheduler.interval_jobs

      expect(jobs.collect { |j| j.original }.sort).to eq(%w[ 7m ])
    end
  end

  #--
  # callbacks
  #++

  describe '#on_pre_trigger' do

    it 'is called right before a job triggers' do

      $out = []

      def @scheduler.on_pre_trigger(job)
        $out << "pre #{job.id}"
      end

      job_id =
        @scheduler.in '0.5s' do |job|
          $out << job.id
        end

      sleep 0.7

      expect($out).to eq([ "pre #{job_id}", job_id ])
    end

    it 'accepts the job and the triggerTime as argument' do

      $tt = nil

      def @scheduler.on_pre_trigger(job, trigger_time)
        $tt = trigger_time
      end

      start = Time.now

      @scheduler.in '0.5s' do; end

      sleep 0.7

      expect($tt.class).to eq(EtOrbi::EoTime)
      expect($tt).to be > start
      expect($tt).to be < Time.now
    end

    context 'when it returns false' do

      it 'prevents the job from triggering' do

        $out = []

        def @scheduler.on_pre_trigger(job)
          $out << "pre #{job.id}"
          false
        end

        job_id =
          @scheduler.in '0.5s' do |job|
            $out << job.id
          end

        sleep 0.7

        expect($out).to eq([ "pre #{job_id}" ])
      end
    end
  end

  describe '#on_post_trigger' do

    it 'is called right after a job triggers' do

      $out = []

      def @scheduler.on_post_trigger(job)
        $out << "post #{job.id}"
      end

      job_id =
        @scheduler.in '0.5s' do |job|
          $out << job.id
        end

      sleep 0.7

      expect($out).to eq([ job_id, "post #{job_id}" ])
    end
  end

  #--
  # misc
  #++

  describe '.singleton / .s' do

    before(:each) do

      Rufus::Scheduler.class_eval { @singleton = nil } # ;-)
    end

    it 'returns a singleton instance of the scheduler' do

      s0 = Rufus::Scheduler.singleton
      s1 = Rufus::Scheduler.s

      expect(s0.class).to eq(Rufus::Scheduler)
      expect(s1.object_id).to eq(s0.object_id)
    end

    it 'accepts initialization parameters' do

      s = Rufus::Scheduler.singleton(:max_work_threads => 77)
      s = Rufus::Scheduler.singleton(:max_work_threads => 42)

      expect(s.max_work_threads).to eq(77)
    end
  end
end

