omgf 0.0.0.GIT

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,201 @@
1
+ # Copyright (C) 2008-2012, Eric Wong <normalperson@yhbt.net>
2
+ # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
3
+ $stdout.sync = $stderr.sync = true
4
+ Thread.abort_on_exception = true
5
+ if ENV["COVERAGE"]
6
+ require "coverage"
7
+ Coverage.start
8
+ at_exit do
9
+ # Dirty little text formatter. I tried simplecov but the default
10
+ # HTML+JS is unusable without a GUI (I hate GUIs :P) and it would've
11
+ # taken me longer to search the Internets to find a plain-text
12
+ # formatter I like...
13
+ res = Coverage.result
14
+ relevant = res.keys.grep(%r{/lib/omgf/\w+\.rb})
15
+ relevant.each do |file|
16
+ cov = res[file]
17
+ puts "==> #{file} <=="
18
+ File.readlines(file).each_with_index do |line, i|
19
+ n = cov[i]
20
+ if n == 0 # BAD
21
+ print(" *** 0 #{line}")
22
+ elsif n
23
+ printf("% 7u %s", n, line)
24
+ elsif line =~ /\S/ # probably a line with just "end" in it
25
+ print(" #{line}")
26
+ else # blank line
27
+ print "\n" # don't output trailing whitespace on blank lines
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ require 'test/unit'
34
+ require 'net/http'
35
+ require 'uri'
36
+ require 'tempfile'
37
+ require 'mogilefs'
38
+ require 'stringio'
39
+ require 'logger'
40
+
41
+ module TestMogileFSIntegration
42
+ def x(*cmd)
43
+ out = Tempfile.new("out")
44
+ err = Tempfile.new("err")
45
+ puts cmd.join(' ') if $VERBOSE
46
+ pid = fork do
47
+ $stderr.reopen(err.path, "a")
48
+ $stdout.reopen(out.path, "a")
49
+ out.close
50
+ err.close
51
+ ObjectSpace.each_object(Tempfile) do |tmp|
52
+ next if tmp.closed?
53
+ ObjectSpace.undefine_finalizer(tmp)
54
+ tmp.close_on_exec = true if tmp.respond_to?(:close_on_exec=)
55
+ end
56
+ exec(*cmd)
57
+ end
58
+ _, status = Process.waitpid2(pid)
59
+ out.rewind
60
+ err.rewind
61
+ [ status, out, err ]
62
+ end
63
+
64
+ def x!(*cmd)
65
+ status, out, err = x(*cmd)
66
+ assert status.success?, "#{status.inspect} / #{out.read} / #{err.read}"
67
+ [ status, out, err ]
68
+ end
69
+
70
+ def setup_mogilefs(plugins = nil)
71
+ @test_host = "127.0.0.1"
72
+ setup_mogstored
73
+ @tracker = TCPServer.new(@test_host, 0)
74
+ @tracker_port = @tracker.addr[1]
75
+
76
+ @dbname = Tempfile.new(["mogfresh", ".sqlite3"])
77
+ @mogilefsd_conf = Tempfile.new(["mogilefsd", "conf"])
78
+ @mogilefsd_pid = Tempfile.new(["mogilefsd", "pid"])
79
+
80
+ cmd = %w(mogdbsetup --yes --type=SQLite --dbname) << @dbname.path
81
+ x!(*cmd)
82
+
83
+ @mogilefsd_conf.puts "db_dsn DBI:SQLite:#{@dbname.path}"
84
+ @mogilefsd_conf.write <<EOF
85
+ conf_port #@tracker_port
86
+ listen #@test_host
87
+ pidfile #{@mogilefsd_pid.path}
88
+ replicate_jobs 1
89
+ fsck_jobs 1
90
+ query_jobs 1
91
+ mogstored_stream_port #@mogstored_mgmt_port
92
+ node_timeout 10
93
+ EOF
94
+ @mogilefsd_conf.flush
95
+
96
+ @trackers = @hosts = [ "#@test_host:#@tracker_port" ]
97
+ @tracker.close
98
+ x!("mogilefsd", "--daemon", "--config=#{@mogilefsd_conf.path}")
99
+ wait_for_port @tracker_port
100
+ @admin = MogileFS::Admin.new(:hosts => @hosts)
101
+ 500.times do
102
+ break if File.size(@mogstored_pid.path) > 0
103
+ sleep 0.05
104
+ end
105
+
106
+ args = { :ip => @test_host, :port => @mogstored_http_port }
107
+ args[:status] = "alive"
108
+ @admin.create_host("me", args)
109
+ yield_for_monitor_update { @admin.get_hosts.empty? or break }
110
+
111
+ mogadm!("device", "add", "me", "dev1")
112
+ yield_for_monitor_update { @admin.get_devices.empty? or break }
113
+ wait_for_usage_file "dev1"
114
+ mogadm!("device", "add", "me", "dev2")
115
+ wait_for_usage_file "dev2"
116
+ yield_for_monitor_update { @admin.get_devices.size == 2 and break }
117
+ end
118
+
119
+ def mogadm(*args)
120
+ x("mogadm", "--trackers=#{@trackers.join(',')}", *args)
121
+ end
122
+
123
+ def mogadm!(*args)
124
+ status, out, err = mogadm(*args)
125
+ assert status.success?, "#{status.inspect} / #{out.read} / #{err.read}"
126
+ [ status, out, err ]
127
+ end
128
+
129
+ def yield_for_monitor_update
130
+ 50.times do
131
+ yield
132
+ sleep 0.1
133
+ end
134
+ end
135
+
136
+
137
+ def wait_for_port(port)
138
+ tries = 500
139
+ begin
140
+ TCPSocket.new(@test_host, port).close
141
+ return
142
+ rescue
143
+ sleep 0.05
144
+ end while (tries -= 1) > 0
145
+ raise "#@test_host:#{port} never became ready"
146
+ end
147
+
148
+ def teardown_mogilefs
149
+ if @mogstored_pid
150
+ pid = File.read(@mogstored_pid.path).to_i
151
+ Process.kill(:TERM, pid) if pid > 0
152
+ end
153
+ if @mogilefsd_pid
154
+ s = TCPSocket.new(@test_host, @tracker_port)
155
+ s.write "!shutdown\r\n"
156
+ s.close
157
+ end
158
+ FileUtils.rmtree(@docroot)
159
+ end
160
+
161
+ def wait_for_usage_file(device)
162
+ uri = URI("http://#@test_host:#@mogstored_http_port/#{device}/usage")
163
+ res = nil
164
+ 100.times do
165
+ res = Net::HTTP.get_response(uri)
166
+ if Net::HTTPOK === res
167
+ puts res.body if $DEBUG
168
+ return
169
+ end
170
+ puts res.inspect if $DEBUG
171
+ sleep 0.1
172
+ end
173
+ raise "#{uri} failed to appear: #{res.inspect}"
174
+ end
175
+
176
+ def setup_mogstored
177
+ @docroot = Dir.mktmpdir(["mogfresh", "docroot"])
178
+ Dir.mkdir("#@docroot/dev1")
179
+ Dir.mkdir("#@docroot/dev2")
180
+ @mogstored_mgmt = TCPServer.new(@test_host, 0)
181
+ @mogstored_http = TCPServer.new(@test_host, 0)
182
+ @mogstored_mgmt_port = @mogstored_mgmt.addr[1]
183
+ @mogstored_http_port = @mogstored_http.addr[1]
184
+ @mogstored_conf = Tempfile.new(["mogstored", "conf"])
185
+ @mogstored_pid = Tempfile.new(["mogstored", "pid"])
186
+ @mogstored_conf.write <<EOF
187
+ pidfile = #{@mogstored_pid.path}
188
+ maxconns = 500
189
+ httplisten = #@test_host:#@mogstored_http_port
190
+ mgmtlisten = #@test_host:#@mogstored_mgmt_port
191
+ docroot = #@docroot
192
+ EOF
193
+ @mogstored_conf.flush
194
+ @mogstored_mgmt.close
195
+ @mogstored_http.close
196
+
197
+ x!("mogstored", "--daemon", "--config=#{@mogstored_conf.path}")
198
+ wait_for_port @mogstored_mgmt_port
199
+ wait_for_port @mogstored_http_port
200
+ end
201
+ end
@@ -0,0 +1,78 @@
1
+ # Copyright (C) 2008-2012, Eric Wong <normalperson@yhbt.net>
2
+ # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
3
+ require './test/integration'
4
+ require 'omgf/hysterical_raisins'
5
+ begin
6
+ require 'unicorn'
7
+ rescue LoadError
8
+ end
9
+
10
+ class TestHystScript < Test::Unit::TestCase
11
+ include TestMogileFSIntegration
12
+ def setup
13
+ setup_mogilefs
14
+ @admin.create_domain("testdom")
15
+ srv = TCPServer.new(@test_host, 0)
16
+ @api_port = srv.addr[1]
17
+ ENV["UNICORN_FD"] = srv.fileno.to_s
18
+ api_addr = "#@test_host:#@api_port"
19
+ @app = OMGF::HystericalRaisins.new(:hosts => @hosts)
20
+ @out = Tempfile.new("out")
21
+ @err = Tempfile.new("err")
22
+ @unicorn_pid = fork do
23
+ $stdin.reopen("/dev/null")
24
+ $stdout.reopen(@out.path, "ab")
25
+ $stderr.reopen(@err.path, "ab")
26
+ # hopefully this Unicorn API doesn't change...
27
+ Unicorn::HttpServer.new(@app, :listeners => [api_addr]).start.join
28
+ end
29
+ srv.close
30
+ ENV["HYST_HOST"] = api_addr
31
+ ENV["MOG_DOMAIN"] = 'testdom'
32
+ @hyst = File.dirname(File.dirname(__FILE__)) + "/examples/hyst.bash"
33
+ assert File.executable?(@hyst)
34
+ end
35
+
36
+ def test_hyst
37
+ out = `#@hyst ls`
38
+ assert_equal $?, 0
39
+ assert_equal '', out
40
+
41
+ # tee is a little tricky, unicorn is one of the few Rack servers that
42
+ # handle chunked PUTs
43
+ out = `echo HI | #@hyst tee foo`
44
+ assert_equal 0, $?
45
+ assert_equal "HI\n", out
46
+ assert_equal "HI\n", `#@hyst cat foo`
47
+
48
+ # /dev/null optimization
49
+ out = `echo NULL | #@hyst tee bar >/dev/null`
50
+ assert_equal 0, $?
51
+ assert_equal "", out
52
+ assert_equal "NULL\n", `#@hyst cat bar`
53
+
54
+ # listings
55
+ assert_equal "foo\n", `#@hyst ls fo`
56
+ assert_equal "bar\n", `#@hyst ls b`
57
+ assert_equal "", `#@hyst ls z`
58
+
59
+ # remove a file once
60
+ assert(system("#@hyst rm bar"))
61
+
62
+ # really removed?
63
+ err = Tempfile.new('curl_err')
64
+ a = `#@hyst cat bar 2>#{err.path}`
65
+ assert_equal 22, $?.exitstatus
66
+ assert_equal '', a
67
+ assert_match(/\s+404\b/, err.read)
68
+ err.close!
69
+ end
70
+
71
+ def teardown
72
+ ENV.delete("HYST_HOST")
73
+ Process.kill :QUIT, @unicorn_pid
74
+ _, status = Process.waitpid2(@unicorn_pid)
75
+ assert status.success?, status.inspect
76
+ teardown_mogilefs
77
+ end
78
+ end if defined?(Unicorn)
@@ -0,0 +1,238 @@
1
+ # Copyright (C) 2008-2012, Eric Wong <normalperson@yhbt.net>
2
+ # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
3
+ require './test/integration'
4
+ require 'rack/mock'
5
+ require 'open-uri'
6
+ require 'omgf/hysterical_raisins'
7
+
8
+ class TestHystericalRaisins < Test::Unit::TestCase
9
+ include TestMogileFSIntegration
10
+ def setup
11
+ setup_mogilefs
12
+ @app = OMGF::HystericalRaisins.new(:hosts => @hosts)
13
+ @req = Rack::MockRequest.new(@app)
14
+ @err = StringIO.new
15
+ @opts = { "rack.logger" => Logger.new(@err) }
16
+ @admin.create_domain("testdom")
17
+ end
18
+
19
+ def test_all
20
+ resp = @req.get("/")
21
+ assert_equal 200, resp.status, "/ response code is 200"
22
+ assert_equal "", resp.body, "/ returns empty body"
23
+
24
+ # domain listing
25
+ resp = @req.get("/testdom", @opts)
26
+ assert_equal 200, resp.status, "domain listing response code is 200"
27
+ assert_equal "", resp.body, "domain listing is empty"
28
+ assert_equal("text/plain", resp.headers["Content-Type"],
29
+ "content-type is text/plain")
30
+
31
+ json = { "HTTP_ACCEPT" => "application/json" }
32
+
33
+ # json domain listing
34
+ resp = @req.get("/testdom", @opts.merge(json))
35
+ assert_equal 200, resp.status, "domain listing response code is 200"
36
+ assert_equal "[]", resp.body, "domain listing is empty array"
37
+ assert_equal("application/json", resp.headers["Content-Type"],
38
+ "content-type is application/json")
39
+
40
+ # missing domain
41
+ resp = @req.get("/non-existent", @opts)
42
+ assert_equal 0, @err.string.size
43
+ assert_equal 404, resp.status, "non-existent domain listing gives 404"
44
+
45
+ # missing domain (json)
46
+ resp = @req.get("/non-existent", @opts.merge(json))
47
+ assert_equal 0, @err.string.size
48
+ assert_equal 404, resp.status, "non-existent domain listing gives 404"
49
+
50
+ # PUT to domain listing fails
51
+ opts = @opts.merge(:input => StringIO.new("HELLO"))
52
+ resp = @req.put("/non-existent", opts)
53
+ assert_equal 0, @err.string.size
54
+ assert_equal 404, resp.status, "non-existent domain listing PUT gives 404"
55
+
56
+ # DELETEs to bad domains fail
57
+ resp = @req.delete("/non-existent", opts)
58
+ assert_equal 0, @err.string.size
59
+ assert_equal 404, resp.status, "non-existent domain DELETE gives 404"
60
+ resp = @req.delete("/non-existent/key", opts)
61
+ assert_equal 0, @err.string.size
62
+ assert_equal 406, resp.status, "non-existent domain DELETE gives 406"
63
+
64
+ # PUT to bad domain fails
65
+ opts = @opts.merge(:input => StringIO.new("HELLO"))
66
+ resp = @req.put("/non-existent/key", opts)
67
+ assert_equal 0, @err.string.size
68
+ assert_equal 406, resp.status, "bad domain PUT w/key: #{resp.inspect}"
69
+ assert_equal "Invalid domain: non-existent\n", resp.body
70
+
71
+ # PUT with bad key fails
72
+ opts = @opts.merge(:input => StringIO.new("HELLO"))
73
+ resp = @req.put("/testdom/key%20space", opts)
74
+ assert_equal 0, @err.string.size, @err.string
75
+ assert_equal 406, resp.status, "bad key"
76
+
77
+ # PUT with long key fails
78
+ opts = @opts.merge(:input => StringIO.new("HELLO"))
79
+ resp = @req.put("/testdom/key#{'a' * 128}", opts)
80
+ assert_equal 0, @err.string.size, @err.string
81
+ assert_equal 406, resp.status, "bad key"
82
+
83
+ # PUT to good domain succeeds
84
+ opts = @opts.merge(:input => StringIO.new("HELLO"))
85
+ resp = @req.put("/testdom/key", opts)
86
+ assert_equal 0, @err.string.size, @err.string
87
+ assert_equal 200, resp.status
88
+
89
+ # GET succeeds with redirect
90
+ resp = @req.get("/testdom/key", @opts)
91
+ assert_equal 0, @err.string.size, @err.string
92
+ assert_equal 302, resp.status
93
+ assert_equal "HELLO", open(resp["Location"]).read
94
+
95
+ # PUT to good domain and REMOTE_USER succeeds succeeds with 201
96
+ opts = @opts.merge(:input => StringIO.new("HELLO"))
97
+ opts["REMOTE_USER"] = "root"
98
+ resp = @req.put("/testdom/user_key", opts)
99
+ assert_equal 0, @err.string.size, @err.string
100
+ assert_equal 201, resp.status
101
+
102
+ # plain-text key listing succeeds
103
+ resp = @req.get("/testdom", @opts)
104
+ assert_equal 0, @err.string.size, @err.string
105
+ assert_equal 200, resp.status, resp.inspect
106
+ assert_equal 2, resp.body.split(/\n/).size
107
+ assert_equal("text/plain", resp.headers["Content-Type"],
108
+ "content-type is text/plain")
109
+ keys = resp.body.split(/\n/)
110
+ assert_match(/\Akey\|5|[12]\z/ ,keys[0])
111
+ assert_match(/\Auser_key\|5|[12]\z/ ,keys[1])
112
+
113
+ # json key listing succeeds
114
+ resp = @req.get("/testdom", @opts.merge(json))
115
+ assert_equal 0, @err.string.size, @err.string
116
+ assert_equal 200, resp.status, resp.inspect
117
+ keys = JSON.parse(resp.body)
118
+ assert_equal 2, keys.size
119
+ assert_equal("application/json", resp.headers["Content-Type"],
120
+ "content-type is application/json")
121
+ assert_equal "key", keys[0][0]
122
+ assert_equal 5, keys[0][1]
123
+ assert_operator keys[0][2], :>=, 1
124
+ assert_operator keys[0][2], :<=, 2
125
+ assert_equal "user_key", keys[1][0]
126
+ assert_equal 5, keys[1][1]
127
+ assert_operator keys[1][2], :>=, 1
128
+ assert_operator keys[1][2], :<=, 2
129
+
130
+ # HEAD of json listing succeeds
131
+ resp = @req.head("/testdom", @opts.merge(json))
132
+ assert_equal("application/json", resp.headers["Content-Type"],
133
+ "content-type is application/json")
134
+ assert_equal 200, resp.status
135
+ assert_equal "", resp.body
136
+
137
+ # HEAD of text listing succeeds
138
+ resp = @req.head("/testdom", @opts)
139
+ assert_equal("text/plain", resp.headers["Content-Type"],
140
+ "content-type is text/plain")
141
+ assert_equal 200, resp.status
142
+ assert_equal "", resp.body
143
+
144
+ # DELETE succeeds
145
+ resp = @req.delete("/testdom/user_key", @opts)
146
+ assert_equal 0, @err.string.size, @err.string
147
+ assert_equal 204, resp.status, resp.inspect
148
+
149
+ # DELETE again fails
150
+ resp = @req.delete("/testdom/user_key", @opts)
151
+ assert_equal 0, @err.string.size, @err.string
152
+ assert_equal 404, resp.status, resp.inspect
153
+
154
+ # unauthed PUT fails to overwrite
155
+ opts = @opts.merge(:input => StringIO.new("FAIL"))
156
+ resp = @req.put("/testdom/key", opts)
157
+ assert_equal 0, @err.string.size, @err.string
158
+ assert_equal 403, resp.status, resp.inspect
159
+
160
+ # GET returns unchanged file
161
+ resp = @req.get("/testdom/key", @opts)
162
+ assert_equal 0, @err.string.size, @err.string
163
+ assert_equal 302, resp.status
164
+ assert_equal "HELLO", open(resp["Location"]).read
165
+
166
+ # PUT succeeds with overwrite
167
+ opts = @opts.merge(:input => StringIO.new("BLAH"))
168
+ opts["REMOTE_USER"] = "root"
169
+ opts["HTTP_X_OMGF_FORCE"] = "true"
170
+ resp = @req.put("/testdom/key", opts)
171
+ assert_equal 0, @err.string.size, @err.string
172
+ assert_equal 204, resp.status, resp.inspect
173
+
174
+ # GET succeeds with redirect
175
+ resp = @req.get("/testdom/key", @opts)
176
+ assert_equal 0, @err.string.size, @err.string
177
+ assert_equal 302, resp.status
178
+ assert_equal "BLAH", open(resp["Location"]).read
179
+
180
+ # HEAD shows size and metadata
181
+ resp = @req.head("/testdom/key", @opts)
182
+ assert_equal 0, @err.string.size, @err.string
183
+ assert_equal 200, resp.status, resp.inspect
184
+ assert_equal "4", resp["Content-Length"]
185
+ assert_equal "BLAH", open(resp["X-URL-0"]).read
186
+
187
+ # wait for replication
188
+ if ENV["EXPENSIVE"]
189
+ 200.times do
190
+ resp["X-URL-1"] and break
191
+ sleep 0.1
192
+ resp = @req.head("/testdom/key", @opts)
193
+ end
194
+ assert_kind_of String, resp["X-URL-1"]
195
+ assert_equal "BLAH", open(resp["X-URL-1"]).read
196
+ end
197
+
198
+ reproxy_test
199
+ end
200
+
201
+ def reproxy_test
202
+ @app.instance_variable_set(:@reproxy_path, "/reproxy")
203
+ opts = @opts.merge("HTTP_X_OMGF_REPROXY" => "1")
204
+ resp = @req.get("/testdom/key", opts)
205
+ assert_equal 0, @err.string.size, @err.string
206
+ t = Time.parse(resp["X-Redirect-Last-Modified"])
207
+ assert_equal "\"#{t.to_i}\"", resp["ETag"]
208
+ assert_equal "application/octet-stream", resp["X-Redirect-Content-Type"]
209
+ assert_equal "BLAH", open(resp["Location"]).read
210
+ assert_equal "/reproxy", resp["X-Accel-Redirect"]
211
+ assert_nil resp.original_headers["Content-Type"], resp.inspect
212
+ assert_nil resp["Last-Modified"]
213
+
214
+ # no point in redirecting HEAD requests
215
+ resp = @req.head("/testdom/key", opts)
216
+ assert_equal 0, @err.string.size, @err.string
217
+ t = Time.parse(resp["Last-Modified"])
218
+ assert_equal "\"#{t.to_i}\"", resp["ETag"]
219
+ assert_nil resp["X-Accel-Redirect"]
220
+ assert_equal "4", resp["Content-Length"]
221
+ assert_equal "application/octet-stream",
222
+ resp.original_headers["Content-Type"], resp.inspect
223
+
224
+ # explicit filename
225
+ resp = @req.head("/testdom/key?inline=foo.txt", opts)
226
+ assert_equal "inline; filename=foo.txt", resp["Content-Disposition"]
227
+ resp = @req.head("/testdom/key?attachment=foo.txt", opts)
228
+ assert_equal "attachment; filename=foo.txt", resp["Content-Disposition"]
229
+ resp = @req.head("/testdom/key?filename=foo.txt", opts)
230
+ assert_equal "attachment; filename=foo.txt", resp["Content-Disposition"]
231
+
232
+ @app.instance_variable_set(:@reproxy_path, nil)
233
+ end
234
+
235
+ def teardown
236
+ teardown_mogilefs
237
+ end
238
+ end