tab_keeper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 29235a8d42173d34b28beb02ec01bb82285aa12c
4
+ data.tar.gz: c3978f99e2fcc255ff2d7f68590e46e162a6c498
5
+ SHA512:
6
+ metadata.gz: 59272e872de28887507c2ec2a7712695d2a442e0d788c96a05327eadb5d8f1aff727e41c4c4ed9179780cea7fcf2507aa41fdf303a7276e3327a0c0c3d7ad415
7
+ data.tar.gz: f92e98d7503c162a0679a145c38e1d39e566137d4b7beb43bf7b164d6522f97053002c6ee6dbffc98a12f267690626b11930bf8ba656f476c4dbadf04ef46338
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ .rspec_log
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format progress
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0
4
+ - 2.1
5
+ - 2.2
6
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tab_keeper.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Isaac Seymour
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # TabKeeper
2
+
3
+ Keep tabs on your crontab.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'tab_keeper'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install tab_keeper
20
+
21
+ ## Usage
22
+
23
+ TODO
24
+
25
+ ## Development
26
+
27
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test`
28
+ to run the tests. You can also run `bin/console` for an interactive prompt that will allow
29
+ you to experiment.
30
+
31
+ ## Contributing
32
+
33
+ Bug reports and pull requests are welcome on GitHub at https://github.com/isaacseymour/tab_keeper.
34
+
35
+ ## License
36
+
37
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
38
+
data/bin/console ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "tab_keeper"
5
+ require "pry"
6
+ Pry.start
@@ -0,0 +1,30 @@
1
+ module TabKeeper
2
+ class Daily
3
+ HOUR_VALUES = (0..23)
4
+ MINUTE_VALUES = (0..59)
5
+
6
+ def initialize(hour: nil, min: 0)
7
+ @hour = hour
8
+ @min = min
9
+ validate!
10
+ end
11
+
12
+ def to_s
13
+ "#{min} #{hour} * * *"
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :hour, :min
19
+
20
+ def validate!
21
+ unless HOUR_VALUES.include?(hour)
22
+ raise ArgumentError, "hour must be between 0 and 23"
23
+ end
24
+
25
+ unless MINUTE_VALUES.include?(min)
26
+ raise ArgumentError, "min must be between 0 and 59"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ module TabKeeper
2
+ class Generator
3
+ def initialize(*pipeline)
4
+ @pipeline = pipeline.flatten
5
+ end
6
+
7
+ def generate(job_list, **options)
8
+ job_list.map do |job, timing|
9
+ timing + " " + @pipeline.reduce(nil) do |previous, pipe|
10
+ pipe.new(previous, job: job, timing: timing, **options).to_s
11
+ end
12
+ end.join("\n\n") + "\n"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ module TabKeeper
2
+ class Hourly
3
+ MINUTE_VALUES = (0..59)
4
+ # Other values don't divide 24, so result in weird behaviour
5
+ EVERY_VALUES = [nil, 2, 3, 4, 6, 8, 12]
6
+ HOUR_VALUES = (0..23)
7
+
8
+ def initialize(min: 0, every: nil, only: nil)
9
+ @min = min
10
+ @every = every
11
+ @only = only
12
+ verify!
13
+ end
14
+
15
+ def to_s
16
+ return "#{min} */#{every} * * *" if every
17
+ return "#{min} #{only.join(',')} * * *" if only
18
+ "#{min} * * * *"
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :min, :every, :only
24
+
25
+ def verify!
26
+ unless MINUTE_VALUES.include?(min)
27
+ raise ArgumentError, "min must be between 0 and 59"
28
+ end
29
+
30
+ unless EVERY_VALUES.include?(every)
31
+ raise ArgumentError, "every must be one of #{EVERY_VALUES.join(', ')}"
32
+ end
33
+
34
+ return if only.nil?
35
+ raise ArgumentError, "every and only don't make sense together!" if every
36
+
37
+ raise ArgumentError, "only must be an array" unless only.is_a?(Array)
38
+
39
+ raise ArgumentError, "use Daily instead of a single `only` list" if only.one?
40
+
41
+ return if only.all? { |hour| HOUR_VALUES.include?(hour) }
42
+ raise ArgumentError, "only must be nil, or a list of hours"
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,41 @@
1
+ module TabKeeper
2
+ class JobList
3
+ def initialize
4
+ @jobs = []
5
+ yield self if block_given?
6
+ end
7
+
8
+ def generate(generator, **options)
9
+ generator.generate(@jobs, **options)
10
+ end
11
+
12
+ def to_a
13
+ @jobs
14
+ end
15
+
16
+ def add(job, timer)
17
+ # TODO: validate `timer.to_s`
18
+ @jobs << [job, timer.to_s]
19
+ end
20
+
21
+ def daily(job, **options)
22
+ add(job, Daily.new(**options))
23
+ end
24
+
25
+ def hourly(job, **options)
26
+ add(job, Hourly.new(**options))
27
+ end
28
+
29
+ def minutely(job, **options)
30
+ add(job, Minutely.new(**options))
31
+ end
32
+
33
+ def monthly(job, **options)
34
+ add(job, Monthly.new(**options))
35
+ end
36
+
37
+ def weekly(job, **options)
38
+ add(job, Weekly.new(**options))
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,96 @@
1
+ module TabKeeper
2
+ class LogRedirection
3
+ def initialize(previous, job: nil,
4
+ job_name_proc: ->(x) { x },
5
+ timing: nil,
6
+ log_directory: nil,
7
+ error_suffix: nil,
8
+ include_date_in_log_name: false,
9
+ **_options)
10
+ if previous.nil?
11
+ raise ArgumentError, "#{self.class.name} must not be first in the cron pipeline!"
12
+ end
13
+
14
+ if log_directory.nil?
15
+ raise ArgumentError, "log_directory must be set for #{self.class.name}"
16
+ end
17
+
18
+ @previous = previous
19
+ @job = job
20
+ # Normalise the job name: if it's an array take the first thing, then get the last
21
+ # thing after a "::" (if it's a class name), or a "/" (if it's a script, and removes
22
+ # the need to escape yay)
23
+ job_name = Array(@job).first.to_s.split(/((::)|\/)/).last
24
+ @job_name = job_name_proc.call(job_name)
25
+ @timing = timing
26
+ @log_directory = log_directory
27
+ @error_suffix = error_suffix
28
+ @include_date_in_log_name = include_date_in_log_name
29
+ end
30
+
31
+ def to_s
32
+ "#{@previous} " \
33
+ ">> #{@log_directory}/#{job_name}#{date_part}.log " \
34
+ "2>#{error_part}"
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :job_name
40
+
41
+ def error_part
42
+ return "&1" unless @error_suffix
43
+ "> #{@log_directory}/#{job_name}#{date_part}.#{@error_suffix}.log"
44
+ end
45
+
46
+ def date_part
47
+ return unless include_date?
48
+ "_`date +#{date_format}`"
49
+ end
50
+
51
+ def include_date?
52
+ @include_date_in_log_name
53
+ end
54
+
55
+ def date_format
56
+ [
57
+ '%Y',
58
+ '%m',
59
+ day_component,
60
+ hour_component,
61
+ min_component,
62
+ second_component
63
+ ].compact.join('-').chars.map { |char| char == '%' ? '\%' : char }.join
64
+ end
65
+
66
+ def day_component
67
+ '%d' if more_than_once_a_month?
68
+ end
69
+
70
+ def hour_component
71
+ '%H' if more_than_once_a_day?
72
+ end
73
+
74
+ def min_component
75
+ '%m' if more_than_once_a_day?
76
+ end
77
+
78
+ def second_component
79
+ '%s' if more_than_once_an_hour?
80
+ end
81
+
82
+ MULTIPLE_CHARACTERS = "*-/,".chars.map(&:freeze).freeze
83
+
84
+ def more_than_once_a_month?
85
+ MULTIPLE_CHARACTERS.any? { |char| @timing.split(' ')[2].include?(char) }
86
+ end
87
+
88
+ def more_than_once_a_day?
89
+ MULTIPLE_CHARACTERS.any? { |char| @timing.split(' ').first(2).join.include?(char) }
90
+ end
91
+
92
+ def more_than_once_an_hour?
93
+ MULTIPLE_CHARACTERS.any? { |char| @timing.split(' ').first.include?(char) }
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,24 @@
1
+ module TabKeeper
2
+ class LoginShell
3
+ def initialize(previous, job: nil, code_directory: nil, **_options)
4
+ @command = previous || job
5
+ @code_directory = code_directory
6
+ return if code_directory
7
+ raise ArgumentError, "code `directory` must be configured for LoginShell"
8
+ end
9
+
10
+ def to_s
11
+ "/bin/bash -l -c 'cd #{escaped_path} && #{escaped_command}'"
12
+ end
13
+
14
+ private
15
+
16
+ def escaped_path
17
+ @code_directory.chars.map { |char| char == "'" ? "'\\''" : char }.join
18
+ end
19
+
20
+ def escaped_command
21
+ @command.chars.map { |char| char == "'" ? "'\\''" : char }.join
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ module TabKeeper
2
+ class Minutely
3
+ # Other values would lead to the non-evenly spaced runs, which is non-obvious
4
+ EVERY_VALUES = [2, 3, 4, 5, 6, 10, 12, 15, 20, 30]
5
+
6
+ def initialize(every: nil)
7
+ @every = every
8
+ verify!
9
+ end
10
+
11
+ def to_s
12
+ return "* * * * *" unless every
13
+ "*/#{every} * * * *"
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :every
19
+
20
+ def verify!
21
+ return if every.nil? || EVERY_VALUES.include?(every)
22
+ raise ArgumentError, "every must be nil, or one of #{EVERY_VALUES.join(', ')}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ module TabKeeper
2
+ class Monthly
3
+ MINUTE_VALUES = (0..59)
4
+ HOUR_VALUES = (0..23)
5
+ # Cron supports 29-31 too, but that's a recipe for sadness
6
+ DAY_VALUES = (0..28)
7
+
8
+ def initialize(day: nil, hour: nil, min: 0)
9
+ @day = day
10
+ @hour = hour
11
+ @min = min
12
+ verify!
13
+ end
14
+
15
+ def to_s
16
+ "#{min} #{hour} #{day} * *"
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :day, :hour, :min
22
+
23
+ def verify!
24
+ unless MINUTE_VALUES.include?(min)
25
+ raise ArgumentError, "min must be between 0 and 59"
26
+ end
27
+
28
+ unless HOUR_VALUES.include?(hour)
29
+ raise ArgumentError, "hour must be between 0 and 23"
30
+ end
31
+
32
+ return if DAY_VALUES.include?(day)
33
+ raise ArgumentError, "day must be between 0 and 28"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ module TabKeeper
2
+ class RailsRunner
3
+ def initialize(previous, job: nil, rails_env: nil, **_options)
4
+ @to_run = previous || job
5
+ @rails_env = rails_env
6
+ end
7
+
8
+ def to_s
9
+ ["bin/rails runner", env_part, "'#{escaped_previous}'"].compact.join(" ")
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :rails_env, :to_run
15
+
16
+ def env_part
17
+ return unless @rails_env
18
+ "-e #{rails_env}"
19
+ end
20
+
21
+ def escaped_previous
22
+ to_run.chars.map { |char| char == "'" ? "'\\''" : char }.join
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module TabKeeper
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,41 @@
1
+ module TabKeeper
2
+ class Weekly
3
+ MINUTE_VALUES = (0..59)
4
+ HOUR_VALUES = (0..23)
5
+ # Order matters here - it must match the ordering used by cron (i.e. 0 = Sunday)
6
+ DAY_VALUES = %i(sunday monday tuesday wednesday thursday friday saturday)
7
+
8
+ def initialize(day: nil, hour: nil, min: 0)
9
+ @day = day
10
+ @hour = hour
11
+ @min = min
12
+ verify!
13
+ end
14
+
15
+ def to_s
16
+ "#{min} #{hour} * * #{day_index}"
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :day, :hour, :min
22
+
23
+ def day_index
24
+ return day if day.is_a?(Fixnum)
25
+ DAY_VALUES.index(day)
26
+ end
27
+
28
+ def verify!
29
+ unless MINUTE_VALUES.include?(min)
30
+ raise ArgumentError, "min must be between 0 and 59"
31
+ end
32
+
33
+ unless HOUR_VALUES.include?(hour)
34
+ raise ArgumentError, "hour must be between 0 and 23"
35
+ end
36
+
37
+ return if DAY_VALUES.include?(day) || (day.is_a?(Fixnum) && DAY_VALUES[day])
38
+ raise ArgumentError, "day must be between 0 and 6, or a symbol day name"
39
+ end
40
+ end
41
+ end
data/lib/tab_keeper.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "tab_keeper/version"
2
+
3
+ module TabKeeper
4
+ end
5
+
6
+ require "tab_keeper/daily"
7
+ require "tab_keeper/hourly"
8
+ require "tab_keeper/minutely"
9
+ require "tab_keeper/monthly"
10
+ require "tab_keeper/weekly"
11
+
12
+ require "tab_keeper/job_list"
13
+
14
+ require "tab_keeper/log_redirection"
15
+ require "tab_keeper/login_shell"
16
+ require "tab_keeper/rails_runner"
17
+
18
+ require "tab_keeper/generator"
@@ -0,0 +1,23 @@
1
+ lib_path = File.expand_path('../../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
3
+ require "tab_keeper"
4
+
5
+ RSpec.configure do |config|
6
+ config.expect_with :rspec do |expectations|
7
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
8
+ end
9
+
10
+ config.mock_with :rspec do |mocks|
11
+ mocks.verify_partial_doubles = true
12
+ end
13
+
14
+ config.example_status_persistence_file_path = ".rspec_log"
15
+
16
+ config.disable_monkey_patching!
17
+
18
+ config.warnings = true
19
+
20
+ config.order = :random
21
+
22
+ Kernel.srand config.seed
23
+ end
@@ -0,0 +1,39 @@
1
+ RSpec.describe TabKeeper::Daily do
2
+ subject { described_class.new(hour: hour, min: min).to_s }
3
+
4
+ context "midnight" do
5
+ let(:hour) { 0 }
6
+ let(:min) { 0 }
7
+ it { is_expected.to eq("0 0 * * *") }
8
+ end
9
+
10
+ context "7:30am" do
11
+ let(:hour) { 7 }
12
+ let(:min) { 30 }
13
+ it { is_expected.to eq("30 7 * * *") }
14
+ end
15
+
16
+ context "without min supplied" do
17
+ let(:hour) { 12 }
18
+ let(:min) { nil }
19
+ specify { expect { subject }.to raise_error(ArgumentError) }
20
+ end
21
+
22
+ context "without hour supplied" do
23
+ let(:hour) { nil }
24
+ let(:min) { 2 }
25
+ specify { expect { subject }.to raise_error(ArgumentError) }
26
+ end
27
+
28
+ context "with a weird hour" do
29
+ let(:hour) { 24 }
30
+ let(:min) { 0 }
31
+ specify { expect { subject }.to raise_error(ArgumentError) }
32
+ end
33
+
34
+ context "with a weird minute" do
35
+ let(:hour) { 12 }
36
+ let(:min) { -1 }
37
+ specify { expect { subject }.to raise_error(ArgumentError) }
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ RSpec.describe TabKeeper::Generator do
2
+ subject { described_class.new(*pipeline).generate(job_list, **options) }
3
+ let(:job_list) do
4
+ TabKeeper::JobList.new do |tab|
5
+ tab.minutely("FrequentJob.run", every: 5)
6
+ tab.daily("MidnightJob.run", hour: 0)
7
+ tab.daily("LunchtimeJob.run", hour: 12, min: 30)
8
+ tab.weekly("HappyMondayJob.run", day: :monday, hour: 7, min: 30)
9
+ tab.monthly("PaydayJob.run", day: 25, hour: 16, min: 15)
10
+ end.to_a
11
+ end
12
+
13
+ let(:pipeline) { [TabKeeper::RailsRunner, TabKeeper::LoginShell] }
14
+ let(:options) do
15
+ {
16
+ code_directory: '/path/to/code',
17
+ rails_env: :production
18
+ }
19
+ end
20
+
21
+ it do
22
+ is_expected.to eq <<-CRONTAB
23
+ */5 * * * * /bin/bash -l -c 'cd /path/to/code && bin/rails runner -e production '\\''FrequentJob.run'\\'''
24
+
25
+ 0 0 * * * /bin/bash -l -c 'cd /path/to/code && bin/rails runner -e production '\\''MidnightJob.run'\\'''
26
+
27
+ 30 12 * * * /bin/bash -l -c 'cd /path/to/code && bin/rails runner -e production '\\''LunchtimeJob.run'\\'''
28
+
29
+ 30 7 * * 1 /bin/bash -l -c 'cd /path/to/code && bin/rails runner -e production '\\''HappyMondayJob.run'\\'''
30
+
31
+ 15 16 25 * * /bin/bash -l -c 'cd /path/to/code && bin/rails runner -e production '\\''PaydayJob.run'\\'''
32
+ CRONTAB
33
+ end
34
+ end
@@ -0,0 +1,45 @@
1
+ RSpec.describe TabKeeper::Hourly do
2
+ subject { described_class.new(every: every, only: only, min: min).to_s }
3
+ let(:every) { nil }
4
+ let(:only) { nil }
5
+
6
+ context "every hour" do
7
+ let(:min) { 0 }
8
+ it { is_expected.to eq("0 * * * *") }
9
+ end
10
+
11
+ context "every half-past" do
12
+ let(:min) { 30 }
13
+ it { is_expected.to eq("30 * * * *") }
14
+ end
15
+
16
+ context "every 3rd hour" do
17
+ let(:min) { 0 }
18
+ let(:every) { 3 }
19
+ it { is_expected.to eq("0 */3 * * *") }
20
+ end
21
+
22
+ context "on specified hours" do
23
+ let(:min) { 15 }
24
+ let(:only) { [3, 9, 17] }
25
+ it { is_expected.to eq("15 3,9,17 * * *") }
26
+ end
27
+
28
+ context "without the minute" do
29
+ let(:min) { nil }
30
+ specify { expect { subject }.to raise_error(ArgumentError) }
31
+ end
32
+
33
+ context "with every and only specified" do
34
+ let(:min) { 0 }
35
+ let(:every) { 3 }
36
+ let(:only) { [1, 2, 3] }
37
+ specify { expect { subject }.to raise_error(ArgumentError) }
38
+ end
39
+
40
+ context "with only as a single hour" do
41
+ let(:only) { 12 }
42
+ let(:min) { 0 }
43
+ specify { expect { subject }.to raise_error(ArgumentError) }
44
+ end
45
+ end
@@ -0,0 +1,51 @@
1
+ RSpec.describe TabKeeper::JobList do
2
+ subject(:instance) { described_class.new }
3
+
4
+ it "yields itself" do
5
+ yielded = nil
6
+ returned = described_class.new { |tab| yielded = tab }
7
+ expect(yielded).to be(returned)
8
+ end
9
+
10
+ describe "#add" do
11
+ pending "rejects invalid timers" do
12
+ expect { instance.add("thing", "* *-2 50 * -1") }.to raise_error(ArgumentError)
13
+ end
14
+ end
15
+
16
+ describe "#daily" do
17
+ it "passes options through to Daily.new" do
18
+ expect(TabKeeper::Daily).to receive(:new).with(hour: 5).and_call_original
19
+ instance.daily("thing", hour: 5)
20
+ end
21
+ end
22
+
23
+ describe "#hourly" do
24
+ it "passes options through to Hourly.new" do
25
+ expect(TabKeeper::Hourly).to receive(:new).with(min: 5).and_call_original
26
+ instance.hourly("thing", min: 5)
27
+ end
28
+ end
29
+
30
+ describe "#minutely" do
31
+ it "passes options through to Minutely.new" do
32
+ expect(TabKeeper::Minutely).to receive(:new).and_call_original
33
+ instance.minutely("thing")
34
+ end
35
+ end
36
+
37
+ describe "#monthly" do
38
+ it "passes options through to Monthly.new" do
39
+ expect(TabKeeper::Monthly).to receive(:new).with(day: 14, hour: 5).and_call_original
40
+ instance.monthly("thing", day: 14, hour: 5)
41
+ end
42
+ end
43
+
44
+ describe "#weekly" do
45
+ it "passes options through to Weekly.new" do
46
+ expect(TabKeeper::Weekly).to receive(:new).with(day: :monday, hour: 5).
47
+ and_call_original
48
+ instance.weekly("thing", day: :monday, hour: 5)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,95 @@
1
+ RSpec.describe TabKeeper::LogRedirection do
2
+ let(:instance) do
3
+ described_class.new("scripts/thing",
4
+ job: "scripts/thing",
5
+ job_name_proc: job_name_proc,
6
+ timing: timing,
7
+ log_directory: "/path/to/logs",
8
+ include_date_in_log_name: include_date_in_log_name,
9
+ error_suffix: error_suffix)
10
+ end
11
+ subject { instance.to_s }
12
+ let(:job_name_proc) { ->(job) { job } }
13
+ let(:error_suffix) { nil }
14
+ let(:include_date_in_log_name) { true }
15
+
16
+ context "without date in the log file name" do
17
+ let(:include_date_in_log_name) { false }
18
+ let(:timing) { "* * * * *" }
19
+ it { is_expected.to eq("scripts/thing >> /path/to/logs/thing.log 2>&1") }
20
+ end
21
+
22
+ context "with a separate error log" do
23
+ let(:include_date_in_log_name) { false }
24
+ let(:error_suffix) { "wah" }
25
+ let(:timing) { "* * * * *" }
26
+ it do
27
+ is_expected.to eq("scripts/thing >> /path/to/logs/thing.log " \
28
+ "2>> /path/to/logs/thing.wah.log")
29
+ end
30
+ end
31
+
32
+ context "with minutely timing" do
33
+ let(:timing) { TabKeeper::Minutely.new(every: 5).to_s }
34
+ it do
35
+ is_expected.to eq(
36
+ "scripts/thing >> /path/to/logs/thing_" \
37
+ "`date +\\%Y-\\%m-\\%d-\\%H-\\%m-\\%s`.log 2>&1")
38
+ end
39
+ end
40
+
41
+ context "with hourly timing" do
42
+ let(:timing) { TabKeeper::Hourly.new(every: 2).to_s }
43
+ it do
44
+ is_expected.to eq(
45
+ "scripts/thing >> /path/to/logs/thing_" \
46
+ "`date +\\%Y-\\%m-\\%d-\\%H-\\%m`.log 2>&1")
47
+ end
48
+ end
49
+
50
+ context "with daily timing" do
51
+ let(:timing) { TabKeeper::Daily.new(hour: 14).to_s }
52
+ it do
53
+ is_expected.to eq(
54
+ "scripts/thing >> /path/to/logs/thing_" \
55
+ "`date +\\%Y-\\%m-\\%d`.log 2>&1")
56
+ end
57
+
58
+ context "with a separate error file" do
59
+ let(:error_suffix) { "error" }
60
+ it do
61
+ is_expected.to eq(
62
+ "scripts/thing " \
63
+ ">> /path/to/logs/thing_`date +\\%Y-\\%m-\\%d`.log " \
64
+ "2>> /path/to/logs/thing_`date +\\%Y-\\%m-\\%d`.error.log")
65
+ end
66
+ end
67
+
68
+ context "with a fun job name proc" do
69
+ let(:job_name_proc) { -> (job) { "#{job}_sadness" } }
70
+ it do
71
+ is_expected.to eq(
72
+ "scripts/thing >> /path/to/logs/thing_sadness_" \
73
+ "`date +\\%Y-\\%m-\\%d`.log 2>&1")
74
+ end
75
+ end
76
+ end
77
+
78
+ context "with weekly timing" do
79
+ let(:timing) { TabKeeper::Weekly.new(hour: 14, day: :tuesday).to_s }
80
+ it do
81
+ is_expected.to eq(
82
+ "scripts/thing >> /path/to/logs/thing_" \
83
+ "`date +\\%Y-\\%m-\\%d`.log 2>&1")
84
+ end
85
+ end
86
+
87
+ context "with monthly timing" do
88
+ let(:timing) { TabKeeper::Monthly.new(day: 5, hour: 12).to_s }
89
+ it do
90
+ is_expected.to eq(
91
+ "scripts/thing >> /path/to/logs/thing_" \
92
+ "`date +\\%Y-\\%m`.log 2>&1")
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,25 @@
1
+ RSpec.describe TabKeeper::LoginShell do
2
+ subject { described_class.new(previous, job: job, code_directory: code_directory).to_s }
3
+ let(:job) { "bin/some_script" }
4
+ let(:code_directory) { "/path/to/code" }
5
+ let(:previous) { nil }
6
+
7
+ it { is_expected.to eq("/bin/bash -l -c 'cd /path/to/code && bin/some_script'") }
8
+
9
+ context "with a previous command" do
10
+ let(:previous) { "SOMEVAR=value bin/some_script" }
11
+ it do
12
+ is_expected.
13
+ to eq("/bin/bash -l -c 'cd /path/to/code && SOMEVAR=value bin/some_script'")
14
+ end
15
+ end
16
+
17
+ context "when the job needs escaping" do
18
+ let(:job) { "bin/run 'MyJob.run('\\''arg'\\'')'" }
19
+ it do
20
+ is_expected.to eq(
21
+ "/bin/bash -l -c 'cd /path/to/code && " \
22
+ "bin/run '\\''MyJob.run('\\''\\'\\'''\\''arg'\\''\\'\\'''\\'')'\\'''")
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ RSpec.describe TabKeeper::Minutely do
2
+ subject { described_class.new(every: every).to_s }
3
+
4
+ context "5 minutes" do
5
+ let(:every) { 5 }
6
+ it { is_expected.to eq("*/5 * * * *") }
7
+ end
8
+
9
+ context "every minute" do
10
+ let(:every) { nil }
11
+ it { is_expected.to eq("* * * * *") }
12
+ end
13
+
14
+ context "every 30 seconds" do
15
+ let(:every) { 0.5 }
16
+ specify { expect { subject }.to raise_error(ArgumentError) }
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ RSpec.describe TabKeeper::Monthly do
2
+ subject { described_class.new(day: day, hour: hour, min: min).to_s }
3
+ let(:day) { 1 }
4
+ let(:hour) { 13 }
5
+ let(:min) { 0 }
6
+
7
+ context "on the first of the month" do
8
+ let(:day) { 1 }
9
+ it { is_expected.to eq("0 13 1 * *") }
10
+ end
11
+
12
+ context "on the 15th of the month" do
13
+ let(:day) { 15 }
14
+ it { is_expected.to eq("0 13 15 * *") }
15
+ end
16
+
17
+ context "without the minute" do
18
+ let(:min) { nil }
19
+ specify { expect { subject }.to raise_error(ArgumentError) }
20
+ end
21
+
22
+ context "without the hour" do
23
+ let(:hour) { nil }
24
+ specify { expect { subject }.to raise_error(ArgumentError) }
25
+ end
26
+
27
+ context "without the day" do
28
+ let(:day) { nil }
29
+ specify { expect { subject }.to raise_error(ArgumentError) }
30
+ end
31
+
32
+ context "with a weird minute" do
33
+ let(:min) { 60 }
34
+ specify { expect { subject }.to raise_error(ArgumentError) }
35
+ end
36
+
37
+ context "with a weird hour" do
38
+ let(:hour) { -1 }
39
+ specify { expect { subject }.to raise_error(ArgumentError) }
40
+ end
41
+
42
+ context "with a weird day" do
43
+ let(:day) { 29 }
44
+ specify { expect { subject }.to raise_error(ArgumentError) }
45
+ end
46
+
47
+ context "with a symbol day" do
48
+ let(:day) { :thursday }
49
+ specify { expect { subject }.to raise_error(ArgumentError) }
50
+ end
51
+ end
@@ -0,0 +1,23 @@
1
+ RSpec.describe TabKeeper::RailsRunner do
2
+ subject { described_class.new(previous, job: "MyJob.run", rails_env: rails_env).to_s }
3
+
4
+ let(:previous) { "MyJob.run" }
5
+ let(:rails_env) { :staging }
6
+
7
+ it { is_expected.to eq("bin/rails runner -e staging 'MyJob.run'") }
8
+
9
+ context "when there are 's to escape" do
10
+ let(:previous) { "MyJob.run('arg')" }
11
+ it { is_expected.to eq("bin/rails runner -e staging 'MyJob.run('\\''arg'\\'')'") }
12
+ end
13
+
14
+ context "when this is the first transformer in the pipeline" do
15
+ let(:previous) { nil }
16
+ it { is_expected.to eq("bin/rails runner -e staging 'MyJob.run'") }
17
+ end
18
+
19
+ context "without a rails_env set" do
20
+ let(:rails_env) { nil }
21
+ it { is_expected.to eq("bin/rails runner 'MyJob.run'") }
22
+ end
23
+ end
@@ -0,0 +1,51 @@
1
+ RSpec.describe TabKeeper::Weekly do
2
+ subject { described_class.new(day: day, hour: hour, min: min).to_s }
3
+ let(:day) { :wednesday }
4
+ let(:hour) { 13 }
5
+ let(:min) { 0 }
6
+
7
+ context "every Thursday" do
8
+ let(:day) { :thursday }
9
+ it { is_expected.to eq("0 13 * * 4") }
10
+ end
11
+
12
+ context "every 3rd day of the week" do
13
+ let(:day) { 3 }
14
+ it { is_expected.to eq("0 13 * * 3") }
15
+ end
16
+
17
+ context "without the minute" do
18
+ let(:min) { nil }
19
+ specify { expect { subject }.to raise_error(ArgumentError) }
20
+ end
21
+
22
+ context "without the hour" do
23
+ let(:hour) { nil }
24
+ specify { expect { subject }.to raise_error(ArgumentError) }
25
+ end
26
+
27
+ context "without the day" do
28
+ let(:day) { nil }
29
+ specify { expect { subject }.to raise_error(ArgumentError) }
30
+ end
31
+
32
+ context "with a weird minute" do
33
+ let(:min) { 60 }
34
+ specify { expect { subject }.to raise_error(ArgumentError) }
35
+ end
36
+
37
+ context "with a weird hour" do
38
+ let(:hour) { -1 }
39
+ specify { expect { subject }.to raise_error(ArgumentError) }
40
+ end
41
+
42
+ context "with a weird day" do
43
+ let(:day) { 8 }
44
+ specify { expect { subject }.to raise_error(ArgumentError) }
45
+ end
46
+
47
+ context "with a string day" do
48
+ let(:day) { "thursday" }
49
+ specify { expect { subject }.to raise_error(ArgumentError) }
50
+ end
51
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tab_keeper/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "tab_keeper"
8
+ spec.version = TabKeeper::VERSION
9
+ spec.authors = ["Isaac Seymour"]
10
+ spec.email = ["i.seymour@oxon.org"]
11
+
12
+ spec.summary = %q{Manage crons}
13
+ spec.homepage = "https://github.com/isaacseymour/tab_keeper"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rspec", "~> 3"
24
+ spec.add_development_dependency "pry", "~> 0"
25
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tab_keeper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Isaac Seymour
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-10-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - i.seymour@oxon.org
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rspec"
64
+ - ".travis.yml"
65
+ - Gemfile
66
+ - LICENSE.txt
67
+ - README.md
68
+ - bin/console
69
+ - lib/tab_keeper.rb
70
+ - lib/tab_keeper/daily.rb
71
+ - lib/tab_keeper/generator.rb
72
+ - lib/tab_keeper/hourly.rb
73
+ - lib/tab_keeper/job_list.rb
74
+ - lib/tab_keeper/log_redirection.rb
75
+ - lib/tab_keeper/login_shell.rb
76
+ - lib/tab_keeper/minutely.rb
77
+ - lib/tab_keeper/monthly.rb
78
+ - lib/tab_keeper/rails_runner.rb
79
+ - lib/tab_keeper/version.rb
80
+ - lib/tab_keeper/weekly.rb
81
+ - spec/spec_helper.rb
82
+ - spec/tab_keeper/daily_spec.rb
83
+ - spec/tab_keeper/generator_spec.rb
84
+ - spec/tab_keeper/hourly_spec.rb
85
+ - spec/tab_keeper/job_list_spec.rb
86
+ - spec/tab_keeper/log_redirection_spec.rb
87
+ - spec/tab_keeper/login_shell_spec.rb
88
+ - spec/tab_keeper/minutely_spec.rb
89
+ - spec/tab_keeper/monthly_spec.rb
90
+ - spec/tab_keeper/rails_runner_spec.rb
91
+ - spec/tab_keeper/weekly_spec.rb
92
+ - tab_keeper.gemspec
93
+ homepage: https://github.com/isaacseymour/tab_keeper
94
+ licenses:
95
+ - MIT
96
+ metadata: {}
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 2.4.5.1
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Manage crons
117
+ test_files:
118
+ - spec/spec_helper.rb
119
+ - spec/tab_keeper/daily_spec.rb
120
+ - spec/tab_keeper/generator_spec.rb
121
+ - spec/tab_keeper/hourly_spec.rb
122
+ - spec/tab_keeper/job_list_spec.rb
123
+ - spec/tab_keeper/log_redirection_spec.rb
124
+ - spec/tab_keeper/login_shell_spec.rb
125
+ - spec/tab_keeper/minutely_spec.rb
126
+ - spec/tab_keeper/monthly_spec.rb
127
+ - spec/tab_keeper/rails_runner_spec.rb
128
+ - spec/tab_keeper/weekly_spec.rb