tamarillo 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/.travis.yml +6 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +83 -0
- data/Rakefile +23 -0
- data/bin/tam +4 -0
- data/features/config.feature +44 -0
- data/features/step_definitions/tamarillo_steps.rb +50 -0
- data/features/support/env.rb +5 -0
- data/features/tamarillo.feature +47 -0
- data/lib/tamarillo/clock.rb +33 -0
- data/lib/tamarillo/command.rb +68 -0
- data/lib/tamarillo/config.rb +95 -0
- data/lib/tamarillo/controller.rb +113 -0
- data/lib/tamarillo/monitor.rb +27 -0
- data/lib/tamarillo/notification/bell.rb +15 -0
- data/lib/tamarillo/notification/growl.rb +13 -0
- data/lib/tamarillo/notification/none.rb +13 -0
- data/lib/tamarillo/notification/speech.rb +13 -0
- data/lib/tamarillo/notification/touch.rb +16 -0
- data/lib/tamarillo/notification.rb +36 -0
- data/lib/tamarillo/storage.rb +118 -0
- data/lib/tamarillo/tomato.rb +96 -0
- data/lib/tamarillo/tomato_file.rb +60 -0
- data/lib/tamarillo/version.rb +3 -0
- data/lib/tamarillo.rb +7 -0
- data/spec/lib/tamarillo/clock_spec.rb +39 -0
- data/spec/lib/tamarillo/command_spec.rb +17 -0
- data/spec/lib/tamarillo/config_spec.rb +133 -0
- data/spec/lib/tamarillo/controller_spec.rb +137 -0
- data/spec/lib/tamarillo/monitor_spec.rb +27 -0
- data/spec/lib/tamarillo/notification/bell_spec.rb +7 -0
- data/spec/lib/tamarillo/notification/growl_spec.rb +7 -0
- data/spec/lib/tamarillo/notification/speech_spec.rb +7 -0
- data/spec/lib/tamarillo/notification_spec.rb +22 -0
- data/spec/lib/tamarillo/storage_spec.rb +171 -0
- data/spec/lib/tamarillo/tomato_file_spec.rb +41 -0
- data/spec/lib/tamarillo/tomato_spec.rb +116 -0
- data/spec/support/invalid-config.yml +1 -0
- data/spec/support/sample-config.yml +3 -0
- data/tamarillo.gemspec +26 -0
- metadata +178 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
module Tamarillo
|
2
|
+
module Notification
|
3
|
+
class Touch
|
4
|
+
# Public: initializes a new notifier.
|
5
|
+
def initialize(path)
|
6
|
+
@path = File.expand_path(path)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Public: executes the notification.
|
10
|
+
def call
|
11
|
+
system("touch #{@path}")
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'tamarillo/notification/bell'
|
2
|
+
require 'tamarillo/notification/growl'
|
3
|
+
require 'tamarillo/notification/none'
|
4
|
+
require 'tamarillo/notification/speech'
|
5
|
+
require 'tamarillo/notification/touch'
|
6
|
+
|
7
|
+
module Tamarillo
|
8
|
+
module Notification
|
9
|
+
BELL = :bell.freeze
|
10
|
+
GROWL = :growl.freeze
|
11
|
+
NONE = :none.freeze
|
12
|
+
SPEECH = :speech.freeze
|
13
|
+
|
14
|
+
VALID = [BELL, GROWL, NONE, SPEECH]
|
15
|
+
|
16
|
+
# Public: Returns a valid notification type.
|
17
|
+
def self.valid!(value)
|
18
|
+
value = value.to_s.downcase.to_sym
|
19
|
+
VALID.include?(value) ? value : nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.default
|
23
|
+
BELL
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: Resolves a notification class.
|
27
|
+
def self.for(type)
|
28
|
+
case type
|
29
|
+
when BELL then Bell.new
|
30
|
+
when GROWL then Growl.new
|
31
|
+
when SPEECH then Speech.new
|
32
|
+
else None.new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'tamarillo/clock'
|
2
|
+
require 'tamarillo/config'
|
3
|
+
require 'tamarillo/tomato'
|
4
|
+
require 'tamarillo/tomato_file'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'pathname'
|
7
|
+
|
8
|
+
# Public: Stores tomatoes using the filesystem.
|
9
|
+
#
|
10
|
+
# This model represents a directory of Tomato files and configuration.
|
11
|
+
# It can read and write them, and find the latest one for the current
|
12
|
+
# day.
|
13
|
+
module Tamarillo
|
14
|
+
class Storage
|
15
|
+
# Returns: the String path to the storage directory.
|
16
|
+
attr_reader :path
|
17
|
+
# Returns: the Config for this storage.
|
18
|
+
# Used to set the duration when reading tomatoes in.
|
19
|
+
attr_reader :config
|
20
|
+
|
21
|
+
# Public: Initialize a new storage object.
|
22
|
+
def initialize(path, config = nil)
|
23
|
+
@config = config || Tamarillo::Config.new
|
24
|
+
@path = Pathname.new(path)
|
25
|
+
FileUtils.mkdir_p(@path)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Write the config to the filesystem.
|
29
|
+
#
|
30
|
+
# Returns the Pathname to the config that was written.
|
31
|
+
def write_config
|
32
|
+
File.open(config_path, 'w') { |f| f << @config.to_yaml }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Public: Write a tomato to the filesystem.
|
36
|
+
#
|
37
|
+
# tomato - A Tomato instance.
|
38
|
+
#
|
39
|
+
# Returns the Pathname to the tomato that was written.
|
40
|
+
def write_tomato(tomato)
|
41
|
+
tomato_file = TomatoFile.new(tomato)
|
42
|
+
tomato_path = @path.join(tomato_file.path)
|
43
|
+
FileUtils.mkdir_p(File.dirname(tomato_path))
|
44
|
+
File.open(tomato_path, 'w') { |f| f << tomato_file.content }
|
45
|
+
|
46
|
+
tomato_path
|
47
|
+
end
|
48
|
+
|
49
|
+
# Public: Writes a monitor pid.
|
50
|
+
#
|
51
|
+
# monitor - The monitor to write.
|
52
|
+
def write_monitor(monitor)
|
53
|
+
File.open(monitor_path, 'w') { |f| f << monitor.pid }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public: Read a tomato from the filesystem.
|
57
|
+
#
|
58
|
+
# path - A String path to a tomato file.
|
59
|
+
#
|
60
|
+
# Returns a Tomato instance if found, nil if not found.
|
61
|
+
def read_tomato(path)
|
62
|
+
return unless File.exist?(path)
|
63
|
+
|
64
|
+
data = File.readlines(path)
|
65
|
+
start_time = Time.iso8601(data[0]).localtime
|
66
|
+
state = data[2]
|
67
|
+
|
68
|
+
clock = Clock.new(start_time)
|
69
|
+
duration = config.duration_in_seconds
|
70
|
+
tomato = Tomato.new(duration, clock)
|
71
|
+
tomato.interrupt! if state == 'interrupted'
|
72
|
+
|
73
|
+
tomato
|
74
|
+
end
|
75
|
+
|
76
|
+
# Public: Returns the pid of monitor.
|
77
|
+
def read_monitor
|
78
|
+
if File.exists?(monitor_path)
|
79
|
+
File.read(monitor_path).to_i
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Public: Removes monitor pid-file.
|
84
|
+
def clear_monitor
|
85
|
+
File.delete(monitor_path) if File.exists?(monitor_path)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Public: Returns a Tomato instance if one exists.
|
89
|
+
def latest
|
90
|
+
return unless File.directory?(tomato_dir)
|
91
|
+
# p Dir.glob(tomato_dir.join('*'))
|
92
|
+
|
93
|
+
# XXX tomato_dir.to_s because FakeFS chokes on Pathname.
|
94
|
+
if latest_name = Dir.glob(tomato_dir.join('*')).sort.last
|
95
|
+
read_tomato(latest_name)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Private: Returns a Pathname to the config.
|
102
|
+
def config_path
|
103
|
+
@path.join('config.yml')
|
104
|
+
end
|
105
|
+
|
106
|
+
# Private: Returns a Pathmame to the monitor pid.
|
107
|
+
def monitor_path
|
108
|
+
@path.join('monitor.pid')
|
109
|
+
end
|
110
|
+
|
111
|
+
# Private: Returns a Pathname to the current day.
|
112
|
+
def tomato_dir
|
113
|
+
base_path = File.dirname(TomatoFile.path(Time.now))
|
114
|
+
@path.join(base_path)
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Tamarillo
|
4
|
+
# Public: A unit of work.
|
5
|
+
#
|
6
|
+
# A Tomato is a 'pomodoro', it keeps track of the amount of time you
|
7
|
+
# have focused on a single task. It can be interrupted or completed.
|
8
|
+
class Tomato
|
9
|
+
# Internal: A set of valid states a Tomato can be in.
|
10
|
+
module States
|
11
|
+
ACTIVE = :active.freeze
|
12
|
+
COMPLETED = :completed.freeze
|
13
|
+
INTERRUPTED = :interrupted.freeze
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: Gets/Sets the length of the tomato in seconds.
|
17
|
+
attr_accessor :duration
|
18
|
+
|
19
|
+
# Public: Initializes a new Tomato.
|
20
|
+
#
|
21
|
+
# duration - The length of the Tomato in seconds.
|
22
|
+
# clock - A Clock instance to keep track of elapsed time.
|
23
|
+
def initialize(duration, clock)
|
24
|
+
@duration = duration
|
25
|
+
@clock = clock
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Returns the starting Time of the Tomato.
|
29
|
+
def started_at
|
30
|
+
@clock.start_time
|
31
|
+
end
|
32
|
+
|
33
|
+
# Public: Returns the Date the Tomato was started on.
|
34
|
+
def date
|
35
|
+
@clock.start_date
|
36
|
+
end
|
37
|
+
|
38
|
+
# Public: Returns true if two Tomatoes share a start Time.
|
39
|
+
def eql?(other)
|
40
|
+
other.started_at == started_at ||
|
41
|
+
super(other)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Public: Returns the number of seconds until completion.
|
45
|
+
def remaining
|
46
|
+
return 0 if @interrupted
|
47
|
+
|
48
|
+
d = @duration - @clock.elapsed
|
49
|
+
d > 0 ? d : 0
|
50
|
+
end
|
51
|
+
|
52
|
+
# Public: Returns the number of seconds elapsed since start.
|
53
|
+
def elapsed
|
54
|
+
@clock.elapsed
|
55
|
+
end
|
56
|
+
|
57
|
+
# Public: Marks the tomato as interrupted.
|
58
|
+
#
|
59
|
+
# Returns the Tomato.
|
60
|
+
def interrupt!
|
61
|
+
@interrupted = true
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
# Public: Returns true if the Tomato has not been completed or
|
66
|
+
# interrupted.
|
67
|
+
def active?
|
68
|
+
States::ACTIVE == state
|
69
|
+
end
|
70
|
+
|
71
|
+
# Public: Returns true if the elapsed Time matches the duration.
|
72
|
+
def completed?
|
73
|
+
States::COMPLETED == state
|
74
|
+
end
|
75
|
+
|
76
|
+
# Public: Returns true if the Tomato has been interrupted.
|
77
|
+
def interrupted?
|
78
|
+
States::INTERRUPTED == state
|
79
|
+
end
|
80
|
+
|
81
|
+
# Public: Returns which state the Tomato is in.
|
82
|
+
#
|
83
|
+
# I'd rather keep this internal, but the storage system needs to
|
84
|
+
# know what state the tamato was in when it was written.
|
85
|
+
def state
|
86
|
+
if @interrupted
|
87
|
+
States::INTERRUPTED
|
88
|
+
elsif remaining == 0
|
89
|
+
States::COMPLETED
|
90
|
+
else
|
91
|
+
States::ACTIVE
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Tamarillo
|
4
|
+
# Internal: Represents a tomato in the filesystem.
|
5
|
+
class TomatoFile
|
6
|
+
FILENAME_FORMAT = '%Y%m%d%H%M%S'
|
7
|
+
PATH_FORMAT = '%Y/%m%d'
|
8
|
+
|
9
|
+
# Public: Initializes a new tomato file.
|
10
|
+
#
|
11
|
+
# tomato - A Tomato instance to serialize.
|
12
|
+
def initialize(tomato)
|
13
|
+
@tomato = tomato
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public; Returns the filename of the Tomato.
|
17
|
+
def name
|
18
|
+
@tomato.started_at.strftime(FILENAME_FORMAT)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: Returns a String path to the Tomato.
|
22
|
+
def path
|
23
|
+
self.class.path(@tomato.started_at)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: Generate a path from a time.
|
27
|
+
#
|
28
|
+
# time - A Tomato's start time Date.
|
29
|
+
#
|
30
|
+
# Returns A String path generated from a Date.
|
31
|
+
def self.path(time)
|
32
|
+
dir = time.strftime(PATH_FORMAT)
|
33
|
+
name = time.strftime(FILENAME_FORMAT)
|
34
|
+
|
35
|
+
"#{dir}/#{name}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Public: Returns the serialized form of a Tomato.
|
39
|
+
def content
|
40
|
+
[time,task,state].join("\n")
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Private: Returns the start time in ISO-8601 format.
|
46
|
+
def time
|
47
|
+
@tomato.started_at.iso8601
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns: The name of the task being worked on during the Tomato.
|
51
|
+
def task
|
52
|
+
"Some task I'm working on"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the state of the Tomato.
|
56
|
+
def state
|
57
|
+
@tomato.state.to_s
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/tamarillo.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative '../../../lib/tamarillo/clock'
|
2
|
+
require 'timecop'
|
3
|
+
|
4
|
+
include Tamarillo
|
5
|
+
|
6
|
+
describe Clock do
|
7
|
+
after do
|
8
|
+
Timecop.return
|
9
|
+
end
|
10
|
+
|
11
|
+
it "has a start_time" do
|
12
|
+
now = Time.new(2012,4,1,6,0,0)
|
13
|
+
clock = Clock.new(now)
|
14
|
+
clock.start_time.should == now
|
15
|
+
end
|
16
|
+
|
17
|
+
it "has a start_date" do
|
18
|
+
now = Time.new(2012,1,1,6,0,0)
|
19
|
+
today = Date.new(2012,1,1)
|
20
|
+
clock = Clock.new(now)
|
21
|
+
clock.start_date.should == today
|
22
|
+
end
|
23
|
+
|
24
|
+
it "has an elapsed value" do
|
25
|
+
now = Time.new(2012,1,1,6,0,0)
|
26
|
+
clock = Clock.new(now)
|
27
|
+
Timecop.freeze(2012,1,1,6,0,30)
|
28
|
+
|
29
|
+
clock.elapsed.should == 30
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#now" do
|
33
|
+
it "creates an instance starting at the current time" do
|
34
|
+
Timecop.freeze(2012,1,1,6,0,30)
|
35
|
+
clock = Clock.now
|
36
|
+
clock.start_time.should == Time.now
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative '../../../lib/tamarillo/command'
|
2
|
+
|
3
|
+
describe Tamarillo::Command do
|
4
|
+
before do
|
5
|
+
@stdout = $stdout
|
6
|
+
$stdout = StringIO.new
|
7
|
+
end
|
8
|
+
|
9
|
+
after do
|
10
|
+
$stdout = @stdout
|
11
|
+
end
|
12
|
+
|
13
|
+
it "has an execute command" do
|
14
|
+
expect { subject.execute }
|
15
|
+
.to_not raise_error
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'active_support/core_ext/numeric/time'
|
2
|
+
require_relative '../../../lib/tamarillo/config'
|
3
|
+
|
4
|
+
describe Tamarillo::Config do
|
5
|
+
let(:default_duration) { Tamarillo::Config::DEFAULT_DURATION_IN_MINUTES }
|
6
|
+
|
7
|
+
describe "empty config" do
|
8
|
+
subject { Tamarillo::Config.new }
|
9
|
+
its(:duration_in_minutes) { should == default_duration }
|
10
|
+
its(:duration_in_seconds) { should == default_duration * 60 }
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "duration_in_minutes" do
|
14
|
+
it "can be read" do
|
15
|
+
config = Tamarillo::Config.new(duration_in_minutes: 15)
|
16
|
+
config.duration_in_minutes.should == 15
|
17
|
+
end
|
18
|
+
|
19
|
+
it "can be assigned" do
|
20
|
+
subject.duration_in_minutes = 10
|
21
|
+
subject.duration_in_minutes.should == 10
|
22
|
+
end
|
23
|
+
|
24
|
+
it "assigns the default when value is nil" do
|
25
|
+
subject.duration_in_minutes = nil
|
26
|
+
subject.duration_in_minutes.should == default_duration
|
27
|
+
end
|
28
|
+
|
29
|
+
it "affects the duration in seconds" do
|
30
|
+
subject.duration_in_minutes = 30
|
31
|
+
subject.duration_in_seconds.should == 30 * 60
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "duration" do
|
36
|
+
it "aliases duration_in_minutes" do
|
37
|
+
expect { subject.duration = 10 }
|
38
|
+
.to change { subject.duration_in_minutes }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "duration_in_seconds" do
|
43
|
+
it "converts the duration in minutes into seconds" do
|
44
|
+
config = Tamarillo::Config.new(duration_in_minutes: 15)
|
45
|
+
config.duration_in_seconds.should == 15 * 60
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "notifier" do
|
50
|
+
it "can be read" do
|
51
|
+
config = Tamarillo::Config.new(notifier: Tamarillo::Notification::SPEECH)
|
52
|
+
config.notifier.should == Tamarillo::Notification::SPEECH
|
53
|
+
end
|
54
|
+
|
55
|
+
it "can be assigned" do
|
56
|
+
config = Tamarillo::Config.new
|
57
|
+
config.notifier = 'Growl'
|
58
|
+
config.notifier.should == Tamarillo::Notification::GROWL
|
59
|
+
end
|
60
|
+
|
61
|
+
it "falls back to the existing value if a new value is invalid" do
|
62
|
+
config = Tamarillo::Config.new(notifier: Tamarillo::Notification::SPEECH)
|
63
|
+
config.notifier = 'bogus'
|
64
|
+
config.notifier.should == Tamarillo::Notification::SPEECH
|
65
|
+
end
|
66
|
+
|
67
|
+
it "has a default value" do
|
68
|
+
config = Tamarillo::Config.new
|
69
|
+
config.notifier.should == Tamarillo::Notification::BELL
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "read from YAML" do
|
74
|
+
let(:sample_config_path) { Pathname.new('spec/support/sample-config.yml') }
|
75
|
+
let(:invalid_config_path) { Pathname.new('spec/support/invalid-config.yml') }
|
76
|
+
|
77
|
+
describe "duration value" do
|
78
|
+
it "can read the duration from YAML" do
|
79
|
+
config = Tamarillo::Config.load(sample_config_path)
|
80
|
+
config.duration_in_seconds.should == 30 * 60
|
81
|
+
end
|
82
|
+
|
83
|
+
it "uses the default duration if the YAML is malformed" do
|
84
|
+
config = Tamarillo::Config.load(invalid_config_path)
|
85
|
+
config.duration_in_seconds.should == default_duration * 60
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "notifier value" do
|
90
|
+
it "can read the notifier from YAML" do
|
91
|
+
config = Tamarillo::Config.load(sample_config_path)
|
92
|
+
config.notifier.should == Tamarillo::Notification::GROWL
|
93
|
+
end
|
94
|
+
|
95
|
+
it "uses the default value if the YAML is malformed" do
|
96
|
+
config = Tamarillo::Config.load(invalid_config_path)
|
97
|
+
config.notifier.should == Tamarillo::Notification::BELL
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it "creates a default config when path is missing" do
|
102
|
+
config_path = Pathname.new('some/invalid/path')
|
103
|
+
config = Tamarillo::Config.load(config_path)
|
104
|
+
config.duration_in_seconds.should == default_duration * 60
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "write to YAML" do
|
109
|
+
before do
|
110
|
+
FileUtils.mkdir('tmp') unless File.directory?('tmp')
|
111
|
+
end
|
112
|
+
|
113
|
+
it "can be written to YAML" do
|
114
|
+
config = Tamarillo::Config.new
|
115
|
+
config.duration_in_minutes = 5
|
116
|
+
config.write('tmp/write-test.yml')
|
117
|
+
|
118
|
+
File.read('tmp/write-test.yml').should include('duration: 5')
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "dumping YAML" do
|
123
|
+
it "outputs a YAML format" do
|
124
|
+
config = Tamarillo::Config.new
|
125
|
+
yaml = config.to_yaml
|
126
|
+
yaml.should == <<EOS
|
127
|
+
---
|
128
|
+
duration: 25
|
129
|
+
notifier: bell
|
130
|
+
EOS
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require_relative '../../../lib/tamarillo/controller'
|
2
|
+
|
3
|
+
describe Tamarillo::Controller do
|
4
|
+
let(:config) { double('config') }
|
5
|
+
let(:storage) { double('storage') }
|
6
|
+
|
7
|
+
subject { Tamarillo::Controller.new(config, storage) }
|
8
|
+
|
9
|
+
describe "#status" do
|
10
|
+
let(:tomato) do
|
11
|
+
stub(:remaining => 1500, :duration => 1500, :active? => true)
|
12
|
+
end
|
13
|
+
let(:storage) { stub(:latest => tomato) }
|
14
|
+
|
15
|
+
it "can return a humanized format" do
|
16
|
+
subject.status(Tamarillo::Formats::HUMAN).should == 'About 25 minutes'
|
17
|
+
end
|
18
|
+
|
19
|
+
it "can return a machine optimized format" do
|
20
|
+
subject.status(Tamarillo::Formats::PROMPT).should == '25:00 1500 1500'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "complains if the format is invalid" do
|
24
|
+
expect { subject.status('invalid') }
|
25
|
+
.should raise_error
|
26
|
+
end
|
27
|
+
|
28
|
+
it "returns nil if no active tomato" do
|
29
|
+
tomato.stub(:active?) { false }
|
30
|
+
subject.status(Tamarillo::Formats::HUMAN).should be_nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#start_new_tomato" do
|
35
|
+
before do
|
36
|
+
config.stub(:duration_in_seconds => 1500)
|
37
|
+
config.stub(:notifier => nil)
|
38
|
+
storage.stub(:write_monitor)
|
39
|
+
# Prevent monitor forking while testing.
|
40
|
+
Tamarillo::Monitor.any_instance.stub(:start)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "stores a new tomato" do
|
44
|
+
storage.stub(:latest => nil)
|
45
|
+
storage.should_receive(:write_tomato)
|
46
|
+
|
47
|
+
subject.start_new_tomato
|
48
|
+
end
|
49
|
+
|
50
|
+
it "does nothing if a tomato is already in progress." do
|
51
|
+
tomato = stub(:active? => true)
|
52
|
+
storage.stub(:latest => tomato)
|
53
|
+
storage.should_not_receive(:write_tomato)
|
54
|
+
|
55
|
+
subject.start_new_tomato
|
56
|
+
end
|
57
|
+
|
58
|
+
it "uses the configured duration" do
|
59
|
+
storage.stub(:latest => nil)
|
60
|
+
storage.stub(:write_tomato)
|
61
|
+
Tamarillo::Tomato.should_receive(:new).with(1500, anything)
|
62
|
+
|
63
|
+
subject.start_new_tomato
|
64
|
+
end
|
65
|
+
|
66
|
+
it "uses a current clock" do
|
67
|
+
storage.stub(:latest => nil)
|
68
|
+
storage.stub(:write_tomato)
|
69
|
+
Tamarillo::Clock.should_receive(:now)
|
70
|
+
|
71
|
+
subject.start_new_tomato
|
72
|
+
end
|
73
|
+
|
74
|
+
it "returns the started tomato" do
|
75
|
+
storage.stub(:latest => nil)
|
76
|
+
storage.stub(:write_tomato)
|
77
|
+
subject.start_new_tomato.should be_a(Tamarillo::Tomato)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "#interrupt_current_tomato" do
|
82
|
+
before do
|
83
|
+
storage.stub(:read_monitor)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "interrupts the current tomato" do
|
87
|
+
tomato = double('tomato')
|
88
|
+
tomato.should_receive(:interrupt!)
|
89
|
+
storage.stub(:write_tomato)
|
90
|
+
storage.should_receive(:latest).and_return(tomato)
|
91
|
+
|
92
|
+
subject.interrupt_current_tomato
|
93
|
+
end
|
94
|
+
|
95
|
+
it "doesn't raise if no tomato is present" do
|
96
|
+
storage.should_receive(:latest).and_return(nil)
|
97
|
+
expect { subject.interrupt_current_tomato }
|
98
|
+
.should_not raise_error(NoMethodError)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "writes the tomato" do
|
102
|
+
tomato = double('tomato')
|
103
|
+
tomato.stub(:interrupt!)
|
104
|
+
storage.stub(:latest => tomato)
|
105
|
+
storage.should_receive(:write_tomato).with(tomato)
|
106
|
+
|
107
|
+
subject.interrupt_current_tomato
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "#config" do
|
112
|
+
it "returns the passed in config" do
|
113
|
+
subject.config.should == config
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe "#update_config" do
|
118
|
+
before do
|
119
|
+
storage.stub(:write_config)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "can update the duration" do
|
123
|
+
config.should_receive(:duration=).with(10)
|
124
|
+
subject.update_config(:duration => 10)
|
125
|
+
end
|
126
|
+
|
127
|
+
it "ignores bogus arguments" do
|
128
|
+
config.should_not_receive(:foo=).with('bar')
|
129
|
+
subject.update_config(:foo => 'bar')
|
130
|
+
end
|
131
|
+
|
132
|
+
it "writes the config" do
|
133
|
+
storage.should_receive(:write_config)
|
134
|
+
subject.update_config
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|