Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions Appraisals
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# frozen_string_literal: true

# Additional dependencies needed for specific gems.
# These work around upstream gems that haven't declared dependencies properly.
# See docs/appraisal-ruby-version-support.md for future Ruby version support.
GEM_DEPENDENCIES = {
"openai" => ["base64"] # openai uses base64 but doesn't declare it (needed for Ruby 3.4+)
}

def gem_dependencies_for(gem_name)
GEM_DEPENDENCIES.fetch(gem_name, [])
end

# Optional dependencies to test
OPTIONAL_GEMS = {
"openai" => {
Expand All @@ -26,10 +37,13 @@ OPTIONAL_GEMS = {

# Generate appraisals for each optional gem
OPTIONAL_GEMS.each do |gem_name, versions|
extra_deps = gem_dependencies_for(gem_name)

versions.each do |name, constraint|
suffix = (name == "latest") ? "" : "-#{name.tr(".", "-")}"
appraise "#{gem_name}#{suffix}" do
gem gem_name, constraint
extra_deps.each { |dep| gem dep }
end
end

Expand Down
25 changes: 25 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,28 @@ for more details.
```bash
rake -T test:vcr
```

### Testing with Different Ruby Versions

CI tests against Ruby 3.2, 3.3, and 3.4. To test locally with a different Ruby version:

```bash
# Install a different Ruby version
mise install ruby@3.4

# Run tests with that version
mise exec ruby@3.4 -- bundle install
mise exec ruby@3.4 -- bundle exec rake test

# Run appraisal tests with that version
mise exec ruby@3.4 -- bundle exec appraisal install
mise exec ruby@3.4 -- bundle exec appraisal openai-0-33 rake test
```

To temporarily switch your shell to a different Ruby:

```bash
mise use ruby@3.4
ruby --version # => 3.4.x
bundle exec rake test
```
4 changes: 1 addition & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ GEM
rake
thor (>= 0.14.0)
ast (2.4.3)
base64 (0.3.0)
bigdecimal (3.3.1)
builder (3.3.0)
crack (1.0.1)
Expand Down Expand Up @@ -122,8 +121,7 @@ GEM
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
vcr (6.3.1)
base64
vcr (6.4.0)
webmock (3.25.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
Expand Down
1 change: 1 addition & 0 deletions gemfiles/openai.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ source "https://rubygems.org"

gem "minitest-reporters", "~> 1.6"
gem "openai", ">= 0.34"
gem "base64"

gemspec path: "../"
1 change: 1 addition & 0 deletions gemfiles/openai_0_33.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ source "https://rubygems.org"

gem "minitest-reporters", "~> 1.6"
gem "openai", "~> 0.33.0"
gem "base64"

gemspec path: "../"
1 change: 1 addition & 0 deletions gemfiles/openai_0_34.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ source "https://rubygems.org"

gem "minitest-reporters", "~> 1.6"
gem "openai", "~> 0.34.0"
gem "base64"

gemspec path: "../"
40 changes: 40 additions & 0 deletions lib/braintrust/internal/encoding.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module Braintrust
module Internal
# Encoding utilities using Ruby's native pack/unpack methods.
# Avoids dependency on external gems that became bundled gems in Ruby 3.4.
module Encoding
# Base64 encoding/decoding using Ruby's native pack/unpack methods.
# Drop-in replacement for the base64 gem's strict methods.
#
# @example Encode binary data
# Encoding::Base64.strict_encode64(image_bytes)
# # => "iVBORw0KGgo..."
#
# @example Decode base64 string
# Encoding::Base64.strict_decode64("iVBORw0KGgo...")
# # => "\x89PNG..."
#
module Base64
module_function

# Encodes binary data to base64 without newlines (strict encoding).
#
# @param data [String] Binary data to encode
# @return [String] Base64-encoded string without newlines
def strict_encode64(data)
[data].pack("m0")
end

# Decodes a base64 string to binary data (strict decoding).
#
# @param str [String] Base64-encoded string
# @return [String] Decoded binary data
def strict_decode64(str)
str.unpack1("m0")
end
end
end
end
end
4 changes: 2 additions & 2 deletions lib/braintrust/trace/attachment.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require "base64"
require "net/http"
require_relative "../internal/encoding"
require "uri"

module Braintrust
Expand Down Expand Up @@ -110,7 +110,7 @@ def self.from_url(url)
# att.to_data_url
# # => "data:image/png;base64,iVBORw0KGgo..."
def to_data_url
encoded = Base64.strict_encode64(@data)
encoded = Internal::Encoding::Base64.strict_encode64(@data)
"data:#{@content_type};base64,#{encoded}"
end

Expand Down
105 changes: 105 additions & 0 deletions test/braintrust/internal/encoding_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# frozen_string_literal: true

require "test_helper"
require "braintrust/internal/encoding"

class Braintrust::Internal::Encoding::Base64Test < Minitest::Test
BASE64_STDLIB_AVAILABLE = begin
require "base64"
true
rescue LoadError
false
end

# strict_encode64

def test_strict_encode64_encodes_string
result = Braintrust::Internal::Encoding::Base64.strict_encode64("Hello, World!")
assert_equal "SGVsbG8sIFdvcmxkIQ==", result
end

def test_strict_encode64_encodes_binary_data
binary = [0x89, 0x50, 0x4E, 0x47].pack("C*")
result = Braintrust::Internal::Encoding::Base64.strict_encode64(binary)
assert_equal "iVBORw==", result
end

def test_strict_encode64_encodes_empty_string
result = Braintrust::Internal::Encoding::Base64.strict_encode64("")
assert_equal "", result
end

def test_strict_encode64_produces_no_newlines
long_string = "x" * 1000
result = Braintrust::Internal::Encoding::Base64.strict_encode64(long_string)
refute_includes result, "\n"
end

def test_strict_encode64_matches_stdlib
skip "base64 stdlib not available" unless BASE64_STDLIB_AVAILABLE

test_cases = [
"Hello, World!",
"",
"x" * 1000,
[0x00, 0xFF, 0x89, 0x50].pack("C*")
]

test_cases.each do |input|
expected = ::Base64.strict_encode64(input)
actual = Braintrust::Internal::Encoding::Base64.strict_encode64(input)
assert_equal expected, actual, "Mismatch for input: #{input.inspect}"
end
end

# strict_decode64

def test_strict_decode64_decodes_string
result = Braintrust::Internal::Encoding::Base64.strict_decode64("SGVsbG8sIFdvcmxkIQ==")
assert_equal "Hello, World!", result
end

def test_strict_decode64_decodes_binary_data
result = Braintrust::Internal::Encoding::Base64.strict_decode64("iVBORw==")
expected = [0x89, 0x50, 0x4E, 0x47].pack("C*")
assert_equal expected, result
end

def test_strict_decode64_decodes_empty_string
result = Braintrust::Internal::Encoding::Base64.strict_decode64("")
assert_equal "", result
end

def test_strict_decode64_matches_stdlib
skip "base64 stdlib not available" unless BASE64_STDLIB_AVAILABLE

test_cases = [
"SGVsbG8sIFdvcmxkIQ==",
"",
"eHh4eHh4eHg=",
"AP+JUA=="
]

test_cases.each do |input|
expected = ::Base64.strict_decode64(input)
actual = Braintrust::Internal::Encoding::Base64.strict_decode64(input)
assert_equal expected, actual, "Mismatch for input: #{input.inspect}"
end
end

# round-trip

def test_round_trip_preserves_string_data
original = "The quick brown fox jumps over the lazy dog"
encoded = Braintrust::Internal::Encoding::Base64.strict_encode64(original)
decoded = Braintrust::Internal::Encoding::Base64.strict_decode64(encoded)
assert_equal original, decoded
end

def test_round_trip_preserves_binary_data
original = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0xFF].pack("C*")
encoded = Braintrust::Internal::Encoding::Base64.strict_encode64(original)
decoded = Braintrust::Internal::Encoding::Base64.strict_decode64(encoded)
assert_equal original, decoded
end
end
3 changes: 2 additions & 1 deletion test/braintrust/trace/attachment_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "test_helper"
require "braintrust/trace/attachment"
require "braintrust/internal/encoding"

class Braintrust::Trace::AttachmentTest < Minitest::Test
def setup
Expand Down Expand Up @@ -58,7 +59,7 @@ def test_to_data_url_returns_correct_format

# Verify it's valid base64 by extracting and decoding
base64_part = data_url.sub(/^data:image\/png;base64,/, "")
decoded = Base64.strict_decode64(base64_part)
decoded = Braintrust::Internal::Encoding::Base64.strict_decode64(base64_part)
assert_equal @test_png_data, decoded
end

Expand Down