diff --git a/Rakefile b/Rakefile index d968e76316..0a2956887b 100644 --- a/Rakefile +++ b/Rakefile @@ -39,6 +39,10 @@ namespace :spec do t.pattern = 'spec/ddtrace/contrib/rails/**/*disable_env*_spec.rb' end + RSpec::Core::RakeTask.new(:contrib) do |t| + t.pattern = 'spec/**/contrib/{configurable,integration,patchable,patcher,registerable,configuration/*}_spec.rb' + end + [ :active_model_serializers, :active_record, @@ -208,6 +212,7 @@ task :ci do # Main library sh 'bundle exec rake test:main' sh 'bundle exec rake spec:main' + sh 'bundle exec rake spec:contrib' if RUBY_PLATFORM != 'java' # Contrib minitests @@ -249,6 +254,7 @@ task :ci do # Main library sh 'bundle exec rake test:main' sh 'bundle exec rake spec:main' + sh 'bundle exec rake spec:contrib' if RUBY_PLATFORM != 'java' # Contrib minitests @@ -293,6 +299,7 @@ task :ci do # Main library sh 'bundle exec rake test:main' sh 'bundle exec rake spec:main' + sh 'bundle exec rake spec:contrib' if RUBY_PLATFORM != 'java' # Contrib minitests @@ -343,6 +350,7 @@ task :ci do # Main library sh 'bundle exec rake test:main' sh 'bundle exec rake spec:main' + sh 'bundle exec rake spec:contrib' if RUBY_PLATFORM != 'java' # Contrib minitests @@ -404,6 +412,7 @@ task :ci do # Main library sh 'bundle exec rake test:main' sh 'bundle exec rake spec:main' + sh 'bundle exec rake spec:contrib' if RUBY_PLATFORM != 'java' # Contrib minitests @@ -464,6 +473,7 @@ task :ci do # Main library sh 'bundle exec rake test:main' sh 'bundle exec rake spec:main' + sh 'bundle exec rake spec:contrib' if RUBY_PLATFORM != 'java' # Contrib minitests diff --git a/lib/ddtrace.rb b/lib/ddtrace.rb index 5127c7868e..58d6594741 100644 --- a/lib/ddtrace.rb +++ b/lib/ddtrace.rb @@ -55,6 +55,7 @@ def configure(target = configuration, opts = {}) end require 'ddtrace/contrib/base' +require 'ddtrace/contrib/integration' require 'ddtrace/contrib/rack/patcher' require 'ddtrace/contrib/rails/patcher' require 'ddtrace/contrib/active_model_serializers/patcher' diff --git a/lib/ddtrace/configuration.rb b/lib/ddtrace/configuration.rb index 9ee3f1e524..91671e8108 100644 --- a/lib/ddtrace/configuration.rb +++ b/lib/ddtrace/configuration.rb @@ -12,16 +12,28 @@ def initialize(options = {}) @wrapped_registry = {} end - def [](integration_name) - @wrapped_registry[integration_name] ||= Proxy.new(fetch_integration(integration_name)) + def [](integration_name, configuration_name = :default) + integration = fetch_integration(integration_name) + + if integration.class <= Datadog::Contrib::Integration + integration.configuration(configuration_name) + else + @wrapped_registry[integration_name] ||= Proxy.new(integration) + end end - def use(integration_name, options = {}) + def use(integration_name, options = {}, &block) integration = fetch_integration(integration_name) - settings = Proxy.new(integration) - integration.sorted_options.each do |name| - settings[name] = options.fetch(name, settings[name]) + if integration.class <= Datadog::Contrib::Integration + configuration_name = options[:describes] || :default + filtered_options = options.reject { |k, _v| k == :describes } + integration.configure(configuration_name, filtered_options, &block) + else + settings = Proxy.new(integration) + integration.sorted_options.each do |name| + settings[name] = options.fetch(name, settings[name]) + end end integration.patch if integration.respond_to?(:patch) diff --git a/lib/ddtrace/contrib/base.rb b/lib/ddtrace/contrib/base.rb index ba31ff4e3f..76163e9a33 100644 --- a/lib/ddtrace/contrib/base.rb +++ b/lib/ddtrace/contrib/base.rb @@ -1,5 +1,6 @@ -require 'ddtrace/registry' require 'ddtrace/configurable' +require 'ddtrace/patcher' +require 'ddtrace/registry/registerable' module Datadog module Contrib @@ -7,8 +8,8 @@ module Contrib module Base def self.included(base) base.send(:include, Registry::Registerable) - base.send(:include, Configurable) - base.send(:include, Patcher) + base.send(:include, Datadog::Configurable) + base.send(:include, Datadog::Patcher) end end end diff --git a/lib/ddtrace/contrib/configurable.rb b/lib/ddtrace/contrib/configurable.rb new file mode 100644 index 0000000000..e8f4ff2781 --- /dev/null +++ b/lib/ddtrace/contrib/configurable.rb @@ -0,0 +1,55 @@ +require 'ddtrace/contrib/configuration/resolver' +require 'ddtrace/contrib/configuration/settings' + +module Datadog + module Contrib + # Defines configurable behavior for integrations + module Configurable + def self.included(base) + base.send(:include, InstanceMethods) + end + + # Configurable instance behavior for integrations + module InstanceMethods + def default_configuration + Configuration::Settings.new + end + + def reset_configuration! + @configurations = nil + @resolver = nil + end + + def configuration(name = :default) + name = :default if name.nil? + name = resolver.resolve(name) + return nil unless configurations.key?(name) + configurations[name] + end + + def configurations + @configurations ||= Hash.new { default_configuration }.tap do |configs| + configs[:default] = default_configuration + end + end + + def configure(name, options = {}, &block) + name = resolver.resolve(name || :default) + + configurations[name].tap do |settings| + settings.configure(options, &block) + configurations[name] = settings + end + end + + protected + + attr_writer :resolver + + def resolver + @resolver ||= Configuration::Resolver.new + end + end + end + end +end diff --git a/lib/ddtrace/contrib/configuration/option.rb b/lib/ddtrace/contrib/configuration/option.rb new file mode 100644 index 0000000000..168f7afa83 --- /dev/null +++ b/lib/ddtrace/contrib/configuration/option.rb @@ -0,0 +1,33 @@ +module Datadog + module Contrib + module Configuration + # Represents an instance of an integration configuration option + class Option + attr_reader \ + :definition + + def initialize(definition, context) + @definition = definition + @context = context + @value = nil + @is_set = false + end + + def set(value) + @value = @context.instance_exec(value, &definition.setter).tap do + @is_set = true + end + end + + def get + return definition.default_value unless @is_set + @value + end + + def reset + set(definition.default_value) + end + end + end + end +end diff --git a/lib/ddtrace/contrib/configuration/option_definition.rb b/lib/ddtrace/contrib/configuration/option_definition.rb new file mode 100644 index 0000000000..170f5f1561 --- /dev/null +++ b/lib/ddtrace/contrib/configuration/option_definition.rb @@ -0,0 +1,29 @@ +module Datadog + module Contrib + module Configuration + # Represents a definition for an integration configuration option + class OptionDefinition + IDENTITY = ->(x) { x } + + attr_reader \ + :default, + :depends_on, + :lazy, + :name, + :setter + + def initialize(name, meta = {}, &block) + @default = meta[:default] + @depends_on = meta[:depends_on] || [] + @lazy = meta[:lazy] || false + @name = name.to_sym + @setter = meta[:setter] || block || IDENTITY + end + + def default_value + lazy ? @default.call : @default + end + end + end + end +end diff --git a/lib/ddtrace/contrib/configuration/option_definition_set.rb b/lib/ddtrace/contrib/configuration/option_definition_set.rb new file mode 100644 index 0000000000..0f0e2c2c43 --- /dev/null +++ b/lib/ddtrace/contrib/configuration/option_definition_set.rb @@ -0,0 +1,20 @@ +require 'ddtrace/configuration/resolver' + +module Datadog + module Contrib + module Configuration + # Represents a set of configuration option definitions for an integration + class OptionDefinitionSet < Hash + def dependency_order + Datadog::Configuration::Resolver.new(dependency_graph).call + end + + def dependency_graph + each_with_object({}) do |(name, option), graph| + graph[name] = option.depends_on + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/configuration/option_set.rb b/lib/ddtrace/contrib/configuration/option_set.rb new file mode 100644 index 0000000000..4e5dd1a59e --- /dev/null +++ b/lib/ddtrace/contrib/configuration/option_set.rb @@ -0,0 +1,8 @@ +module Datadog + module Contrib + module Configuration + class OptionSet < Hash + end + end + end +end diff --git a/lib/ddtrace/contrib/configuration/options.rb b/lib/ddtrace/contrib/configuration/options.rb new file mode 100644 index 0000000000..1aab275d99 --- /dev/null +++ b/lib/ddtrace/contrib/configuration/options.rb @@ -0,0 +1,95 @@ +require 'ddtrace/contrib/configuration/option' +require 'ddtrace/contrib/configuration/option_set' +require 'ddtrace/contrib/configuration/option_definition' +require 'ddtrace/contrib/configuration/option_definition_set' + +module Datadog + module Contrib + module Configuration + # Behavior for a configuration object that has options + module Options + def self.included(base) + base.send(:extend, ClassMethods) + base.send(:include, InstanceMethods) + end + + # Class behavior for a configuration object with options + module ClassMethods + def options + @options ||= begin + # Allows for class inheritance of option definitions + superclass <= Options ? superclass.options.dup : OptionDefinitionSet.new + end + end + + protected + + def option(name, meta = {}, &block) + options[name] = OptionDefinition.new(name, meta, &block).tap do + define_option_accessors(name) + end + end + + private + + def define_option_accessors(name) + option_name = name + + define_method(option_name) do + get_option(option_name) + end + + define_method("#{option_name}=") do |value| + set_option(option_name, value) + end + end + end + + # Instance behavior for a configuration object with options + module InstanceMethods + def options + @options ||= OptionSet.new + end + + def set_option(name, value) + add_option(name) unless options.key?(name) + options[name].set(value) + end + + def get_option(name) + add_option(name) unless options.key?(name) + options[name].get + end + + def to_h + options.each_with_object({}) do |(key, _), hash| + hash[key] = get_option(key) + end + end + + def reset_options! + options.values.each(&:reset) + end + + private + + def add_option(name) + assert_valid_option!(name) + definition = self.class.options[name] + Option.new(definition, self).tap do |option| + options[name] = option + end + end + + def assert_valid_option!(name) + unless self.class.options.key?(name) + raise(InvalidOptionError, "#{self.class.name} doesn't define the option: #{name}") + end + end + end + + InvalidOptionError = Class.new(StandardError) + end + end + end +end diff --git a/lib/ddtrace/contrib/configuration/resolver.rb b/lib/ddtrace/contrib/configuration/resolver.rb new file mode 100644 index 0000000000..b369b43718 --- /dev/null +++ b/lib/ddtrace/contrib/configuration/resolver.rb @@ -0,0 +1,12 @@ +module Datadog + module Contrib + module Configuration + # Resolves a value to a configuration key + class Resolver + def resolve(name) + name + end + end + end + end +end diff --git a/lib/ddtrace/contrib/configuration/settings.rb b/lib/ddtrace/contrib/configuration/settings.rb new file mode 100644 index 0000000000..c0973e5dc8 --- /dev/null +++ b/lib/ddtrace/contrib/configuration/settings.rb @@ -0,0 +1,35 @@ +require 'ddtrace/contrib/configuration/options' + +module Datadog + module Contrib + module Configuration + # Common settings for all integrations + class Settings + include Options + + option :service_name + option :tracer, default: Datadog.tracer + + def initialize(options = {}) + configure(options) + end + + def configure(options = {}) + self.class.options.dependency_order.each do |name| + self[name] = options.fetch(name, self[name]) + end + + yield(self) if block_given? + end + + def [](name) + respond_to?(name) ? send(name) : get_option(name) + end + + def []=(name, value) + respond_to?("#{name}=") ? send("#{name}=", value) : set_option(name, value) + end + end + end + end +end diff --git a/lib/ddtrace/contrib/integration.rb b/lib/ddtrace/contrib/integration.rb new file mode 100644 index 0000000000..71d63db418 --- /dev/null +++ b/lib/ddtrace/contrib/integration.rb @@ -0,0 +1,16 @@ +require 'ddtrace/contrib/configurable' +require 'ddtrace/contrib/patchable' +require 'ddtrace/contrib/registerable' + +module Datadog + module Contrib + # Base provides features that are shared across all integrations + module Integration + def self.included(base) + base.send(:include, Configurable) + base.send(:include, Patchable) + base.send(:include, Registerable) + end + end + end +end diff --git a/lib/ddtrace/contrib/patchable.rb b/lib/ddtrace/contrib/patchable.rb new file mode 100644 index 0000000000..b49d2e5e08 --- /dev/null +++ b/lib/ddtrace/contrib/patchable.rb @@ -0,0 +1,30 @@ +module Datadog + module Contrib + # Base provides features that are shared across all integrations + module Patchable + def self.included(base) + base.send(:extend, ClassMethods) + base.send(:include, InstanceMethods) + end + + # Class methods for integrations + module ClassMethods + def compatible? + RUBY_VERSION >= '1.9.3' + end + end + + # Instance methods for integrations + module InstanceMethods + def patcher + nil + end + + def patch + return if !self.class.compatible? || patcher.nil? + patcher.patch + end + end + end + end +end diff --git a/lib/ddtrace/contrib/patcher.rb b/lib/ddtrace/contrib/patcher.rb new file mode 100644 index 0000000000..8449338b4c --- /dev/null +++ b/lib/ddtrace/contrib/patcher.rb @@ -0,0 +1,28 @@ +require 'ddtrace/patcher' + +module Datadog + module Contrib + # Common behavior for patcher modules + module Patcher + def self.included(base) + base.send(:include, Datadog::Patcher) + base.send(:extend, InstanceMethods) + base.send(:include, InstanceMethods) + end + + # Class methods for patchers + module ClassMethods + def patch + raise NotImplementedError, '#patch not implemented for Patcher!' + end + end + + # Instance methods for patchers + module InstanceMethods + def patch + raise NotImplementedError, '#patch not implemented for Patcher!' + end + end + end + end +end diff --git a/lib/ddtrace/contrib/registerable.rb b/lib/ddtrace/contrib/registerable.rb new file mode 100644 index 0000000000..ba6d702d5e --- /dev/null +++ b/lib/ddtrace/contrib/registerable.rb @@ -0,0 +1,33 @@ +require 'ddtrace/registry' + +module Datadog + module Contrib + # Defines registerable behavior for integrations + module Registerable + def self.included(base) + base.send(:extend, ClassMethods) + base.send(:include, InstanceMethods) + end + + # Class methods for registerable behavior + module ClassMethods + def register_as(name, options = {}) + registry = options.fetch(:registry, Datadog.registry) + auto_patch = options.fetch(:auto_patch, false) + + registry.add(name, new(name, options), auto_patch) + end + end + + # Instance methods for registerable behavior + module InstanceMethods + attr_reader \ + :name + + def initialize(name, options = {}) + @name = name + end + end + end + end +end diff --git a/lib/ddtrace/patcher.rb b/lib/ddtrace/patcher.rb index 96c5ee2039..15561fbf11 100644 --- a/lib/ddtrace/patcher.rb +++ b/lib/ddtrace/patcher.rb @@ -32,6 +32,11 @@ def do_once(key = nil) @done_once[key] = true end end + + def done?(key) + return false unless instance_variable_defined?(:@done_once) + !@done_once.nil? && @done_once.key?(key) + end end # Extend the common methods so they're available as a module function. diff --git a/spec/ddtrace/configuration_spec.rb b/spec/ddtrace/configuration_spec.rb index 3eeebcfed1..0121505daa 100644 --- a/spec/ddtrace/configuration_spec.rb +++ b/spec/ddtrace/configuration_spec.rb @@ -7,7 +7,7 @@ let(:registry) { Datadog::Registry.new } describe '#use' do - subject(:result) { configuration.use name, options } + subject(:result) { configuration.use(name, options) } let(:name) { :example } let(:integration) { double('integration') } let(:options) { {} } @@ -112,6 +112,34 @@ it { expect(configuration[name].to_h).to eq(option1: :foo, option2: :bar) } end end + + context 'for an integration that includes Datadog::Contrib::Integration' do + let(:integration_class) do + Class.new do + include Datadog::Contrib::Integration + end + end + + let(:integration) do + integration_class.new(name) + end + + context 'which is provided only a name' do + it do + expect(integration).to receive(:configure).with(:default, {}) + configuration.use(name) + end + end + + context 'which is provided a block' do + it do + expect(integration).to receive(:configure).with(:default, {}).and_call_original + expect { |b| configuration.use(name, options, &b) }.to yield_with_args( + a_kind_of(Datadog::Contrib::Configuration::Settings) + ) + end + end + end end describe '#tracer' do diff --git a/spec/ddtrace/contrib/configurable_spec.rb b/spec/ddtrace/contrib/configurable_spec.rb new file mode 100644 index 0000000000..ba050650c6 --- /dev/null +++ b/spec/ddtrace/contrib/configurable_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Contrib::Configurable do + describe 'implemented' do + subject(:configurable_class) do + Class.new.tap do |klass| + klass.send(:include, described_class) + end + end + + describe 'instance behavior' do + subject(:configurable_object) { configurable_class.new } + + describe '#default_configuration' do + subject(:configuration) { configurable_object.default_configuration } + it { is_expected.to be_a_kind_of(Datadog::Contrib::Configuration::Settings) } + end + + describe '#reset_configuration!' do + subject(:reset) { configurable_object.reset_configuration! } + + context 'when a configuration has been added' do + before(:each) { configurable_object.configure(:foo, service_name: 'bar') } + + it do + expect { reset }.to change { configurable_object.configurations.keys } + .from([:default, :foo]) + .to([:default]) + end + end + end + + describe '#configuration' do + context 'when no name is provided' do + subject(:configuration) { configurable_object.configuration } + it { is_expected.to be_a_kind_of(Datadog::Contrib::Configuration::Settings) } + it { is_expected.to be(configurable_object.configurations[:default]) } + end + + context 'when a name is provided' do + subject(:configuration) { configurable_object.configuration(name) } + let(:name) { :foo } + + context 'and the configuration exists' do + before(:each) { configurable_object.configure(:foo, service_name: 'bar') } + it { is_expected.to be_a_kind_of(Datadog::Contrib::Configuration::Settings) } + it { is_expected.to be(configurable_object.configurations[:foo]) } + end + + context 'but the configuration doesn\'t exist' do + it { is_expected.to be nil } + end + end + end + + describe '#configurations' do + subject(:configurations) { configurable_object.configurations } + + context 'when nothing has been explicitly configured' do + it { is_expected.to include(default: a_kind_of(Datadog::Contrib::Configuration::Settings)) } + end + + context 'when a configuration has been added' do + before(:each) { configurable_object.configure(:foo, service_name: 'bar') } + + it do + is_expected.to include( + default: a_kind_of(Datadog::Contrib::Configuration::Settings), + foo: a_kind_of(Datadog::Contrib::Configuration::Settings) + ) + end + end + end + + describe '#configure' do + context 'when provided a name' do + subject(:configure) { configurable_object.configure(name, service_name: 'bar') } + let(:name) { :foo } + + context 'that matches an existing configuration' do + before(:each) { configurable_object.configure(name, service_name: 'baz') } + + it do + expect { configure }.to change { configurable_object.configuration(name).service_name } + .from('baz') + .to('bar') + end + end + + context 'that does not match any configuration' do + it do + expect { configure }.to change { configurable_object.configuration(name) } + .from(nil) + .to(a_kind_of(Datadog::Contrib::Configuration::Settings)) + end + end + end + end + end + end +end diff --git a/spec/ddtrace/contrib/configuration/option_definition_set_spec.rb b/spec/ddtrace/contrib/configuration/option_definition_set_spec.rb new file mode 100644 index 0000000000..f7cb683151 --- /dev/null +++ b/spec/ddtrace/contrib/configuration/option_definition_set_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Contrib::Configuration::OptionDefinitionSet do + subject(:set) { described_class.new } + + it { is_expected.to be_a_kind_of(Hash) } + + shared_context 'dependent option set' do + before(:each) do + set[:foo] = instance_double( + Datadog::Contrib::Configuration::OptionDefinition, + depends_on: [:bar] + ) + + set[:bar] = instance_double( + Datadog::Contrib::Configuration::OptionDefinition, + depends_on: [:baz] + ) + + set[:baz] = instance_double( + Datadog::Contrib::Configuration::OptionDefinition, + depends_on: [] + ) + end + end + + describe '#dependency_order' do + subject(:dependency_order) { set.dependency_order } + + context 'when invoked' do + let(:resolver) { instance_double(Datadog::Configuration::Resolver) } + + it do + expect(Datadog::Configuration::Resolver).to receive(:new) + .with(a_kind_of(Hash)) + .and_return(resolver) + expect(resolver).to receive(:call) + dependency_order + end + end + + context 'when given some options' do + include_context 'dependent option set' + it { is_expected.to eq([:baz, :bar, :foo]) } + end + end + + describe '#dependency_graph' do + subject(:dependency_graph) { set.dependency_graph } + + context 'when set contains options' do + include_context 'dependent option set' + it { is_expected.to eq(foo: [:bar], bar: [:baz], baz: []) } + end + end +end diff --git a/spec/ddtrace/contrib/configuration/option_definition_spec.rb b/spec/ddtrace/contrib/configuration/option_definition_spec.rb new file mode 100644 index 0000000000..9e856a5a3e --- /dev/null +++ b/spec/ddtrace/contrib/configuration/option_definition_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Contrib::Configuration::OptionDefinition do + subject(:definition) { described_class.new(name, meta, &block) } + + let(:name) { :enabled } + let(:meta) { {} } + let(:block) { nil } + + describe '#default' do + subject(:default) { definition.default } + + context 'when not initialized with a value' do + it { is_expected.to be nil } + end + + context 'when initialized with a value' do + let(:meta) { { default: default_value } } + let(:default_value) { double('default') } + it { is_expected.to be default_value } + end + end + + describe '#depends_on' do + subject(:default) { definition.depends_on } + + context 'when not initialized with a value' do + it { is_expected.to eq([]) } + end + + context 'when initialized with a value' do + let(:meta) { { depends_on: depends_on_value } } + let(:depends_on_value) { double('depends_on') } + it { is_expected.to be depends_on_value } + end + end + + describe '#lazy' do + subject(:lazy) { definition.lazy } + + context 'when not initialized with a value' do + it { is_expected.to be false } + end + + context 'when initialized with a value' do + let(:meta) { { lazy: lazy_value } } + let(:lazy_value) { double('lazy') } + it { is_expected.to be lazy_value } + end + end + + describe '#name' do + subject(:result) { definition.name } + + context 'when given a String' do + let(:name) { 'enabled' } + it { is_expected.to be name.to_sym } + end + + context 'when given a Symbol' do + let(:name) { :enabled } + it { is_expected.to be name } + end + end + + describe '#setter' do + subject(:setter) { definition.setter } + + context 'when given a value' do + let(:meta) { { setter: setter_value } } + let(:setter_value) { double('setter') } + it { is_expected.to be setter_value } + end + + context 'when initialized with a block' do + let(:block) { proc {} } + it { is_expected.to be block } + end + + context 'when not initialized' do + it { is_expected.to be described_class::IDENTITY } + end + end + + describe '#default_value' do + subject(:result) { definition.default_value } + let(:meta) { { default: default } } + let(:default) { double('default') } + + context 'when lazy is true' do + let(:meta) { super().merge(lazy: true) } + let(:default_value) { double('default_value') } + before(:each) { expect(default).to receive(:call).and_return(default_value) } + it { is_expected.to be default_value } + end + + context 'when lazy is false' do + let(:meta) { super().merge(lazy: false) } + it { is_expected.to be default } + end + end +end diff --git a/spec/ddtrace/contrib/configuration/option_set_spec.rb b/spec/ddtrace/contrib/configuration/option_set_spec.rb new file mode 100644 index 0000000000..ef0acec1f6 --- /dev/null +++ b/spec/ddtrace/contrib/configuration/option_set_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Contrib::Configuration::OptionSet do + subject(:set) { described_class.new } + + it { is_expected.to be_a_kind_of(Hash) } +end diff --git a/spec/ddtrace/contrib/configuration/option_spec.rb b/spec/ddtrace/contrib/configuration/option_spec.rb new file mode 100644 index 0000000000..6c09454dd2 --- /dev/null +++ b/spec/ddtrace/contrib/configuration/option_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Contrib::Configuration::Option do + subject(:option) { described_class.new(definition, context) } + let(:definition) do + instance_double( + Datadog::Contrib::Configuration::OptionDefinition, + default_value: default_value, + setter: setter + ) + end + let(:default_value) { double('default value') } + let(:setter) { proc { setter_value } } + let(:setter_value) { double('setter_value') } + let(:context) { double('configuration object') } + + describe '#initialize' do + it { expect(option.definition).to be(definition) } + end + + describe '#set' do + subject(:set) { option.set(value) } + let(:value) { double('value') } + + before(:each) { expect(context).to receive(:instance_exec).with(value, &setter) } + + it { is_expected.to be(setter_value) } + end + + describe '#get' do + subject(:get) { option.get } + + context 'when #set' do + context 'hasn\'t been called' do + it { is_expected.to be(default_value) } + end + + context 'has been called' do + let(:value) { double('value') } + + before(:each) do + allow(context).to receive(:instance_exec).with(value, &setter) + option.set(value) + end + + it { is_expected.to be(setter_value) } + end + end + end + + describe '#reset' do + subject(:reset) { option.reset } + + context 'when a value has been set' do + let(:value) { double('value') } + + before(:each) do + allow(context).to receive(:instance_exec).with(value, &setter) + allow(context).to receive(:instance_exec).with(default_value, &setter).and_return(default_value) + option.set(value) + end + + it { is_expected.to be(default_value) } + end + end +end diff --git a/spec/ddtrace/contrib/configuration/options_spec.rb b/spec/ddtrace/contrib/configuration/options_spec.rb new file mode 100644 index 0000000000..91f0495684 --- /dev/null +++ b/spec/ddtrace/contrib/configuration/options_spec.rb @@ -0,0 +1,139 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Contrib::Configuration::Options do + describe 'implemented' do + subject(:options_class) do + Class.new.tap do |klass| + klass.send(:include, described_class) + end + end + + describe 'class behavior' do + describe '#options' do + subject(:options) { options_class.options } + + context 'for a class directly implementing Options' do + it { is_expected.to be_a_kind_of(Datadog::Contrib::Configuration::OptionDefinitionSet) } + end + + context 'on class inheriting from a class implementing Options' do + let(:parent_class) do + Class.new.tap do |klass| + klass.send(:include, described_class) + end + end + let(:options_class) { Class.new(parent_class) } + + context 'which defines some options' do + before(:each) { parent_class.send(:option, :foo) } + + it { is_expected.to be_a_kind_of(Datadog::Contrib::Configuration::OptionDefinitionSet) } + it { is_expected.to_not be(parent_class.options) } + it { is_expected.to include(:foo) } + end + end + end + + describe '#option' do + subject(:option) { options_class.send(:option, name, meta, &block) } + + let(:name) { :foo } + let(:meta) { {} } + let(:block) { proc {} } + + it 'creates an option definition' do + is_expected.to be_a_kind_of(Datadog::Contrib::Configuration::OptionDefinition) + expect(options_class.options).to include(name) + expect(options_class.new).to respond_to(name) + expect(options_class.new).to respond_to("#{name}=") + end + end + end + + describe 'instance behavior' do + subject(:options_object) { options_class.new } + + describe '#options' do + subject(:options) { options_object.options } + it { is_expected.to be_a_kind_of(Datadog::Contrib::Configuration::OptionSet) } + end + + describe '#set_option' do + subject(:set_option) { options_object.set_option(name, value) } + let(:name) { :foo } + let(:value) { double('value') } + + context 'when the option is defined' do + before(:each) { options_class.send(:option, name) } + it { expect { set_option }.to change { options_object.send(name) }.from(nil).to(value) } + end + + context 'when the option is not defined' do + it { expect { set_option }.to raise_error(described_class::InvalidOptionError) } + end + end + + describe '#get_option' do + subject(:get_option) { options_object.get_option(name) } + let(:name) { :foo } + + context 'when the option is defined' do + before(:each) { options_class.send(:option, name, meta) } + let(:meta) { {} } + + context 'and a value is set' do + let(:value) { double('value') } + before(:each) { options_object.set_option(name, value) } + it { is_expected.to be(value) } + end + + context 'and a value is not set' do + let(:meta) { super().merge(default: default_value) } + let(:default_value) { double('default_value') } + it { is_expected.to be(default_value) } + end + end + + context 'when the option is not defined' do + it { expect { get_option }.to raise_error(described_class::InvalidOptionError) } + end + end + + describe '#to_h' do + subject(:hash) { options_object.to_h } + + context 'when no options are defined' do + it { is_expected.to eq({}) } + end + + context 'when options are set' do + before(:each) do + options_class.send(:option, :foo) + options_object.set_option(:foo, :bar) + end + + it { is_expected.to eq(foo: :bar) } + end + end + + describe '#reset_options!' do + subject(:reset_options) { options_object.reset_options! } + + context 'when an option is defined' do + let(:option) { options_object.options[:foo] } + + before(:each) do + options_class.send(:option, :foo, default: :bar) + options_object.set_option(:foo, :baz) + end + + it 'resets the option to its default value' do + expect { reset_options }.to change { options_object.get_option(:foo) }.from(:baz).to(:bar) + end + end + end + end + end +end diff --git a/spec/ddtrace/contrib/configuration/resolver_spec.rb b/spec/ddtrace/contrib/configuration/resolver_spec.rb new file mode 100644 index 0000000000..9fcd50a812 --- /dev/null +++ b/spec/ddtrace/contrib/configuration/resolver_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Contrib::Configuration::Resolver do + subject(:resolver) { described_class.new } + + describe '#resolve' do + subject(:resolve) { resolver.resolve(name) } + let(:name) { double('name') } + it { is_expected.to be name } + end +end diff --git a/spec/ddtrace/contrib/configuration/settings_spec.rb b/spec/ddtrace/contrib/configuration/settings_spec.rb new file mode 100644 index 0000000000..cf80e41aeb --- /dev/null +++ b/spec/ddtrace/contrib/configuration/settings_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Contrib::Configuration::Settings do + subject(:settings) { described_class.new } + + it { is_expected.to be_a_kind_of(Datadog::Contrib::Configuration::Options) } + + describe '#options' do + subject(:options) { settings.options } + it { is_expected.to include(:service_name) } + it { is_expected.to include(:tracer) } + end +end diff --git a/spec/ddtrace/contrib/integration_spec.rb b/spec/ddtrace/contrib/integration_spec.rb new file mode 100644 index 0000000000..bce39dbb63 --- /dev/null +++ b/spec/ddtrace/contrib/integration_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Contrib::Integration do + describe 'implemented' do + subject(:integration_class) do + Class.new.tap do |klass| + klass.send(:include, described_class) + end + end + + describe 'instance behavior' do + subject(:integration_object) { integration_class.new(name) } + let(:name) { :foo } + + it { is_expected.to be_a_kind_of(Datadog::Contrib::Configurable) } + it { is_expected.to be_a_kind_of(Datadog::Contrib::Patchable) } + it { is_expected.to be_a_kind_of(Datadog::Contrib::Registerable) } + end + end +end diff --git a/spec/ddtrace/contrib/patchable_spec.rb b/spec/ddtrace/contrib/patchable_spec.rb new file mode 100644 index 0000000000..76b4693129 --- /dev/null +++ b/spec/ddtrace/contrib/patchable_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Contrib::Patchable do + describe 'implemented' do + subject(:patchable_class) do + Class.new.tap do |klass| + klass.send(:include, described_class) + end + end + + describe 'class behavior' do + describe '#compatible?' do + subject(:compatible) { patchable_class.compatible? } + let(:expected_compatibility) { RUBY_VERSION >= '1.9.3' ? true : false } + it { is_expected.to be expected_compatibility } + end + end + + describe 'instance behavior' do + subject(:patchable_object) { patchable_class.new } + + describe '#patcher' do + subject(:patcher) { patchable_object.patcher } + it { is_expected.to be nil } + end + + describe '#patch' do + subject(:patch) { patchable_object.patch } + + context 'when the patchable object' do + context 'is compatible' do + before(:each) { allow(patchable_class).to receive(:compatible?).and_return(true) } + + context 'and the patcher is defined' do + let(:patcher) { double('patcher') } + before(:each) { allow(patchable_object).to receive(:patcher).and_return(patcher) } + + it 'applies the patch' do + expect(patcher).to receive(:patch) + patch + end + end + + context 'and the patcher is nil' do + it 'does not applies the patch' do + is_expected.to be nil + end + end + end + + context 'is not compatible' do + it 'does not applies the patch' do + is_expected.to be nil + end + end + end + end + end + end +end diff --git a/spec/ddtrace/contrib/patcher_spec.rb b/spec/ddtrace/contrib/patcher_spec.rb new file mode 100644 index 0000000000..3639c51bf2 --- /dev/null +++ b/spec/ddtrace/contrib/patcher_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +require 'ddtrace' +require 'ddtrace/contrib/patcher' + +RSpec.describe Datadog::Contrib::Patcher do + describe 'implemented' do + subject(:patcher_class) do + Class.new.tap do |klass| + klass.send(:include, described_class) + end + end + + describe 'class behavior' do + describe '#patch' do + subject(:patch) { patcher_class.patch } + it { expect { patch }.to raise_error(NotImplementedError) } + end + end + + describe 'instance behavior' do + subject(:patcher_object) { patcher_class.new } + + it { is_expected.to be_a_kind_of(Datadog::Patcher) } + + describe '#patch' do + subject(:patch) { patcher_object.patch } + it { expect { patch }.to raise_error(NotImplementedError) } + end + end + end +end diff --git a/spec/ddtrace/contrib/registerable_spec.rb b/spec/ddtrace/contrib/registerable_spec.rb new file mode 100644 index 0000000000..d81a3aa695 --- /dev/null +++ b/spec/ddtrace/contrib/registerable_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Contrib::Registerable do + describe 'implemented' do + subject(:registerable_class) do + Class.new.tap do |klass| + klass.send(:include, described_class) + end + end + + describe 'class behavior' do + describe '#register_as' do + subject(:register_as) { registerable_class.register_as(name, options) } + let(:name) { :foo } + let(:options) { {} } + + context 'when a registry' do + context 'is provided' do + let(:options) { { registry: registry } } + let(:registry) { instance_double(Datadog::Registry) } + + it do + expect(registry).to receive(:add) + .with(name, a_kind_of(registerable_class), false) + register_as + end + end + + context 'is not provided' do + it do + expect(Datadog.registry).to receive(:add) + .with(name, a_kind_of(registerable_class), false) + register_as + end + end + end + + context 'when auto_patch' do + context 'is provided' do + let(:options) { { auto_patch: true } } + + it do + expect(Datadog.registry).to receive(:add) + .with(name, a_kind_of(registerable_class), true) + register_as + end + end + + context 'is not provided' do + it do + expect(Datadog.registry).to receive(:add) + .with(name, a_kind_of(registerable_class), false) + register_as + end + end + end + end + end + + describe 'instance behavior' do + subject(:registerable_object) { registerable_class.new(name, options) } + let(:name) { :foo } + let(:options) { {} } + + it { is_expected.to have_attributes(name: name) } + end + end +end diff --git a/spec/ddtrace/patcher_spec.rb b/spec/ddtrace/patcher_spec.rb index 5e41415c9c..c7edb06700 100644 --- a/spec/ddtrace/patcher_spec.rb +++ b/spec/ddtrace/patcher_spec.rb @@ -106,6 +106,21 @@ end end end + + describe '#done?' do + context 'when called before do_once' do + subject(:done) { patcher.done?(key) } + let(:key) { double('key') } + it { is_expected.to be false } + end + + context 'when called after do_once' do + subject(:done) { patcher.done?(key) } + let(:key) { double('key') } + before(:each) { patcher.do_once(key) { 'Perform patch' } } + it { is_expected.to be true } + end + end end describe 'implemented' do