├── assets ├── screenshot.png └── container.haml ├── MIT-LICENSE.txt ├── run.rb ├── lib ├── cron_job.rb ├── cron_parser.rb └── crontab.rb ├── README.md └── test └── cronviz_test.rb /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federatedmedia/cronviz/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Federated Media Publishing, Inc., http://www.federatedmedia.net/ 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. 21 | -------------------------------------------------------------------------------- /run.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'json' 3 | require 'haml' 4 | 5 | require './lib/crontab' 6 | require './lib/cron_job' 7 | require './lib/cron_parser' 8 | 9 | 10 | EVENT_DATA = { 11 | :default => { 12 | "color" => "#7FFFD4", 13 | "textColor" => "#000000", 14 | "classname" => "default", 15 | "durationEvent" => false}, 16 | 17 | :every_minute => { 18 | "title_prefix" => "Every minute: ", 19 | "color" => "#f00", 20 | "durationEvent" => true}, 21 | 22 | :every_five_minutes => { 23 | "title_prefix" => "Every five minutes: ", 24 | "color" => "#fa0", 25 | "durationEvent" => true} 26 | } 27 | 28 | 29 | def main 30 | earliest_time = "2011-10-17 00:00" 31 | latest_time = "2011-10-17 23:59" 32 | 33 | json = Cronviz::Crontab.new(:earliest_time=>earliest_time, :latest_time=>latest_time, :event_data=>EVENT_DATA).to_json 34 | haml = open("assets/container.haml").read 35 | html = Haml::Engine.new(haml).render(Object.new, 36 | :earliest_time => earliest_time, 37 | :latest_time => latest_time, 38 | :cron_json => json) 39 | 40 | open("output.html", "w") do |f| 41 | f.write html 42 | end 43 | print "./output.html successfully created!" 44 | 45 | end 46 | 47 | main() 48 | -------------------------------------------------------------------------------- /lib/cron_job.rb: -------------------------------------------------------------------------------- 1 | module Cronviz 2 | class CronJob 3 | attr_reader :events, :command, :times 4 | 5 | # Take a hash describing a job and massage its contents to produce 6 | # event data for the SIMILE timeline widget. 7 | def initialize options 8 | @times = options[:times] 9 | @command = options[:command] 10 | @event_data = options[:event_data] 11 | merge_event_data options 12 | end 13 | 14 | def merge_event_data options 15 | @events = [] 16 | 17 | if @event_data.keys.include? options[:times].size 18 | # Rollup */1 and */5 jobs into a single job for display purposes. 19 | seed = @event_data[options[:times].size] 20 | data = { 21 | "start" => options[:times][0].iso8601, 22 | "end" => options[:times][-1].iso8601, 23 | "title" => "#{seed['title_prefix']}#{@command}", 24 | "description" => "#{seed['title_prefix']}#{@command}"} 25 | @events = [@event_data[:default].merge(seed).merge(data)] 26 | else 27 | options[:times].each do |time| 28 | data = { 29 | "start" => time.iso8601, 30 | "title" => "%02d:%02d %s" % [time.hour, time.min, @command], 31 | "description" => @command} 32 | @events << @event_data[:default].merge(data) 33 | end 34 | end 35 | end 36 | 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/cron_parser.rb: -------------------------------------------------------------------------------- 1 | module Cronviz 2 | class CronParser 3 | # Syntax respected: 4 | # N Integer 5 | # N,N,N Set 6 | # N-N Range inclusive 7 | # * All 8 | # */N Every N 9 | 10 | # Syntax TODO 11 | # jan-dec 12 | # sun-sat 13 | # @yearly @monthly @weekly @daily @hourly 14 | 15 | # Syntax WONTDO 16 | # ? L W # 17 | 18 | # Syntax CANTDO 19 | # @reboot 20 | 21 | # Disregarding http://en.wikipedia.org/wiki/Cron on... 22 | # Use of sixth field as year value 23 | # Loose match on *either* field when both day of month and day of week are specified. 24 | 25 | # Expand cron syntax to the corresponding integers, return as iterable. 26 | def self.expand field, interval 27 | case 28 | when interval == interval[/\d+/] # Passed "N" form? We're done. 29 | [interval.to_i] 30 | when interval[/,/] # 6,7 31 | interval.split(",").map(&:to_i) 32 | when interval[/-/] # 1-5 33 | start, stop = interval.split("-").map(&:to_i) 34 | start.step(stop).to_a 35 | else # "*" or "*/17" 36 | expand_recurring field, interval 37 | end 38 | end 39 | 40 | # Expand "*" and "*/N" forms, producing collections of integers. 41 | # Cron interprets "*/17" to include 0, giving us occurrences on 42 | # :0, :17, :34, :51 43 | def self.expand_recurring field, interval 44 | if interval == "*" 45 | interval = 1 46 | else 47 | interval = interval[/\d+/].to_i # Nix any leading "*/". 48 | end 49 | case field # :mi, 17 => [0, 17, 34, 51] 50 | when :mi then 0.step(59, interval) 51 | when :ho then 0.step(23, interval) 52 | when :da then 1.step(31, interval) 53 | when :mo then 1.step(12, interval) 54 | when :dw then 0.step(6, interval) 55 | end.to_a 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /assets/container.haml: -------------------------------------------------------------------------------- 1 | !!! Strict 2 | %html 3 | %head 4 | %meta{:content => "text/html;charset=UTF-8", "http-equiv" => "Content-Type"}/ 5 | :css 6 | #my_timeline { 7 | border: 1px solid #aaa; 8 | height: 400px; 9 | font-size: 8pt; 10 | } 11 | %script{:src => "http://api.simile-widgets.org/timeline/2.3.1/timeline-api.js?bundle=true", :type => "text/javascript"} 12 | :javascript 13 | var tl; 14 | function onLoad() { 15 | var eventSource = new Timeline.DefaultEventSource(); 16 | var zones = [ 17 | { start: "#{Time.parse(earliest_time)}", 18 | end: "#{Time.parse(latest_time)}", 19 | magnify: 10, 20 | unit: Timeline.DateTime.MINUTE, 21 | multiple: 10 22 | } 23 | ]; 24 | var bandInfos = [ 25 | Timeline.createBandInfo({ 26 | eventSource: eventSource, 27 | timeZone: -4, 28 | date: "#{Time.parse(earliest_time)}", 29 | width: "80%", 30 | intervalUnit: Timeline.DateTime.HOUR, 31 | intervalPixels: 600 32 | }), 33 | Timeline.createBandInfo({ 34 | eventSource: eventSource, 35 | timeZone: -4, 36 | date: "#{Time.parse(earliest_time)}", 37 | width: "20%", 38 | intervalUnit: Timeline.DateTime.DAY, 39 | intervalPixels: 600, 40 | layout: 'overview' 41 | }) 42 | ]; 43 | 44 | bandInfos[1].syncWith = 0; 45 | bandInfos[1].highlight = true; 46 | 47 | tl = Timeline.create(document.getElementById("my_timeline"), bandInfos); 48 | eventSource.loadJSON(#{cron_json}, "."); 49 | } 50 | 51 | var resizeTimerID = null; 52 | function onResize() { 53 | if (resizeTimerID == null) { 54 | resizeTimerID = window.setTimeout(function() { 55 | resizeTimerID = null; 56 | tl.layout(); 57 | }, 500); 58 | } 59 | } 60 | %body{:onload => "onLoad();", :onresize => "onResize();"} 61 | #my_timeline 62 | %noscript 63 | This page uses Javascript to show you a Timeline. Please enable Javascript in your browser to see the full page. Thank you. 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cronviz: Visualize your cron jobs. 2 | 3 | It's 3 AM. Do you know where your cron jobs are? 4 | 5 | ![](https://github.com/federatedmedia/cronviz/raw/master/assets/screenshot.png) 6 | 7 | ## Use case 8 | 9 | You have a problem: something's causing performance issues on the application server between 1 and 4 AM, and the cron jobs seem a likely culprit. 10 | 11 | Naturally, you eyeball your crontab to find out what's running during those hours. 12 | 13 | Now you have two problems. 14 | 15 | Over time, cron jobs accrete into an impenetrable, opaque mass of text. Trying to get a comprehensive sense of all the various run times, and finding patterns therein, can be exceedingly difficult. Crontabs are written for computers to interpret -- not humans. 16 | 17 | Cronviz can help, by producing this... 18 | 19 | ![](https://github.com/federatedmedia/cronviz/raw/master/assets/screenshot.png) 20 | 21 | out of this... 22 | 23 | ```` 24 | * * * * * /usr/bin/foo 25 | */10 * * * * /usr/bin/bar 26 | */15 * * * * /usr/bin/baz 27 | */30 * * * * /usr/bin/qux 28 | 8 */8 * * * /usr/bin/quux 29 | * * * * * /usr/bin/corge 30 | */30 23,0,1 * * * /usr/bin/grault 31 | */5 * * * * /usr/bin/garply 32 | 0 * * * * /usr/bin/waldo 33 | 0 0 4,22 * * /usr/bin/fred 34 | 0 1 * * * /usr/bin/plugh 35 | 0 13 * * * /usr/bin/xyzzy 36 | 0 2 * * * /usr/bin/thud 37 | 30 6 * * 1,2,3,4,5 /usr/bin/wibble 38 | 30 7 * * * /usr/bin/wobble 39 | 30 8 * * * /usr/bin/wubble 40 | 33 */2 * * * /usr/bin/flob 41 | 35 1 * * * /usr/bin/whatever 42 | 45 * * * * /usr/bin/whoever 43 | 45 1 * * * /usr/bin/whomever 44 | * * * * * /usr/bin/whenever 45 | ```` 46 | 47 | 48 | ## RUNNING 49 | 50 | Requires haml and json gems. 51 | 52 | ```` 53 | gem install haml json 54 | ```` 55 | 56 | Requires a crontab file named "crontab" in the current directory. You can also use a filepath, as outlined below. 57 | 58 | Look to run.rb for an example that renders a HAML template to ./output.html, by passing cronviz' JSON output to the SIMILE timeline widget to produce a graph as seen in the screenshot above. 59 | 60 | You'll need to pass the filepath to a crontab file, or accept the default of "crontab" in the current directory. 61 | 62 | You'll also need to pass two dates to act as book-ends for the period of time we're interested in graphing. cronviz will graph all matching datetimes from the crontab contents... 63 | 64 | ```` 65 | earliest_time = "2011-12-08 00:00" # Zeroth minute on Dec 8. 66 | latest_time = "2011-12-08 23:59" # Final minute on Dec 8. 67 | ```` 68 | 69 | Using strings lets us compare dates faster than casting to DateTime or int objects. 70 | 71 | ## ROLLUPS 72 | 73 | Events that occur every minute and every five minutes will swamp the display, so these are rolled up into single events which get displayed as a continuous line, one per rolled-up job. 74 | 75 | With a bit of cron-style math, you can define custom rollups targeting whatever interval you like. 76 | 77 | Let's say you've got an event that occurs every 21 minutes and you'd like to roll it up. Cron defines "*/21" as occurrences on the following minutes of each hour: :0, :21, :42. This results in three executions per hour, for as many hours are between your earliest_time and latest_time. So 72 occurrences. 78 | 79 | Crontab math caveat: if your earliest_time starts at, say, :01 instead of :00, that hour's first execution on :00 won't happen. Thus, given the following... 80 | 81 | ```` 82 | earliest_time = "2011-12-08 00:01" 83 | latest_time = "2011-12-09 00:00" 84 | ```` 85 | 86 | then a job defined as ````"*/21 * * * *"```` will occur 71 times. You'd define a new hash in ````EVENT_DATA```` with your desired rollup interval as an integer, as follows... 87 | 88 | ```` 89 | 71 => { 90 | "color" => "#f00", 91 | "class" => "noisy_event", 92 | "title_prefix" => "Every 21 minutes: ", 93 | "durationEvent" => true}, 94 | ```` 95 | 96 | durationEvent must equal true. title_prefix can be defined as any string. Color and class are optional. 97 | 98 | 99 | ## Shortcomings 100 | 101 | - Unfortunately there's no simple way to know what time a job *finishes* short of 1) altering the crontab command or the job it fires, and 2) getting that information into cronviz. Minus that, cronviz can only tell you what time a job has *started*. 102 | 103 | - Date generation should be faster. 104 | 105 | 106 | ## Next steps 107 | 108 | - Timezone is currently hardcoded and, all things remaining static, is effectively transparent as long as the server's timezone is yours. It would be nice to allow an offset parameter so that someone in Pacific could view an Eastern server's crontab translated to their local time. 109 | 110 | - Exceedingly lengthy cron commands such as ````"/usr/bin/bash /full/path/to/script.sh >> /some/output/path.log 2>> /some/error/path.log"```` can uglify the resulting graph display. It might be possible to find a simple way to allow specifying which bits of the cron command are used as the resulting title... possibly via comments in the crontab file? 111 | -------------------------------------------------------------------------------- /lib/crontab.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | DEFAULT_FILE = "crontab" 4 | 5 | 6 | module Cronviz 7 | class Crontab 8 | attr_reader :jobs 9 | 10 | def initialize options={} 11 | @earliest_time = options[:earliest_time] 12 | @latest_time = options[:latest_time] 13 | @input = options[:input] || DEFAULT_FILE 14 | 15 | prepare_event_data options[:event_data] 16 | 17 | @jobs = [] 18 | get_lines { |line| @jobs << line_to_jobs(line) } 19 | end 20 | 21 | # Find out how many minutes there are between the two times user specified 22 | # so that CronJob can roll up every-X-minute jobs even for unusual periods. 23 | def prepare_event_data event_data 24 | # Subtracting a minute from @earliest_time via - 60 because 25 | # we're not trying to determine a difference between two times, 26 | # but rather a duration across them, so avoid the off-by-one. 27 | # That is, 1:05 - 1:01 gives us :04, when really there are 5 28 | # whole minutes to consider, since execution happens at the 29 | # start of each -- :01, :02, :03, :04, :05. 30 | num_minutes = (((Time.parse(@latest_time)) - (Time.parse(@earliest_time) - 60)) / 60).to_i 31 | 32 | # Minutes is easy because it's cron's finest level of granularity. 33 | # Five minutes is a bit different since cron doesn't fire, 34 | # e.g. "every five minutes" so much as "on the minutes marked 35 | # :00, :05, :10", etc. So if user has passed in a 36 | # non-evenly-divisible start or start period, we have to know 37 | # not the quantity of minutes contained, but which particular 38 | # intervals are inside the earliest/latest times. We get this 39 | # by doing a mini-cronparse call for */5 minutes. 40 | h = Hash.new.tap do |h| 41 | h[:mi] = Cronviz::CronParser.expand(:mi, "*/5") 42 | [:ho, :da, :mo, :dw].each do |k| # * the remaining fields. 43 | h[k] = Cronviz::CronParser.expand(k, "*") 44 | end 45 | end 46 | num_five_minutes = fan_out(h).size 47 | 48 | # We now know how many minute and five-minute intervals are in 49 | # the user's selected period. Merge that in event_data so that 50 | # we can roll these periods up along with whatever custom 51 | # settings they've defined. 52 | @event_data = event_data.merge(num_minutes => event_data[:every_minute], 53 | num_five_minutes => event_data[:every_five_minutes]) 54 | end 55 | 56 | def get_lines 57 | # Allow a filepath or a string of cron jobs to be passed in. 58 | # If nothing passed in, default to filename DEFAULT_FILE in the 59 | # working directory. 60 | # If that doesn't exist, raise and exit. 61 | begin 62 | open @input 63 | rescue Errno::ENOENT 64 | raise "Defaulted to ./crontab but no such file found!" if @input == DEFAULT_FILE 65 | @input 66 | end.each_line do |x| 67 | yield x.chop if x.strip.match /^[\*0-9]/ 68 | end 69 | end 70 | 71 | # Turn a cronjob line into a command and list of occurring times. 72 | def line_to_jobs line 73 | elements = {} 74 | 75 | # minute hour day month dayofweek command 76 | mi, ho, da, mo, dw, *co = line.split 77 | {:mi => mi, :ho => ho, :da => da, :mo => mo, :dw => dw}.each_pair do |k, v| 78 | elements[k] = CronParser.expand(k, v) 79 | end 80 | 81 | CronJob.new(:command => co, :times => fan_out(elements), :event_data => @event_data) 82 | end 83 | 84 | # Accept a blueprint of a job as an exploded list of time elements, 85 | # and return any qualifying times as Time objects. 86 | # minutes=[15,45] hours=[4] => [Time(4:15), Time(4:45)] 87 | # day-of-week is a filter on top of the day field, so we filter by 88 | # it but do not iterate over it. 89 | def fan_out els 90 | good = [] 91 | 92 | # "2011-...", "2011-..." => [2011] 93 | years = (@earliest_time.split("-")[0].to_i..@latest_time.split("-")[0].to_i).to_a 94 | 95 | years.each do |ye| 96 | els[:mo].each do |mo| 97 | els[:da].each do |da| 98 | els[:ho].each do |ho| 99 | els[:mi].each do |mi| 100 | good << Time.parse("#{ye}-#{mo}-#{da} #{ho}:#{mi}") if date_in_bounds?(ye, mo, da, ho, mi) 101 | end 102 | end 103 | end 104 | end 105 | end 106 | apply_weekday_filter(good, els[:dw]) 107 | end 108 | 109 | def date_in_bounds? ye, mo, da, ho, mi 110 | # Comparing dates element-wise in string form turns out to be 111 | # like 6x faster than casting to DateTime objects first... so, don't. 112 | date = "%s-%02d-%02d %02d:%02d" % [ye, mo, da, ho, mi] 113 | @earliest_time <= date and date <= @latest_time 114 | end 115 | 116 | def apply_weekday_filter dates, filter 117 | # Avoid comparison if we can. 118 | dates.reject! {|d| !filter.include? d.wday } unless filter.size == 7 119 | dates 120 | end 121 | 122 | def to_json(*a) 123 | events = [] 124 | @jobs.each do |job| 125 | job.events.each do |event| 126 | events << event 127 | end 128 | end 129 | {"dateTimeFormat" => "iso8601", "events" => events.sort_by {|x| x['title']}}.to_json 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /test/cronviz_test.rb: -------------------------------------------------------------------------------- 1 | require 'lib/crontab' 2 | require 'lib/cron_job' 3 | require 'lib/cron_parser' 4 | 5 | EARLIEST_TIME = "2011-10-11 00:00" 6 | LATEST_TIME = "2011-10-12 00:00" 7 | 8 | describe Cronviz::Crontab do 9 | def init_crontab options 10 | event_data = {:default => {}, :every_minute => {}, :every_five_minutes => {}} 11 | @options = { 12 | :earliest_time=>EARLIEST_TIME, 13 | :latest_time=>LATEST_TIME, 14 | :event_data=>event_data 15 | } 16 | crontab = Cronviz::Crontab.new @options.merge options 17 | end 18 | 19 | it "should generate dates no earlier than EARLIEST_TIME" do 20 | crontab = init_crontab :input => "17 */3 11 10 * do_some_stuff" 21 | crontab.jobs[0].times[0].strftime("%Y-%m-%d %H:%M").should_not < EARLIEST_TIME 22 | end 23 | it "should generate dates no later than LATEST_TIME" do 24 | crontab = init_crontab :input => "17 */3 */2 * * do_some_stuff" 25 | crontab.jobs[0].times[-1].strftime("%Y-%m-%d %H:%M").should_not > LATEST_TIME 26 | end 27 | 28 | it "should generate dates respecting day-of-week field" do 29 | crontab = init_crontab(:earliest_time=> "2011-10-06 00:00", 30 | :latest_time => "2011-10-17 23:59", 31 | :input => "0 17 * * 4,5 launch_happy_hour") 32 | crontab.jobs[0].times.count.should == 4 33 | end 34 | 35 | it "should handle single strings properly" do 36 | crontab = init_crontab :input => "*/5 5,6,7 11 10 2 do_some_stuff" 37 | crontab.jobs.count.should == 1 38 | crontab.jobs[0].times.count.should == 36 39 | end 40 | it "should handle multiple strings properly" do 41 | crontab = init_crontab :input => "17-21 */3 11 10 * do_some_stuff\n* * 11 10 * do_other_stuff" 42 | crontab.jobs.count.should == 2 43 | crontab.jobs[0].times.count.should == 40 44 | crontab.jobs[1].times.count.should == 1440 45 | end 46 | 47 | it "should rollup every-minute jobs" do 48 | crontab = init_crontab :input => "* * * * * run_every_minute" 49 | crontab.jobs[0].events.count.should == 1 50 | end 51 | it "should rollup every-five-minute jobs" do 52 | crontab = init_crontab :input => "*/5 * * * * run_every_five_minute" 53 | crontab.jobs[0].events.count.should == 1 54 | end 55 | it "should not rollup every-six-minute jobs" do 56 | crontab = init_crontab :input => "*/6 * * * * run_every_five_minute" 57 | crontab.jobs[0].events.count.should == 241 58 | end 59 | it "should rollup every-minute jobs when the graph period is a few minutes" do 60 | crontab = init_crontab(:earliest_time => "2011-10-11 00:00", 61 | :latest_time => "2011-10-11 00:08", 62 | :input => "* * * * * run_every_minute") 63 | crontab.jobs[0].events.count.should == 1 64 | end 65 | it "should rollup every-five-minute jobs when the graph period is a few hours" do 66 | crontab = init_crontab(:earliest_time => "2011-10-11 00:01", 67 | :latest_time => "2011-10-11 02:59", 68 | :input => "*/5 * * * * run_every_five_minutes") 69 | crontab.jobs[0].events.count.should == 1 70 | end 71 | it "should rollup every-minute jobs when the graph period is a few days" do 72 | crontab = init_crontab(:earliest_time => "2011-10-11 00:00", 73 | :latest_time => "2011-10-13 00:12", 74 | :input => "* * * * * run_every_minute") 75 | crontab.jobs[0].events.count.should == 1 76 | end 77 | it "should rollup every-five-minute jobs when the graph period is a few days" do 78 | crontab = init_crontab(:earliest_time => "2011-10-11 00:00", 79 | :latest_time => "2011-10-12 12:59", 80 | :input => "*/5 * * * * run_every_five_minutes") 81 | crontab.jobs[0].events.count.should == 1 82 | end 83 | end 84 | 85 | 86 | describe Cronviz::CronParser do 87 | before(:all) do 88 | @parser = Cronviz::CronParser 89 | end 90 | 91 | it "should expand a minute to a single iterable value" do 92 | @parser.expand(:mi, "17").should == [17] 93 | end 94 | it "should expand an hour to a single iterable value" do 95 | @parser.expand(:ho, "3").should == [3] 96 | end 97 | 98 | it "should expand a set of minutes to two values" do 99 | @parser.expand(:mi, "16,46").should == [16,46] 100 | end 101 | it "should expand a set of hours to three values" do 102 | @parser.expand(:ho, "3,6,9").should == [3,6,9] 103 | end 104 | 105 | it "should expand a range of minutes to a range of values" do 106 | @parser.expand(:mi, "1-5").should == [1,2,3,4,5] 107 | end 108 | it "should expand a range of hours to a range of values" do 109 | @parser.expand(:ho, "12-23").should == [12,13,14,15,16,17,18,19,20,21,22,23] 110 | end 111 | 112 | it "should expand all minutes to 60 minutes" do 113 | @parser.expand(:mi, "*").should == (0..59).to_a 114 | end 115 | it "should expand all hours to 24 hours" do 116 | @parser.expand(:ho, "*").should == (0..23).to_a 117 | end 118 | 119 | it "should expand all days to 31 days, 1-indexed" do 120 | @parser.expand(:da, "*").should == (1..31).to_a 121 | end 122 | it "should expand all months to 12 months, 1-indexed" do 123 | @parser.expand(:mo, "*").should == (1..12).to_a 124 | end 125 | 126 | it "should expand every X minutes to the proper values" do 127 | @parser.expand(:mi, "*/13").should == [0, 13, 26, 39, 52] 128 | end 129 | it "should expand every X hours to the proper values" do 130 | @parser.expand(:ho, "*/11").should == [0, 11, 22] 131 | end 132 | it "should expand every 13 hours to 0 and 13" do 133 | @parser.expand(:ho, "*/13").should == [0, 13] 134 | end 135 | it "should expand every 5 days to be one-indexed" do 136 | @parser.expand(:da, "*/5").should == [1, 6, 11, 16, 21, 26, 31] 137 | end 138 | it "should expand every 2 months to be one-indexed" do 139 | @parser.expand(:mo, "*/2").should == [1, 3, 5, 7, 9, 11] 140 | end 141 | 142 | end 143 | --------------------------------------------------------------------------------