ruport 0.2.2 → 0.2.4

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.
data/AUTHORS CHANGED
@@ -1 +1,7 @@
1
- {Gregory Brown}[mailto:gregory.t.brown@gmail.com]
1
+ {Gregory Brown}[mailto:gregory.t.brown@gmail.com]:
2
+
3
+ Original Author and Ruport::Report hacker.
4
+
5
+ {Robert Canieso}[mailto:rcanieso@gmail.com]:
6
+
7
+ Guy in charge of the Ruport::Format module
data/CHANGELOG CHANGED
@@ -1,4 +1,25 @@
1
- The current version of Ruby Reports is 0.2.2
1
+ The current version of Ruby Reports is 0.2.4
2
+
3
+ changes since Ruport 0.2.2:
4
+
5
+ - Report::DataSet and Report::DataRow are now enumerable
6
+
7
+ - DataSet#eql? fixed
8
+
9
+ - Format::Builder has been added to handle formatting
10
+ (Supports CSV / HTML currently)
11
+
12
+ - Format::Builder is easily extendable via send("render_#{@type}")
13
+
14
+ - DataSet#to_html convenience method added
15
+
16
+ - DataSet#fields can now be arbitrary objects (does not need to be Strings)
17
+
18
+ - Dropped DataRow#middle? because it was useless. (And poorly named)
19
+
20
+ - FakeMailer added for testing goodness.
21
+
22
+ - Dropped select() and execute() in favor of query()
2
23
 
3
24
  changes since Ruport 0.2.0:
4
25
 
data/README CHANGED
@@ -81,6 +81,9 @@ point, unless you're magic, you'll probably need to either read the
81
81
  {API Documentation}[http://ruport.rubyforge.org/docs/] or the source, whichever
82
82
  you're most comfortable with.
83
83
 
84
+ There is also a set of examples at:
85
+ http://rubyforge.org/frs/?group_id=856&release_id=3481
86
+
84
87
  I hope you enjoy this software and that it is useful to you.
85
88
 
86
89
  -Greg
data/Rakefile CHANGED
@@ -14,7 +14,7 @@ end
14
14
 
15
15
  spec = Gem::Specification.new do |spec|
16
16
  spec.name = "ruport"
17
- spec.version = "0.2.2"
17
+ spec.version = "0.2.4"
18
18
  spec.platform = Gem::Platform::RUBY
19
19
  spec.summary = "A generalized Ruby report generation and templating engine."
20
20
 
data/TODO CHANGED
@@ -1,15 +1,12 @@
1
1
  TODO:
2
2
 
3
3
  Bugfixes:
4
- - Fix potential failure in Ruport::Report::DataSet#eql?
5
- - Is there an OS X packaging problem with the gems?
6
4
  - Fix the manual that is in the example package so it it reads FakeDB instead
7
- of MockDB
5
+ of MockDB. Update the manual to cover new features in 0.2.4
8
6
 
9
7
  Improvement:
10
8
 
11
9
  - Mail system
12
- Hook up unit tests
13
10
  Make attachments doable
14
11
 
15
12
  - Template Config / Line Editing
@@ -27,6 +24,7 @@ Improvement:
27
24
  Expand so that it covers most common SQL commands.
28
25
  Form better more complete unit tests.
29
26
 
27
+ - Queries from file / db need to use ERb or some other replacement ability
30
28
 
31
29
  Begin implementing new features:
32
30
 
@@ -120,5 +118,6 @@ Other:
120
118
  This is only the tip of the iceburg. Please feel free to continue to fill my
121
119
  plate by sending any suggestions to gregory.t.brown@gmail.com
122
120
 
123
-
124
-
121
+ JEG2 Code Review 2005.11.14: (email me if you find any of this interesting)
122
+ - use the many levels of logger
123
+ - RQL (Ruport Query Language)
data/bin/ruport CHANGED
@@ -65,7 +65,7 @@ if ARGV[0].eql?("generate")
65
65
  }
66
66
  exit
67
67
  elsif ARGV[0].eql?("-v")
68
- puts "Ruport Version 0.2.2 \nA ruby report generation system by Gregory " +
68
+ puts "Ruport Version 0.2.4 \nA ruby report generation system by Gregory " +
69
69
  "Brown.\nThis application is Free Software under the GPL/Ruby License. " +
70
70
  "\nAll praise and/or criticism can be directed to "+
71
71
  "gregory.t.brown@gmail.com"
@@ -0,0 +1,34 @@
1
+ module Ruport
2
+ module Format
3
+ class Builder
4
+
5
+ def initialize( data_set )
6
+ @data = data_set
7
+ @type = nil
8
+ end
9
+
10
+ attr_accessor :type
11
+
12
+ def render
13
+ send("render_#{@type}")
14
+ end
15
+
16
+ def render_csv
17
+ @data.inject(CSV.generate_line(@data.fields) + "\n") do |out,r|
18
+ out << CSV.generate_line(@data.fields.map { |f| r[f] }) + "\n"
19
+ end
20
+ end
21
+
22
+ def render_html
23
+ fields_html = "<tr>\n <td>#{@data.fields.join('</td><td>')}</td>\n </tr>"
24
+ @data.inject("<table>\n #{fields_html}"){|html,row|
25
+ html << row.inject(" <tr>\n "){ |row_html, field|
26
+ row_html << "<td>#{field}</td>"
27
+ } + "\n </tr>"
28
+ } + "\n</table>"
29
+ end
30
+
31
+ end
32
+ end
33
+ end
34
+
@@ -11,6 +11,8 @@ module Ruport
11
11
  module Report
12
12
  class DataRow
13
13
 
14
+ include Enumerable
15
+
14
16
  attr_accessor :fields
15
17
 
16
18
  # DataRows are essentially arrays with named ordinal fields and
@@ -27,7 +29,7 @@ module Ruport
27
29
  #
28
30
  # i.e. row["phone"]
29
31
  def [](key)
30
- key.kind_of?(String) ? @data[@fields.index(key)] : @data[key]
32
+ key.kind_of?(Fixnum) ? @data[key] : @data[@fields.index(key)]
31
33
  end
32
34
 
33
35
  # Lets you set field values
@@ -53,13 +55,17 @@ module Ruport
53
55
  # DataRow#first? DataRow#last? DataRow#middle?
54
56
  # DataRow#odd? DataRow#even? which are all conditional methods.
55
57
  def method_missing(method)
56
- if %[last? first? middle?].include? method.to_s
58
+ if %[last? first? center?].include? method.to_s
57
59
  return @position.eql?(method.to_s[0..-2].to_sym)
58
60
  elsif %[odd? even?].include? method.to_s
59
61
  return @oddness.eql?(method.to_s[0..-2].to_sym)
60
62
  end
61
63
  super
62
64
  end
65
+
66
+ def each(&action)
67
+ @data.each(&action)
68
+ end
63
69
 
64
70
  attr_accessor :position
65
71
  end
@@ -9,6 +9,9 @@
9
9
  module Ruport
10
10
  module Report
11
11
  class DataSet
12
+
13
+ include Enumerable
14
+
12
15
  def initialize
13
16
  @data = []
14
17
  end
@@ -48,19 +51,21 @@ module Ruport
48
51
 
49
52
  oddness = (@data.length % 2 == 0 ? :even : :odd)
50
53
  position = (@data.length == 0 ? :first : :last)
51
- @data[@data.length - 1].position = :middle if @data.length > 1
54
+ @data[@data.length - 1].position = nil if @data.length > 1
52
55
  @data << DataRow.new(new_row,@fields,oddness,position)
53
56
  end
54
57
 
55
58
  # This works in best case scenario. It should return true if
56
59
  # both DataSets have the same values. Still working out the kinks here.
57
60
  def eql?(data2)
58
- return false unless (@data.length == data2.data.length)
59
- 0.upto(@data.length - 1) do |index|
60
- (@fields + data2.fields).uniq.each do |key|
61
- return false unless @data[index][key] == data2[index][key]
61
+ return false unless ( @data.length == data2.data.length and
62
+ @fields.eql?(data2.fields) )
63
+ @data.each_with_index do |row, r_index|
64
+ row.each_with_index do |field, f_index|
65
+ return false unless field.eql?(data2[r_index][f_index])
62
66
  end
63
67
  end
68
+
64
69
  return true
65
70
  end
66
71
 
@@ -82,21 +87,19 @@ module Ruport
82
87
 
83
88
  # Converts a DataSet to CSV
84
89
  def to_csv
85
- output = CSV.generate_line(@fields) + "\n"
86
- @data.each do |row|
87
- output << CSV.generate_line(@fields.map { |f| row[f] }) + "\n"
88
- end
89
- return output
90
+ builder = Format::Builder.new(self)
91
+ builder.type = :csv
92
+ builder.render
93
+ end
94
+ # Converts a Dataset to Html
95
+ def to_html
96
+ builder = Format::Builder.new(self)
97
+ builder.type = :html
98
+ builder.render
90
99
  end
91
-
92
100
  # Works like a standard each iterator
93
101
  def each(&action)
94
- @data[0..-1].each(&action)
95
- end
96
-
97
- # Works like a standard each_with_index iterator
98
- def each_with_index(&action)
99
- @data[0..-1].each_with_index(&action)
102
+ @data.each(&action)
100
103
  end
101
104
 
102
105
  end
@@ -21,69 +21,25 @@ module Ruport
21
21
  module Report
22
22
  class Engine
23
23
  def initialize( dsn, user, password, mailer=nil )
24
+
25
+ File.exists?("log") or FileUtils.mkdir("log")
26
+ @logger = Logger.new("log/ruport.log")
27
+
24
28
  @dsn = dsn
25
29
  @user = user
26
30
  @password = password
27
- @report_name = ""
28
31
  @mailer = mailer
29
- File.exists?("log") or FileUtils.mkdir("log")
30
- @logger = Logger.new("log/ruport.log")
31
32
  @generate = true
33
+ @report_name = ""
32
34
  @report = ""
35
+ @query_table = nil
36
+ @file = nil
37
+ @config = nil
33
38
  end
34
39
 
35
40
  attr_accessor :query_table, :file
36
41
  attr_reader :mailer, :generate, :config, :logger
37
42
 
38
- # * DEPRECATED: Use query() *
39
- #
40
- # Takes a query, an optional sourcetype, and a block which is passed the
41
- # results row by row. When passed a query in string form, it adds the
42
- # SELECT clause to the string and executes the query. When passed a
43
- # filename and a source :file, it looks in queries/ for the file specified.
44
- # When given a database query label, it looks in config[query_table] for a
45
- # query with the label specified. If no source is specified, it uses
46
- # string by default for the source.
47
- #
48
- # Example:
49
- #
50
- # select ( "* FROM test" )
51
- # Passes "SELECT * FROM test" to the database
52
- #
53
- # select ( "test.sql", :file )
54
- # Passes the contents of queries/test.sql to the database
55
- #
56
- # select ( "TEST", :db )
57
- # Calls the query TEST stored in the database and query_table specified in
58
- # config/ruport.yaml
59
-
60
- def select( query, source = :string, &action )
61
- source != :string || query = "SELECT " + query
62
- execute( query, source ) do |sth|
63
- @column_names = sth.column_names
64
- @first_row = true
65
- sth.fetch do |row|
66
- action.call(row) if block_given?
67
- @first_row = false
68
- end
69
- end
70
- end
71
-
72
- # * DEPRECATED, Use query() *
73
- #
74
- # Takes a query and an optional sourcetype and then runs the query
75
- # against the database. The output is not returned. This is useful for
76
- # doing construction and destruction actions.
77
- def execute( query, source = :string )
78
- query = get_query(source, query)
79
- DBI.connect(@dsn, @user, @password) do |dbh|
80
- dbh.prepare(query) do |sth|
81
- sth.execute()
82
- yield(sth)
83
- end
84
- end
85
- end
86
-
87
43
  # Takes a query and an optional sourcetype and then runs the query
88
44
  # against the database. The return from the query is then converted
89
45
  # into a DataSet which can then be manipulated.
@@ -159,8 +115,10 @@ module Ruport
159
115
  when :file
160
116
  load_file( query )
161
117
  when :db
162
- select ( "query FROM #{@query_table} WHERE " +
163
- "label LIKE '#{query}';" ) do |row| return row["query"] end
118
+ query ( "SELECT query FROM #{@query_table} WHERE " +
119
+ "label LIKE '#{query}';" ) do |data|
120
+ return data[0]["query"]
121
+ end
164
122
  end
165
123
  end
166
124
 
@@ -13,32 +13,10 @@ module Ruport
13
13
 
14
14
  attr_accessor :fake_db
15
15
 
16
- def execute (query, source = :string)
17
- query = get_query(source, query)
18
- yield(@fake_db.process(@dsn,@user,@password, query))
19
- end
20
-
21
- def select( query, source = :string, &action )
22
- source != :string || query = "SELECT " + query
23
- execute( query, source ) do |table|
24
- if table.kind_of?(DataSet)
25
- @column_names = table.fields
26
- else
27
- @column_names = table[0].keys
28
- end
29
- @first_row = true
30
- table.each do |row|
31
- row = row.to_hash if table.kind_of?(DataSet)
32
- action.call(row) if block_given?
33
- @first_row = false
34
- end
35
- end
36
- end
37
16
 
38
17
  def query ( query, source = :string, &action)
39
- execute( query, source ) do |table|
40
- action.call(table)
41
- end
18
+ query = get_query(source, query)
19
+ yield(@fake_db.process(@dsn,@user,@password, query))
42
20
  end
43
21
 
44
22
  end
@@ -0,0 +1,23 @@
1
+ module Ruport
2
+ module Report
3
+ class FakeMailer < Mailer
4
+
5
+ @@fake_server = Hash.new
6
+ @@fake_server[:port] = 25
7
+
8
+ def self.[]=(key,value)
9
+ @@fake_server[key] = value
10
+ end
11
+
12
+ def send_report(report_name="No Subject")
13
+ raise "Invalid Host" unless @@fake_server[:host].eql?(@host)
14
+ raise "Invalid Account" unless @@fake_server[:account].eql?(@account)
15
+ raise "Bad Email Address" unless @@fake_server[:address].eql?(@address)
16
+ raise "Bad Password" unless @@fake_server[:password].eql?(@password)
17
+ raise "Invalid Port" unless @@fake_server[:port].eql?(@port)
18
+ raise "Bad Authorization Type" unless @@fake_server[:auth].eql?(@auth)
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -5,4 +5,6 @@ require "ruport/report/fake_db"
5
5
  require "ruport/report/fake_engine"
6
6
  require "ruport/report/data_set"
7
7
  require "ruport/report/data_row"
8
+ require "ruport/report/fake_mailer"
9
+ require "ruport/format/builder"
8
10
  include Ruport
@@ -0,0 +1,25 @@
1
+
2
+ require "test/unit"
3
+ require "lib/ruportlib"
4
+ class TestBuilder < Test::Unit::TestCase
5
+ def setup
6
+ @data = Report::DataSet.new
7
+ @data.fields = %w[ col1 col2 col3 ]
8
+ @data.default = ""; @data << %w[ a b c ]; @data << %w[ d e f ]
9
+ @builder = Format::Builder.new( @data )
10
+ end
11
+
12
+ def test_render_html
13
+ @builder.type = :html
14
+ assert_equal("<table>\n <tr>\n <td>col1</td><td>col2</td>" +
15
+ "<td>col3</td>\n </tr>" +
16
+ " <tr>\n <td>a</td><td>b</td><td>c</td>\n </tr>" +
17
+ " <tr>\n <td>d</td><td>e</td><td>f</td>\n </tr>" +
18
+ "\n</table>", @builder.render )
19
+ end
20
+
21
+ def test_render_csv
22
+ @builder.type = :csv
23
+ assert_equal("col1,col2,col3\na,b,c\nd,e,f\n", @builder.render)
24
+ end
25
+ end
@@ -11,6 +11,7 @@ class TestDataRow < Test::Unit::TestCase
11
11
  @rows << [ 3 , 4 ]
12
12
  @rows << [ 5 , 6 ]
13
13
  @rows << { "foo" => 7, "bar" => 8 }
14
+ @rows << [ 9, 10 ]
14
15
  end
15
16
 
16
17
  def test_first?
@@ -20,14 +21,6 @@ class TestDataRow < Test::Unit::TestCase
20
21
  end
21
22
  end
22
23
 
23
- def test_middle?
24
- assert(!@rows[0].middle?)
25
- assert(!@rows[-1].middle?)
26
- @rows[1..-2].each do |row|
27
- assert(row.middle?)
28
- end
29
- end
30
-
31
24
  def test_last?
32
25
  @rows[0..-2].each do |row|
33
26
  assert(!row.last?)
@@ -54,8 +54,12 @@ class TestDataSet < Test::Unit::TestCase
54
54
  end
55
55
 
56
56
  def test_load
57
- loaded_data = Report::DataSet.load("test/data.csv")
58
- assert(@data.eql?(loaded_data))
57
+ loaded_data = Report::DataSet.load("test/data.csv")
58
+ @data.each_with_index do |row,r_index|
59
+ row.each_with_index do |field,f_index|
60
+ assert_equal(field,loaded_data[r_index][f_index])
61
+ end
62
+ end
59
63
  end
60
64
 
61
65
  def test_to_csv
@@ -64,4 +68,12 @@ class TestDataSet < Test::Unit::TestCase
64
68
  assert_equal("col1,col2,col3\na,b,c\nd,\"\",e\n",csv)
65
69
  end
66
70
 
71
+ def test_to_html
72
+ assert_equal("<table>\n <tr>\n <td>col1</td><td>col2</td>" +
73
+ "<td>col3</td>\n </tr>" +
74
+ " <tr>\n <td>a</td><td>b</td><td>c</td>\n </tr>" +
75
+ " <tr>\n <td>d</td><td></td><td>e</td>\n </tr>" +
76
+ "\n</table>", @data.to_html )
77
+ end
78
+
67
79
  end
@@ -5,27 +5,28 @@
5
5
 
6
6
  require "test/unit"
7
7
  require "lib/ruportlib"
8
- class TestRuport < Test::Unit::TestCase
8
+ class TestEngine < Test::Unit::TestCase
9
9
 
10
- def setup
11
- @report = Report::FakeEngine.new( "DBI:mysql:ruport:localhost", "test", "123")
12
- @report.query_table = "ruport_queries"
10
+ def setup
11
+ @report = Report::FakeEngine.new( "DBI:mysql:ruport:localhost",
12
+ "test", "123")
13
+ @report.query_table = "ruport_queries"
13
14
 
14
- @report.fake_db = Report::FakeDB.new([ "DBI:mysql","ruport",
15
- "localhost", "test", "123"])
15
+ @report.fake_db = Report::FakeDB.new([ "DBI:mysql","ruport",
16
+ "localhost", "test", "123"])
16
17
 
17
- @report.fake_db["SELECT * FROM ruport_test"] = @data =
18
- [ { "a" => "a column, row 1", "b" => "b column, row 1",
19
- "c" => "c column, row 1", "d" => "d column, row 1" },
20
- { "a" => "a column, row 2", "b" => "b column, row 2",
21
- "c" => "c column, row 2", "d" => "d column, row 2" },
22
- { "a" => "a column, row 3", "b" => "b column, row 3",
23
- "c" => "c column, row 3", "d" => "d column, row 3" } ]
24
-
25
- @report.fake_db[ "SELECT query FROM #{@report.query_table} " +
26
- "WHERE label LIKE 'sql_stored_test';"] =
27
- [ "query" => "SELECT * FROM ruport_test" ]
28
- end
18
+ @report.fake_db["SELECT * FROM ruport_test"] = @data =
19
+ [ { "a" => "a column, row 1", "b" => "b column, row 1",
20
+ "c" => "c column, row 1", "d" => "d column, row 1" },
21
+ { "a" => "a column, row 2", "b" => "b column, row 2",
22
+ "c" => "c column, row 2", "d" => "d column, row 2" },
23
+ { "a" => "a column, row 3", "b" => "b column, row 3",
24
+ "c" => "c column, row 3", "d" => "d column, row 3" } ]
25
+
26
+ @report.fake_db[ "SELECT query FROM #{@report.query_table} " +
27
+ "WHERE label LIKE 'sql_stored_test';"] =
28
+ [ "query" => "SELECT * FROM ruport_test" ]
29
+ end
29
30
 
30
31
  def test_load_file
31
32
  contents = "SELECT * FROM ruport_test"
@@ -62,36 +63,15 @@ end
62
63
  end
63
64
  end
64
65
 
65
- def test_sql
66
- row_i = 0
67
- @report.select("* FROM ruport_test") do |row|
68
- assert_equal(@data[row_i]["a"], row["a"].to_s)
69
- assert_equal(@data[row_i]["b"], row["b"].to_s)
70
- assert_equal(@data[row_i]["c"], row["c"].to_s)
71
- assert_equal(@data[row_i]["d"], row["d"].to_s)
72
- row_i += 1
73
- end
74
- end
75
-
76
66
  def test_sql_file
77
- row_i = 0
78
- @report.select("test/test.sql",:file) do |row|
79
- assert_equal(@data[row_i]["a"], row["a"].to_s)
80
- assert_equal(@data[row_i]["b"], row["b"].to_s)
81
- assert_equal(@data[row_i]["c"], row["c"].to_s)
82
- assert_equal(@data[row_i]["d"], row["d"].to_s)
83
- row_i += 1
67
+ @report.query("test/test.sql",:file) do |result|
68
+ assert_equal(@data,result)
84
69
  end
85
70
  end
86
71
 
87
72
  def test_sql_stored
88
- row_i = 0
89
- @report.select("sql_stored_test",:db) do |row|
90
- assert_equal(@data[row_i]["a"], row["a"].to_s)
91
- assert_equal(@data[row_i]["b"], row["b"].to_s)
92
- assert_equal(@data[row_i]["c"], row["c"].to_s)
93
- assert_equal(@data[row_i]["d"], row["d"].to_s)
94
- row_i += 1
73
+ @report.query("sql_stored_test",:db) do |result|
74
+ assert_equal(@data,result)
95
75
  end
96
76
  end
97
77
 
@@ -0,0 +1,21 @@
1
+ require "test/unit"
2
+ require "ruportlib"
3
+
4
+ class TestMailer < Test::Unit::TestCase
5
+
6
+ def setup
7
+ Report::FakeMailer[:host] = "mail.fakeruport.org"
8
+ Report::FakeMailer[:address] = "greg@fakeruport.org"
9
+ Report::FakeMailer[:account] = "greg"
10
+ Report::FakeMailer[:password] = "bubbles"
11
+ Report::FakeMailer[:auth] = :login
12
+
13
+ @mailer = Report::FakeMailer.new( "mail.fakeruport.org",
14
+ "greg@fakeruport.org",
15
+ "greg","bubbles", 25, :login )
16
+ end
17
+ def test_send
18
+ assert_nothing_raised { @mailer.send_report }
19
+ end
20
+
21
+ end
@@ -2,3 +2,5 @@ require "test/unit"
2
2
  require "test/tc_engine"
3
3
  require "test/tc_data_set"
4
4
  require "test/tc_data_row"
5
+ require "test/tc_builder"
6
+ require "test/tc_mailer"
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.8.10
2
+ rubygems_version: 0.8.11
3
3
  specification_version: 1
4
4
  name: ruport
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.2.2
7
- date: 2005-11-11
6
+ version: 0.2.4
7
+ date: 2005-11-16 00:00:00 -06:00
8
8
  summary: A generalized Ruby report generation and templating engine.
9
9
  require_paths:
10
10
  - lib
@@ -24,21 +24,27 @@ required_ruby_version: !ruby/object:Gem::Version::Requirement
24
24
  version: 0.0.0
25
25
  version:
26
26
  platform: ruby
27
+ signing_key:
28
+ cert_chain:
27
29
  authors:
28
30
  - Gregory Brown
29
31
  files:
30
32
  - lib/ruportlib.rb
33
+ - lib/ruport/format/builder.rb
31
34
  - lib/ruport/format/chart.rb
32
35
  - lib/ruport/report/data_row.rb
33
36
  - lib/ruport/report/data_set.rb
34
37
  - lib/ruport/report/engine.rb
35
38
  - lib/ruport/report/fake_db.rb
36
39
  - lib/ruport/report/fake_engine.rb
40
+ - lib/ruport/report/fake_mailer.rb
37
41
  - lib/ruport/report/mailer.rb
38
42
  - lib/ruport/report/sql.rb
43
+ - test/tc_builder.rb
39
44
  - test/tc_data_row.rb
40
45
  - test/tc_data_set.rb
41
46
  - test/tc_engine.rb
47
+ - test/tc_mailer.rb
42
48
  - test/ts_all.rb
43
49
  - Rakefile
44
50
  - README