curb 1.3.6 → 1.3.7

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/tests/helper.rb CHANGED
@@ -238,6 +238,85 @@ module BugTestServerSetupTeardown
238
238
  end
239
239
  end
240
240
 
241
+ # Test::Unit creates a new test instance for every test method. Keep the
242
+ # common WEBrick fixture process-scoped so those instances can share it while
243
+ # still allowing an explicit stop_test_server call to exercise lifecycle code.
244
+ module CurbTestServerRegistry
245
+ Entry = Struct.new(:key, :port, :server, :thread, :lock_file, :pid)
246
+
247
+ @mutex = Mutex.new
248
+ @entries = {}
249
+
250
+ class << self
251
+ def acquire(port, servlet)
252
+ key = [port, servlet]
253
+
254
+ @mutex.synchronize do
255
+ entry = @entries[key]
256
+ return entry if active?(entry)
257
+
258
+ @entries.delete(key)
259
+ entry = yield
260
+ @entries[key] = entry if entry
261
+ entry
262
+ end
263
+ end
264
+
265
+ def stop(entry, timeout)
266
+ return false unless remove(entry)
267
+
268
+ shutdown_entry(entry, timeout)
269
+ true
270
+ end
271
+
272
+ def register_shutdown_hook(entry)
273
+ # Test::Unit can run a directly executed test file from its own at_exit
274
+ # callback. Register after startup so this hook runs after that callback.
275
+ at_exit { stop(entry, 5) }
276
+ end
277
+
278
+ private
279
+
280
+ def active?(entry)
281
+ entry && entry.server && entry.thread && entry.thread.alive? && entry.server.status == :Running
282
+ rescue StandardError
283
+ false
284
+ end
285
+
286
+ def remove(entry)
287
+ @mutex.synchronize do
288
+ return false unless @entries[entry.key].equal?(entry)
289
+
290
+ @entries.delete(entry.key)
291
+ true
292
+ end
293
+ end
294
+
295
+ def shutdown_entry(entry, timeout)
296
+ return unless Process.pid == entry.pid
297
+
298
+ begin
299
+ entry.server.shutdown if entry.server
300
+ rescue StandardError
301
+ nil
302
+ end
303
+
304
+ thread = entry.thread
305
+ if thread
306
+ thread.join(timeout)
307
+ if thread.alive?
308
+ thread.kill
309
+ thread.join(timeout)
310
+ end
311
+ end
312
+
313
+ File.unlink(entry.lock_file) if entry.lock_file && File.exist?(entry.lock_file)
314
+ rescue Errno::ENOENT
315
+ nil
316
+ end
317
+ end
318
+ end
319
+
241
320
  module TestServerMethods
242
321
  def server_responding?(port)
243
322
  socket = TCPSocket.new('127.0.0.1', port)
@@ -267,6 +346,22 @@ module TestServerMethods
267
346
  end
268
347
  end
269
348
 
349
+ # Waiting on WEBrick's state avoids opening a probe connection for every
350
+ # startup. On Windows those short-lived connections make server teardown
351
+ # disproportionately expensive.
352
+ def wait_for_server_started(server, port, thread: nil)
353
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + server_startup_timeout
354
+
355
+ loop do
356
+ return true if server.status == :Running
357
+ return false if thread && !thread.alive?
358
+
359
+ raise "Failed to startup test server on port #{port}" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
360
+
361
+ sleep 0.01
362
+ end
363
+ end
364
+
270
365
  def server_shutdown_timeout
271
366
  [server_startup_timeout, 0.25].max
272
367
  end
@@ -337,6 +432,13 @@ module TestServerMethods
337
432
  end
338
433
 
339
434
  def stop_test_server
435
+ shared_entry = instance_variable_defined?(:@__shared_test_server_entry) ? @__shared_test_server_entry : nil
436
+ if shared_entry
437
+ CurbTestServerRegistry.stop(shared_entry, server_shutdown_timeout)
438
+ release_shared_test_server
439
+ return
440
+ end
441
+
340
442
  server = instance_variable_defined?(:@server) ? @server : nil
341
443
  pid = instance_variable_defined?(:@__pid) ? @__pid : nil
342
444
  return unless server || pid
@@ -382,11 +484,28 @@ module TestServerMethods
382
484
  def teardown
383
485
  super
384
486
  ensure
385
- stop_test_server
487
+ if instance_variable_defined?(:@__shared_test_server_entry) && @__shared_test_server_entry
488
+ release_shared_test_server
489
+ else
490
+ stop_test_server
491
+ end
386
492
  end
387
493
 
388
494
  def server_setup(port=9129,servlet=TestServlet)
389
495
  @__port = port
496
+
497
+ unless TEST_SINGLE_THREADED
498
+ entry = CurbTestServerRegistry.acquire(port, servlet) do
499
+ start_shared_test_server(port, servlet)
500
+ end
501
+
502
+ if entry
503
+ attach_shared_test_server(entry)
504
+ end
505
+
506
+ return
507
+ end
508
+
390
509
  @server = nil unless instance_variable_defined?(:@server)
391
510
  @__pid = nil unless instance_variable_defined?(:@__pid)
392
511
  @test_thread = nil unless instance_variable_defined?(:@test_thread)
@@ -474,6 +593,64 @@ module TestServerMethods
474
593
  end
475
594
  rescue Errno::EADDRINUSE
476
595
  end
596
+
597
+ def attach_shared_test_server(entry)
598
+ @server = entry.server
599
+ @test_thread = entry.thread
600
+ @__pid = nil
601
+ @__shared_test_server_entry = entry
602
+ end
603
+
604
+ def release_shared_test_server
605
+ @server = nil
606
+ @test_thread = nil
607
+ @__pid = nil
608
+ @__shared_test_server_entry = nil
609
+ end
610
+
611
+ def start_shared_test_server(port, servlet)
612
+ clear_stale_server_lock(port)
613
+
614
+ if File.exist?(locked_file)
615
+ begin
616
+ wait_for_server_ready(port)
617
+ return nil
618
+ rescue RuntimeError
619
+ clear_stale_server_lock(port)
620
+ end
621
+ end
622
+
623
+ write_server_lock
624
+ server = WEBrick::HTTPServer.new(:Port => port, :DocumentRoot => File.expand_path(File.dirname(__FILE__)), :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
625
+
626
+ server.mount(servlet.path, servlet)
627
+ server.mount("/ext", WEBrick::HTTPServlet::FileHandler, File.join(File.dirname(__FILE__),'..','ext'))
628
+
629
+ thread = Thread.new(server) { |srv| srv.start }
630
+ unless wait_for_server_started(server, port, thread: thread)
631
+ server.shutdown
632
+ thread.join(server_shutdown_timeout)
633
+ raise "Failed to startup test server on port #{port}"
634
+ end
635
+
636
+ entry = CurbTestServerRegistry::Entry.new([port, servlet], port, server, thread, locked_file, Process.pid)
637
+ CurbTestServerRegistry.register_shutdown_hook(entry)
638
+ entry
639
+ rescue Errno::EADDRINUSE
640
+ File.unlink(locked_file) if File.exist?(locked_file)
641
+ return nil if server_responding?(port)
642
+
643
+ raise
644
+ rescue StandardError
645
+ begin
646
+ server.shutdown if defined?(server) && server
647
+ thread.join(server_shutdown_timeout) if defined?(thread) && thread
648
+ rescue StandardError
649
+ nil
650
+ end
651
+ File.unlink(locked_file) if File.exist?(locked_file)
652
+ raise
653
+ end
477
654
  end
478
655
 
479
656
 
@@ -0,0 +1,105 @@
1
+ # Reproduces the FIBER_TODO.md [Critical] finding: a Fiber scheduler that
2
+ # implements io_wait/kernel_sleep/block/unblock but NOT the optional io_select
3
+ # hook. rb_fiber_scheduler_io_select returns Qundef (without waiting) for such
4
+ # schedulers; if the multi-fd wait path of Curl::Multi#_socket_perform treats
5
+ # that as a completed wait, the drive loop spins at 100% CPU with no interrupt
6
+ # checkpoint and this script never exits — it cannot even service SIGTERM.
7
+ #
8
+ # Run by tc_fiber_scheduler.rb in a subprocess guarded by a SIGKILL fuse.
9
+ # Usage: ruby io_select_less_scheduler_probe.rb <port>
10
+ # <port> must serve HTTP GET /test (see BugTestServerSetupTeardown).
11
+
12
+ require 'fiber'
13
+ require 'curb'
14
+
15
+ port = Integer(ARGV[0])
16
+ url = "http://127.0.0.1:#{port}/test"
17
+
18
+ class NoIoSelectScheduler
19
+ def fiber(&block)
20
+ Fiber.new(blocking: false, &block)
21
+ end
22
+
23
+ def io_wait(io, events, timeout = nil)
24
+ readers = (events & IO::READABLE) != 0 ? [io] : nil
25
+ writers = (events & IO::WRITABLE) != 0 ? [io] : nil
26
+ deadline = monotonic_now + timeout if timeout
27
+
28
+ loop do
29
+ readable, writable = IO.select(readers, writers, nil, 0)
30
+ ready = 0
31
+ ready |= IO::READABLE if readable && !readable.empty?
32
+ ready |= IO::WRITABLE if writable && !writable.empty?
33
+ return ready unless ready.zero?
34
+ return false if deadline && monotonic_now >= deadline
35
+
36
+ Fiber.yield
37
+ end
38
+ end
39
+
40
+ def kernel_sleep(duration = nil)
41
+ deadline = monotonic_now + (duration || 0)
42
+ Fiber.yield while monotonic_now < deadline
43
+ duration
44
+ end
45
+
46
+ def block(_blocker, timeout = nil)
47
+ kernel_sleep(timeout)
48
+ false
49
+ end
50
+
51
+ def unblock(*); end
52
+
53
+ def close; end
54
+
55
+ def fiber_interrupt(*); end
56
+
57
+ private
58
+
59
+ def monotonic_now
60
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
61
+ end
62
+ end
63
+
64
+ results = []
65
+ ticks = 0
66
+ done = false
67
+
68
+ Fiber.set_scheduler(NoIoSelectScheduler.new)
69
+ fiber = Fiber.schedule do
70
+ multi = Curl::Multi.new
71
+ begin
72
+ # Two easies so the drive loop tracks more than one socket and takes the
73
+ # multi-fd (io_select) wait branch.
74
+ 2.times do
75
+ easy = Curl::Easy.new(url)
76
+ easy.on_complete { results << easy.response_code }
77
+ multi.add(easy)
78
+ end
79
+ multi.send(:_socket_perform)
80
+ ensure
81
+ multi.close
82
+ done = true
83
+ end
84
+ end
85
+ ticker = Fiber.schedule do
86
+ until done
87
+ sleep 0.005
88
+ ticks += 1 unless done
89
+ end
90
+ end
91
+
92
+ while fiber.alive? || ticker.alive?
93
+ fiber.resume if fiber.alive?
94
+ ticker.resume if ticker.alive?
95
+ sleep 0.001
96
+ end
97
+ Fiber.set_scheduler(nil)
98
+
99
+ if results == [200, 200]
100
+ puts "OK ticks=#{ticks}"
101
+ exit 0
102
+ else
103
+ puts "FAILED results=#{results.inspect}"
104
+ exit 1
105
+ end
@@ -46,6 +46,7 @@ class TestCurbCurlEasy < Test::Unit::TestCase
46
46
 
47
47
  http = Curl::Easy.new(url)
48
48
  http.dns_cache_timeout = 0
49
+ http.proxy_url = ""
49
50
  http.resolve = [mapping]
50
51
  http.get
51
52
  assert_match(/GET/, http.body)
@@ -22,6 +22,7 @@ class TestCurbCurlEasyResolve < Test::Unit::TestCase
22
22
  mapping = "#{host}:#{TestServlet.port}:127.0.0.1"
23
23
 
24
24
  @easy.url = "http://#{host}:#{TestServlet.port}#{TestServlet.path}"
25
+ @easy.proxy_url = ""
25
26
  @easy.dns_cache_timeout = 0
26
27
  @easy.set(:resolve, [mapping])
27
28
 
@@ -39,6 +40,7 @@ class TestCurbCurlEasyResolve < Test::Unit::TestCase
39
40
  entry.define_singleton_method(:to_s) { mapping }
40
41
 
41
42
  @easy.url = "http://#{host}:#{TestServlet.port}#{TestServlet.path}"
43
+ @easy.proxy_url = ""
42
44
  @easy.dns_cache_timeout = 0
43
45
  @easy.resolve = [entry]
44
46
 
@@ -70,6 +70,7 @@ class TestCurbCurlNativeCoverage < Test::Unit::TestCase
70
70
 
71
71
  def test_clone_preserves_native_lists_after_original_handle_closes
72
72
  easy = Curl::Easy.new("http://curb.invalid:#{TestServlet.port}#{TestServlet.path}")
73
+ easy.proxy_url = ""
73
74
  easy.headers['X-Test'] = '1'
74
75
  easy.proxy_headers['X-Proxy'] = '2'
75
76
  easy.ftp_commands = ['PWD']