so2db 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ #--
2
+ # Copyright (c) 2012 Chad Taylor
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ # SOFTWARE.
21
+
22
+ require 'active_record'
23
+
24
+ module SO2DB::Models
25
+
26
+ class Badge < ActiveRecord::Base
27
+ def self.exported_fields
28
+ return [ :id, :user_id, :name, :date ]
29
+ end
30
+ end
31
+
32
+ class Comment < ActiveRecord::Base
33
+ def self.exported_fields
34
+ return [ :id, :post_id, :score, :text, :creation_date, :user_id,
35
+ :user_display_name ]
36
+ end
37
+ end
38
+
39
+ class Post < ActiveRecord::Base
40
+ def self.exported_fields
41
+ return [ :id, :post_type_id, :parent_id, :accepted_answer_id,
42
+ :creation_date, :score, :view_count, :body, :owner_user_id,
43
+ :last_editor_user_id, :last_editor_display_name, :last_edit_date,
44
+ :last_activity_date, :community_owned_date, :closed_date, :title,
45
+ :tags, :answer_count, :comment_count, :favorite_count,
46
+ :owner_display_name ]
47
+ end
48
+ end
49
+
50
+ class PostHistory < ActiveRecord::Base
51
+ self.table_name = "post_history"
52
+
53
+ def self.exported_fields
54
+ return [ :id, :post_history_type_id, :post_id, :revision_guid,
55
+ :creation_date, :user_id, :user_display_name, :comment, :text,
56
+ :close_reason_id ]
57
+ end
58
+ end
59
+
60
+ class User < ActiveRecord::Base
61
+ def self.exported_fields
62
+ return [ :id, :reputation, :creation_date, :display_name, :email_hash,
63
+ :last_access_date, :website_url, :location, :age, :about_me,
64
+ :views, :up_votes, :down_votes ]
65
+ end
66
+ end
67
+
68
+ class Vote < ActiveRecord::Base
69
+ def self.exported_fields
70
+ return [ :id, :post_id, :vote_type_id, :creation_date, :user_id,
71
+ :bounty_amount ]
72
+ end
73
+ end
74
+
75
+ class PostType < ActiveRecord::Base
76
+ end
77
+
78
+ class PostHistoryType < ActiveRecord::Base
79
+ end
80
+
81
+ class CloseReason < ActiveRecord::Base
82
+ end
83
+
84
+ class VoteType < ActiveRecord::Base
85
+ end
86
+
87
+ # Infrastructure. Do not call this from your code.
88
+ class Lookup
89
+
90
+ @@map = { "badges" => :Badge, "comments" => :Comment,
91
+ "posthistory" => :PostHistory, "posts" => :Post, "users" => :User,
92
+ "votes" => :Vote }
93
+
94
+ def self.find_class(file_name)
95
+ Object.const_get("SO2DB").const_get("Models")
96
+ .const_get(@@map[file_name].to_s)
97
+ end
98
+
99
+ def self.get_required_attrs(file_name)
100
+ raw = find_class(file_name).send :exported_fields
101
+ return raw.map {|f| f.to_s.camelize.sub(/Guid/, 'GUID')}
102
+ end
103
+
104
+ end
105
+
106
+ end
data/lib/so2db.rb ADDED
@@ -0,0 +1,126 @@
1
+ #--
2
+ # Copyright (c) 2012 Chad Taylor
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ # SOFTWARE.
21
+
22
+ module SO2DB
23
+
24
+ # Base class for StackOverflow data importers. Drives database setup and
25
+ # data importing files from a directory.
26
+ #
27
+ # Implementations of this class must provide a method with the following
28
+ # signature:
29
+ #
30
+ # import_stream(formatter)
31
+ #
32
+ # This method may be private. The purpose of this method is to actually
33
+ # perform the data import with data from the provided formatter. The
34
+ # formatter is provided to support scenarios of streaming data to STDIN
35
+ # (e.g., PostgreSQL's COPY command) as well as pushing data to a file before
36
+ # import (e.g., for MySQL's mysqlimport utility). It has type
37
+ # SO2DB::Formatter.
38
+ #
39
+ # The importer uses ActiveRecord for table creation and Foreigner for creating
40
+ # table relationships. You are limited to the databases supported by these
41
+ # libraries. In addition, a 'uuid' method must be avaiable to the adapter
42
+ # provided to ActiveRecord. (See so2pg for an example of an adapter extension
43
+ # that provides the method.)
44
+ #
45
+ # In addition, it provides two accessors for subclasses:
46
+ #
47
+ # attr_reader :conn_opts
48
+ # attr_accessor :delimiter
49
+ #
50
+ # The conn_opts property provides the ActiveRecord connection data (e.g.,
51
+ # :database, :host, etc.). The delimiter property sets the delimiter used by
52
+ # the formatter. The delimiter is \v (0xB) by default.
53
+ class Importer
54
+
55
+ # Initializes the importer.
56
+ #
57
+ # Arguments:
58
+ # relations: (Boolean) Indicates whether database relationships should
59
+ # be created.
60
+ # optionals: (Boolean) Indicates whether optional database tables and
61
+ # content should be created.
62
+ # adapter: (String) The ActiveRecord adapter name (e.g., 'postgresql').
63
+ # options: (Hash) The database connection options, as required by
64
+ # ActiveRecord for the provided adapter.
65
+ def initialize(relations = false, optionals = false, adapter = '', options = {})
66
+ @relations = relations
67
+ @optionals = optionals
68
+ @conn_opts = options.merge( { :adapter => adapter } )
69
+ @format_delimiter = 11.chr.to_s
70
+ end
71
+
72
+ # Creates the database tables and relationships, and imports the data in
73
+ # the files in the specified directory.
74
+ #
75
+ # Arguments:
76
+ # dir: (String) The directory path containting the StackOverflow data
77
+ # dump XML files (e.g., badges.xml, posts.xml, etc.).
78
+ def import(dir)
79
+ setup
80
+ create_basics
81
+ import_data(dir)
82
+ create_relations if @relations
83
+ create_optionals if @optionals
84
+ create_optional_relations if @relations and @optionals
85
+ end
86
+
87
+ private
88
+
89
+ attr_reader :conn_opts
90
+ attr_accessor :format_delimiter
91
+
92
+ def setup
93
+ ActiveRecord::Base.establish_connection @conn_opts
94
+ Foreigner.load
95
+ end
96
+
97
+ def create_basics
98
+ SO2DB::CreateBasicTables.new.up
99
+ end
100
+
101
+ def import_data(dir)
102
+ files = Dir.entries(dir).delete_if { |x| !x.end_with? 'xml' }
103
+ files.each do |f|
104
+ f = Formatter.new(File.join(dir, f), @format_delimiter)
105
+ import_stream f
106
+ end
107
+ end
108
+
109
+ def create_relations
110
+ SO2DB::CreateRelationships.new.up
111
+ end
112
+
113
+ def create_optionals
114
+ SO2DB::CreateOptionals.new.up
115
+ end
116
+
117
+ def create_optional_relations
118
+ SO2DB::CreateOptionalRelationships.new.up
119
+ end
120
+
121
+ end
122
+ end
123
+
124
+ require 'so2db/formatter'
125
+ require 'so2db/migrations'
126
+ require 'so2db/models'
data/lib/so2pg.rb ADDED
@@ -0,0 +1,179 @@
1
+ #--
2
+ # Copyright (c) 2012 Chad Taylor
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ # SOFTWARE.
21
+
22
+ require 'optparse'
23
+ require 'so2db'
24
+
25
+ module ActiveRecord::ConnectionAdapters
26
+ class PostgreSQLAdapter < AbstractAdapter
27
+
28
+ # Extends the adapter to include support for a uuid type. This is required
29
+ # by the importer (see SO2DB::Importer for more information). For
30
+ # PostgreSQL, simply use the native 'uuid' type (for MySQL, use something
31
+ # a bit more contrived, like CHAR(16)).
32
+ def uuid
33
+ return 'uuid', {}
34
+ end
35
+ end
36
+ end
37
+
38
+ # Imports the StackOverflow data into a PostgreSQL data.
39
+ class PgImporter < SO2DB::Importer
40
+
41
+ # (See SO2DB::Importer.initialize documentation)
42
+ def initialize(relations = false, optionals = false, options = {})
43
+ super(relations, optionals, "postgresql", options)
44
+ end
45
+
46
+ def self.import_from_argv(argv)
47
+ # Parse the command-line options
48
+ cmd_opts = PgOptionsParser.parse(ARGV)
49
+
50
+ # If all validation passed, then execute the import!
51
+ if cmd_opts
52
+ start = Time.now
53
+ pg = PgImporter.new(cmd_opts.has_key?(:relationships),
54
+ cmd_opts.has_key?(:optionals),
55
+ cmd_opts)
56
+ pg.import(cmd_opts[:dir])
57
+ puts "Import completed in #{Time.now - start}s"
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # Imports the data from the formatter into the PostgreSQL database.
64
+ #
65
+ # Note that what follows is just one way to implement the importer. You
66
+ # could just as easily push the formatted data into a file and then ask
67
+ # the database to suck that file in.
68
+ def import_stream(formatter)
69
+ puts "Importing file #{formatter.file_name}..."
70
+ start = Time.now
71
+
72
+ sql = build_sql(formatter.value_str)
73
+ cmd = build_cmd(sql)
74
+ execute_cmd(cmd, formatter)
75
+
76
+ puts " -> #{Time.now - start}s"
77
+ end
78
+
79
+ # Builds the SQL command used for bulk loading the tables.
80
+ def build_sql(value_str)
81
+ "COPY #{value_str} FROM STDIN WITH (FORMAT csv, DELIMITER E'\x0B')"
82
+ end
83
+
84
+ # Builds the import command with the given SQL command and the global
85
+ # connection options.
86
+ #
87
+ # Example:
88
+ # >> sql = "COPY ..."
89
+ # >> puts build_cmd(sql)
90
+ # => psql -d test -h localhost -c "COPY ..."
91
+ def build_cmd(sql)
92
+ # Only exists within the context of this script (not exported), so this
93
+ # does not degrade security posture after the script has completed
94
+ ENV['PGPASSWORD'] = conn_opts[:password] if conn_opts.has_key? :password
95
+
96
+ cmd = "psql"
97
+ cmd << " -d #{conn_opts[:database]}" if conn_opts.has_key? :database
98
+ cmd << " -h #{conn_opts[:host]}" if conn_opts.has_key? :host
99
+ cmd << " -U #{conn_opts[:username]}" if conn_opts.has_key? :username
100
+ cmd << " -p #{conn_opts[:port]}" if conn_opts.has_key? :port
101
+ cmd << " -c \"#{sql}\""
102
+
103
+ return cmd
104
+ end
105
+
106
+ # Executes the provided shell command and pumps the data from the formatter
107
+ # to it over stdin.
108
+ def execute_cmd(cmd, formatter)
109
+ IO.popen(cmd, 'r+') do |s|
110
+ formatter.format(s)
111
+ s.close_write
112
+ end
113
+ end
114
+
115
+ end
116
+
117
+ class PgOptionsParser
118
+
119
+ # Parses the command-line arguments into a Hash object. Note that the members
120
+ # of the Hash have the same name as the ActiveRecord parameters (e.g., :host,
121
+ # :database, etc.). This Hash will actually be passed to ActiveRecord for
122
+ # consumption.
123
+ def self.parse(args)
124
+ options = {}
125
+
126
+ opts = OptionParser.new do |opts|
127
+ opts.banner = <<-EOB
128
+ Imports a StackOverflow data dump into a PostgreSQL database.
129
+ Usage: so2pg [options]
130
+ EOB
131
+
132
+ opts.on("-H", "--host HOST", "The database host") do |host|
133
+ options[:host] = host
134
+ end
135
+
136
+ opts.on("-d", "--database DBNAME", "The name of the database (REQUIRED)") do |dbname|
137
+ options[:database] = dbname
138
+ end
139
+
140
+ opts.on("-D", "--directory DIRECTORY", "The data directory path (REQUIRED)") do |dir|
141
+ options[:dir] = dir
142
+ end
143
+
144
+ opts.on("-u", "--user USER", "The user name") do |user|
145
+ options[:username] = user
146
+ end
147
+
148
+ opts.on("-p", "--password PASSWORD", "The user's password") do |password|
149
+ options[:password] = password
150
+ end
151
+
152
+ opts.on("-P", "--port PORT_NUMBER", "The port number") do |port|
153
+ options[:port] = port
154
+ end
155
+
156
+ opts.on("-O", "--include-optionals", "Includes optional tables") do
157
+ options[:optionals] = true
158
+ end
159
+
160
+ opts.on("-R", "--include-relationships", "Includes table relationships") do
161
+ options[:relationships] = true
162
+ end
163
+
164
+ opts.on("-h", "--help", "Show this help screen") do |help|
165
+ options[:help] = true
166
+ end
167
+
168
+ end
169
+
170
+ opts.parse!(args)
171
+ if(options[:help] or !options.has_key? :dir or !options.has_key? :database)
172
+ puts opts.help
173
+ nil
174
+ else
175
+ options
176
+ end
177
+ end
178
+
179
+ end
@@ -0,0 +1,120 @@
1
+ require 'test/unit'
2
+ require 'mocha'
3
+ require 'so2db'
4
+
5
+ class FormatterTest < Test::Unit::TestCase
6
+ include Rake::DSL
7
+
8
+ def setup
9
+ @formatter = SO2DB::Formatter.new("/tmp/badges.xml")
10
+ end
11
+
12
+ def test_initializer_with_default_args
13
+ f = SO2DB::Formatter.new
14
+ assert_equal '', f.instance_variable_get(:@path)
15
+ assert_equal 11.chr.to_s, f.instance_variable_get(:@delimiter)
16
+ end
17
+
18
+ def test_initializer_with_provided_args
19
+ path = '/my/test/path'
20
+ delimiter = 12.chr.to_s
21
+ f = SO2DB::Formatter.new(path, delimiter)
22
+
23
+ assert_equal path, f.instance_variable_get(:@path)
24
+ assert_equal delimiter, f.instance_variable_get(:@delimiter)
25
+ end
26
+
27
+ def test_format
28
+ file = "file"
29
+ outstream = "outstream"
30
+ attrs = [ :a, :b, :c ]
31
+ SO2DB::Models::Lookup.expects(:get_required_attrs).with("badges").once.returns(attrs)
32
+ File.expects(:open).with("/tmp/badges.xml").once.returns(file)
33
+ @formatter.expects(:format_from_stream).with(file, attrs, outstream).once.returns("x")
34
+
35
+ result = @formatter.format(outstream)
36
+ assert_equal "x", result
37
+ end
38
+
39
+ def test_format_from_stream
40
+ x = <<-eoxml
41
+ <data>
42
+ <fake-row id="3"/>
43
+ <row Id="1" UserId="2" Name="Autobiographer" Date="2010-07-20T19:07:22.990" />
44
+ </data>
45
+ eoxml
46
+
47
+ r, w = IO.pipe
48
+
49
+ arr = [ "Id", "UserId", "Name", "Date", "Missing" ]
50
+ @formatter.send(:format_from_stream, x, arr, w)
51
+
52
+ values = [ '2010-07-20T19:07:22.990', '1', '', 'Autobiographer', '2' ]
53
+ expected = values.join(11.chr.to_s) << "\n"
54
+ actual = r.gets
55
+
56
+ assert_equal expected, actual
57
+ end
58
+
59
+ def test_file_name
60
+ assert_equal "badges.xml", @formatter.file_name
61
+ end
62
+
63
+ def test_value_str
64
+ assert_equal "badges(date,id,name,user_id)", @formatter.value_str
65
+ end
66
+
67
+ def create_node_stub(name, node_type)
68
+ obj = mock()
69
+ obj.stubs(:name).returns(name)
70
+ obj.stubs(:node_type).returns(node_type)
71
+
72
+ obj
73
+ end
74
+
75
+ def test_element_start_with_good_values
76
+ node = create_node_stub("row", Nokogiri::XML::Reader::TYPE_ELEMENT)
77
+ assert @formatter.send(:element_start?, node)
78
+ end
79
+
80
+ def test_element_start_with_invalid_type
81
+ node = create_node_stub("row", Nokogiri::XML::Reader::TYPE_END_ELEMENT)
82
+ assert_equal false, @formatter.send(:element_start?, node)
83
+ end
84
+
85
+ def test_element_start_with_invalid_name
86
+ node = create_node_stub("badges", Nokogiri::XML::Reader::TYPE_ELEMENT)
87
+ assert_equal false, @formatter.send(:element_start?, node)
88
+ end
89
+
90
+ def test_format_node
91
+ node = mock()
92
+ node.stubs(:attribute).with("Id").returns("1")
93
+ node.stubs(:attribute).with("Name").returns("Anony Mous")
94
+
95
+ @formatter.expects(:scrub).with("1").once.returns("1")
96
+ @formatter.expects(:scrub).with("Anony Mous").once.returns("Anony Mous")
97
+
98
+ result = @formatter.send(:format_node, node, [ "Id", "Name" ])
99
+
100
+ assert_equal "1\vAnony Mous", result
101
+ end
102
+
103
+ def test_format_node_with_missing_attribute
104
+ node = mock()
105
+ node.stubs(:attribute).with("Id").returns("1")
106
+ node.stubs(:attribute).with("Name").returns(nil)
107
+
108
+ @formatter.expects(:scrub).once.with("1").returns("1")
109
+
110
+ result = @formatter.send(:format_node, node, [ "Id", "Name" ])
111
+
112
+ assert_equal "1\v", result
113
+ end
114
+
115
+ def test_scrub
116
+ assert_equal '&lt;asdffdsa&gt;', @formatter.send(:scrub, "<asdf\nfdsa\r>")
117
+ end
118
+
119
+ end
120
+
@@ -0,0 +1,20 @@
1
+ require 'test/unit'
2
+ require 'so2db'
3
+
4
+ class LookupTest < Test::Unit::TestCase
5
+ include Rake::DSL
6
+
7
+ def test_lookup_badges
8
+ assert_equal SO2DB::Models::Badge, SO2DB::Models::Lookup::find_class("badges")
9
+ end
10
+
11
+ def test_required_attrs_badges
12
+ attrs = SO2DB::Models::Lookup::get_required_attrs("badges")
13
+ assert_equal [ "Id", "UserId", "Name", "Date" ], attrs
14
+ end
15
+
16
+ def test_guid_capitalization
17
+ attrs = SO2DB::Models::Lookup::get_required_attrs("posthistory")
18
+ assert_block { attrs.include? 'RevisionGUID' }
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ require 'test/unit'
2
+ require 'mocha'
3
+ require 'so2db'
4
+
5
+ class ImporterTest < Test::Unit::TestCase
6
+
7
+ def test_import_data
8
+ importer = SO2DB::Importer.new
9
+ Dir.expects(:entries).once.with('/tmp').returns([ 'test.bak', 'test.xml' ])
10
+ SO2DB::Formatter.expects(:new).once
11
+ .with('/tmp/test.xml', 11.chr.to_s).returns('formatter')
12
+ importer.expects(:import_stream).once.with('formatter')
13
+
14
+ importer.send(:import_data, '/tmp')
15
+ end
16
+
17
+ end
@@ -0,0 +1,120 @@
1
+ require 'test/unit'
2
+ require 'mocha'
3
+ require 'so2pg'
4
+
5
+ class PgImporterTest < Test::Unit::TestCase
6
+
7
+ def setup
8
+ @importer = PgImporter.new(true, true, { :database => "dbname", :dir => "dir" })
9
+ end
10
+
11
+ def test_import_stream
12
+ formatter = mock()
13
+ formatter.stubs(:file_name).returns("file_name")
14
+ formatter.expects(:value_str).once.returns('badges(id,name)')
15
+
16
+ @importer.expects(:build_sql).once.with('badges(id,name)').returns("COPY...")
17
+ @importer.expects(:build_cmd).once.with("COPY...").returns("cmd")
18
+ @importer.stubs(:execute_cmd).returns('')
19
+ @importer.expects(:execute_cmd).once.with("cmd", formatter).returns('')
20
+
21
+ $stdout.stubs(:puts).returns('')
22
+
23
+ @importer.send(:import_stream, formatter)
24
+ end
25
+
26
+ def test_build_sql
27
+ exp = "COPY badges(id,name) FROM STDIN WITH (FORMAT csv, DELIMITER E'\x0B')"
28
+ assert_equal exp, @importer.send(:build_sql, "badges(id,name)")
29
+ end
30
+
31
+ def test_build_cmd
32
+ expected = "psql -d dbname -c \"COPY...\""
33
+ actual = @importer.send(:build_cmd, "COPY...")
34
+
35
+ assert_equal expected, actual
36
+ end
37
+
38
+ def test_build_cmd_sets_env_password
39
+ importer = PgImporter.new(true, true, { :database => "dbname",
40
+ :dir => "dir",
41
+ :password => "asdf1234" })
42
+
43
+ importer.send(:build_cmd, "COPY...")
44
+ assert_equal "asdf1234", ENV['PGPASSWORD']
45
+ end
46
+
47
+ def test_execute_cmd
48
+ strm = mock()
49
+ formatter = mock()
50
+
51
+ IO.expects(:popen).once.with("cmd", "r+").yields(strm)
52
+ formatter.expects(:format).once.with(strm)
53
+ strm.expects(:close_write).once
54
+
55
+ @importer.send(:execute_cmd, "cmd", formatter)
56
+ end
57
+
58
+ end
59
+
60
+ class PgOptionsParserTest < Test::Unit::TestCase
61
+
62
+ def test_all_options
63
+ host = 'localhost'
64
+ database = 'database_name'
65
+ directory = 'data_directory'
66
+ user = 'anony'
67
+ password = 'mous'
68
+ port = '1234'
69
+
70
+ cmd = [ '-H', host, '-d', database, '-D', directory, '-u', user,
71
+ '-p', password, '-P', port, '-O', '-R' ]
72
+
73
+ options = PgOptionsParser.parse(cmd)
74
+
75
+ assert_equal host, options[:host]
76
+ assert_equal database, options[:database]
77
+ assert_equal directory, options[:dir]
78
+ assert_equal user, options[:username]
79
+ assert_equal password, options[:password]
80
+ assert_equal port, options[:port]
81
+ assert options[:optionals]
82
+ assert options[:relationships]
83
+ end
84
+
85
+ def assert_help_displayed(cmd)
86
+ $stdout.expects(:puts).returns('')
87
+ options = PgOptionsParser.parse(cmd)
88
+
89
+ assert_nil options
90
+ end
91
+
92
+ def test_options_without_database
93
+ assert_help_displayed [ '-D', 'data_dir' ]
94
+ end
95
+
96
+ def test_options_without_data_dir
97
+ assert_help_displayed [ '-d', 'database_name' ]
98
+ end
99
+
100
+ def test_options_with_help
101
+ assert_help_displayed [ '-D', 'data_dir', '-d', 'database_name', '-h' ]
102
+ end
103
+
104
+ def test_options_without_optionals
105
+ cmd = [ '-D', 'data_dir', '-d', 'database_name', '-R' ]
106
+ options = PgOptionsParser.parse(cmd)
107
+
108
+ assert options[:relationships]
109
+ assert !options[:optionals]
110
+ end
111
+
112
+ def test_options_without_relationships
113
+ cmd = [ '-D', 'data_dir', '-d', 'database_name', '-O' ]
114
+ options = PgOptionsParser.parse(cmd)
115
+
116
+ assert !options[:relationships]
117
+ assert options[:optionals]
118
+ end
119
+
120
+ end