|
1 | 1 | # frozen_string_literal: true |
2 | 2 |
|
| 3 | +# rubocop:disable Style/GlobalVars |
3 | 4 | # rubocop:disable RSpec/NoExpectationExample |
4 | 5 |
|
5 | 6 | # Copyright 2025 Google LLC |
|
17 | 18 | # limitations under the License. |
18 | 19 |
|
19 | 20 | require "minitest/autorun" |
20 | | - |
21 | | -require "grpc" |
22 | | -require "google/rpc/error_details_pb" |
23 | | -require "google/spanner/v1/spanner_pb" |
24 | | -require "google/spanner/v1/spanner_services_pb" |
25 | 21 | require "google/cloud/spanner/v1/spanner" |
26 | 22 |
|
27 | | -require_relative "mock_server/statement_result" |
28 | | -require_relative "mock_server/spanner_mock_server" |
29 | | - |
30 | 23 | require_relative "../lib/spannerlib/ffi" |
31 | 24 | require_relative "../lib/spannerlib/connection" |
32 | | -require_relative "../lib/spannerlib/rows" |
33 | | - |
34 | | -READ_PIPE, WRITE_PIPE = IO.pipe |
35 | | -GRPC.prefork |
36 | | - |
37 | | -SERVER_PID = fork do |
38 | | - GRPC.postfork_child |
39 | | - READ_PIPE.close |
40 | | - $stdout.sync = true |
41 | | - $stderr.sync = true |
42 | | - |
43 | | - begin |
44 | | - server = GRPC::RpcServer.new |
45 | | - port = server.add_http2_port "localhost:0", :this_port_is_insecure |
46 | | - mock = SpannerMockServer.new |
47 | | - server.handle mock |
48 | | - |
49 | | - WRITE_PIPE.puts port |
50 | | - WRITE_PIPE.close |
51 | | - |
52 | | - server.run |
53 | | - rescue StandardError => e |
54 | | - warn "Mock server failed to start: #{e.message}" |
55 | | - exit(1) |
| 25 | + |
| 26 | +$server_pid = nil |
| 27 | +$server_port = nil |
| 28 | + |
| 29 | +Minitest.after_run do |
| 30 | + if $server_pid |
| 31 | + begin |
| 32 | + Process.kill("TERM", $server_pid) |
| 33 | + Process.wait($server_pid) |
| 34 | + rescue Errno::ESRCH, Errno::ECHILD, Errno::EINVAL |
| 35 | + # Process already dead or OS rejected the signal; ignore safely. |
| 36 | + end |
56 | 37 | end |
57 | 38 | end |
58 | 39 |
|
59 | | -GRPC.postfork_parent |
60 | | -WRITE_PIPE.close |
| 40 | +describe "Connection" do |
| 41 | + def self.spawn_server |
| 42 | + runner_path = File.expand_path(File.join(__dir__, "mock_server_runner.rb")) |
| 43 | + r_log, w_log = IO.pipe |
| 44 | + cmd = [Gem.ruby, "-Ilib", "-Ispec", runner_path] |
| 45 | + |
| 46 | + pid = spawn(*cmd, out: w_log, err: %i[child out]) |
| 47 | + w_log.close |
| 48 | + [pid, r_log] |
| 49 | + end |
61 | 50 |
|
62 | | -SERVER_PORT = READ_PIPE.gets.strip.to_i |
63 | | -READ_PIPE.close |
| 51 | + # rubocop:disable Metrics/MethodLength |
| 52 | + def self.wait_for_port(r_log) |
| 53 | + unless r_log.wait_readable(20) # 20 second timeout |
| 54 | + begin |
| 55 | + Process.kill("TERM", $server_pid) |
| 56 | + rescue StandardError |
| 57 | + nil |
| 58 | + end |
| 59 | + raise "Timed out waiting for mock server to start." |
| 60 | + end |
64 | 61 |
|
65 | | -describe "Connection" do |
66 | | - before do |
67 | | - server_address = "localhost:#{SERVER_PORT}" |
68 | | - database_path = "projects/p/instances/i/databases/d" |
| 62 | + line = r_log.gets |
| 63 | + if line.nil? |
| 64 | + Process.wait($server_pid) |
| 65 | + raise "Mock server exited early. Exit code: #{$CHILD_STATUS.exitstatus}" |
| 66 | + end |
69 | 67 |
|
70 | | - @dsn = "#{server_address}/#{database_path}?useplaintext=true" |
| 68 | + port = line.strip.to_i |
| 69 | + if port <= 0 |
| 70 | + remaining_output = begin |
| 71 | + r_log.read_nonblock(4096) |
| 72 | + rescue StandardError |
| 73 | + "" |
| 74 | + end |
| 75 | + raise "Mock server failed to start.\nOutput:\n#{line}#{remaining_output}" |
| 76 | + end |
| 77 | + |
| 78 | + port |
| 79 | + end |
| 80 | + |
| 81 | + def self.ensure_server_running! |
| 82 | + return if $server_port |
| 83 | + |
| 84 | + $server_pid, r_log = spawn_server |
| 85 | + |
| 86 | + begin |
| 87 | + $server_port = wait_for_port(r_log) |
| 88 | + # CRITICAL: Drain sub-process output in a background thread to prevent |
| 89 | + # the pipe buffer from filling up and hanging the server. |
| 90 | + Thread.new { while r_log.gets; end } |
| 91 | + rescue StandardError |
| 92 | + if $server_pid |
| 93 | + begin |
| 94 | + Process.kill("TERM", $server_pid) |
| 95 | + rescue StandardError |
| 96 | + nil |
| 97 | + end |
| 98 | + begin |
| 99 | + Process.wait($server_pid) |
| 100 | + rescue StandardError |
| 101 | + nil |
| 102 | + end |
| 103 | + end |
| 104 | + raise |
| 105 | + end |
| 106 | + end |
| 107 | + # rubocop:enable Metrics/MethodLength |
| 108 | + |
| 109 | + before do |
| 110 | + self.class.ensure_server_running! |
| 111 | + @dsn = "127.0.0.1:#{$server_port}/projects/p/instances/i/databases/d?useplaintext=true" |
71 | 112 |
|
72 | 113 | @pool_id = SpannerLib.create_pool(@dsn) |
73 | 114 | @conn_id = SpannerLib.create_connection(@pool_id) |
|
108 | 149 | _(commit_resp.commit_timestamp).wont_be_nil |
109 | 150 | end |
110 | 151 | end |
111 | | - |
112 | 152 | # rubocop:enable RSpec/NoExpectationExample |
113 | | - |
114 | | -# --- 5. GLOBAL SHUTDOWN HOOK --- |
115 | | -Minitest.after_run do |
116 | | - if SERVER_PID |
117 | | - Process.kill("KILL", SERVER_PID) |
118 | | - Process.wait(SERVER_PID) |
119 | | - end |
120 | | -end |
| 153 | +# rubocop:enable Style/GlobalVars |
0 commit comments