tamarillo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. data/.gitignore +19 -0
  2. data/.travis.yml +6 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +22 -0
  5. data/README.md +83 -0
  6. data/Rakefile +23 -0
  7. data/bin/tam +4 -0
  8. data/features/config.feature +44 -0
  9. data/features/step_definitions/tamarillo_steps.rb +50 -0
  10. data/features/support/env.rb +5 -0
  11. data/features/tamarillo.feature +47 -0
  12. data/lib/tamarillo/clock.rb +33 -0
  13. data/lib/tamarillo/command.rb +68 -0
  14. data/lib/tamarillo/config.rb +95 -0
  15. data/lib/tamarillo/controller.rb +113 -0
  16. data/lib/tamarillo/monitor.rb +27 -0
  17. data/lib/tamarillo/notification/bell.rb +15 -0
  18. data/lib/tamarillo/notification/growl.rb +13 -0
  19. data/lib/tamarillo/notification/none.rb +13 -0
  20. data/lib/tamarillo/notification/speech.rb +13 -0
  21. data/lib/tamarillo/notification/touch.rb +16 -0
  22. data/lib/tamarillo/notification.rb +36 -0
  23. data/lib/tamarillo/storage.rb +118 -0
  24. data/lib/tamarillo/tomato.rb +96 -0
  25. data/lib/tamarillo/tomato_file.rb +60 -0
  26. data/lib/tamarillo/version.rb +3 -0
  27. data/lib/tamarillo.rb +7 -0
  28. data/spec/lib/tamarillo/clock_spec.rb +39 -0
  29. data/spec/lib/tamarillo/command_spec.rb +17 -0
  30. data/spec/lib/tamarillo/config_spec.rb +133 -0
  31. data/spec/lib/tamarillo/controller_spec.rb +137 -0
  32. data/spec/lib/tamarillo/monitor_spec.rb +27 -0
  33. data/spec/lib/tamarillo/notification/bell_spec.rb +7 -0
  34. data/spec/lib/tamarillo/notification/growl_spec.rb +7 -0
  35. data/spec/lib/tamarillo/notification/speech_spec.rb +7 -0
  36. data/spec/lib/tamarillo/notification_spec.rb +22 -0
  37. data/spec/lib/tamarillo/storage_spec.rb +171 -0
  38. data/spec/lib/tamarillo/tomato_file_spec.rb +41 -0
  39. data/spec/lib/tamarillo/tomato_spec.rb +116 -0
  40. data/spec/support/invalid-config.yml +1 -0
  41. data/spec/support/sample-config.yml +3 -0
  42. data/tamarillo.gemspec +26 -0
  43. metadata +178 -0
@@ -0,0 +1,13 @@
1
+ module Tamarillo
2
+ module Notification
3
+ # Public: Provides a no-op for the notification system.
4
+ class None
5
+
6
+ # Public: executes the notification.
7
+ def call
8
+ # NO-OP
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Tamarillo
2
+ module Notification
3
+ class Speech
4
+ SPEECH_COMMAND = %Q{say "Tomato complete."}
5
+
6
+ # Public: executes the notification.
7
+ def call
8
+ system(SPEECH_COMMAND)
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Tamarillo
2
+ VERSION = "0.1.0"
3
+ end
data/lib/tamarillo.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'tamarillo/version'
2
+ require 'tamarillo/config'
3
+ require 'tamarillo/controller'
4
+ require 'tamarillo/notification'
5
+ require 'tamarillo/monitor'
6
+ require 'tamarillo/tomato'
7
+ require 'tamarillo/storage'
@@ -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