Skip to content

Commit 103f49a

Browse files
committed
chore(ruby): Add CI workflow to run mock server tests
1 parent b09720f commit 103f49a

File tree

3 files changed

+179
-50
lines changed

3 files changed

+179
-50
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Ruby Wrapper Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
jobs:
10+
test:
11+
name: Test ${{ matrix.os }} (Ruby ${{ matrix.ruby-version }})
12+
runs-on: ${{ matrix.os }}
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
os: [ubuntu-latest, macos-latest]
17+
ruby-version: ['3.3']
18+
19+
defaults:
20+
run:
21+
shell: bash
22+
working-directory: ./spannerlib/wrappers/spannerlib-ruby
23+
24+
steps:
25+
- name: Checkout code
26+
uses: actions/checkout@v4
27+
28+
- name: Set up Go
29+
uses: actions/setup-go@v5
30+
with:
31+
go-version: '1.25.x'
32+
33+
- name: Set up Ruby
34+
uses: ruby/setup-ruby@v1
35+
with:
36+
ruby-version: ${{ matrix.ruby-version }}
37+
bundler-cache: true
38+
working-directory: ./spannerlib/wrappers/spannerlib-ruby
39+
40+
- name: Build shared library (Linux)
41+
if: runner.os == 'Linux'
42+
run: bundle exec rake compile:x86_64-linux
43+
44+
- name: Build shared library (macOS)
45+
if: runner.os == 'macOS'
46+
run: |
47+
ARCH=$(uname -m)
48+
if [ "$ARCH" == "arm64" ]; then
49+
bundle exec rake compile:aarch64-darwin
50+
else
51+
bundle exec rake compile:x86_64-darwin
52+
fi
53+
54+
- name: Run Tests
55+
run: |
56+
bundle exec ruby -Ilib -Ispec spec/spannerlib_ruby_spec.rb
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright 2025 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
$stdout.sync = true
18+
19+
require "grpc"
20+
require "google/cloud/spanner/v1/spanner"
21+
require "google/spanner/v1/spanner_pb"
22+
require "google/spanner/v1/spanner_services_pb"
23+
24+
require_relative "mock_server/spanner_mock_server"
25+
26+
begin
27+
server = GRPC::RpcServer.new
28+
port = server.add_http2_port "127.0.0.1:0", :this_port_is_insecure
29+
server.handle SpannerMockServer.new
30+
31+
# 2. Print ONLY the port number to stdout for the parent to read
32+
puts port
33+
server.run_till_terminated
34+
rescue SignalException
35+
exit(0)
36+
rescue StandardError => e
37+
warn "Mock server crashed: #{e.message}"
38+
warn e.backtrace.join("\n")
39+
exit(1)
40+
end
Lines changed: 83 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
# rubocop:disable Style/GlobalVars
34
# rubocop:disable RSpec/NoExpectationExample
45

56
# Copyright 2025 Google LLC
@@ -17,57 +18,97 @@
1718
# limitations under the License.
1819

1920
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"
2521
require "google/cloud/spanner/v1/spanner"
2622

27-
require_relative "mock_server/statement_result"
28-
require_relative "mock_server/spanner_mock_server"
29-
3023
require_relative "../lib/spannerlib/ffi"
3124
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
5637
end
5738
end
5839

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
6150

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
6461

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
6967

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"
71112

72113
@pool_id = SpannerLib.create_pool(@dsn)
73114
@conn_id = SpannerLib.create_connection(@pool_id)
@@ -108,13 +149,5 @@
108149
_(commit_resp.commit_timestamp).wont_be_nil
109150
end
110151
end
111-
112152
# 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

Comments
 (0)