rcron 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
@@ -0,0 +1,3 @@
1
+ === 0.1.0 / 2011/08/31
2
+ * Initial release
3
+
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "yard", "~> 0.6.0"
10
+ gem "bundler", "~> 1.0.0"
11
+ gem "jeweler", "~> 1.6.4"
12
+ gem "rcov", ">= 0"
13
+ gem "simplecov"
14
+ end
@@ -0,0 +1,26 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ git (1.2.5)
5
+ jeweler (1.6.4)
6
+ bundler (~> 1.0)
7
+ git (>= 1.2.5)
8
+ rake
9
+ rake (0.9.2)
10
+ rcov (0.9.10)
11
+ rcov (0.9.10-java)
12
+ simplecov (0.4.2)
13
+ simplecov-html (~> 0.4.4)
14
+ simplecov-html (0.4.5)
15
+ yard (0.6.8)
16
+
17
+ PLATFORMS
18
+ java
19
+ ruby
20
+
21
+ DEPENDENCIES
22
+ bundler (~> 1.0.0)
23
+ jeweler (~> 1.6.4)
24
+ rcov
25
+ simplecov
26
+ yard (~> 0.6.0)
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Junegunn Choi
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,75 @@
1
+ = rcron
2
+
3
+ A simple cron-like scheduler for Ruby.
4
+
5
+ == Installation
6
+ gem install rcron
7
+
8
+ == Cron format
9
+ As of now, most of the expressions except for ? and W are supported.
10
+
11
+ http://en.wikipedia.org/wiki/Cron#Format
12
+
13
+ == Examples
14
+
15
+ === Basic
16
+ require 'rcron'
17
+ rcron = RCron.new
18
+
19
+ # Enqueue a task
20
+ rcron.q('runs every two minutes', '*/2 * * * *') do |task|
21
+ # Task logic
22
+ # ...
23
+ end
24
+
25
+ # You can `q' any number of tasks before starting rcron
26
+
27
+ rcron.start
28
+
29
+ === One-time only task
30
+ rcron = RCron.new
31
+ rcron.q('will run once at 8pm next second friday', '0 8 * * fri#2') do |task|
32
+ # Removes the task from the queue
33
+ task.dq
34
+
35
+ # Task logic
36
+ # ...
37
+ end
38
+ rcron.start
39
+
40
+ === Options
41
+ rcron = RCron.new
42
+
43
+ # :exclusive - Only one instance of this task will run simultaneously.
44
+ # :timeout - Task will be terminated if it takes longer than the specified seconds.
45
+ rcron.q('Every ten-minutes during summer',
46
+ '*/10 * * jun-aug *',
47
+ :exclusive => true,
48
+ :timeout => 1200) do |task|
49
+ # Task logic
50
+ # ...
51
+ end
52
+
53
+ # log to $stderr instead of default $stdout
54
+ rcron.start $stderr
55
+
56
+ == Notes
57
+ - Minimum interval for each task is one-minute just like cron. So rcron usually sleeps most of the time and wakes up only once a minute. (Except when short timeouts for the tasks are specified. In that case, rcron wakes up more frequently to check whether the task should be terminated)
58
+ - rcron checks to start tasks at the very first second of every minute. e.g. When you first start it at 12:00:45, it will sleep 15 seconds before doing anything.
59
+ - Tested on Ruby 1.8.7, Ruby 1.9.2 and JRuby 1.6.4
60
+ - 99.67% test coverage. (a false sense of security, though)
61
+
62
+ == Contributing to rcron
63
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
64
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
65
+ * Fork the project
66
+ * Start a feature/bugfix branch
67
+ * Commit and push until you are happy with your contribution
68
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
69
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
70
+
71
+ == Copyright
72
+
73
+ Copyright (c) 2011 Junegunn Choi. See LICENSE.txt for
74
+ further details.
75
+
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "rcron"
18
+ gem.homepage = "http://github.com/junegunn/rcron"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{A simple cron-like scheduler}
21
+ gem.description = %Q{A simple cron-like scheduler for Ruby}
22
+ gem.email = "junegunn.c@gmail.com"
23
+ gem.authors = ["Junegunn Choi"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/test_*.rb'
39
+ test.verbose = true
40
+ test.rcov_opts << '--exclude "gems/*"'
41
+ end
42
+
43
+ task :default => :test
44
+
45
+ require 'yard'
46
+ YARD::Rake::YardocTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,5 @@
1
+ require 'rcron/task'
2
+ require 'rcron/alarm'
3
+ require 'rcron/parser'
4
+ require 'rcron/timeout'
5
+ require 'rcron/scheduler'
@@ -0,0 +1,6 @@
1
+ class RCron
2
+ # Custom exception class for waking up scheduler
3
+ class Alarm < Exception
4
+ end#Alarm
5
+ end#RCron
6
+
@@ -0,0 +1,86 @@
1
+ class RCron
2
+ module Parser
3
+ # @param [String] cron Cron-format schedule string
4
+ # @return [Hash] Parsed schedule
5
+ def self.parse(cron)
6
+ elements = cron.strip.split(/\s+/)
7
+ raise ArgumentError.new("Invalid format: '#{cron}'") unless [5, 6].include? elements.length
8
+
9
+ parser = lambda { |type, min, len, element, subs, extra|
10
+ return nil if element.nil?
11
+
12
+ max = min + len
13
+
14
+ (subs || {}).each do |k, v|
15
+ element = element.gsub(/\b#{k}\b/i, v.to_s)
16
+ end
17
+
18
+ ret = element.split(',').map { |e|
19
+ err = ArgumentError.new("Invalid #{type} specification: '#{cron}'")
20
+ case e
21
+ when '*'
22
+ nil
23
+ when %r|^[0-9]+$|
24
+ ei = e.to_i
25
+ raise err if ei < min || ei > max
26
+ ei
27
+ when %r|^\*/([1-9][0-9]*)$|
28
+ (min...max).select { |m| m % $1.to_i == 0 }
29
+ when %r|^([0-9]+)-([0-9]+)$|
30
+ f, t = $1.to_i, $2.to_i
31
+
32
+ raise err if f < min || f > max
33
+ raise err if t < min || t > max
34
+
35
+ if f < t
36
+ (f..t).to_a
37
+ else
38
+ (f...max).to_a + (min..t).to_a
39
+ end
40
+ else
41
+ extra && extra.call(e) || raise(err)
42
+ end
43
+ }.flatten.compact.uniq.inject({}) { |h, k|
44
+ if k.is_a? Hash
45
+ h[k.first.first] = k.first.last
46
+ else
47
+ h[k] = true
48
+ end
49
+ h
50
+ }
51
+ ret.empty? ? nil : ret
52
+ }
53
+
54
+ schedule = {
55
+ :minutes => parser.call('minute', 0, 60, elements[0], nil, nil),
56
+ :hours => parser.call('hour', 0, 24, elements[1], nil, nil),
57
+ :days => parser.call('day/month', 1, 31, elements[2], nil,
58
+ lambda { |e|
59
+ case e.upcase
60
+ when 'L'
61
+ -1
62
+ when /W/
63
+ raise NotImplementedError.new("Nearest weekday not implemeneted: '#{cron}'")
64
+ end
65
+ }),
66
+ :months => parser.call('month', 1, 12, elements[3],
67
+ { 'jan' => 1, 'feb' => 2, 'mar' => 3,
68
+ 'apr' => 4, 'may' => 5, 'jun' => 6,
69
+ 'jul' => 7, 'aug' => 8, 'sep' => 9,
70
+ 'oct' => 10, 'nov' => 11, 'dec' => 12 }, nil),
71
+ :weekdays => parser.call('day/week', 0, 7, elements[4],
72
+ { 'sun' => 0, 'mon' => 1, 'tue' => 2,
73
+ 'wed' => 3, 'thu' => 4, 'fri' => 5, 'sat' => 6 },
74
+ lambda { |e|
75
+ if e =~ /^([0-6]+)#([1-5])$/
76
+ {$1.to_i => $2.to_i}
77
+ end
78
+ }),
79
+ :years => parser.call('year', 1970, 130, elements[5], nil, nil)
80
+ }
81
+ schedule
82
+ end
83
+ end#Parser
84
+ end#RCron
85
+
86
+ #puts RCron::Parser.parse('*/8 * */2,L * sun,wed#2')
@@ -0,0 +1,102 @@
1
+ require 'thread'
2
+
3
+ class RCron
4
+ def initialize
5
+ @tasks = []
6
+ @mutex = Mutex.new
7
+ @sleeping = false
8
+ @log_mutex = Mutex.new
9
+ @log_ostream = $stdout
10
+ end
11
+
12
+ # Enqueues a task to be run
13
+ # @param [String] name Name of the task
14
+ # @param [String] schedule Cron-format schedule string
15
+ # @param [Hash] options Additional options for the task. :exclusive and :timeout.
16
+ # @return [RCron::Task]
17
+ def q name, schedule, options = {}, &block
18
+ raise ArgumentError.new("Block not given") unless block_given?
19
+
20
+ new_task = nil
21
+ @mutex.synchronize do
22
+ @tasks << new_task = Task.send(:new,
23
+ self, name, schedule,
24
+ options[:exclusive], options[:timeout],
25
+ &block)
26
+ end
27
+ return new_task
28
+ end
29
+
30
+ # Starts the scheduler
31
+ # @param log_output_stream Stream to output scheduler log. Should implement puts method.
32
+ def start log_output_stream = $stdout
33
+ raise ArgumentError.new(
34
+ "Log output stream should implement puts method") unless
35
+ log_output_stream.respond_to? :puts
36
+
37
+ @log_ostream = log_output_stream
38
+ @thread = Thread.current
39
+
40
+ log "rcron started"
41
+
42
+ now = Time.now
43
+ while @tasks.length > 0
44
+ # At every minute
45
+ next_tick = Time.at( (now + 60 - now.sec).to_i )
46
+ interval = @tasks.select(&:running?).map(&:timeout).compact.min
47
+ begin
48
+ @mutex.synchronize { @sleeping = true }
49
+ #puts [ next_tick - now, interval ].compact.min
50
+ sleep [ next_tick - now, interval ].compact.min
51
+ @mutex.synchronize { @sleeping = false }
52
+ rescue RCron::Alarm => e
53
+ # puts 'woke up'
54
+ end
55
+
56
+ # Join completed threads
57
+ @tasks.select(&:running?).each do |t|
58
+ t.send :join
59
+ end
60
+
61
+ # Removed dequeued tasks
62
+ @tasks.reject { |e| e.running? || e.queued? }.each do |t|
63
+ @mutex.synchronize { @tasks.delete t }
64
+ end
65
+
66
+ # Start new task threads if it's time
67
+ now = Time.now
68
+ @tasks.select { |e| e.queued? && e.scheduled?(now) }.each do |t|
69
+ if t.running? && t.exclusive?
70
+ log "[#{t.name}] already running exclusively"
71
+ next
72
+ end
73
+
74
+ log "[#{t.name}] started"
75
+ t.send :start, now
76
+ end if now >= next_tick
77
+ end#while
78
+ log "rcron completed"
79
+ end#start
80
+
81
+ # Crontab-like tasklist
82
+ # @return [String]
83
+ def tab
84
+ @tasks.map { |t| "#{t.schedule_expression} #{t.name}" }.join($/)
85
+ end
86
+
87
+ private
88
+ def wake_up
89
+ @mutex.synchronize {
90
+ if @sleeping
91
+ @sleeping = false
92
+ @thread.raise(RCron::Alarm.new)
93
+ end
94
+ }
95
+ end
96
+
97
+ def log msg
98
+ @log_mutex.synchronize do
99
+ @log_ostream.puts "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] #{msg}"
100
+ end
101
+ end
102
+ end#RCron
@@ -0,0 +1,172 @@
1
+ require 'date'
2
+ require 'time'
3
+
4
+ class RCron
5
+ class Task
6
+ # RCron scheduler for this task
7
+ attr_reader :rcron
8
+
9
+ # Name of the task
10
+ attr_reader :name
11
+
12
+ # Cron schedule expression
13
+ attr_reader :schedule_expression
14
+
15
+ # Parsed cron schedule
16
+ attr_reader :schedule
17
+
18
+ # Timeout for the task
19
+ attr_reader :timeout
20
+
21
+ # Threads running this task
22
+ def threads
23
+ @mutex.synchronize { return @threads.dup }
24
+ end
25
+
26
+ # Executes the task manually
27
+ def run
28
+ if @block.arity >= 1
29
+ @block.call self
30
+ else
31
+ @block.call
32
+ end
33
+ end
34
+
35
+ # Returns if the task is being executed by one or more threads
36
+ # @return [Boolean]
37
+ def running?
38
+ @mutex.synchronize {
39
+ return @threads.empty? == false
40
+ }
41
+ end
42
+
43
+ # Returns whether if the same task should not run simultaneously
44
+ # @return [Boolean]
45
+ def exclusive?
46
+ @exclusive
47
+ end
48
+
49
+ # Returns if the task is queued to the scheduler
50
+ # @return [Boolean]
51
+ def queued?
52
+ @queued
53
+ end
54
+
55
+ # Removes the task from the scheduler
56
+ def dq
57
+ @queued = false
58
+ nil
59
+ end
60
+
61
+ # Returns if the task is supposed to be triggered at the given moment.
62
+ # @param [Time] at
63
+ # @return [Boolean]
64
+ def scheduled? at
65
+ if @previous_start.nil? || (at - at.sec).to_i > (@previous_start - @previous_start.sec).to_i
66
+ s, m, h, day, mon, year, wd = at.to_a
67
+
68
+ td = Date.new(year, mon, day) # at.to_date # Doesn't work with current JRuby
69
+ wom = ((td - td.day + 1).wday + td.day - 1) / 7 + 1
70
+ last_day = (td + 1).month > td.month
71
+
72
+ (@schedule[:years].nil? || @schedule[:years].has_key?(year)) &&
73
+ (@schedule[:months].nil? || @schedule[:months].has_key?(mon)) &&
74
+ (@schedule[:weekdays].nil? || [true, wom].include?(@schedule[:weekdays][wd])) &&
75
+ (@schedule[:days].nil? || @schedule[:days].has_key?(day) || (last_day && @schedule[:days].has_key?(-1)) ) &&
76
+ (@schedule[:hours].nil? || @schedule[:hours].has_key?(h)) &&
77
+ (@schedule[:minutes].nil? || @schedule[:minutes].has_key?(m))
78
+ else
79
+ false
80
+ end
81
+ end
82
+
83
+ private
84
+ def start now
85
+ @previous_start = now
86
+ @mutex.synchronize do
87
+ @threads << TaskThread.new(self, now)
88
+ end
89
+ end
90
+
91
+ def join
92
+ @threads.dup.each do |thr|
93
+ if thr.alive?
94
+ # Timeout!
95
+ if @timeout && thr.elapsed > @timeout
96
+ thr.kill!
97
+ @mutex.synchronize { @threads.delete thr }
98
+ end
99
+ else
100
+ # Finished already
101
+ thr.cleanup
102
+ @mutex.synchronize { @threads.delete thr }
103
+ end
104
+ end
105
+ end
106
+
107
+ private
108
+ private_class_method :new
109
+
110
+ def initialize scheduler, name, schedule, exclusive, timeout, &block
111
+ if timeout && (timeout.is_a?(Numeric) == false || timeout < 1)
112
+ raise ArgumentError.new("Invalid timeout: #{timeout} (sec)")
113
+ end
114
+ unless [true, false, nil].include? exclusive
115
+ raise ArgumentError.new("exclusive option must be true or false")
116
+ end
117
+
118
+ @rcron = scheduler
119
+ @name = name
120
+ @schedule = Parser.parse schedule
121
+ @schedule_expression = schedule
122
+ @exclusive = exclusive == true
123
+ @timeout = timeout
124
+ @block = block
125
+ @queued = true
126
+ @threads = []
127
+ @mutex = Mutex.new
128
+
129
+ @previous_start = nil
130
+ end
131
+
132
+ class TaskThread
133
+ attr_reader :started_at, :ended_at
134
+ attr_reader :exception
135
+
136
+ def initialize task, started_at
137
+ @started_at = started_at
138
+ @thread = Thread.new {
139
+ begin
140
+ task.run
141
+ @ended_at = Time.now
142
+
143
+ task.rcron.send :log, "[#{task.name}] completed (#{'%.2f' % elapsed}s)"
144
+ rescue Exception => e
145
+ @ended_at = Time.now
146
+
147
+ task.rcron.send :log, "[#{task.name}] terminated: #{[e, e.backtrace].join($/)}"
148
+ # Ignore exception?
149
+ end
150
+ task.rcron.send :wake_up
151
+ }
152
+ end
153
+
154
+ def cleanup
155
+ @thread.join
156
+ end
157
+
158
+ def elapsed
159
+ (@ended_at || Time.now) - @started_at
160
+ end
161
+
162
+ def alive?
163
+ @thread.alive?
164
+ end
165
+
166
+ def kill!
167
+ @thread.raise RCron::Timeout.new("Timeout: task terminated by rcron", @started_at, Time.now)
168
+ cleanup
169
+ end
170
+ end
171
+ end#Task
172
+ end#RCron
@@ -0,0 +1,14 @@
1
+ class RCron
2
+ # Timeout exception
3
+ class Timeout < Exception
4
+ attr_reader :started_at
5
+ attr_reader :terminated_at
6
+
7
+ def initialize(msg, started_at, terminated_at)
8
+ super(msg)
9
+ @started_at = started_at
10
+ @terminated_at = terminated_at
11
+ end
12
+ end#Timeout
13
+ end#RCron
14
+
@@ -0,0 +1,39 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'simplecov'
4
+ SimpleCov.start
5
+
6
+ begin
7
+ Bundler.setup(:default, :development)
8
+ rescue Bundler::BundlerError => e
9
+ $stderr.puts e.message
10
+ $stderr.puts "Run `bundle install` to install missing gems"
11
+ exit e.status_code
12
+ end
13
+ require 'test/unit'
14
+
15
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
16
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
17
+ require 'rcron'
18
+
19
+ class Test::Unit::TestCase
20
+ end
21
+
22
+ # Overrides timings constructs to boost up the test speed
23
+ # - This might break some test results
24
+ class Time
25
+ class << self
26
+ alias org_now now
27
+ end
28
+ def self.now
29
+ @@first_call ||= Time.org_now
30
+ return @@first_call + (Time.org_now - @@first_call) * 20
31
+ end
32
+ end
33
+
34
+ def sleep n
35
+ Kernel.sleep n * 0.05
36
+ end
37
+
38
+ Thread.abort_on_exception = false # FIXME: Without this, test fails.
39
+
@@ -0,0 +1,176 @@
1
+ $LOAD_PATH << "."
2
+ require 'helper'
3
+
4
+ class TestRcron < Test::Unit::TestCase
5
+ class LogStream
6
+ def initialize
7
+ @lines = []
8
+ end
9
+
10
+ def puts str
11
+ $stdout.puts str
12
+ @lines << str
13
+ end
14
+
15
+ def count pat
16
+ @lines.select { |e| e =~ pat }.count
17
+ end
18
+ end
19
+
20
+ def test_empty_q
21
+ rcron = RCron.new
22
+ log = LogStream.new
23
+ rcron.start(log)
24
+
25
+ assert_equal 1, log.count(/completed/)
26
+ end
27
+
28
+ def test_blockless
29
+ rcron = RCron.new
30
+ assert_raise(ArgumentError) { rcron.q('test task 1', "* * * * *") }
31
+ end
32
+
33
+ def test_invalid_schedule
34
+ rcron = RCron.new
35
+ assert_raise(ArgumentError) { rcron.q('test task 1', "* *") { |task| } }
36
+ end
37
+
38
+ def test_basic_task_dq
39
+ puts 'basic task eq'
40
+ counter = 0
41
+ rcron = RCron.new
42
+ rcron.q('basic task 1 - auto dq', "* * * * *") do |task|
43
+ counter += 1
44
+ sleep 1
45
+ task.dq
46
+ end
47
+
48
+ @task = rcron.q('basic task 2 - auto dq', "* * * * *") do
49
+ counter += 2
50
+ sleep 3
51
+ @task.dq
52
+ end
53
+
54
+ st = Time.now
55
+ rcron.start
56
+ assert_equal 3, counter
57
+ assert Time.now - st > 3
58
+ end
59
+
60
+ def test_basic_task
61
+ puts 'basic task'
62
+ counter = 0
63
+ rcron = RCron.new
64
+ rcron.q('basic task', "* * * * *") do |task|
65
+ task.dq if counter >= 2
66
+
67
+ counter += 1
68
+ sleep 60 + 10
69
+ end
70
+
71
+ st = Time.now
72
+ rcron.start
73
+ assert_equal 3, counter
74
+ assert Time.now - st > 3 * 60
75
+ end
76
+
77
+ def test_dq
78
+ counter = 0
79
+ rcron = RCron.new
80
+ task = rcron.q('never', "* * * * *") do |task|
81
+ counter += 1
82
+ end
83
+
84
+ task.dq
85
+ rcron.start
86
+ assert_equal 0, counter
87
+ end
88
+
89
+ def test_timeout
90
+ puts 'timeout'
91
+ counter = 0
92
+ rcron = RCron.new
93
+ rcron.q('timeout', '*/5 * * * *', :timeout => 10) do |task|
94
+ task.dq # no more
95
+ loop do
96
+ counter += 1
97
+ sleep 1
98
+ end
99
+ end
100
+
101
+ assert_raise(ArgumentError) { rcron.q('inv timeout', '* * * * *', :timeout => -3) { } }
102
+ assert_raise(ArgumentError) { rcron.q('inv timeout', '* * * * *', :timeout => 'not too long') { } }
103
+
104
+ rcron.start
105
+ assert_equal 10, counter
106
+ end
107
+
108
+ def test_non_exclusive
109
+ puts 'non exclusive'
110
+ counter = 0
111
+ log = LogStream.new
112
+ rcron = RCron.new
113
+ truth = true
114
+ rcron.q('non-exclusive', '* * * * *') do |task|
115
+ counter += 1
116
+ truth &&= counter == task.threads.length
117
+ puts task.threads.length
118
+ sleep 60 * 2 + 30
119
+ task.dq
120
+ end
121
+ rcron.start(log)
122
+
123
+ assert truth
124
+ assert_equal 3, counter
125
+ end
126
+
127
+ def test_exclusive
128
+ puts 'exclusive'
129
+ counter = 0
130
+ log = LogStream.new
131
+ rcron = RCron.new
132
+ rcron.q('exclusive', '* * * * *', :exclusive => true) do |task|
133
+ counter += 1
134
+ sleep 60 * 2 + 10
135
+ task.dq
136
+ end
137
+
138
+ assert_raise(ArgumentError) {
139
+ rcron.q('inv exclusive', '* * * * *', :exclusive => 'i guess') { }
140
+ }
141
+ rcron.start(log)
142
+
143
+ assert_equal 1, counter
144
+ assert log.count(/exclusively/i) > 0
145
+ end
146
+
147
+ def test_invalid_output_stream
148
+ rcron = RCron.new
149
+ assert_raise(ArgumentError) { rcron.start("invalid") }
150
+ end
151
+
152
+ def test_tab
153
+ rcron = RCron.new
154
+ rcron.q('task number 1', '* * * * *') {}
155
+ rcron.q('task number 2', '*/2 * * * *') {}
156
+ rcron.q('task number 3', '*/3 * * * *') {}
157
+
158
+ assert_equal ["* * * * * task number 1",
159
+ "*/2 * * * * task number 2",
160
+ "*/3 * * * * task number 3"].join($/), rcron.tab
161
+ end
162
+
163
+ def test_exception
164
+ log = LogStream.new
165
+ rcron = RCron.new
166
+ rcron.q('Exceptional', '* * * * *') do |task|
167
+ task.dq
168
+ sleep 80
169
+ raise Exception.new("this-error")
170
+ end
171
+ rcron.start(log)
172
+
173
+ assert_equal 1, log.count(/this-error/)
174
+ end
175
+ end
176
+
@@ -0,0 +1,88 @@
1
+ $LOAD_PATH << "."
2
+ require 'helper'
3
+
4
+ class TestRcronParser < Test::Unit::TestCase
5
+ def test_invalid_format
6
+ assert_raise(ArgumentError) { RCron::Parser.parse('* * * *') }
7
+ assert_raise(ArgumentError) { RCron::Parser.parse('* * * * * * *') }
8
+ assert_raise(ArgumentError) { RCron::Parser.parse('a b c d e') }
9
+ end
10
+
11
+ def test_asterisk
12
+ RCron::Parser.parse('* * * * *').each do |k, v|
13
+ assert_nil v
14
+ end
15
+
16
+ RCron::Parser.parse('* * * * * *').each do |k, v|
17
+ assert_nil v
18
+ end
19
+ end
20
+
21
+ def test_single_number
22
+ RCron::Parser.parse('1 1 1 1 1 2000').each do |k, v|
23
+ assert_equal(
24
+ case k
25
+ when :minutes
26
+ {1 => true}
27
+ when :hours
28
+ {1 => true}
29
+ when :days
30
+ {1 => true}
31
+ when :months
32
+ {1 => true}
33
+ when :weekdays
34
+ {1 => true}
35
+ when :years
36
+ {2000 => true}
37
+ end, v)
38
+ end
39
+
40
+ # Invalid range
41
+ assert_raise(ArgumentError) { RCron::Parser.parse('61 * * * *') }
42
+ assert_raise(ArgumentError) { RCron::Parser.parse('* 25 * * *') }
43
+ assert_raise(ArgumentError) { RCron::Parser.parse('* * 50 * *') }
44
+ assert_raise(ArgumentError) { RCron::Parser.parse('* * * 14 *') }
45
+ assert_raise(ArgumentError) { RCron::Parser.parse('* * * * 10000') }
46
+ end
47
+
48
+ def test_range
49
+ assert_equal({10 => true, 11 => true, 12 => true, 59 => true, 0 => true, 1 => true, 2 => true},
50
+ RCron::Parser.parse('10-12,59-2 * * * *')[:minutes])
51
+ end
52
+
53
+ def test_dividend
54
+ assert_equal({3 => true, 6 => true, 9 => true, 12 =>true},
55
+ RCron::Parser.parse('* * * */3 *')[:months])
56
+ assert_equal({0 => true, 23 => true, 24 => true, 46 => true, 48 => true},
57
+ RCron::Parser.parse('*/23,*/24 * * * *')[:minutes])
58
+ end
59
+
60
+ def test_months
61
+ assert_equal({1 => true, 2 => true, 3 => true, 6 => true, 7 => true, 8 => true},
62
+ RCron::Parser.parse('* * * jan-MAR,Jun-Aug *')[:months])
63
+ assert_equal((1..12).to_a.sort,
64
+ RCron::Parser.parse('* * * jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec *')[:months].keys.sort)
65
+ end
66
+
67
+ def test_weekdays
68
+ assert_equal([0,1,2,3,4,5,6],
69
+ RCron::Parser.parse('* * * * sun,mon,tue,wed,thu,fri,sat,sun')[:weekdays].keys.sort)
70
+ assert_equal([0,1,2,3,4,5,6],
71
+ RCron::Parser.parse('* * * * SUN,MON,TUE,WED,THU,FRI,SAT,SUN')[:weekdays].keys.sort)
72
+ assert_equal([0,1,2,3,4,6],
73
+ RCron::Parser.parse('* * * * WED-THU,SAT-TUE')[:weekdays].keys.sort)
74
+ end
75
+
76
+ def test_nearest_weekday
77
+ # TODO FIXME TODO
78
+ assert_raise(NotImplementedError) { RCron::Parser.parse('* * W * *') }
79
+ end
80
+
81
+ def test_last_day
82
+ assert_equal([-1, 31], RCron::Parser.parse('* * L,31 * *')[:days].keys.sort)
83
+ end
84
+
85
+ def test_year
86
+ assert_equal([2010,2011,2012,2020], RCron::Parser.parse('* * L,31 * * 2010-2012,2020')[:years].keys.sort)
87
+ end
88
+ end
@@ -0,0 +1,175 @@
1
+ $LOAD_PATH << "."
2
+ require 'helper'
3
+
4
+ class TestRcronTask < Test::Unit::TestCase
5
+ def test_name
6
+ rcron = RCron.new
7
+ name = nil
8
+
9
+ task = rcron.q('my name', '* * * * *') { |t|
10
+ name = t.name
11
+ t.dq
12
+ }
13
+ assert_equal 'my name', task.name
14
+ rcron.start
15
+ assert_equal 'my name', name
16
+ end
17
+
18
+ def test_schedule
19
+ rcron = RCron.new
20
+ schedule = '* * * * *'
21
+ task = rcron.q('test_schedule', schedule) { }
22
+ assert_equal RCron::Parser.parse(schedule), task.schedule
23
+ end
24
+
25
+ def test_rcron
26
+ rcron = RCron.new
27
+ schedule = '* * * * *'
28
+ task = rcron.q('test_rcron', schedule) { }
29
+ assert_equal rcron, task.rcron
30
+ end
31
+
32
+ def test_timeout
33
+ rcron = RCron.new
34
+ task = rcron.q('test_timeout', '* * * * *', :timeout => 100) { }
35
+ assert_equal 100, task.timeout
36
+ end
37
+
38
+ def test_exclusive
39
+ rcron = RCron.new
40
+ task = rcron.q('test_exclusive', '* * * * *', :exclusive => true) { }
41
+ assert_equal true, task.exclusive?
42
+ task = rcron.q('test_exclusive', '* * * * *', :exclusive => false) { }
43
+ assert_equal false, task.exclusive?
44
+ task = rcron.q('test_exclusive', '* * * * *') { }
45
+ assert_equal false, task.exclusive?
46
+ end
47
+
48
+ def test_running
49
+ rcron = RCron.new
50
+ running = nil
51
+ task = rcron.q('test_running', '* * * * *') { |t|
52
+ running = t.running?
53
+ t.dq
54
+ }
55
+ rcron.start
56
+ assert_equal false, task.running?
57
+ assert_equal true, running
58
+ end
59
+
60
+ def test_queued
61
+ rcron = RCron.new
62
+ task = rcron.q('test_queued', '* * * * *') { }
63
+ assert_equal true, task.queued?
64
+ task.dq
65
+ assert_equal false, task.queued?
66
+ end
67
+
68
+ def test_threads
69
+ rcron = RCron.new
70
+ num_threads = nil
71
+ task = rcron.q('test_threads', '* * * * *') { |t|
72
+ p t.threads
73
+ num_threads = t.threads.length
74
+ sleep 60 + 10
75
+ t.dq
76
+ }
77
+ assert_equal 0, task.threads.length
78
+ rcron.start
79
+ assert_equal 2, num_threads
80
+ assert_equal 0, task.threads.length
81
+ end
82
+
83
+ def test_now
84
+ now = Time.parse('2011-08-27 16:16:16')
85
+ rcron = RCron.new
86
+ {
87
+ '* * * * *' => true,
88
+ '0 * * * *' => false,
89
+ '16 * * * *' => true,
90
+ '*/2 * * * *' => true,
91
+ '10-17 * * * *' => true,
92
+ '*/3 * * * *' => false,
93
+ '17-15 * * * *' => false,
94
+
95
+ '* 16 * * *' => true,
96
+ '* */2 * * *' => true,
97
+ '* */4 * * *' => true,
98
+ '* 10-17 * * *' => true,
99
+ '* */3 * * *' => false,
100
+ '* 17-15 * * *' => false,
101
+
102
+ '* * 27 * *' => true,
103
+ '* * */3 * *' => true,
104
+ '* * */9 * *' => true,
105
+ '* * 20-29 * *' => true,
106
+ '* * 20-22,27,*/4 * *' => true,
107
+ '* * L * *' => false,
108
+ '* * 28 * *' => false,
109
+
110
+ '* * * 8 *' => true,
111
+ '* * * */1 *' => true,
112
+ '* * * */2 *' => true,
113
+ '* * * */4 *' => true,
114
+ '* * * */8 *' => true,
115
+ '* * * Aug *' => true,
116
+ '* * * AuG *' => true,
117
+ '* * * 5,7,8 *' => true,
118
+ '* * * 5-9 *' => true,
119
+ '* * * 12-9 *' => true,
120
+ '* * * may-sep *' => true,
121
+ '* * * nov-sep *' => true,
122
+ '* * * jan,dec,aug *' => true,
123
+ '* * * sep *' => false,
124
+ '* * * 9-10 *' => false,
125
+ '* * * */7 *' => false,
126
+
127
+ '* * * * sat' => true,
128
+ '* * * * fri-sun' => true,
129
+ '* * * * mon,tue,sat' => true,
130
+ '* * * * 6' => true,
131
+ '* * * * 5-6' => true,
132
+ '* * * * 5-0' => true,
133
+ '* * * * 1,6' => true,
134
+ '* * * * */1' => true,
135
+ '* * * * */2' => true,
136
+ '* * * * */3' => true,
137
+ '* * * * */6' => true,
138
+ '* * * * tue' => false,
139
+ '* * * * fri' => false,
140
+ '* * * * sun-fri' => false,
141
+ '* * * * 0-5' => false,
142
+
143
+ '* * * * sat#1' => false,
144
+ '* * * * sat#2' => false,
145
+ '* * * * sat#3' => false,
146
+ '* * * * sat#4' => true,
147
+ '* * * * sat#5' => false,
148
+ '* * * * 6#1' => false,
149
+ '* * * * 6#2' => false,
150
+ '* * * * 6#3' => false,
151
+ '* * * * 6#4' => true,
152
+ '* * * * 6#5' => false,
153
+
154
+ '* * * * * 2011' => true,
155
+ '* * * * * */2011' => true,
156
+ '* * * * * 2010-2012' => true,
157
+ '* * * * * 2010' => false,
158
+
159
+ '16 16 27 8 6 2010' => false,
160
+ '16 16 27 8 6 2011' => true,
161
+ '16 16 27 8 sat 2011' => true,
162
+ '16 16 27 aug sat 2010-2012' => true,
163
+ '16 16 27 aug sun 2010-2012' => false,
164
+ '*/8 16 27 aug 5-6 2010-2012' => true,
165
+ '17 16 27 aug sat 2010-2012' => false,
166
+ '16 17 27 aug * 2010-2012' => false,
167
+ '16 16 L aug * 2010-2012' => false
168
+ }.each do |sch, ass|
169
+ task = rcron.q('test_now', sch) { }
170
+ puts sch
171
+ assert_equal ass, task.scheduled?(now)
172
+ end
173
+ end
174
+ end
175
+
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rcron
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Junegunn Choi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-08-31 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: yard
16
+ requirement: &2156282420 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.6.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *2156282420
25
+ - !ruby/object:Gem::Dependency
26
+ name: bundler
27
+ requirement: &2156281700 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 1.0.0
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2156281700
36
+ - !ruby/object:Gem::Dependency
37
+ name: jeweler
38
+ requirement: &2156281080 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 1.6.4
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2156281080
47
+ - !ruby/object:Gem::Dependency
48
+ name: rcov
49
+ requirement: &2156280220 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2156280220
58
+ - !ruby/object:Gem::Dependency
59
+ name: simplecov
60
+ requirement: &2156279280 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *2156279280
69
+ description: A simple cron-like scheduler for Ruby
70
+ email: junegunn.c@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files:
74
+ - LICENSE.txt
75
+ - README.rdoc
76
+ files:
77
+ - .document
78
+ - CHANGELOG.rdoc
79
+ - Gemfile
80
+ - Gemfile.lock
81
+ - LICENSE.txt
82
+ - README.rdoc
83
+ - Rakefile
84
+ - VERSION
85
+ - lib/rcron.rb
86
+ - lib/rcron/alarm.rb
87
+ - lib/rcron/parser.rb
88
+ - lib/rcron/scheduler.rb
89
+ - lib/rcron/task.rb
90
+ - lib/rcron/timeout.rb
91
+ - test/helper.rb
92
+ - test/test_rcron.rb
93
+ - test/test_rcron_parser.rb
94
+ - test/test_rcron_task.rb
95
+ homepage: http://github.com/junegunn/rcron
96
+ licenses:
97
+ - MIT
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ! '>='
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ segments:
109
+ - 0
110
+ hash: -1983967147733122663
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ none: false
113
+ requirements:
114
+ - - ! '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 1.8.6
120
+ signing_key:
121
+ specification_version: 3
122
+ summary: A simple cron-like scheduler
123
+ test_files: []