tuomas-tweetwine 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,9 @@
1
+ === 0.1.2 released 2009-05-04
2
+
3
+ * Renamed command "friends" to "home".
4
+ * Added command "mentions".
5
+ * Improved command line argument and configuration file parsing.
6
+
1
7
  === 0.1.1 released 2009-04-23
2
8
 
3
9
  * Renamed command "msg" to "update".
data/README.rdoc CHANGED
@@ -14,28 +14,29 @@ The program is compatible with Ruby 1.9.1.
14
14
 
15
15
  In the command line, run the program by entering
16
16
 
17
- $ tweetwine [-a username:password] [command]
17
+ $ tweetwine [options...] [command]
18
18
 
19
19
  The program needs the user's username and password for authentication. This
20
- information can be supplied either as an option to the program or via a
21
- configuration file. Without option <tt>[-a]</tt>, the program attempts to
22
- read the configuration file for authentication.
20
+ information can be supplied either via a configuration file or as an option
21
+ (<tt>-a USERNAME:PASSWORD</tt>) to the program. It is recommended to use the
22
+ former method over the latter.
23
23
 
24
- The supported commands are
25
-
26
- <tt>friends</tt>:: Fetch friends' latest statuses (the contents of the user's home).
27
- <tt>user [name]</tt>:: Fetch a specific user's latest statuses, identified by the argument; if given no argument, fetch your own statuses.
28
- <tt>update [status]</tt>:: Send a status update, but confirm the action first before actually sending. The status update can either be given as an argument or via STDIN if no argument is given.
29
-
30
- If <tt>[command]</tt> is not given, it defaults to <tt>friends</tt>.
31
-
32
- The configuration file, in <tt>~/.tweetwine</tt>, is in YAML syntax. It can
33
- contain the following settings:
24
+ The configuration file, in <tt>~/.tweetwine</tt>, is in YAML syntax. The
25
+ program recognizes the following settings:
34
26
 
35
27
  username: <your_username>
36
28
  password: <your_password>
37
29
  colorize: true|false
38
30
 
31
+ When invoking the program, the supported commands are
32
+
33
+ <tt>home</tt>:: Fetch friends' latest statuses (the contents of the authenticated user's home).
34
+ <tt>mentions</tt>:: Fetch latest mentions for the authenticated user.
35
+ <tt>user [name]</tt>:: Fetch a specific user's latest statuses, identified by the argument; if given no argument, fetch the statuses of the authenticated user.
36
+ <tt>update [status]</tt>:: Send a status update, but confirm the action first before actually sending. The status update can either be given as an argument or via STDIN if no argument is given.
37
+
38
+ If no <tt>[command]</tt> is given, the default action is <tt>home</tt>.
39
+
39
40
  For all the options, see:
40
41
 
41
42
  $ tweetwine -h
data/Rakefile CHANGED
@@ -2,7 +2,7 @@ require "rubygems"
2
2
 
3
3
  full_name = "Tweetwine"
4
4
  package_name = "tweetwine"
5
- version = "0.1.1"
5
+ version = "0.1.2"
6
6
 
7
7
  require "lib/#{package_name}"
8
8
 
@@ -20,7 +20,7 @@ spec = Gem::Specification.new do |s|
20
20
  s.email = "tkareine@gmail.com"
21
21
 
22
22
  s.platform = Gem::Platform::RUBY
23
- s.files = FileList["Rakefile", "*.rdoc", "bin/**/*", "lib/**/*", "spec/**/*"].to_a
23
+ s.files = FileList["Rakefile", "*.rdoc", "bin/**/*", "lib/**/*", "test/**/*"].to_a
24
24
  s.executables = ["tweetwine"]
25
25
 
26
26
  s.add_dependency("json", ">= 1.1.4")
@@ -30,7 +30,7 @@ spec = Gem::Specification.new do |s|
30
30
  s.extra_rdoc_files = FileList["*.rdoc"].to_a
31
31
  s.rdoc_options << "--title" << "#{full_name} #{version}" \
32
32
  << "--main" << "README.rdoc" \
33
- << "--exclude" << "spec" \
33
+ << "--exclude" << "test" \
34
34
  << "--line-numbers"
35
35
  end
36
36
 
data/bin/tweetwine CHANGED
@@ -1,102 +1,48 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "rubygems"
4
- require "getoptlong"
4
+ require "optparse"
5
+
5
6
  require File.dirname(__FILE__) << "/../lib/tweetwine"
6
7
 
7
8
  include Tweetwine
8
9
 
9
- class HelpNeeded < StandardError; end
10
- class AlreadyReportedArgumentError < ArgumentError; end
11
-
12
- DEFAULT_OPTIONS = {
13
- :colorize => false,
14
- :num_statuses => Client::MAX_NUM_STATUSES
15
- }
16
-
17
- def exit_with_error(why = nil)
18
- puts "Error: #{why}" if why
19
- exit(2)
20
- end
21
-
22
- def exit_with_usage(why = nil)
23
- puts "#{why}\n\n" if why
24
- puts <<-END
10
+ begin
11
+ config = StartupConfig.new(Client::COMMANDS)
12
+ config.parse(ARGV, ENV["HOME"] + "/.tweetwine") do |args|
13
+ options = {}
14
+ begin
15
+ OptionParser.new do |opt|
16
+ opt.banner =<<-END
25
17
  Usage: tweetwine [options...] [command]
26
18
 
27
- The program needs the user's username and password for authentication.
28
- This information can be given either as an option to the program or via a
29
- configuration file. Without option [-a], the program attempts to read the
30
- configuration file in YAML format at "~/.tweetwine" for authentication.
31
-
32
- Argument [command] can be one of #{Client::COMMANDS.map {|cmd| "\"#{cmd}\"" }.join(", ")}.
33
- If [command] is not given, it defaults to "#{Client::COMMANDS[0]}".
19
+ Commands: #{Client::COMMANDS.join(", ")}
34
20
 
35
21
  Options:
36
22
 
37
- -a, --auth <username>:<password> Authentication data.
38
- -c, --color Colorize output with ANSI escape codes.
39
- -h, --help This help message.
40
- -n, --num <num> The number of statuses to fetch,
41
- defaults to #{DEFAULT_OPTIONS[:num_statuses]}.
42
-
43
- END
44
- exit(1)
45
- end
46
-
47
- def parse_args
48
- options = DEFAULT_OPTIONS.dup
49
-
50
- opts = GetoptLong.new(
51
- [ "--auth", "-a", GetoptLong::REQUIRED_ARGUMENT ],
52
- [ "--color", "-c", GetoptLong::NO_ARGUMENT ],
53
- [ "--help", "-h", GetoptLong::NO_ARGUMENT ],
54
- [ "--num", "-n", GetoptLong::REQUIRED_ARGUMENT ]
55
- )
56
-
57
- begin
58
- opts.each do |opt, arg|
59
- case opt
60
- when "--auth" then options[:username], options[:password] = arg.split(":", 2)
61
- when "--color" then options[:colorize] = true
62
- when "--help" then raise HelpNeeded
63
- when "--num"
64
- arg = arg.to_i
65
- if (1..Client::MAX_NUM_STATUSES).include? arg
23
+ END
24
+ opt.on("-a", "--auth USERNAME:PASSWORD", "Authentication") do |arg|
25
+ options[:username], options[:password] = arg.split(":", 2)
26
+ end
27
+ opt.on("-c", "--colorize", "Colorize output with ANSI escape codes") do
28
+ options[:colorize] = true
29
+ end
30
+ opt.on("-n", "--num N", Integer, "The number of statuses to fetch, defaults to #{Client::DEFAULT_NUM_STATUSES}") do |arg|
66
31
  options[:num_statuses] = arg
67
- else
68
- raise ArgumentError, "Invalid number of statuses to show -- must be between 1..#{Client::MAX_NUM_STATUSES}"
69
32
  end
70
- end
71
- end
72
- rescue GetoptLong::InvalidOption, GetoptLong::MissingArgument
73
- raise AlreadyReportedArgumentError
74
- end
75
-
76
- unless options[:username]
77
- begin
78
- config = Tweetwine::Config.load(ENV["HOME"] + "/.tweetwine")
79
- options[:username], options[:password] = [config.username, config.password]
80
- options[:colorize] = config.colorize if config.colorize?
81
- rescue Exception => e
82
- raise ArgumentError, "No auth info given as argument and no configuration file (~/.tweetwine) found."
33
+ opt.on_tail("-h", "--help", "Show this help message") do
34
+ puts opt
35
+ exit(1)
36
+ end
37
+ end.parse!(args)
38
+ rescue OptionParser::ParseError => e
39
+ raise ArgumentError.new(e)
83
40
  end
41
+ options
84
42
  end
85
-
86
- command = if ARGV.empty? then Client::COMMANDS[0] else ARGV.shift end
87
- raise ArgumentError, "Unknown command." unless Client::COMMANDS.include? command
88
-
89
- [options, command, ARGV]
90
- end
91
-
92
- begin
93
- options, command, args = parse_args
94
- client = Client.new(options)
95
- client.send(command.to_sym, *args)
96
- rescue HelpNeeded, AlreadyReportedArgumentError
97
- exit_with_usage
98
- rescue ArgumentError => e
99
- exit_with_usage e.message
100
- rescue ClientError => e
101
- exit_with_error e.message
43
+ client = Client.new(config.options)
44
+ client.send(config.command, *config.args)
45
+ rescue ArgumentError, ClientError => e
46
+ puts "Error: #{e.message}"
47
+ exit(2)
102
48
  end
data/lib/tweetwine.rb CHANGED
@@ -1,3 +1,3 @@
1
- %w{util config io client}.each do |f|
1
+ %w{util startup_config io client}.each do |f|
2
2
  require File.dirname(__FILE__) << "/tweetwine/#{f}"
3
3
  end
@@ -5,23 +5,39 @@ module Tweetwine
5
5
  class ClientError < RuntimeError; end
6
6
 
7
7
  class Client
8
- COMMANDS = %w{friends user update}
8
+ attr_reader :num_statuses
9
9
 
10
- MAX_NUM_STATUSES = 20
10
+ COMMANDS = [:home, :mentions, :user, :update]
11
+
12
+ DEFAULT_NUM_STATUSES = 20
13
+ MAX_NUM_STATUSES = 200
11
14
  MAX_STATUS_LENGTH = 140
12
15
 
13
16
  def initialize(options)
14
- @username, password = options[:username].to_s, options[:password].to_s
15
- @base_url = "https://#{@username}:#{password}@twitter.com/"
17
+ @username = options[:username].to_s
18
+ raise ArgumentError, "No authentication data given" if @username.empty?
19
+ @base_url = "https://#{@username}:#{options[:password]}@twitter.com/"
16
20
  @colorize = options[:colorize] || false
17
- @num_statuses = options[:num_statuses] || MAX_NUM_STATUSES
21
+ @num_statuses = if options[:num_statuses]
22
+ if (1..MAX_NUM_STATUSES).include? options[:num_statuses]
23
+ options[:num_statuses]
24
+ else
25
+ raise ArgumentError, "Invalid number of statuses to show -- must be between 1..#{Client::MAX_NUM_STATUSES}"
26
+ end
27
+ else
28
+ DEFAULT_NUM_STATUSES
29
+ end
18
30
  @io = IO.new(options)
19
31
  end
20
32
 
21
- def friends
33
+ def home
22
34
  @io.show_statuses JSON.parse(get("statuses/friends_timeline.json?count=#{@num_statuses}"))
23
35
  end
24
36
 
37
+ def mentions
38
+ @io.show_statuses JSON.parse(get("statuses/mentions.json?count=#{@num_statuses}"))
39
+ end
40
+
25
41
  def user(user = @username)
26
42
  @io.show_statuses JSON.parse(get("statuses/user_timeline/#{user}.json?count=#{@num_statuses}"))
27
43
  end
data/lib/tweetwine/io.rb CHANGED
@@ -30,10 +30,17 @@ module Tweetwine
30
30
  time_diff_value, time_diff_unit = Util.humanize_time_diff(Time.now, status["created_at"])
31
31
  from_user = status["user"]["screen_name"]
32
32
  from_user = colorize(:green, from_user) if @colorize
33
+ in_reply_to = status["in_reply_to_screen_name"]
34
+ in_reply_to = if in_reply_to && !in_reply_to.empty?
35
+ in_reply_to = colorize(:green, in_reply_to) if @colorize
36
+ "in reply to #{in_reply_to}, "
37
+ else
38
+ ""
39
+ end
33
40
  text = status["text"]
34
41
  text = colorize(:red, text, /@\w+/) if @colorize
35
42
  @output.puts <<-END
36
- #{from_user}, #{time_diff_value} #{time_diff_unit} ago:
43
+ #{from_user}, #{in_reply_to}#{time_diff_value} #{time_diff_unit} ago:
37
44
  #{text}
38
45
 
39
46
  END
@@ -44,8 +51,7 @@ module Tweetwine
44
51
 
45
52
  COLOR_CODES = {
46
53
  :green => "\033[32m",
47
- :red => "\033[31m",
48
- :neutral => "\033[0m"
54
+ :red => "\033[31m"
49
55
  }
50
56
 
51
57
  def colorize(color, str, matcher = nil)
@@ -59,7 +65,7 @@ module Tweetwine
59
65
  end
60
66
 
61
67
  def colorize_str(color_code, str)
62
- "#{color_code}#{str}#{COLOR_CODES[:neutral]}"
68
+ "#{color_code}#{str}\033[0m"
63
69
  end
64
70
  end
65
71
  end
@@ -0,0 +1,41 @@
1
+ module Tweetwine
2
+ class StartupConfig
3
+ attr_reader :options, :command, :args, :supported_commands
4
+
5
+ def initialize(supported_commands)
6
+ @supported_commands = supported_commands.to_a
7
+ raise ArgumentError, "Must give at least one supported command" if @supported_commands.empty?
8
+ @options = {}
9
+ @command = @supported_commands.first
10
+ @args = []
11
+ end
12
+
13
+ def parse(args = [], config_file = nil, &cmd_parser)
14
+ options = parse_options(args, config_file, &cmd_parser)
15
+ command = if args.empty? then @supported_commands.first else args.shift.to_sym end
16
+ raise ArgumentError, "Unknown command." unless @supported_commands.include? command
17
+ @options, @command, @args = options, command, args
18
+ self
19
+ end
20
+
21
+ private
22
+
23
+ def parse_options(args, config_file, &cmd_parser)
24
+ cmd_options = if cmd_parser then parse_cmdline_args(args, &cmd_parser) else {} end
25
+ config_options = if config_file && File.exists?(config_file) then parse_config_file(config_file) else {} end
26
+ config_options.merge(cmd_options)
27
+ end
28
+
29
+ def parse_cmdline_args(args, &cmd_parser)
30
+ cmd_parser.call(args)
31
+ end
32
+
33
+ def parse_config_file(config_file)
34
+ options = YAML.load(File.read(config_file))
35
+ options.inject({}) do |result, pair|
36
+ result[pair.first.to_sym] = pair.last
37
+ result
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,159 @@
1
+ require File.dirname(__FILE__) << "/test_helper"
2
+ require "json"
3
+
4
+ module Tweetwine
5
+
6
+ class ClientTest < Test::Unit::TestCase
7
+ context "Upon initializing a client" do
8
+ should "raise exception when no authentication data is given" do
9
+ assert_raises(ArgumentError) { Client.new({}) }
10
+ assert_raises(ArgumentError) { Client.new({ :password => "bar" }) }
11
+ assert_raises(ArgumentError) { Client.new({ :username => "", :password => "bar" }) }
12
+ end
13
+
14
+ should "use default num of statuses if not configured otherwise" do
15
+ @client = Client.new({ :username => "foo", :password => "bar" })
16
+ assert_equal Client::DEFAULT_NUM_STATUSES, @client.num_statuses
17
+ end
18
+
19
+ should "use configured num of statuses if in allowed range" do
20
+ @client = Client.new({ :username => "foo", :password => "bar", :num_statuses => 12 })
21
+ assert_equal 12, @client.num_statuses
22
+ end
23
+
24
+ should "raise an exception for configured num of statuses if not in allowed range" do
25
+ assert_raises(ArgumentError) { Client.new({ :username => "foo", :password => "bar", :num_statuses => Client::MAX_NUM_STATUSES + 1 }) }
26
+ end
27
+ end
28
+
29
+ context "A client" do
30
+ setup do
31
+ @client = Client.new({ :username => "foo", :password => "bar" })
32
+ @io = mock()
33
+ @client.instance_variable_set(:@io, @io)
34
+ end
35
+
36
+ should "raise ClientError for invalid request" do
37
+ RestClient.expects(:get) \
38
+ .with("https://foo:bar@twitter.com/statuses/friends_timeline.json?count=20") \
39
+ .raises(RestClient::Unauthorized)
40
+ assert_raises(ClientError) { @client.home }
41
+ end
42
+
43
+ should "fetch friends' statuses (home view)" do
44
+ statuses = [
45
+ {
46
+ "created_at" => Time.at(1).to_s,
47
+ "user" => { "username" => "zanzibar" },
48
+ "text" => "wassup?"
49
+ },
50
+ {
51
+ "created_at" => Time.at(2).to_s,
52
+ "user" => { "username" => "lulzwoo" },
53
+ "text" => "nuttin"
54
+ }
55
+ ]
56
+ RestClient.expects(:get) \
57
+ .with("https://foo:bar@twitter.com/statuses/friends_timeline.json?count=20") \
58
+ .returns(statuses.to_json)
59
+ @io.expects(:show_statuses).with(statuses)
60
+ @client.home
61
+ end
62
+
63
+ should "fetch mentions" do
64
+ statuses = [
65
+ {
66
+ "created_at" => Time.at(1).to_s,
67
+ "in_reply_to_screen_name" => "foo",
68
+ "user" => { "username" => "zanzibar" },
69
+ "text" => "wassup, @foo?"
70
+ },
71
+ {
72
+ "created_at" => Time.at(2).to_s,
73
+ "in_reply_to_screen_name" => "foo",
74
+ "user" => { "username" => "lulzwoo" },
75
+ "text" => "@foo, doing nuttin"
76
+ }
77
+ ]
78
+ RestClient.expects(:get) \
79
+ .with("https://foo:bar@twitter.com/statuses/mentions.json?count=20") \
80
+ .returns(statuses.to_json)
81
+ @io.expects(:show_statuses).with(statuses)
82
+ @client.mentions
83
+ end
84
+
85
+ should "fetch a specific user's statuses, with the user identified by given argument" do
86
+ statuses = [
87
+ {
88
+ "created_at" => Time.at(1).to_s,
89
+ "user" => { "username" => "zanzibar" },
90
+ "text" => "wassup?"
91
+ }
92
+ ]
93
+ RestClient.expects(:get) \
94
+ .with("https://foo:bar@twitter.com/statuses/user_timeline/zanzibar.json?count=20") \
95
+ .returns(statuses.to_json)
96
+ @io.expects(:show_statuses).with(statuses)
97
+ @client.user("zanzibar")
98
+ end
99
+
100
+ should "fetch a specific user's statuses, with the user being the authenticated user itself when given no argument" do
101
+ statuses = [
102
+ {
103
+ "created_at" => Time.at(1).to_s,
104
+ "user" => { "username" => "foo" },
105
+ "text" => "wassup?"
106
+ }
107
+ ]
108
+ RestClient.expects(:get) \
109
+ .with("https://foo:bar@twitter.com/statuses/user_timeline/foo.json?count=20") \
110
+ .returns(statuses.to_json)
111
+ @io.expects(:show_statuses).with(statuses)
112
+ @client.user
113
+ end
114
+
115
+ should "post a status update, when positive confirmation" do
116
+ status = {
117
+ "created_at" => Time.at(1).to_s,
118
+ "user" => { "username" => "foo" },
119
+ "text" => "wondering about"
120
+ }
121
+ RestClient.expects(:post) \
122
+ .with("https://foo:bar@twitter.com/statuses/update.json", {:status => "wondering about"}) \
123
+ .returns(status.to_json)
124
+ @io.expects(:confirm).with("Really send?").returns(true)
125
+ @io.expects(:info).with("Sent status update.\n\n")
126
+ @io.expects(:show_statuses).with([status])
127
+ @client.update("wondering about")
128
+ end
129
+
130
+ should "cancel a status update, when negative confirmation" do
131
+ RestClient.expects(:post).never
132
+ @io.expects(:confirm).with("Really send?").returns(false)
133
+ @io.expects(:info).with("Cancelled.")
134
+ @io.expects(:show_statuses).never
135
+ @client.update("wondering about")
136
+ end
137
+
138
+ should "truncate a status update too long and warn the user" do
139
+ long_status_update = "x aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll mmm nnn ooo ppp qqq rrr sss ttt uuu vvv www xxx yyy zzz 111 222 333 444 555 666 777 888 999 000"
140
+ truncated_status_update = "x aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll mmm nnn ooo ppp qqq rrr sss ttt uuu vvv www xxx yyy zzz 111 222 333 444 555 666 777 888 99"
141
+ status = {
142
+ "created_at" => Time.at(1).to_s,
143
+ "user" => { "username" => "foo" },
144
+ "text" => truncated_status_update
145
+ }
146
+
147
+ RestClient.expects(:post) \
148
+ .with("https://foo:bar@twitter.com/statuses/update.json", {:status => truncated_status_update}) \
149
+ .returns(status.to_json)
150
+ @io.expects(:warn).with("Update will be truncated: #{truncated_status_update}")
151
+ @io.expects(:confirm).with("Really send?").returns(true)
152
+ @io.expects(:info).with("Sent status update.\n\n")
153
+ @io.expects(:show_statuses).with([status])
154
+ @client.update(long_status_update)
155
+ end
156
+ end
157
+ end
158
+
159
+ end
data/test/io_test.rb ADDED
@@ -0,0 +1,139 @@
1
+ require File.dirname(__FILE__) << "/test_helper"
2
+
3
+ module Tweetwine
4
+
5
+ class IOTest < Test::Unit::TestCase
6
+ context "An IO" do
7
+ setup do
8
+ @input = mock()
9
+ @output = mock()
10
+ @io = IO.new({ :input => @input, :output => @output })
11
+ end
12
+
13
+ should "output prompt and return input as trimmed" do
14
+ @output.expects(:print).with("The answer: ")
15
+ @input.expects(:gets).returns(" 42 ")
16
+ assert_equal "42", @io.prompt("The answer")
17
+ end
18
+
19
+ should "output info message" do
20
+ @output.expects(:puts).with("foo")
21
+ @io.info("foo")
22
+ end
23
+
24
+ should "output warning message" do
25
+ @output.expects(:puts).with("Warning: monkey patching ahead")
26
+ @io.warn("monkey patching ahead")
27
+ end
28
+
29
+ should "confirm action, with positive answer" do
30
+ @output.expects(:print).with("Fire nukes? [yN] ")
31
+ @input.expects(:gets).returns("y")
32
+ assert_equal true, @io.confirm("Fire nukes?")
33
+ end
34
+
35
+ should "confirm action, with negative answer" do
36
+ @output.expects(:print).with("Fire nukes? [yN] ")
37
+ @input.expects(:gets).returns("n")
38
+ assert_equal false, @io.confirm("Fire nukes?")
39
+ end
40
+
41
+ should "confirm action, with default answer" do
42
+ @output.expects(:print).with("Fire nukes? [yN] ")
43
+ @input.expects(:gets).returns("")
44
+ assert_equal false, @io.confirm("Fire nukes?")
45
+ end
46
+ end
47
+
48
+ context "An IO, with colorization disabled" do
49
+ setup do
50
+ @input = mock()
51
+ @output = mock()
52
+ @io = IO.new({ :input => @input, :output => @output, :colorize => false })
53
+ end
54
+
55
+ should "print statuses without in-reply info" do
56
+ statuses = [
57
+ {
58
+ "created_at" => Time.at(1),
59
+ "user" => { "screen_name" => "fooman" },
60
+ "text" => "Hi, @barman! Lulz woo!"
61
+ }
62
+ ]
63
+ Util.expects(:humanize_time_diff).returns([2, "secs"])
64
+ @output.expects(:puts).with(<<-END
65
+ fooman, 2 secs ago:
66
+ Hi, @barman! Lulz woo!
67
+
68
+ END
69
+ )
70
+ @io.show_statuses(statuses)
71
+ end
72
+
73
+ should "print statuses with in-reply info" do
74
+ statuses = [
75
+ {
76
+ "created_at" => Time.at(1),
77
+ "in_reply_to_screen_name" => "barman",
78
+ "user" => { "screen_name" => "fooman" },
79
+ "text" => "Hi, @barman! Lulz woo!"
80
+ }
81
+ ]
82
+ Util.expects(:humanize_time_diff).returns([2, "secs"])
83
+ @output.expects(:puts).with(<<-END
84
+ fooman, in reply to barman, 2 secs ago:
85
+ Hi, @barman! Lulz woo!
86
+
87
+ END
88
+ )
89
+ @io.show_statuses(statuses)
90
+ end
91
+ end
92
+
93
+ context "An IO, with colorization enabled" do
94
+ setup do
95
+ @input = mock()
96
+ @output = mock()
97
+ @io = IO.new({ :input => @input, :output => @output, :colorize => true })
98
+ end
99
+
100
+ should "print statuses without in-reply info" do
101
+ statuses = [
102
+ {
103
+ "created_at" => Time.at(1),
104
+ "user" => { "screen_name" => "fooman" },
105
+ "text" => "Hi, @barman! Lulz woo!"
106
+ }
107
+ ]
108
+ Util.expects(:humanize_time_diff).returns([2, "secs"])
109
+ @output.expects(:puts).with(<<-END
110
+ \033[32mfooman\033[0m, 2 secs ago:
111
+ Hi, \033[31m@barman\033[0m! Lulz woo!
112
+
113
+ END
114
+ )
115
+ @io.show_statuses(statuses)
116
+ end
117
+
118
+ should "print statuses with in-reply info" do
119
+ statuses = [
120
+ {
121
+ "created_at" => Time.at(1),
122
+ "in_reply_to_screen_name" => "barman",
123
+ "user" => { "screen_name" => "fooman" },
124
+ "text" => "Hi, @barman! Lulz woo!"
125
+ }
126
+ ]
127
+ Util.expects(:humanize_time_diff).returns([2, "secs"])
128
+ @output.expects(:puts).with(<<-END
129
+ \033[32mfooman\033[0m, in reply to \033[32mbarman\033[0m, 2 secs ago:
130
+ Hi, \033[31m@barman\033[0m! Lulz woo!
131
+
132
+ END
133
+ )
134
+ @io.show_statuses(statuses)
135
+ end
136
+ end
137
+ end
138
+
139
+ end
@@ -0,0 +1,67 @@
1
+ require File.dirname(__FILE__) << "/test_helper"
2
+
3
+ module Tweetwine
4
+
5
+ class StartupConfigTest < Test::Unit::TestCase
6
+ TEST_CONFIG_FILE = File.dirname(__FILE__) << "/test_config.yaml"
7
+
8
+ context "To initialize a StartupConfig" do
9
+ should "require at least one supported command" do
10
+ assert_raise(ArgumentError) { StartupConfig.new([]) }
11
+ assert_nothing_raised { StartupConfig.new([:default_action]) }
12
+ end
13
+ end
14
+
15
+ context "An initialized StartupConfig" do
16
+ setup do
17
+ @config = StartupConfig.new([:default_action, :another_action])
18
+ end
19
+
20
+ should "use the first supported command as a default command when given no command as a cmdline argument" do
21
+ @config.parse
22
+ assert_equal :default_action, @config.command
23
+ end
24
+
25
+ context "when given no cmdline args and a config file" do
26
+ setup do
27
+ @config.parse([], TEST_CONFIG_FILE)
28
+ end
29
+
30
+ should "have the parsed option defined" do
31
+ assert_equal false, @config.options[:colorize]
32
+ end
33
+ end
34
+
35
+ context "when given cmdline args and no config file" do
36
+ setup do
37
+ @config.parse(%w{--opt foo}) do |args|
38
+ args.clear
39
+ {:opt => "foo"}
40
+ end
41
+ end
42
+
43
+ should "have the parsed option defined" do
44
+ assert_equal "foo", @config.options[:opt]
45
+ end
46
+ end
47
+
48
+ context "when given an option both as a cmdline option and in a config file" do
49
+ setup do
50
+ @config.parse(%w{--colorize}, TEST_CONFIG_FILE) do |args|
51
+ args.clear
52
+ {:colorize => true}
53
+ end
54
+ end
55
+
56
+ should "the command line option should override the config file option" do
57
+ assert_equal true, @config.options[:colorize]
58
+ end
59
+
60
+ should "have nil for an undefined option" do
61
+ assert_nil @config.options[:num_statuses]
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ end
@@ -0,0 +1,3 @@
1
+ username: foo
2
+ password: bar
3
+ colorize: false
@@ -0,0 +1,7 @@
1
+ require "rubygems"
2
+ require File.dirname(__FILE__) << "/../lib/tweetwine"
3
+ require "test/unit"
4
+ require "shoulda"
5
+ require "mocha"
6
+
7
+ Mocha::Configuration.prevent(:stubbing_non_existent_method)
data/test/util_test.rb ADDED
@@ -0,0 +1,28 @@
1
+ require File.dirname(__FILE__) << "/test_helper"
2
+ require "time"
3
+
4
+ module Tweetwine
5
+
6
+ class UtilTest < Test::Unit::TestCase
7
+ context "The module" do
8
+ should "humanize time difference" do
9
+ assert_equal [1, "sec"], Util.humanize_time_diff(Time.parse("2009-01-01 00:00:59").to_s, Time.parse("2009-01-01 00:01:00"))
10
+ assert_equal [0, "sec"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00:00").to_s, Time.parse("2009-01-01 01:00:00"))
11
+ assert_equal [1, "sec"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00:00").to_s, Time.parse("2009-01-01 01:00:01"))
12
+ assert_equal [59, "sec"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00:00").to_s, Time.parse("2009-01-01 01:00:59"))
13
+ assert_equal [59, "min"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00").to_s, Time.parse("2009-01-01 01:59"))
14
+ assert_equal [59, "min"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00:30").to_s, Time.parse("2009-01-01 01:59:00"))
15
+ assert_equal [57, "min"], Util.humanize_time_diff(Time.parse("2009-01-01 01:01:00").to_s, Time.parse("2009-01-01 01:58:00"))
16
+ assert_equal [56, "min"], Util.humanize_time_diff(Time.parse("2009-01-01 01:01:31").to_s, Time.parse("2009-01-01 01:58:00"))
17
+ assert_equal [57, "min"], Util.humanize_time_diff(Time.parse("2009-01-01 01:01:00").to_s, Time.parse("2009-01-01 01:58:29"))
18
+ assert_equal [58, "min"], Util.humanize_time_diff(Time.parse("2009-01-01 01:01:00").to_s, Time.parse("2009-01-01 01:58:30"))
19
+ assert_equal [1, "hour"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00").to_s, Time.parse("2009-01-01 02:00"))
20
+ assert_equal [1, "hour"], Util.humanize_time_diff(Time.parse("2009-01-01 02:00").to_s, Time.parse("2009-01-01 01:00"))
21
+ assert_equal [2, "hours"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00").to_s, Time.parse("2009-01-01 03:00"))
22
+ assert_equal [1, "day"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00").to_s, Time.parse("2009-01-02 03:00"))
23
+ assert_equal [2, "days"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00").to_s, Time.parse("2009-01-03 03:00"))
24
+ end
25
+ end
26
+ end
27
+
28
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tuomas-tweetwine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tuomas Kareinen
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-04-23 00:00:00 -07:00
12
+ date: 2009-05-04 00:00:00 -07:00
13
13
  default_executable: tweetwine
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -48,20 +48,26 @@ files:
48
48
  - bin/tweetwine
49
49
  - lib/tweetwine
50
50
  - lib/tweetwine/client.rb
51
- - lib/tweetwine/config.rb
52
51
  - lib/tweetwine/io.rb
52
+ - lib/tweetwine/startup_config.rb
53
53
  - lib/tweetwine/util.rb
54
54
  - lib/tweetwine.rb
55
+ - test/client_test.rb
56
+ - test/io_test.rb
57
+ - test/startup_config_test.rb
58
+ - test/test_config.yaml
59
+ - test/test_helper.rb
60
+ - test/util_test.rb
55
61
  has_rdoc: true
56
62
  homepage: http://github.com/tuomas/tweetwine
57
63
  post_install_message:
58
64
  rdoc_options:
59
65
  - --title
60
- - Tweetwine 0.1.1
66
+ - Tweetwine 0.1.2
61
67
  - --main
62
68
  - README.rdoc
63
69
  - --exclude
64
- - spec
70
+ - test
65
71
  - --line-numbers
66
72
  require_paths:
67
73
  - lib
@@ -1,25 +0,0 @@
1
- require "yaml"
2
-
3
- module Tweetwine
4
- class Config
5
- def self.load(file)
6
- new(file)
7
- end
8
-
9
- private_class_method :new
10
-
11
- def initialize(file)
12
- @config = YAML.load(File.read(file))
13
- end
14
-
15
- def method_missing(sym)
16
- key = sym.to_s
17
- if key[-1,1] == "?"
18
- result = @config.has_key? key[0...-1]
19
- else
20
- result = @config[key]
21
- end
22
- result
23
- end
24
- end
25
- end