Skip to content

Commit

Permalink
✨ GemBench::Jersey
Browse files Browse the repository at this point in the history
- Re-write, and re-namespace, a gem in a temp directory (useful for benchmarking)
  • Loading branch information
pboling committed Jul 29, 2024
1 parent dfc0236 commit 3fcd407
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 50 deletions.
70 changes: 26 additions & 44 deletions .rubocop_gradual.lock
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
{
"gem_bench.gemspec:4074847279": [
[58, 3, 44, "Gemspec/DependencyVersion: Dependency version specification is required.", 1445362512],
[59, 3, 39, "Gemspec/DependencyVersion: Dependency version specification is required.", 837793628],
[60, 3, 44, "Gemspec/DependencyVersion: Dependency version specification is required.", 4064900235],
[62, 3, 48, "Gemspec/DependencyVersion: Dependency version specification is required.", 2848714075],
[64, 3, 39, "Gemspec/DependencyVersion: Dependency version specification is required.", 1345440847],
[65, 3, 40, "Gemspec/DependencyVersion: Dependency version specification is required.", 1844711205],
[66, 3, 58, "Gemspec/DependencyVersion: Dependency version specification is required.", 2795510341],
[69, 3, 48, "Gemspec/DependencyVersion: Dependency version specification is required.", 3819206526],
[70, 3, 44, "Gemspec/DependencyVersion: Dependency version specification is required.", 997926854]
"gem_bench.gemspec:3127209845": [
[57, 3, 39, "Gemspec/DependencyVersion: Dependency version specification is required.", 837793628],
[58, 3, 44, "Gemspec/DependencyVersion: Dependency version specification is required.", 4064900235],
[60, 3, 48, "Gemspec/DependencyVersion: Dependency version specification is required.", 2848714075],
[62, 3, 39, "Gemspec/DependencyVersion: Dependency version specification is required.", 1345440847],
[63, 3, 40, "Gemspec/DependencyVersion: Dependency version specification is required.", 1844711205],
[64, 3, 58, "Gemspec/DependencyVersion: Dependency version specification is required.", 2795510341],
[67, 3, 48, "Gemspec/DependencyVersion: Dependency version specification is required.", 3819206526],
[68, 3, 44, "Gemspec/DependencyVersion: Dependency version specification is required.", 997926854]
],
"lib/gem_bench.rb:3402614684": [
[28, 5, 21, "ThreadSafety/ClassAndModuleAttributes: Avoid mutating class and module attributes.", 1291041767],
Expand All @@ -31,45 +30,28 @@
[197, 31, 14, "Style/RedundantInterpolation: Prefer `to_s` over string interpolation.", 1897999348],
[201, 11, 6, "Lint/NonLocalExitFromIterator: Non-local exit from iterator, without return value. `next`, `break`, `Array#find`, `Array#any?`, etc. is preferred.", 2123913871]
],
"spec/gem_bench/player_spec.rb:4210126272": [
[4, 20, 16, "RSpec/DescribedClass: Use `described_class` instead of `GemBench::Player`.", 2899854907]
],
"spec/gem_bench/scout_spec.rb:2201159713": [
[4, 20, 15, "RSpec/DescribedClass: Use `described_class` instead of `GemBench::Scout`.", 1646578838],
[11, 13, 15, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2290690197],
[12, 7, 17, "RSpec/NestedGroups: Maximum example group nesting exceeded [4/3].", 1968335926],
[12, 15, 9, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1548243657],
[13, 26, 15, "RSpec/DescribedClass: Use `described_class` instead of `GemBench::Scout`.", 1646578838],
[20, 7, 18, "RSpec/NestedGroups: Maximum example group nesting exceeded [4/3].", 2670365],
[20, 15, 10, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 80617602],
[21, 26, 15, "RSpec/DescribedClass: Use `described_class` instead of `GemBench::Scout`.", 1646578838],
[28, 7, 16, "RSpec/NestedGroups: Maximum example group nesting exceeded [4/3].", 2011698347],
[28, 15, 8, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3821487988],
[29, 26, 15, "RSpec/DescribedClass: Use `described_class` instead of `GemBench::Scout`.", 1646578838],
"spec/gem_bench/scout_spec.rb:607736076": [
[9, 13, 15, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2290690197],
[10, 7, 17, "RSpec/NestedGroups: Maximum example group nesting exceeded [4/3].", 1968335926],
[10, 15, 9, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1548243657],
[18, 7, 18, "RSpec/NestedGroups: Maximum example group nesting exceeded [4/3].", 2670365],
[18, 15, 10, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 80617602],
[26, 7, 16, "RSpec/NestedGroups: Maximum example group nesting exceeded [4/3].", 2011698347],
[26, 15, 8, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3821487988],
[67, 13, 21, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1121198777],
[68, 24, 15, "RSpec/DescribedClass: Use `described_class` instead of `GemBench::Scout`.", 1646578838],
[78, 7, 31, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 2746935144],
[84, 13, 22, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2265298994],
[85, 24, 15, "RSpec/DescribedClass: Use `described_class` instead of `GemBench::Scout`.", 1646578838],
[100, 13, 20, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3677567620],
[101, 24, 15, "RSpec/DescribedClass: Use `described_class` instead of `GemBench::Scout`.", 1646578838],
[111, 7, 31, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 2746935144],
[119, 13, 21, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1121198777],
[120, 24, 15, "RSpec/DescribedClass: Use `described_class` instead of `GemBench::Scout`.", 1646578838],
[130, 7, 31, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 3149234057],
[139, 13, 22, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2265298994],
[140, 24, 15, "RSpec/DescribedClass: Use `described_class` instead of `GemBench::Scout`.", 1646578838],
[155, 13, 20, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3677567620],
[156, 24, 15, "RSpec/DescribedClass: Use `described_class` instead of `GemBench::Scout`.", 1646578838],
[166, 7, 31, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 3149234057],
[177, 22, 15, "RSpec/DescribedClass: Use `described_class` instead of `GemBench::Scout`.", 1646578838],
[191, 5, 26, "RSpec/MultipleExpectations: Example has too many expectations [3/1].", 1356703934]
],
"spec/gem_bench/team_spec.rb:4124190485": [
[4, 20, 14, "RSpec/DescribedClass: Use `described_class` instead of `GemBench::Team`.", 3564205461]
],
"spec/gem_bench_spec.rb:267936009": [
[121, 13, 21, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1121198777],
[132, 7, 31, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 3149234057],
[141, 13, 22, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2265298994],
[157, 13, 20, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3677567620],
[168, 7, 31, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 3149234057],
[195, 5, 26, "RSpec/MultipleExpectations: Example has too many expectations [3/1].", 1356703934]
],
"spec/gem_bench_spec.rb:1027238616": [
[1, 1, 0, "RSpec/SpecFilePathFormat: Spec path should end with `gem_bench/version*_spec.rb`.", 5381],
[3, 1, 32, "RSpec/FilePath: Spec path should end with `gem_bench/version*_spec.rb`.", 4129755814]
[1, 1, 32, "RSpec/FilePath: Spec path should end with `gem_bench/version*_spec.rb`.", 4129755814]
]
}
57 changes: 54 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
# GemBench

Scene: You are a spectator at a game of Ruby Sports Gem Ball.

Gem wearing jersey namespace **#23**:

> "Put me in coach!"
Other Gem, also wearing jersey namespace **#23**:

> "Put me in coach!"
Coach:

> ❨╯°□°❩╯︵┻━┻ fine, but one of you change your jersey first!
## What's it do?

`gem_bench` is for static Gemfile and installed gem library source code analysis.

`gem_bench` can also be used to trim down app load times by keeping your worst players on the bench.
`gem_bench` can be used to re-namespace a gem at run-time so that you can run simultaneously:

Gem: "Put me in coach!"
You: ❨╯°□°❩╯︵┻━┻
- two versions of the same library, or
- two different things that happen to have a namespace collision,

for benchmarking or other purposes.

`gem_bench` can also be used to trim down app load times by keeping your worst players on the bench.

| Project | GemBench |
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
Expand Down Expand Up @@ -35,6 +55,37 @@ You: ❨╯°□°❩╯︵┻━┻
[🏘chat]: https://matrix.to/#/%23pboling_gem_bench:gitter.im
[🏘chati]: https://badges.gitter.im/Join%20Chat.svg

### New for 2.0.1 - `GemBench::Jersey`

Allows you to re-namespace any gem.
You can, for example, benchmark a gem against another version of itself.

The gem `alt_memery` uses a namespace, `Memery`, that does not match the gem name.
```ruby
require "gem_bench/jersey"

jersey = GemBench::Jersey.new(
gem_name: "alt_memery",
trades: {"Memery" => "AltMemery"},
metadata: {
something: "a value here",
something_else: :obviously,
},
)
jersey.doff_and_don
# The re-namespaced constant is now available!
AltMemery # => AltMemery
jersey.as_klass # => AltMemery

# The original, unmodified, gem is still there!
require "alt_memery"

Memery # => Memery
# So you can use both!
```

NOTE: It is not required by default, so you do need to require the Jersey if you want to use it!

### New for 2.0.0 - Dropped Support for Ruby 2.0, 2.1, 2.2

-- Required Ruby is now 2.3+
Expand Down
90 changes: 90 additions & 0 deletions lib/gem_bench/jersey.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Std Libs Dependencies
require "tempfile"

# Re-write a gem to a temp directory, re-namespace the primary namespace of that gem module, and load it.
# If the original gem defines multiple top-level namespaces, they can all be renamed by providing more key value pairs.
# If the original gem monkey patches other libraries, that behavior can't be isolated, so YMMV.
#
# NOTE: Non-top-level namespaces do not need to be renamed, as they are isolated within their parent namespace.
#
# Usage
#
# jersey = GemBench::Jersey.new(
# gem_name: "alt_memery"
# trades: {
# "Memery" => "AltMemery"
# },
# metadata: {
# something: "a value here",
# something_else: :obviously,
# },
# )
# jersey.doff_and_don
# # The re-namespaced constant is now available!
# AltMemery # => AltMemery
#
module GemBench
class Jersey
attr_reader :gem_name
attr_reader :gem_path
attr_reader :trades
attr_reader :metadata
attr_reader :files

def initialize(gem_name:, trades:, metadata: {})
@gem_name = gem_name
@gem_path = Gem.loaded_specs[gem_name]&.full_gem_path
@trades = trades
@metadata = metadata
end

def required?
gem_path && trades.values.all? { |new_namespace| Object.const_defined?(new_namespace) }
end

# Generates tempfiles and requires them, resulting
# in a loaded gem that will not have namespace
# collisions when alongside the original-namespaced gem.
# If a block is provided the contents of each file will be yielded to the block,
# after all namespace substitutions are complete, but before the contents
# are written to the re-namespaced gem. The return value of the block will be
# written to the file in this scenario.
#
# @return void
def doff_and_don(&block)
return unless gem_path

Dir.mktmpdir do |directory|
Dir["#{gem_path}/lib/**/*.rb"].map do |file|
Tempfile.open([File.basename(file)[0..-4], ".rb"], directory) do |tempfile|
new_jersey(file, tempfile, &block)
end
end
end

nil
end

def primary_namespace
trades.values.first
end

# Will raise NameError if called before #doff_and_don
def as_klass
Object.const_get(primary_namespace) if gem_path
end

private

def new_jersey(file, tempfile)
nj = File.read(file)
trades.each do |old_namespace, new_namespace|
nj.gsub!(old_namespace, new_namespace)
end
nj = yield nj if block_given?
tempfile.write(nj)
tempfile.rewind
require tempfile.path
end
end
end
110 changes: 110 additions & 0 deletions spec/gem_bench/jersey_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
require "gem_bench/jersey"

RSpec.describe GemBench::Jersey do
let(:args) do
{
gem_name: gem_name,
trades: trades,
metadata: metadata,
}
end
let(:gem_name) { "gem_bench" }
let(:old_namespace) { "GemBench" }
let(:new_namespace) { "#{Constants::ALPHABET[rand(26)]}#{Constants::ALPHABET[rand(26)].downcase}#{Constants::ALPHABET[rand(26)].downcase}#{Constants::ALPHABET[rand(26)]}#{Constants::ALPHABET[rand(26)].downcase}#{Constants::ALPHABET[rand(26)].downcase}#{Constants::ALPHABET[rand(26)]}#{Constants::ALPHABET[rand(26)].downcase}#{Constants::ALPHABET[rand(26)].downcase}" }
let(:trades) { {old_namespace => new_namespace} }
let(:metadata) { {} }
let(:instance) { described_class.new(**args) }

describe "#initialize" do
subject(:init) { instance }

it "does not raise error" do
block_is_expected.not_to raise_error
end
end

describe "#required?" do
subject(:required) { instance.required? }

it "does not raise error" do
block_is_expected.not_to raise_error
end

it "returns false" do
expect(required).to be(false)
end

context "when doff and donned" do
before { instance.doff_and_don }

it "returns true" do
expect(required).to be(true)
end
end
end

describe "#doff_and_don" do
subject(:doff_and_don) { instance.doff_and_don }

it "does not raise error" do
block_is_expected.not_to raise_error
end

it "returns nil" do
expect(doff_and_don).to be_nil
end
end

describe "#primary_namespace" do
subject(:primary_namespace) { instance.primary_namespace }

it "does not raise error" do
block_is_expected.not_to raise_error
end

it "returns first new namespace" do
expect(primary_namespace).to eq(new_namespace)
end

context "when multiple namespaces" do
let(:trades) do
{
"Blue" => "Green",
old_namespace => new_namespace,
}
end

it "returns first new namespace" do
expect(primary_namespace).to eq("Green")
end
end
end

describe "#gem_path" do
it "includes the gem's name" do
expect(instance.gem_path).to include("gem_bench")
end
end

describe "#as_klass" do
subject(:as_klass) { instance.as_klass }

context "when not doffed and donned" do
it "raises error" do
block_is_expected.to raise_error(NameError, "uninitialized constant #{new_namespace}")
end
end

context "when doff and donned" do
before { instance.doff_and_don }

it "does not raise error" do
block_is_expected.not_to raise_error
end

it "returns a module/class with name of new namespace" do
expect(as_klass.name).to eq(new_namespace)
end
end
end
end
7 changes: 4 additions & 3 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
require "byebug" if ENV.fetch("DEBUG", "false").casecmp?("true")

# External Gems
require "byebug" # For debugging
require "byebug" if ENV.fetch("DEBUG", "false").casecmp?("true")
require "rspec/block_is_expected" # For RSpec Macros
require "version_gem/rspec" # For RSpec Matchers

# This gem's helpers
require_relative "support/constants"

RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
Expand Down
3 changes: 3 additions & 0 deletions spec/support/constants.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module Constants
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
end

0 comments on commit 3fcd407

Please sign in to comment.