# frozen_string_literal: truerequire"test_prof/core"require"test_prof/recipes/rspec/before_all"moduleTestProf# Just like `let`, but persist the result for the whole group.# NOTE: Experimental and magical, for more control use `before_all`.moduleLetItBeclassDuplicationError<StandardError;endModifier=Struct.new(:scope,:block)dodefcall(record,config)block.call(record,config)endendclassConfigurationattr_accessor:report_duplicates# Define an alias for `let_it_be` with the predefined options:## TestProf::LetItBe.configure do |config|# config.alias_to :let_it_be_reloaded, reload: true# enddefalias_to(name,**default_args)LetItBe.define_let_it_be_alias(name,**default_args)end# Register modifier by providing the name of key,# optional scope (when to apply the modifier, on initialization (:initialize)# or when accessed # via let (:let))defregister_modifier(key,on: :let,&block)raiseArgumentError,"Modifier #{key} is already defined for let_it_be"ifLetItBe.modifiers.key?(key)LetItBe.modifiers[key]=Modifier.new(on,block)enddefdefault_modifiers@default_modifiers||={}endendclass<<selfdefconfig@config||=Configuration.newenddefconfigureyieldconfigenddefmodifiers@modifiers||={}enddefwrap_with_modifiers(mods,on: :let,&block)mods=mods.select{|k,val|LetItBe.modifiers.fetch(k).scope==on}returnblockifmods.empty?validate_modifiers!modsprocdorecord=instance_eval(&block)mods.inject(record)do|rec,(k,v)|LetItBe.modifiers.fetch(k).call(rec,v)endendenddefmodule_for(group)modules[group]||=Module.new.tap{|mod|group.prepend(mod)}endprivatedefmodules@modules||={}enddefvalidate_modifiers!(mods)unknown=mods.keys-modifiers.keysreturnifunknown.empty?raiseArgumentError,"Unknown let_it_be modifiers were used: #{unknown.join(", ")}. "\"Available modifiers are: #{modifiers.keys.join(", ")}"endend# Use uniq prefix for instance variables to avoid collisions# We want to use the power of Ruby's unicode support)# And we love cats!)# Allow overriding the prefix (there are some intermittent issues on JRuby still)PREFIX=ENV.fetch("LET_IT_BE_IVAR_PREFIX","@😸")FROZEN_ERROR_HINT="\nIf you are using `let_it_be`, you may want to pass `reload: true` or `refind: true` modifier to it."defself.define_let_it_be_alias(name,**default_args)define_method(name)do|identifier,**options,&blk|let_it_be(identifier,**default_args.merge(options),&blk)endenddeflet_it_be(identifier,**options,&block)initializer=procdoinstance_variable_set(:"#{TestProf::LetItBe::PREFIX}#{identifier}",instance_exec(&block))rescueFrozenError=>eraisee.exception("#{e.message}#{TestProf::LetItBe::FROZEN_ERROR_HINT}")enddefault_options=LetItBe.config.default_modifiers.dupdefault_options.merge!(metadata[:let_it_be_modifiers])ifmetadata[:let_it_be_modifiers]options=default_options.merge(options)initializer=LetItBe.wrap_with_modifiers(options,on: :initialize,&initializer)before_all(&initializer)let_accessor=LetItBe.wrap_with_modifiers(options,on: :let)doinstance_variable_get(:"#{PREFIX}#{identifier}")endreport_duplicates(identifier)ifLetItBe.config.report_duplicatesLetItBe.module_for(self).module_evaldodefine_method(identifier)do# Trying to detect the context# First, check for ::RSpec.current_scope (modern RSpec) and then read @__inspect_output# (based on https://github.com/rspec/rspec-rails/commit/7cb796db064f58da7790a92e73ab906ef50b1f34)if::RSpec.respond_to?(:current_scope)&&%i[before_all before_context_hook after_context_hook].include?(::RSpec.current_scope)instance_variable_get(:"#{PREFIX}#{identifier}")elsif/(before|after)\(:context\)/.match?(@__inspect_output)||@__inspect_output.include?("before_all")instance_variable_get(:"#{PREFIX}#{identifier}")elsesuper()endendendlet(identifier,&let_accessor)endprivatedefreport_duplicates(identifier)ifinstance_methods.include?(identifier)&&File.basename(__FILE__)==File.basename(instance_method(identifier).source_location[0])error_msg="let_it_be(:#{identifier}) was redefined in nested group"report_level=LetItBe.config.report_duplicates.to_symifreport_level==:warn::RSpec.warn_with(error_msg)elsifreport_level==:raiseraiseDuplicationError,error_msgendendendmoduleFreezer# Stoplist to prevent freezing objects and theirs associations that are defined# with `let_it_be`'s `freeze: false` options during deep freezing.## To only keep track of objects that are available in current example group,# `begin` adds a new layer, and `rollback` removes a layer of unrelated objects# along with rolling back the transaction where they were created.## Stoplist holds records declared with `freeze: false` (so we do not freeze them even if they're used as# associated records for frozen objects)moduleStoplistclass<<selfdefstop?(record)@stoplist.any?{|layer|layer.include?(record)}enddefstop!(record)@stoplist.last.push(record)enddefbegin@stoplist.push([])enddefrollback@stoplist.popendend# Stack of example group-related variable definitions@stoplist=[]endclass<<self# Rerucsively freezes the object to detect modificationsdefdeep_freeze(record)returnifrecord.frozen?returnifStoplist.stop?(record)record.freeze# Support `let_it_be` with `create_list`returnrecord.each{|rec|deep_freeze(rec)}ifrecord.respond_to?(:each)# Freeze associations as well.returnunlessdefined?(::ActiveRecord::Base)returnunlessrecord.is_a?(::ActiveRecord::Base)record.class.reflections.keys.eachdo|reflection|# But only if they are already loaded. If not yet loaded, they weren't# created by factories, and it's ok to mutate them.nextunlessrecord.association(reflection.to_sym).loaded?target=record.association(reflection.to_sym).targetdeep_freeze(target)iftarget.is_a?(::ActiveRecord::Base)||target.respond_to?(:each)endendendendendendifdefined?(::ActiveRecord::Base)require"test_prof/ext/active_record_refind"usingTestProf::Ext::ActiveRecordRefindTestProf::LetItBe.configuredo|config|config.register_modifier:reloaddo|record,val|nextrecordunlessvalnextrecord.reloadifrecord.is_a?(::ActiveRecord::Base)ifrecord.respond_to?(:to_ary)nextrecord.mapdo|rec|rec.is_a?(::ActiveRecord::Base)?rec.reload:recendendrecordendconfig.register_modifier:refinddo|record,val|nextrecordunlessvalnextrecord.refindifrecord.is_a?(::ActiveRecord::Base)ifrecord.respond_to?(:to_ary)nextrecord.mapdo|rec|rec.is_a?(::ActiveRecord::Base)?rec.refind:recendendrecordendconfig.register_modifier:freeze,on: :initializedo|record,val|ifval==falseTestProf::LetItBe::Freezer::Stoplist.stop!(record)nextrecordendTestProf::LetItBe::Freezer.deep_freeze(record)recordendendendRSpec::Core::ExampleGroup.extendTestProf::LetItBeTestProf::BeforeAll.configuredo|config|config.before(:begin)doTestProf::LetItBe::Freezer::Stoplist.beginendconfig.after(:rollback)doTestProf::LetItBe::Freezer::Stoplist.rollbackendendRSpec.configuredo|config|config.after(:example)do|example|ifexample.exception&.is_a?(FrozenError)example.exception.message<<TestProf::LetItBe::FROZEN_ERROR_HINTunlessexample.exception.message.frozen?endendend