TEST_MODE = true require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'timetrap')) require 'rspec' require 'fakefs/safe' def local_time(str) Timetrap::Timer.process_time(str) end def local_time_cli(str) local_time(str).strftime('%Y-%m-%d %H:%M:%S') end module Timetrap::StubConfig def with_stubbed_config options = {} defaults = Timetrap::Config.defaults.dup Timetrap::Config.stub(:[]).and_return do |k| defaults.merge(options)[k] end yield if block_given? end end describe Timetrap do include Timetrap::StubConfig before do with_stubbed_config end def create_entry atts = {} Timetrap::Entry.create({ :sheet => 'default', :start => Time.now, :end => Time.now, :note => 'note'}.merge(atts)) end before :each do Timetrap::Entry.create_table! Timetrap::Meta.create_table! $stdout = StringIO.new $stdin = StringIO.new $stderr = StringIO.new end describe 'CLI' do describe "COMMANDS" do def invoke command Timetrap::CLI.parse command Timetrap::CLI.invoke end describe 'with no command' do it "should invoke --help" do with_stubbed_config('default_command' => nil) do invoke '' $stdout.string.should include "Usage" end end end describe 'with default command configured' do it "should invoke the default command" do with_stubbed_config('default_command' => 'n') do invoke '' $stderr.string.should include('*default: not running') end end it "should allow a complicated default command" do with_stubbed_config('default_command' => 'display -f csv', 'formatter_search_paths' => '/tmp') do invoke 'in foo bar' invoke 'out' invoke '' $stdout.string.should include(',"foo bar"') end end end describe 'with an invalid command' do it "should tell me I'm wrong" do invoke 'poo' $stderr.string.should include 'Invalid command: "poo"' end end describe 'archive' do before do 3.times do |i| create_entry({:note => 'grep'}) end 3.times do |i| create_entry end end it "should only archive entries matched by the provided regex" do $stdin.string = "yes\n" invoke 'archive --grep [g][r][e][p]' Timetrap::Entry.each do |e| if e.note == 'grep' e.sheet.should == '_default' else e.sheet.should == 'default' end end end it "should put the entries in a hidden sheet" do $stdin.string = "yes\n" invoke 'archive' Timetrap::Entry.each do |e| e.sheet.should == '_default' end end it "should leave the running entry alone" do invoke "in" $stdin.string = "yes\n" invoke 'archive' Timetrap::Entry.order(:id).last.sheet.should == 'default' end end describe 'config' do it "should write a config file" do FakeFS do FileUtils.mkdir_p(ENV['HOME']) config_file = ENV['HOME'] + '/.timetrap.yml' FileUtils.rm(config_file) if File.exist? config_file File.exist?(config_file).should be_false invoke "configure" File.exist?(config_file).should be_true end end it "should describe config file" do FakeFS do invoke "configure" $stdout.string.should == "Config file is at \"#{ENV['HOME']}/.timetrap.yml\"\n" end end end describe 'edit' do before do Timetrap::Timer.start "running entry", nil end it "should edit the description of the active period" do Timetrap::Timer.active_entry.note.should == 'running entry' invoke 'edit new description' Timetrap::Timer.active_entry.note.should == 'new description' end it "should allow you to move an entry to another sheet" do invoke 'edit --move blahblah' Timetrap::Timer.active_entry[:sheet].should == 'blahblah' invoke 'edit -m blahblahblah' Timetrap::Timer.active_entry[:sheet].should == 'blahblahblah' end it "should change the current sheet if the current entry's sheet is changed" do Timetrap::Timer.current_sheet.should_not == 'blahblahblah' invoke 'edit -m blahblahblah' Timetrap::Timer.active_entry[:sheet].should == 'blahblahblah' Timetrap::Timer.current_sheet.should == 'blahblahblah' end it "should change the current sheet if a non current entry's sheet is changed" do sheet = Timetrap::Timer.current_sheet id = Timetrap::Timer.active_entry[:id] invoke 'out' invoke "edit -m blahblahblah -i #{id}" Timetrap::Timer.current_sheet.should == sheet Timetrap::Entry[id][:sheet].should == 'blahblahblah' end it "should allow appending to the description of the active period" do with_stubbed_config('append_notes_delimiter' => '//') Timetrap::Timer.active_entry.note.should == 'running entry' invoke 'edit --append new' Timetrap::Timer.active_entry.note.should == 'running entry//new' invoke 'edit -z more' Timetrap::Timer.active_entry.note.should == 'running entry//new//more' end it "should edit the start time of the active period" do invoke 'edit --start "yesterday 10am"' Timetrap::Timer.active_entry.start.should == Chronic.parse("yesterday 10am") Timetrap::Timer.active_entry.note.should == 'running entry' end it "should edit the end time of the active period" do entry = Timetrap::Timer.active_entry invoke 'edit --end "yesterday 10am"' entry.refresh.end.should == Chronic.parse("yesterday 10am") entry.refresh.note.should == 'running entry' end it "should edit a non running entry based on id" do not_running = Timetrap::Timer.active_entry Timetrap::Timer.stop(Timetrap::Timer.current_sheet) Timetrap::Timer.start "another entry", nil # create a few more entries to ensure we're not falling back on "last # checked out of" feature. Timetrap::Timer.stop(Timetrap::Timer.current_sheet) Timetrap::Timer.start "another entry", nil Timetrap::Timer.stop(Timetrap::Timer.current_sheet) Timetrap::Timer.start "another entry", nil invoke "edit --id #{not_running.id} a new description" not_running.refresh.note.should == 'a new description' end it "should edit the entry last checked out of if none is running" do not_running = Timetrap::Timer.active_entry Timetrap::Timer.stop(Timetrap::Timer.current_sheet) invoke "edit -z 'a new description'" not_running.refresh.note.should include 'a new description' end it "should edit the entry last checked out of if none is running even if the sheet is changed" do not_running = Timetrap::Timer.active_entry Timetrap::Timer.stop(Timetrap::Timer.current_sheet) invoke "edit -z 'a new description'" invoke "sheet another second sheet" not_running.refresh.note.should include 'a new description' not_running.refresh.sheet.should == 'default' Timetrap::Timer.current_sheet.should == 'another second sheet' end end describe 'auto_sheet' do describe "using dotfiles auto_sheet" do describe 'with a .timetrap-sheet in cwd' do it 'should use sheet defined in dorfile' do Dir.chdir('spec/dotfile') do with_stubbed_config('auto_sheet' => 'dotfiles') Timetrap::Timer.current_sheet.should == 'dotfile-sheet' end end end end describe "using YamlCwd autosheet" do describe 'with cwd in auto_sheet_paths' do it 'should use sheet defined in config' do with_stubbed_config( 'auto_sheet_paths' => { 'a sheet' => ['/not/cwd/', Dir.getwd] }, 'auto_sheet' => 'yaml_cwd') Timetrap::Timer.current_sheet.should == 'a sheet' end end describe 'with ancestor of cwd in auto_sheet_paths' do it 'should use sheet defined in config' do with_stubbed_config( 'auto_sheet_paths' => {'a sheet' => '/'}, 'auto_sheet' => 'yaml_cwd' ) Timetrap::Timer.current_sheet.should == 'a sheet' end end describe 'with cwd not in auto_sheet_paths' do it 'should not use sheet defined in config' do with_stubbed_config( 'auto_sheet_paths' => { 'a sheet' => '/not/the/current/working/directory/' },'auto_sheet' => 'yaml_cwd') Timetrap::Timer.current_sheet.should == 'default' end end describe 'with cwd and ancestor in auto_sheet_paths' do it 'should use the most specific config' do with_stubbed_config( 'auto_sheet_paths' => { 'general sheet' => '/', 'more specific sheet' => Dir.getwd }, 'auto_sheet' => 'yaml_cwd') Timetrap::Timer.current_sheet.should == 'more specific sheet' with_stubbed_config( 'auto_sheet_paths' => { 'more specific sheet' => Dir.getwd, 'general sheet' => '/' }, 'auto_sheet' => 'yaml_cwd') Timetrap::Timer.current_sheet.should == 'more specific sheet' end end end describe "using nested_dotfiles auto_sheet" do describe 'with a .timetrap-sheet in cwd' do it 'should use sheet defined in dotfile' do Dir.chdir('spec/dotfile') do with_stubbed_config('auto_sheet' => 'nested_dotfiles') Timetrap::Timer.current_sheet.should == 'dotfile-sheet' end end it 'should use top-most sheet found in dir heirarchy' do Dir.chdir('spec/dotfile/nested') do with_stubbed_config('auto_sheet' => 'nested_dotfiles') Timetrap::Timer.current_sheet.should == 'nested-sheet' end end end describe 'with no .timetrap-sheet in cwd' do it 'should use sheet defined in ancestor\'s dotfile' do Dir.chdir('spec/dotfile/nested/no-sheet') do with_stubbed_config('auto_sheet' => 'nested_dotfiles') Timetrap::Timer.current_sheet.should == 'nested-sheet' end end end end end describe "backend" do it "should open an sqlite console to the db" do Timetrap::CLI.should_receive(:exec).with("sqlite3 #{Timetrap::DB_NAME}") invoke 'backend' end end describe "format" do before do create_entry end it "should be deprecated" do invoke 'format' $stderr.string.should == <<-WARN The "format" command is deprecated in favor of "display". Sorry for the inconvenience. WARN end end describe "display" do describe "text" do before do Timetrap::Entry.create( :sheet => 'another', :note => 'a long entry note', :start => '2008-10-05 18:00:00' ) Timetrap::Entry.create( :sheet => 'SpecSheet', :note => 'entry 2', :start => '2008-10-03 16:00:00', :end => '2008-10-03 18:00:00' ) Timetrap::Entry.create( :sheet => 'SpecSheet', :note => 'entry 1', :start => '2008-10-03 12:00:00', :end => '2008-10-03 14:00:00' ) Timetrap::Entry.create( :sheet => 'SpecSheet', :note => 'entry 3', :start => '2008-10-05 16:00:00', :end => '2008-10-05 18:00:00' ) Timetrap::Entry.create( :sheet => 'SpecSheet', :note => 'entry 4', :start => '2008-10-05 18:00:00' ) now = local_time('2008-10-05 20:00:00') Time.stub(:now).and_return now @desired_output = <<-OUTPUT Timesheet: SpecSheet Day Start End Duration Notes Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 1 16:00:00 - 18:00:00 2:00:00 entry 2 4:00:00 Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 entry 3 18:00:00 - 2:00:00 entry 4 4:00:00 ----------------------------------------------------------- Total 8:00:00 OUTPUT @desired_output_grepped = <<-OUTPUT Timesheet: SpecSheet Day Start End Duration Notes Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 1 2:00:00 Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 entry 3 2:00:00 ----------------------------------------------------------- Total 4:00:00 OUTPUT @desired_output_with_ids = <<-OUTPUT Timesheet: SpecSheet Id Day Start End Duration Notes 3 Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 1 2 16:00:00 - 18:00:00 2:00:00 entry 2 4:00:00 4 Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 entry 3 5 18:00:00 - 2:00:00 entry 4 4:00:00 ----------------------------------------------------------- Total 8:00:00 OUTPUT @desired_output_with_long_ids = <<-OUTPUT Timesheet: SpecSheet Id Day Start End Duration Notes 3 Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 1 2 16:00:00 - 18:00:00 2:00:00 entry 2 4:00:00 40000 Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 entry 3 5 18:00:00 - 2:00:00 entry 4 4:00:00 ----------------------------------------------------------- Total 8:00:00 OUTPUT @desired_output_for_all = <<-OUTPUT Timesheet: SpecSheet Day Start End Duration Notes Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 1 16:00:00 - 18:00:00 2:00:00 entry 2 4:00:00 Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 entry 3 18:00:00 - 2:00:00 entry 4 4:00:00 --------------------------------------------------------------------- Total 8:00:00 Timesheet: another Day Start End Duration Notes Sun Oct 05, 2008 18:00:00 - 2:00:00 a long entry note 2:00:00 --------------------------------------------------------------------- Total 2:00:00 ------------------------------------------------------------------------- Grand Total 10:00:00 OUTPUT end it "should display the current timesheet" do Timetrap::Timer.current_sheet = 'SpecSheet' invoke 'display' $stdout.string.should == @desired_output end it "should display a non current timesheet" do Timetrap::Timer.current_sheet = 'another' invoke 'display SpecSheet' $stdout.string.should == @desired_output end it "should display a non current timesheet based on a partial name match" do Timetrap::Timer.current_sheet = 'another' invoke 'display S' $stdout.string.should == @desired_output end it "should prefer an exact match of a named sheet to a partial match" do Timetrap::Timer.current_sheet = 'Spec' Timetrap::Entry.create( :sheet => 'Spec', :note => 'entry 5', :start => '2008-10-05 18:00:00' ) invoke 'display Spec' $stdout.string.should include("entry 5") end it "should only display entries that are matched by the provided regex" do Timetrap::Timer.current_sheet = 'SpecSheet' invoke 'display --grep [13]' $stdout.string.should == @desired_output_grepped end it "should display a timesheet with ids" do invoke 'display S --ids' $stdout.string.should == @desired_output_with_ids end it "should properly format a timesheet with long ids" do Timetrap::DB["UPDATE entries SET id = 40000 WHERE id = 4"].all invoke 'display S --ids' $stdout.string.should == @desired_output_with_long_ids end it "should properly format a timesheet with no ids even if long ids are in the db" do Timetrap::DB["UPDATE entries SET id = 40000 WHERE id = 4"].all invoke 'display S' $stdout.string.should == @desired_output end it "should display all timesheets" do Timetrap::Timer.current_sheet = 'another' invoke 'display all' $stdout.string.should == @desired_output_for_all end it "should not display archived for all timesheets" do $stdin.string = "yes\n" invoke 'archive SpecSheet' $stdout.string = '' invoke 'display all' $stdout.string.should_not =~ /_SpecSheet/ end it "it should find a user provided formatter class and require it" do create_entry create_entry dir = '/tmp/timetrap/foo/bar' with_stubbed_config('formatter_search_paths' => dir) FileUtils.mkdir_p(dir) File.open(dir + '/baz.rb', 'w') do |f| f.puts <<-RUBY class Timetrap::Formatters::Baz def initialize(entries); end def output "yeah I did it" end end RUBY end invoke 'd -fbaz' $stdout.string.should == "yeah I did it\n" FileUtils.rm_r dir end it "should work when there's no note" do Timetrap::Entry.create( :sheet => 'SpecSheet', :note => nil ) invoke 'd SpecSheet' # check it doesn't error and produces valid looking output $stdout.string.should include('Timesheet: SpecSheet') end end describe "default" do before do create_entry(:start => '2008-10-03 12:00:00', :end => '2008-10-03 14:00:00') create_entry(:start => '2008-10-05 12:00:00', :end => '2008-10-05 14:00:00') end it "should allow another formatter to be set as the default" do with_stubbed_config 'default_formatter' => 'ids', 'formatter_search_paths' => nil invoke 'd' $stdout.string.should == Timetrap::Entry.all.map(&:id).join(" ") + "\n" end end describe 'ids' do before do create_entry(:start => '2008-10-03 12:00:00', :end => '2008-10-03 14:00:00') create_entry(:start => '2008-10-05 12:00:00', :end => '2008-10-05 14:00:00') end it "should not export running items" do invoke 'in' invoke 'display --format ids' $stdout.string.should == Timetrap::Entry.all.map(&:id).join(" ") + "\n" end end describe 'csv' do before do create_entry(:start => '2008-10-03 12:00:00', :end => '2008-10-03 14:00:00') create_entry(:start => '2008-10-05 12:00:00', :end => '2008-10-05 14:00:00') end it "should not export running items" do invoke 'in' invoke 'display --format csv' $stdout.string.should == <<-EOF start,end,note,sheet "2008-10-03 12:00:00","2008-10-03 14:00:00","note","default" "2008-10-05 12:00:00","2008-10-05 14:00:00","note","default" EOF end it "should filter events by the passed dates" do invoke 'display --format csv --start 2008-10-03 --end 2008-10-03' $stdout.string.should == <<-EOF start,end,note,sheet "2008-10-03 12:00:00","2008-10-03 14:00:00","note","default" EOF end it "should not filter events by date when none are passed" do invoke 'display --format csv' $stdout.string.should == <<-EOF start,end,note,sheet "2008-10-03 12:00:00","2008-10-03 14:00:00","note","default" "2008-10-05 12:00:00","2008-10-05 14:00:00","note","default" EOF end end describe 'json' do before do create_entry(:start => local_time_cli('2008-10-03 12:00:00'), :end => local_time_cli('2008-10-03 14:00:00')) create_entry(:start => local_time_cli('2008-10-05 12:00:00'), :end => local_time_cli('2008-10-05 14:00:00')) end it "should export to json not including running items" do invoke 'in' invoke 'display -f json' JSON.parse($stdout.string).should == JSON.parse(<<-EOF) [{\"sheet\":\"default\",\"end\":\"#{local_time('2008-10-03 14:00:00')}\",\"start\":\"#{local_time('2008-10-03 12:00:00')}\",\"note\":\"note\",\"id\":1},{\"sheet\":\"default\",\"end\":\"#{local_time('2008-10-05 14:00:00')}\",\"start\":\"#{local_time('2008-10-05 12:00:00')}\",\"note\":\"note\",\"id\":2}] EOF end end describe 'ical' do before do create_entry(:start => local_time_cli('2008-10-03 12:00:00'), :end => local_time_cli('2008-10-03 14:00:00')) create_entry(:start => local_time_cli('2008-10-05 12:00:00'), :end => local_time_cli('2008-10-05 14:00:00')) end it "should not export running items" do invoke 'in' invoke 'display --format ical' $stdout.string.scan(/BEGIN:VEVENT/).should have(2).item end it "should filter events by the passed dates" do invoke 'display --format ical --start 2008-10-03 --end 2008-10-03' $stdout.string.scan(/BEGIN:VEVENT/).should have(1).item end it "should not filter events by date when none are passed" do invoke 'display --format ical' $stdout.string.scan(/BEGIN:VEVENT/).should have(2).item end it "should export a sheet to an ical format" do invoke 'display --format ical --start 2008-10-03 --end 2008-10-03' desired = <<-EOF BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH PRODID:iCalendar-Ruby BEGIN:VEVENT SEQUENCE:0 DTEND:20081003T140000 SUMMARY:note DTSTART:20081003T120000 END:VEVENT END:VCALENDAR EOF desired.each_line do |line| $stdout.string.should =~ /#{line.chomp}/ end end end end describe "in" do it "should start the time for the current timesheet" do lambda do invoke 'in' end.should change(Timetrap::Entry, :count).by(1) end it "should set the note when starting a new entry" do invoke 'in working on something' Timetrap::Entry.order_by(:id).last.note.should == 'working on something' end it "should set the start when starting a new entry" do @time = Time.now Time.stub(:now).and_return @time invoke 'in working on something' Timetrap::Entry.order_by(:id).last.start.to_i.should == @time.to_i end it "should not start the time if the timetrap is running" do Timetrap::Timer.stub(:running?).and_return true lambda do invoke 'in' end.should_not change(Timetrap::Entry, :count) end it "should allow the sheet to be started at a certain time" do invoke 'in work --at "10am 2008-10-03"' Timetrap::Entry.order_by(:id).last.start.should == Time.parse('2008-10-03 10:00') end it "should fail with a warning for misformatted cli options it can't parse" do now = Time.now Time.stub(:now).and_return now invoke 'in work --at="18 minutes ago"' Timetrap::Entry.order_by(:id).last.should be_nil $stderr.string.should =~ /\w+/ end it "should fail with a time argurment of total garbage" do now = Time.now Time.stub(:now).and_return now invoke 'in work --at "total garbage"' Timetrap::Entry.order_by(:id).last.should be_nil $stderr.string.should =~ /\w+/ end describe "with require_note config option set" do before do with_stubbed_config 'require_note' => true end it "should prompt for a note if one isn't passed" do $stdin.string = "an interactive note\n" invoke "in" $stderr.string.should include('enter a note') Timetrap::Timer.active_entry.note.should == "an interactive note" end it "should not prompt for a note if one is passed" do $stdin.string = "an interactive note\n" invoke "in a normal note" Timetrap::Timer.active_entry.note.should == "a normal note" end it "should not stop the running entry or prompt" do invoke "in a normal note" $stdin.string = "an interactive note\n" invoke "in" Timetrap::Timer.active_entry.note.should == "a normal note" end end describe "with auto_checkout config option set" do before do with_stubbed_config 'auto_checkout' => true end it "should check in normally if nothing else is running" do Timetrap::Timer.should_not be_running #precondition invoke 'in' Timetrap::Timer.should be_running end describe "with a running entry on current sheet" do before do invoke 'sheet sheet1' invoke 'in first task' end it "should check out and back in" do entry = Timetrap::Timer.active_entry('sheet1') invoke 'in second task' Timetrap::Timer.active_entry('sheet1').note.should == 'second task' end it "should tell me what it's doing" do invoke 'in second task' $stderr.string.should include "Checked out" end end describe "with a running entry on another sheet" do before do invoke 'sheet sheet1' invoke 'in first task' invoke 'sheet sheet2' end it "should check out of the running entry" do Timetrap::Timer.active_entry('sheet1').should be_a(Timetrap::Entry) invoke 'in second task' Timetrap::Timer.active_entry('sheet1').should be nil end it "should check out of the running entry at another time" do now = Time.at(Time.now - 5 * 60) # 5 minutes ago entry = Timetrap::Timer.active_entry('sheet1') entry.should be_a(Timetrap::Entry) invoke "in -a '#{now}' second task" entry.reload.end.to_s.should == now.to_s end it "should check out of the running entry without having to start a new entry" do entry = Timetrap::Timer.active_entry('sheet1') entry.should be_a(Timetrap::Entry) entry.end.should be_nil invoke "out" entry.reload.end.should_not be_nil end end end end describe "today" do it "should only show entries for today" do yesterday = Time.now - (24 * 60 * 60) create_entry( :start => yesterday, :end => yesterday ) create_entry invoke 'today' $stdout.string.should include Time.now.strftime('%a %b %d, %Y') $stdout.string.should_not include yesterday.strftime('%a %b %d, %Y') end end describe "yesterday" do it "should only show entries for yesterday" do yesterday = Time.now - (24 * 60 * 60) create_entry( :start => yesterday, :end => yesterday ) create_entry invoke 'yesterday' $stdout.string.should include yesterday.strftime('%a %b %d, %Y') $stdout.string.should_not include Time.now.strftime('%a %b %d, %Y') end end describe "week" do it "should only show entries from this week" do create_entry( :start => Time.local(2012, 2, 1, 1, 2, 3), :end => Time.local(2012, 2, 1, 2, 2, 3) ) create_entry invoke 'week' $stdout.string.should include Time.now.strftime('%a %b %d, %Y') $stdout.string.should_not include 'Feb 01, 2012' end end describe "month" do it "should display all entries for the month" do create_entry( :start => Time.local(2012, 2, 5, 1, 2, 3), :end => Time.local(2012, 2, 5, 2, 2, 3) ) create_entry( :start => Time.local(2012, 2, 6, 1, 2, 3), :end => Time.local(2012, 2, 6, 2, 2, 3) ) create_entry( :start => Time.local(2012, 1, 5, 1, 2, 3), :end => Time.local(2012, 1, 5, 2, 2, 3) ) Date.should_receive(:today).and_return(Date.new(2012, 2, 5)) invoke "month" $stdout.string.should include 'Feb 05, 2012' $stdout.string.should include 'Feb 06, 2012' $stdout.string.should_not include 'Jan' end it "should work in December" do create_entry( :start => Time.local(2012, 12, 5, 1, 2, 3), :end => Time.local(2012, 12, 5, 2, 2, 3) ) Date.should_receive(:today).and_return(Date.new(2012, 12, 5)) invoke "month" $stdout.string.should include 'Wed Dec 05, 2012 01:02:03 - 02:02:03' end end describe "kill" do it "should give me a chance not to fuck up" do entry = create_entry lambda do $stdin.string = "" invoke "kill #{entry.sheet}" end.should_not change(Timetrap::Entry, :count).by(-1) end it "should delete a timesheet" do create_entry entry = create_entry lambda do $stdin.string = "yes\n" invoke "kill #{entry.sheet}" end.should change(Timetrap::Entry, :count).by(-2) end it "should delete an entry" do create_entry entry = create_entry lambda do $stdin.string = "yes\n" invoke "kill --id #{entry.id}" end.should change(Timetrap::Entry, :count).by(-1) end it "should not prompt the user if the --yes flag is passed" do create_entry entry = create_entry lambda do invoke "kill --id #{entry.id} --yes" end.should change(Timetrap::Entry, :count).by(-1) end describe "with a numeric sheet name" do before do now = local_time("2008-10-05 18:00:00") Time.stub(:now).and_return now create_entry( :sheet => 1234, :start => local_time_cli('2008-10-03 12:00:00'), :end => local_time_cli('2008-10-03 14:00:00')) end it "should kill the sheet" do lambda do invoke 'kill -y 1234' end.should change(Timetrap::Entry, :count).by(-1) end end end describe "list" do describe "with no sheets defined" do it "should list the default sheet" do invoke 'list' $stdout.string.chomp.should == " Timesheet Running Today Total Time\n*default 0:00:00 0:00:00 0:00:00" end end describe "with a numeric sheet name" do before do now = local_time("2008-10-05 18:00:00") Time.stub(:now).and_return now create_entry( :sheet => '1234', :start => local_time_cli('2008-10-03 12:00:00'), :end => local_time_cli('2008-10-03 14:00:00')) end it "should list the sheet" do invoke 'list' $stdout.string.should == " Timesheet Running Today Total Time\n 1234 0:00:00 0:00:00 2:00:00\n*default 0:00:00 0:00:00 0:00:00\n" end end describe "with a numeric current_sheet" do before do Timetrap::Timer.current_sheet = '1234' end it "should list the sheet" do invoke 'list' $stdout.string.should == " Timesheet Running Today Total Time\n*1234 0:00:00 0:00:00 0:00:00\n" end end describe "with sheets defined" do before :each do now = local_time("2008-10-05 18:00:00") Time.stub(:now).and_return now create_entry( :sheet => 'A Longly Named Sheet 2', :start => local_time_cli('2008-10-03 12:00:00'), :end => local_time_cli('2008-10-03 14:00:00')) create_entry( :sheet => 'A Longly Named Sheet 2', :start => local_time_cli('2008-10-03 12:00:00'), :end => local_time_cli('2008-10-03 14:00:00')) create_entry( :sheet => 'A Longly Named Sheet 2', :start => local_time_cli('2008-10-05 12:00:00'), :end => local_time_cli('2008-10-05 14:00:00')) create_entry( :sheet => 'A Longly Named Sheet 2', :start => local_time_cli('2008-10-05 14:00:00'), :end => nil) create_entry( :sheet => 'Sheet 1', :start => local_time_cli('2008-10-03 16:00:00'), :end => local_time_cli('2008-10-03 18:00:00')) Timetrap::Timer.current_sheet = 'A Longly Named Sheet 2' end it "should list available timesheets" do invoke 'list' $stdout.string.should == <<-OUTPUT Timesheet Running Today Total Time *A Longly Named Sheet 2 4:00:00 6:00:00 10:00:00 Sheet 1 0:00:00 0:00:00 2:00:00 OUTPUT end it "should mark the last sheet with '-' if it exists" do invoke 'sheet Sheet 1' $stdout.string = '' invoke 'list' $stdout.string.should == <<-OUTPUT Timesheet Running Today Total Time -A Longly Named Sheet 2 4:00:00 6:00:00 10:00:00 *Sheet 1 0:00:00 0:00:00 2:00:00 OUTPUT end it "should not mark the last sheet with '-' if it doesn't exist" do invoke 'sheet Non-existent' invoke 'sheet Sheet 1' $stdout.string = '' invoke 'list' $stdout.string.should == <<-OUTPUT Timesheet Running Today Total Time A Longly Named Sheet 2 4:00:00 6:00:00 10:00:00 *Sheet 1 0:00:00 0:00:00 2:00:00 OUTPUT end it "should include the active timesheet even if it has no entries" do invoke 'sheet empty sheet' $stdout.string = '' invoke 'list' $stdout.string.should == <<-OUTPUT Timesheet Running Today Total Time -A Longly Named Sheet 2 4:00:00 6:00:00 10:00:00 *empty sheet 0:00:00 0:00:00 0:00:00 Sheet 1 0:00:00 0:00:00 2:00:00 OUTPUT end end end describe "now" do before do Timetrap::Timer.current_sheet = 'current sheet' end describe "when the current timesheet isn't running" do it "should show that it isn't running" do invoke 'now' $stderr.string.should == <<-OUTPUT *current sheet: not running OUTPUT end end describe "when the current timesheet is running" do before do invoke 'in a timesheet that is running' @entry = Timetrap::Timer.active_entry @entry.start = Time.at(0) @entry.save Time.stub(:now).and_return Time.at(60) end it "should show how long the current item is running for" do invoke 'now' $stdout.string.should == <<-OUTPUT *current sheet: 0:01:00 (a timesheet that is running) OUTPUT end describe "and another timesheet is running too" do before do invoke 'sheet another-sheet' invoke 'in also running' @entry = Timetrap::Timer.active_entry @entry.start = Time.at(0) @entry.save Time.stub(:now).and_return Time.at(60) end it "should show both entries" do invoke 'now' $stdout.string.should == <<-OUTPUT current sheet: 0:01:00 (a timesheet that is running) *another-sheet: 0:01:00 (also running) OUTPUT end end end end describe "out" do before :each do invoke 'in' @active = Timetrap::Timer.active_entry @now = Time.now Time.stub(:now).and_return @now end it "should set the stop for the running entry" do @active.refresh.end.should == nil invoke 'out' @active.refresh.end.to_i.should == @now.to_i end it "should not do anything if nothing is running" do lambda do invoke 'out' invoke 'out' end.should_not raise_error end it "should allow the sheet to be stopped at a certain time" do invoke 'out --at "10am 2008-10-03"' @active.refresh.end.should == Time.parse('2008-10-03 10:00') end it "should allow you to check out of a non active sheet" do invoke 'sheet SomeOtherSheet' invoke 'in' @new_active = Timetrap::Timer.active_entry @active.should_not == @new_active invoke %'out #{@active.sheet} --at "10am 2008-10-03"' @active.refresh.end.should == Time.parse('2008-10-03 10:00') @new_active.refresh.end.should be_nil end end describe "resume" do before :each do @time = Time.now Time.stub(:now).and_return @time invoke 'in A previous task that isnt last' @previous = Timetrap::Timer.active_entry invoke 'out' invoke 'in Some strange task' @last_active = Timetrap::Timer.active_entry invoke 'out' Timetrap::Timer.active_entry.should be_nil @last_active.should_not be_nil end it "should allow to resume the last active sheet" do invoke 'resume' Timetrap::Timer.active_entry.note.should ==(@last_active.note) Timetrap::Timer.active_entry.start.to_s.should == @time.to_s end it "should allow to resume a specific entry" do invoke "resume --id #{@previous.id}" Timetrap::Timer.active_entry.note.should ==(@previous.note) Timetrap::Timer.active_entry.start.to_s.should == @time.to_s end it "should allow to resume a specific entry with a given time" do invoke "resume --id #{@previous.id} --at \"10am 2008-10-03\"" Timetrap::Timer.active_entry.note.should ==(@previous.note) Timetrap::Timer.active_entry.start.should eql(Time.parse('2008-10-03 10:00')) end it "should allow to resume the activity with a given time" do invoke 'resume --at "10am 2008-10-03"' Timetrap::Timer.active_entry.start.should eql(Time.parse('2008-10-03 10:00')) end describe "no existing entries" do before(:each) do Timetrap::Timer.entries(Timetrap::Timer.current_sheet).each do |e| e.destroy end Timetrap::Timer.entries(Timetrap::Timer.current_sheet).should be_empty Timetrap::Timer.active_entry.should be_nil end end describe "with only archived entries" do before(:each) do $stdin.string = "yes\n" invoke "archive" Timetrap::Timer.entries(Timetrap::Timer.current_sheet).should be_empty Timetrap::Timer.active_entry.should be_nil end it "retrieves the note of the most recent archived entry" do invoke "resume" Timetrap::Timer.active_entry.should_not be_nil Timetrap::Timer.active_entry.note.should == @last_active.note Timetrap::Timer.active_entry.start.to_s.should == @time.to_s end end describe "with auto_checkout config option set" do before do with_stubbed_config 'auto_checkout' => true end it "should check in normally if nothing else is running" do Timetrap::Timer.should_not be_running #precondition invoke 'resume' Timetrap::Timer.should be_running end describe "with a running entry on current sheet" do before do invoke 'sheet sheet1' invoke 'in first task' end it "should check out and back in" do entry = Timetrap::Timer.active_entry('sheet1') invoke 'resume second task' Timetrap::Timer.active_entry('sheet1').id.should_not == entry.id end end describe "with a running entry on another sheet" do before do invoke 'sheet sheet1' invoke 'in first task' invoke 'sheet sheet2' end it "should check out of the running entry" do Timetrap::Timer.active_entry('sheet1').should be_a(Timetrap::Entry) invoke 'resume second task' Timetrap::Timer.active_entry('sheet1').should be nil end it "should check out of the running entry at another time" do now = Time.at(Time.now - 5 * 60) # 5 minutes ago entry = Timetrap::Timer.active_entry('sheet1') entry.should be_a(Timetrap::Entry) invoke "resume -a '#{now}' second task" entry.reload.end.to_s.should == now.to_s end end end end describe "sheet" do it "should switch to a new timesheet" do invoke 'sheet sheet 1' Timetrap::Timer.current_sheet.should == 'sheet 1' invoke 'sheet sheet 2' Timetrap::Timer.current_sheet.should == 'sheet 2' end it "should not switch to an blank timesheet" do invoke 'sheet sheet 1' invoke 'sheet' Timetrap::Timer.current_sheet.should == 'sheet 1' end it "should list timesheets when there are no arguments" do invoke 'sheet sheet 1' invoke 'sheet' $stdout.string.should == " Timesheet Running Today Total Time\n*sheet 1 0:00:00 0:00:00 0:00:00\n" end describe "using - to switch to the last sheet" do it "should warn if there isn't a sheet set" do lambda do invoke 'sheet -' end.should_not change(Timetrap::Timer, :current_sheet) $stderr.string.should include 'LAST_SHEET is not set' end it "should switch to the last active sheet" do invoke 'sheet second' lambda do invoke 'sheet -' end.should change(Timetrap::Timer, :current_sheet). from('second').to('default') end it "should toggle back and forth" do invoke 'sheet first' invoke 'sheet second' 5.times do invoke 's -' Timetrap::Timer.current_sheet.should == 'first' invoke 's -' Timetrap::Timer.current_sheet.should == 'second' end end end end describe '--version' do it 'should print the version number if asked' do begin invoke '--version' rescue SystemExit #Getopt::Declare calls exit after --version is invoked end $stdout.string.should include(::Timetrap::VERSION) end end end end describe "entries" do it "should give the entires for a sheet" do e = create_entry :sheet => 'sheet' Timetrap::Timer.entries('sheet').all.should include(e) end end describe "start" do it "should start an new entry" do @time = Time.now Timetrap::Timer.current_sheet = 'sheet1' lambda do Timetrap::Timer.start 'some work', @time end.should change(Timetrap::Entry, :count).by(1) Timetrap::Entry.order(:id).last.sheet.should == 'sheet1' Timetrap::Entry.order(:id).last.note.should == 'some work' Timetrap::Entry.order(:id).last.start.to_i.should == @time.to_i Timetrap::Entry.order(:id).last.end.should be_nil end it "should be running if it is started" do Timetrap::Timer.should_not be_running Timetrap::Timer.start 'some work', @time Timetrap::Timer.should be_running end it "should raise an error if it is already running" do lambda do Timetrap::Timer.start 'some work', @time Timetrap::Timer.start 'some work', @time end.should raise_error(Timetrap::Timer::AlreadyRunning) end end describe "stop" do it "should stop a new entry" do @time = Time.now Timetrap::Timer.start 'some work', @time entry = Timetrap::Timer.active_entry entry.end.should be_nil Timetrap::Timer.stop Timetrap::Timer.current_sheet, @time entry.refresh.end.to_i.should == @time.to_i end it "should not be running if it is stopped" do Timetrap::Timer.should_not be_running Timetrap::Timer.start 'some work', @time Timetrap::Timer.stop Timetrap::Timer.current_sheet Timetrap::Timer.should_not be_running end it "should not stop it twice" do Timetrap::Timer.start 'some work' e = Timetrap::Timer.active_entry Timetrap::Timer.stop Timetrap::Timer.current_sheet time = e.refresh.end Timetrap::Timer.stop Timetrap::Timer.current_sheet time.to_i.should == e.refresh.end.to_i end it "should track the last entry that was checked out of" do Timetrap::Timer.start 'some work' e = Timetrap::Timer.active_entry Timetrap::Timer.stop Timetrap::Timer.current_sheet Timetrap::Timer.last_checkout.id.should == e.id end end describe Timetrap::Helpers do before do @helper = Object.new @helper.extend Timetrap::Helpers end it "should correctly format positive durations" do @helper.format_duration(1234).should == " 0:20:34" end it "should correctly format negative durations" do @helper.format_duration(-1234).should == "- 0:20:34" end end describe Timetrap::Entry do include Timetrap::StubConfig describe "with an instance" do before do @time = Time.now @entry = Timetrap::Entry.new end describe '.sheets' do it "should output a list of all the available sheets" do Timetrap::Entry.create( :sheet => 'another', :note => 'entry 4', :start => '2008-10-05 18:00:00' ) Timetrap::Entry.create( :sheet => 'SpecSheet', :note => 'entry 2', :start => '2008-10-03 16:00:00', :end => '2008-10-03 18:00:00' ) Timetrap::Entry.sheets.should == %w(another SpecSheet).sort end end describe 'attributes' do it "should have a note" do @entry.note = "world takeover" @entry.note.should == "world takeover" end it "should have a start" do @entry.start = @time @entry.start.to_i.should == @time.to_i end it "should have a end" do @entry.end = @time @entry.end.to_i.should == @time.to_i end it "should have a sheet" do @entry.sheet= 'name' @entry.sheet.should == 'name' end def with_rounding_on old_val = Timetrap::Entry.round begin Timetrap::Entry.round = true block_return_value = yield ensure Timetrap::Entry.round = old_val end end it "should use round start if the global round attribute is set" do with_rounding_on do with_stubbed_config('round_in_seconds' => 900) do @time = Chronic.parse("12:55") @entry.start = @time @entry.start.should == Chronic.parse("1") end end end it "should use round start if the global round attribute is set" do with_rounding_on do with_stubbed_config('round_in_seconds' => 900) do @time = Chronic.parse("12:50") @entry.start = @time @entry.start.should == Chronic.parse("12:45") end end end it "should have a rounded start" do with_stubbed_config('round_in_seconds' => 900) do @time = Chronic.parse("12:50") @entry.start = @time @entry.rounded_start.should == Chronic.parse("12:45") end end it "should not round nil times" do @entry.start = nil @entry.rounded_start.should be_nil end end describe "parsing natural language times" do it "should set start time using english" do @entry.start = "yesterday 10am" @entry.start.should_not be_nil @entry.start.should == Chronic.parse("yesterday 10am") end it "should set end time using english" do @entry.end = "tomorrow 1pm" @entry.end.should_not be_nil @entry.end.should == Chronic.parse("tomorrow 1pm") end end describe "with times specfied like 12:12:12" do it "should assume a <24 hour duration" do @entry.start= Time.at(Time.now - 3600) # 1.hour.ago @entry.end = Time.at(Time.now - 300).strftime("%H:%M:%S") # ambiguous 5.minutes.ago # should be about 55 minutes duration. Allow for second rollover # within this test. (3299..3301).should === @entry.duration end it "should not assume negative durations around 12 hour length" do @entry.start= Time.at(Time.now - (15 * 3600)) # 15.hour.ago @entry.end = Time.at(Time.now - 300).strftime("%H:%M:%S") # ambiguous 5.minutes.ago (53699..53701).should === @entry.duration end it "should assume a start time near the current time" do time = Time.at(Time.now - 300) @entry.start= time.strftime("%H:%M:%S") # ambiguous 5.minutes.ago @entry.start.to_i.should == time.to_i end end end end describe 'bins' do # https://github.com/samg/timetrap/pull/80 it 'should include a t bin and an equivalent timetrap bin' do timetrap = File.open(File.expand_path(File.join(File.dirname(__FILE__), '..', 'bin', 'timetrap'))) t = File.open(File.expand_path(File.join(File.dirname(__FILE__), '..', 'bin', 't'))) t.read.should == timetrap.read t.stat.mode.should == timetrap.stat.mode end end end