Skip to content
Open
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
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
code_teams (1.1.0)
code_teams (1.2.0)
sorbet-runtime

GEM
Expand Down Expand Up @@ -99,7 +99,7 @@ GEM
thor (1.3.2)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
unicode-emoji (4.2.0)
yard (0.9.37)
yard-sorbet (0.9.0)
sorbet-runtime
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,32 @@ if errors.any?
end
```

## Testing

`code_teams` provides test helpers for creating temporary teams in your specs without writing YML files to disk. Add the following to your `spec_helper.rb` (or `rails_helper.rb`):

```ruby
require 'code_teams/testing'

CodeTeams::Testing.enable!
```

This gives you:
- A `code_team_with_config` helper method available in all specs
- Automatic cleanup of testing teams between examples

Example usage in a spec:

```ruby
RSpec.describe 'my feature' do
it 'works with a team' do
team = code_team_with_config(name: 'Test Team')

expect(CodeTeams.find('Test Team')).to eq(team)
end
end
```

## Contributing

Bug reports and pull requests are welcome!
Expand Down
2 changes: 1 addition & 1 deletion code_teams.gemspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Gem::Specification.new do |spec|
spec.name = 'code_teams'
spec.version = '1.1.0'
spec.version = '1.2.0'
spec.authors = ['Gusto Engineers']
spec.email = ['dev@gusto.com']
spec.summary = 'A low-dependency gem for declaring and querying engineering teams'
Expand Down
6 changes: 3 additions & 3 deletions lib/code_teams.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def self.from_yml(config_yml)
)
end

sig { params(raw_hash: T::Hash[T.untyped, T.untyped]).returns(Team) }
sig { params(raw_hash: T::Hash[String, T.untyped]).returns(Team) }
def self.from_hash(raw_hash)
new(
config_yml: nil,
Expand All @@ -104,7 +104,7 @@ def self.register_plugins
end
end

sig { returns(T::Hash[T.untyped, T.untyped]) }
sig { returns(T::Hash[String, T.untyped]) }
attr_reader :raw_hash

sig { returns(T.nilable(String)) }
Expand All @@ -113,7 +113,7 @@ def self.register_plugins
sig do
params(
config_yml: T.nilable(String),
raw_hash: T::Hash[T.untyped, T.untyped]
raw_hash: T::Hash[String, T.untyped]
).void
end
def initialize(config_yml:, raw_hash:)
Expand Down
6 changes: 3 additions & 3 deletions lib/code_teams/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ def self.data_accessor_name(key = default_data_accessor_name)
sig { returns(String) }
def self.default_data_accessor_name
# e.g., MyNamespace::MyPlugin -> my_plugin
Utils.underscore(Utils.demodulize(name))
Utils.underscore(Utils.demodulize(T.must(name)))
end

sig { params(base: T.untyped).void }
sig { params(base: T.class_of(Plugin)).void }
def self.inherited(base) # rubocop:disable Lint/MissingSuper
all_plugins << T.cast(base, T.class_of(Plugin))
all_plugins << base
end

sig { returns(T::Array[T.class_of(Plugin)]) }
Expand Down
16 changes: 16 additions & 0 deletions lib/code_teams/rspec_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true
#
# typed: false

require 'securerandom'
require 'code_teams/testing'

module CodeTeams
module RSpecHelpers
def code_team_with_config(team_config = {})
team_config = team_config.dup
team_config[:name] ||= "Fake Team #{SecureRandom.hex(4)}"
CodeTeams::Testing.create_code_team(team_config)
end
end
end
94 changes: 94 additions & 0 deletions lib/code_teams/testing.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# frozen_string_literal: true
#
# typed: strict

require 'securerandom'
require 'code_teams'
require 'code_teams/rspec_helpers'

module CodeTeams
# Utilities for tests that need a controlled set of teams without writing YML
# files to disk.
#
# Opt-in by requiring `code_teams/testing`.
module Testing
extend T::Sig

THREAD_KEY = T.let(:__code_teams_collection, Symbol)
@enabled = T.let(false, T::Boolean)

sig { void }
def self.enable!
return if @enabled

CodeTeams.prepend(CodeTeamsExtension)
@enabled = true

return unless defined?(RSpec)

T.unsafe(RSpec).configure do |config|
config.include CodeTeams::RSpecHelpers

config.around do |example|
example.run
# Bust caches because plugins may hang onto stale data between examples.
if CodeTeams::Testing.code_teams.any?
CodeTeams.bust_caches!
CodeTeams::Testing.reset!
end
end
end
end

sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(CodeTeams::Team) }
def self.create_code_team(attributes)
attributes = attributes.dup
attributes[:name] ||= "Fake Team #{SecureRandom.hex(4)}"

code_team = CodeTeams::Team.new(
config_yml: 'tmp/fake_config.yml',
raw_hash: Utils.deep_stringify_keys(attributes)
)

code_teams << code_team
code_team
end

sig { returns(T::Array[CodeTeams::Team]) }
def self.code_teams
existing = Thread.current[THREAD_KEY]
return existing if existing.is_a?(Array)

Thread.current[THREAD_KEY] = []
T.cast(Thread.current[THREAD_KEY], T::Array[CodeTeams::Team])
end

sig { void }
def self.reset!
Thread.current[THREAD_KEY] = []
end

module CodeTeamsExtension
extend T::Sig

sig { params(base: Module).void }
def self.prepended(base)
base.singleton_class.prepend(ClassMethods)
end

module ClassMethods
extend T::Sig

sig { returns(T::Array[CodeTeams::Team]) }
def all
CodeTeams::Testing.code_teams + super
end

sig { params(name: String).returns(T.nilable(CodeTeams::Team)) }
def find(name)
CodeTeams::Testing.code_teams.find { |t| t.name == name } || super
end
end
end
end
end
25 changes: 24 additions & 1 deletion lib/code_teams/utils.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
# frozen_string_literal: true
#
# typed: strict

module CodeTeams
module Utils
extend T::Sig

module_function

sig { params(string: String).returns(String) }
def underscore(string)
string.gsub('::', '/')
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
Expand All @@ -10,8 +17,24 @@ def underscore(string)
.downcase
end

sig { params(string: String).returns(String) }
def demodulize(string)
string.split('::').last
T.must(string.split('::').last)
end

# Recursively converts symbol keys to strings. Top-level input should be a Hash.
sig { params(value: T.untyped).returns(T.untyped) }
def deep_stringify_keys(value)
case value
when Hash
value.each_with_object({}) do |(k, v), acc|
acc[k.to_s] = deep_stringify_keys(v)
end
when Array
value.map { |v| deep_stringify_keys(v) }
else
value
end
end
end
end
16 changes: 16 additions & 0 deletions spec/lib/code_teams/r_spec_helpers_integration_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require 'code_teams/testing'

CodeTeams::Testing.enable!

RSpec.describe CodeTeams::RSpecHelpers do
it 'exposes code_team_with_config and makes the team discoverable' do
code_team_with_config(name: 'RSpec Team')

expect(CodeTeams.find('RSpec Team')).not_to be_nil
end

it 'cleans up testing teams between examples' do
expect(CodeTeams::Testing.code_teams).to be_empty
expect(CodeTeams.find('RSpec Team')).to be_nil
end
end
15 changes: 15 additions & 0 deletions spec/lib/code_teams/testing_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'code_teams/testing'

CodeTeams::Testing.enable!

RSpec.describe CodeTeams::Testing do
describe '.create_code_team' do
it 'adds the team to CodeTeams.all and CodeTeams.find' do
team = described_class.create_code_team({ name: 'Temp Team', extra_data: { foo: { bar: 1 } } })

expect(CodeTeams.all).to include(team)
expect(CodeTeams.find('Temp Team')).to eq(team)
expect(team.raw_hash.dig('extra_data', 'foo', 'bar')).to eq(1)
end
end
end