tamarillo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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