Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1199031
feat: Resumable Media Upload functionality implementation
viacheslav-rostovtsev Aug 17, 2026
1bb0c88
chore: fix toys (again)
viacheslav-rostovtsev Sep 15, 2026
52949a7
feat: remove Session and replace with ::Gapic::ResumableUpload, and t…
viacheslav-rostovtsev Sep 17, 2026
01f19fd
docs(resumable-upload): fix stale resume example and document handle …
viacheslav-rostovtsev Sep 18, 2026
47d1acb
test(resumable-upload): pin handle replacement and inject a state dou…
viacheslav-rostovtsev Sep 18, 2026
e656e82
refactor(common): add RetryPolicy#overrides and derive initiation ret…
viacheslav-rostovtsev Sep 18, 2026
2ec24b3
chore: ignore the temporary Gemfiles toys leaves behind
viacheslav-rostovtsev Sep 18, 2026
4d1d83f
refactor(resumable-upload): let call options override the initiation …
viacheslav-rostovtsev Sep 18, 2026
5755dae
test(resumable-upload): run the integration suite through the coordin…
viacheslav-rostovtsev Sep 18, 2026
1ebe40a
chore: fixup comment
viacheslav-rostovtsev Sep 18, 2026
1dcbd01
chore: remove duplicated tests and collapse certain cases
viacheslav-rostovtsev Sep 22, 2026
2b19444
fix: some 200 responses are invalid from the protocol view and should…
viacheslav-rostovtsev Sep 23, 2026
59a3c27
fix: duplicate class
viacheslav-rostovtsev Sep 23, 2026
3badfe7
fix: don't pass internal events to retry policy
viacheslav-rostovtsev Sep 24, 2026
df9da12
fix: retry transport errors on start and control plane
viacheslav-rostovtsev Sep 25, 2026
02e6a9d
fix: mark InternalError as private in the docs, note that `Common::E…
viacheslav-rostovtsev Sep 25, 2026
8a1ff50
chore: mark error factories and other API surface elements that are n…
viacheslav-rostovtsev Sep 25, 2026
a4bcb93
fix: less noise on retry policy tests
viacheslav-rostovtsev Sep 26, 2026
d1c8295
feat: back off recovery queries across a recovery episode
viacheslav-rostovtsev Sep 28, 2026
04947ff
fix: handle missing/malformed granularity and offset
viacheslav-rostovtsev Sep 28, 2026
b9f5122
fix: re-query when a query answer has no parseable offset
viacheslav-rostovtsev Sep 28, 2026
b0854f2
fix: fail a start response that has no upload URL
viacheslav-rostovtsev Sep 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions gapic-common/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ vendor
*~
.idea
*.iml

# Modified Gemfiles toys writes while running a step; bundler re-creates the lock
# just after toys deletes it, so a stray one is left behind by every `toys ci`.
.toys-tmp-gemfile-*
4 changes: 0 additions & 4 deletions gapic-common/.toys.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@
end
tool "yard", delegate_to: "yardoc"

expand :gem_build

expand :gem_build, name: "install", install_gem: true

tool "ci" do
include :exec, e: true
include :terminal
Expand Down
139 changes: 139 additions & 0 deletions gapic-common/.toys/test-integration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# frozen_string_literal: true

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require "socket"
require "tmpdir"

expand :minitest, name: "" do |t|
t.libs = ["lib", "integration"]
t.files = ["integration/**/*_test.rb"]
t.bundler = true
end

alias_method :run_minitest, :run

def run
if !ENV["SHOWCASE_ENDPOINT"].to_s.empty?
run_minitest
return
end

bin = resolve_showcase_bin
if bin.nil?
if !ENV["CI"].to_s.empty?
logger.error "No SHOWCASE_ENDPOINT or gapic-showcase binary found in CI environment."
exit 1
else
logger.warn "Skipping integration tests: no SHOWCASE_ENDPOINT or gapic-showcase binary found."
return
end
end

verify_showcase_version! bin

port = allocate_port
fallback_port = allocate_port
log_path = File.join Dir.tmpdir, "gapic-showcase-#{Process.pid}-#{Time.now.to_i}.log"

pid = Process.spawn(
bin, "run",
"--port", ":#{port}",
"--fallback-port", ":#{fallback_port}",
out: log_path,
err: log_path,
pgroup: true
)

begin
wait_for_showcase! pid, port, log_path
ENV["SHOWCASE_ENDPOINT"] = "http://localhost:#{port}"
run_minitest
ensure
if pid
begin
Process.kill "-TERM", pid
Process.waitpid pid
rescue Errno::ESRCH, Errno::ECHILD
# Process already terminated
end
end
end
end

def resolve_showcase_bin
env_bin = ENV["SHOWCASE_BIN"].to_s
return env_bin unless env_bin.empty?

ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
candidate = File.join dir, "gapic-showcase"
return candidate if File.executable?(candidate) && !File.directory?(candidate)
end
nil
end

def verify_showcase_version!(bin)
output = begin
IO.popen([bin, "--version"], err: [:child, :out], &:read).strip
rescue StandardError => e
logger.error "Failed to execute '#{bin} --version': #{e.message}"
exit 1
end

version_match = output[/\d+\.\d+(?:\.\d+)*/]
if version_match.nil?
logger.error "Could not parse version from '#{bin} --version' output: #{output.inspect}"
exit 1
end

if Gem::Version.new(version_match) < Gem::Version.new("0.43")
logger.error "gapic-showcase version #{version_match} is too old (minimum required is 0.43)."
exit 1
end
end

def allocate_port
server = TCPServer.open "127.0.0.1", 0
port = server.addr[1]
server.close
port
end

def wait_for_showcase!(pid, port, log_path)
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 10.0

loop do
exited_pid, status = Process.waitpid2 pid, Process::WNOHANG
if exited_pid
logger.error "gapic-showcase exited prematurely (status: #{status.exitstatus}). Log file: #{log_path}"
exit 1
end

begin
sock = TCPSocket.new "127.0.0.1", port
sock.close
return
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
# Server not ready yet
end

if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
logger.error "Timed out waiting 10s for gapic-showcase to listen on port #{port}. Log file: #{log_path}"
exit 1
end

sleep 0.1
end
end
750 changes: 750 additions & 0 deletions gapic-common/design/resumable_upload/implementation-guide.md

Large diffs are not rendered by default.

291 changes: 291 additions & 0 deletions gapic-common/design/resumable_upload/integration-test-plan.md

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions gapic-common/integration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Integration Tests

This directory contains integration tests for `gapic-common`, executed against a running `gapic-showcase` server via the `toys test-integration` command.

## Running Integration Tests

```bash
toys test-integration
```

You can pass standard Minitest flags to filter or seed test runs:

```bash
toys test-integration --name /resumable_upload/ --seed 1234
```

## Showcase Server Management & Lifecycle

The `toys test-integration` command (`.toys/test-integration.rb`) manages the `gapic-showcase` server lifecycle automatically:

1. **Existing Endpoint (`SHOWCASE_ENDPOINT`)**:
- If `ENV["SHOWCASE_ENDPOINT"]` is present and non-empty, `toys test-integration` skips binary resolution and runs the Minitest suite directly against that endpoint.

2. **Binary Resolution (`SHOWCASE_BIN` / `PATH`)**:
- When `SHOWCASE_ENDPOINT` is not set, the runner checks `ENV["SHOWCASE_BIN"]` first, then searches `ENV["PATH"]` for an executable `gapic-showcase` binary.
- **Version Check**: The runner executes `<binary> --version` and verifies that the version is at least `0.43`. If the version is older than `0.43`, it logs an error and exits with status `1`.

3. **Missing Endpoint and Binary**:
- **CI Environment (`ENV["CI"]` set)**: Fails immediately with exit status `1`.
- **Local Environment (`ENV["CI"]` unset)**: Logs an informational message and skips integration tests cleanly (exit status `0`).

4. **Ephemeral Port Allocation & Polling**:
- Allocates two ephemeral TCP ports on `127.0.0.1` for `--port :<port>` and `--fallback-port :<fallback_port>` to avoid port collisions across concurrent runs.
- Spawns `gapic-showcase run --port :<port> --fallback-port :<fallback_port>` in a dedicated process group (`pgroup: true`) with `stdout` and `stderr` redirected to a temporary log file in `Dir.tmpdir`.
- Polls `127.0.0.1:<port>` with a 10-second monotonic clock budget while checking `Process.waitpid2` (`WNOHANG`) on each iteration. If the process exits prematurely or fails to accept TCP connections within 10 seconds, the runner prints the path to the log file and exits with status `1`.

5. **Execution & Teardown**:
- Sets `ENV["SHOWCASE_ENDPOINT"] = "http://localhost:#{port}"` and runs the Minitest suite (`integration/**/*_test.rb`).
- An `ensure` block sends `SIGTERM` to the entire process group (`-TERM`) and reaps the child process so no background showcase processes are leaked.

## Logging and Diagnostics

Each integration test captures `DEBUG`-level client and driver logs into an in-memory buffer during execution:

- **Automatic Failure Dump**: If a test fails or raises an unhandled exception, the captured trace is automatically dumped to `stderr` during `teardown`.
- **Force Log Dump (`SHOWCASE_LOG`)**: Set `SHOWCASE_LOG=1` (or any non-empty value) to dump the captured trace for all executed tests, including passing ones:

```bash
SHOWCASE_LOG=1 toys test-integration
```

Loading
Loading