-
Notifications
You must be signed in to change notification settings - Fork 3.2k
fix: Faker::Config.lazy_loading configuration must be respected (WIP)
#3256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1a2f01a
bf1b146
12e7ee5
0bd1e51
31afb27
cfe9e19
5bfd5d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Faker | ||
| class Loader | ||
| INFLECTIONS = { 'DnD' => 'dnd' }.freeze | ||
|
|
||
| def initialize(base_dir, config, requirer: method(:require)) | ||
| @base_dir = base_dir | ||
| @config = config | ||
| @eager_loaded = false | ||
| @mutex = Mutex.new | ||
| @requirer = requirer | ||
| end | ||
|
|
||
| def load_const(context_name, class_name) | ||
| @mutex.synchronize do | ||
| if loading_strategy == :lazy | ||
| resolve_const(context_name, class_name) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is currently broken on lazy loading |
||
| else | ||
| eager_load! | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def install_on(klass) | ||
| loader = self | ||
|
|
||
| klass.define_singleton_method(:const_missing) do |class_name| | ||
| loader.resolve_const(name, class_name) | ||
| loader.fetch_const(self, name, class_name) | ||
| end | ||
| end | ||
|
|
||
| def fetch_const(klass, context_name, class_name) | ||
| unless klass.const_defined?(class_name, false) | ||
| raise NameError, "uninitialized constant #{context_name}::#{class_name}" | ||
| end | ||
|
|
||
| klass.const_get(class_name) | ||
| end | ||
|
|
||
| def loading_strategy | ||
| @loading_strategy ||= if @config.lazy_loading? | ||
| :lazy | ||
| else | ||
| :eager | ||
| end | ||
| end | ||
|
|
||
| def resolve_const(context_name, class_name) | ||
| load_path = build_path(context_name, class_name) | ||
|
|
||
| @requirer.call(load_path) | ||
| rescue LoadError | ||
| # try to load default generators | ||
| @requirer.call load_path.gsub('faker/', 'faker/default/') | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def eager_load! | ||
| return if @eager_loaded | ||
|
|
||
| @eager_loaded = true | ||
|
|
||
| parents = Dir.glob("#{@base_dir}/faker/*.rb") | ||
| nested = Dir.glob("#{@base_dir}/faker/**/*.rb") - parents | ||
|
|
||
| (parents + nested).each { |f| @requirer.call(f) } | ||
| end | ||
|
|
||
| def build_path(*constants) | ||
| constants.map do |c| | ||
| INFLECTIONS | ||
| .reduce(c.to_s) { |s, (word, replacement)| s.gsub(word, replacement) } | ||
| .gsub('::', '/') | ||
| .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') | ||
| .gsub(/([a-z\d])([A-Z])/, '\1_\2') | ||
| .tr('-', '_') | ||
| .downcase | ||
| end.join('/') | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require_relative '../test_helper' | ||
|
|
||
| class TestLoader < Test::Unit::TestCase | ||
| FIXTURES_DIR = File.expand_path('../fixtures', __dir__) | ||
| LIB_DIR = File.expand_path('../../lib', __dir__) | ||
|
|
||
| class FakeRequirer | ||
| attr_reader :loaded_files | ||
|
|
||
| def initialize(failing_paths: []) | ||
| @failing_paths = failing_paths | ||
| @loaded_files = [] | ||
| @mutex = Mutex.new | ||
| end | ||
|
|
||
| def call(path) | ||
| raise LoadError if @failing_paths.any? do |suffix| | ||
| path.end_with?(suffix) | ||
| end | ||
|
|
||
| @mutex.synchronize { @loaded_files << path } | ||
| end | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. stub
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could pass |
||
| end | ||
|
|
||
| FakeConfig = Struct.new(:lazy_loading?) | ||
|
|
||
| def eager_loader(requirer:) | ||
| Faker::Loader.new(FIXTURES_DIR, FakeConfig.new(false), requirer: requirer) | ||
| end | ||
|
|
||
| def lazy_loader(requirer:) | ||
| Faker::Loader.new(FIXTURES_DIR, FakeConfig.new(true), requirer: requirer) | ||
| end | ||
|
|
||
| def test_strategy_does_not_change_after_first_use | ||
| config = FakeConfig.new(false) | ||
| loader = Faker::Loader.new(FIXTURES_DIR, config, requirer: FakeRequirer.new) | ||
|
|
||
| loader.load_const('Faker', :Gadget) | ||
| config[:lazy_loading?] = true | ||
|
|
||
| assert_equal :eager, loader.loading_strategy | ||
| end | ||
|
|
||
| def test_falls_back_to_default_path_on_load_error | ||
| non_default_path = ['faker/gadget'] | ||
| requirer = FakeRequirer.new(failing_paths: non_default_path) | ||
|
|
||
| loader = lazy_loader(requirer: requirer) | ||
|
|
||
| loader.load_const('Faker', :Gadget) | ||
|
|
||
| assert requirer.loaded_files.any? { |f| f.include?('faker/default/gadget') } | ||
| end | ||
|
|
||
| def test_inflection_resolves_correctly | ||
| requirer = FakeRequirer.new | ||
| loader = lazy_loader(requirer: requirer) | ||
|
|
||
| loader.load_const('Faker::Games', :DnD) | ||
|
|
||
| assert_includes requirer.loaded_files.first, 'faker/games/dnd' | ||
| refute_includes requirer.loaded_files.first, 'dn_d' | ||
| end | ||
|
|
||
| def test_install_on_installs_const_missing | ||
| requirer = FakeRequirer.new | ||
| loader = lazy_loader(requirer: requirer) | ||
| klass = Class.new | ||
|
|
||
| loader.install_on(klass) | ||
|
|
||
| assert_equal klass.singleton_class, klass.method(:const_missing).owner | ||
| end | ||
|
|
||
| def test_eager_loads_all_files_on_first_const_access | ||
| requirer = FakeRequirer.new | ||
| loader = eager_loader(requirer: requirer) | ||
|
|
||
| loader.load_const('Faker', :Gadget) | ||
|
|
||
| actual_files = requirer.loaded_files.map do |loaded| | ||
| loaded.match(/fixtures\/(?<path>.*)/)[:path] | ||
| end.uniq | ||
|
|
||
| expected_files = %w[ | ||
| faker/gadget.rb | ||
| faker/default/widget.rb | ||
| faker/games/dnd.rb | ||
| ] | ||
|
|
||
| expected_files.each do |file| | ||
| assert_includes actual_files, file, "expected #{file} to be loaded" | ||
| end | ||
| end | ||
|
|
||
| def test_requires_namespace_parents_before_nested_generators | ||
| requirer = FakeRequirer.new | ||
| loader = Faker::Loader.new(LIB_DIR, FakeConfig.new(false), requirer: requirer) | ||
|
|
||
| loader.load_const('Faker', :Name) | ||
|
|
||
| parents, nested = requirer.loaded_files.partition do |file| | ||
| File.dirname(file).end_with?('/faker') | ||
| end | ||
|
|
||
| assert_equal requirer.loaded_files, parents + nested | ||
| end | ||
|
|
||
| def test_eager_loads_only_once | ||
| requirer = FakeRequirer.new | ||
| loader = eager_loader(requirer: requirer) | ||
|
|
||
| loader.load_const('Faker', :Gadget) | ||
|
|
||
| count = requirer.loaded_files.size | ||
|
|
||
| loader.load_const('Faker', :Gadget) | ||
|
|
||
| assert_equal count, requirer.loaded_files.size | ||
| end | ||
|
|
||
| def test_lazy_loads_single_file_on_const_access | ||
| requirer = FakeRequirer.new | ||
| loader = lazy_loader(requirer: requirer) | ||
|
|
||
| loader.load_const('Faker', :Gadget) | ||
|
|
||
| assert_equal 1, requirer.loaded_files.size | ||
| assert_includes requirer.loaded_files.first, 'faker/gadget' | ||
| end | ||
|
|
||
| def test_eager_loads_only_once_across_threads | ||
| requirer = FakeRequirer.new | ||
| loader = eager_loader(requirer: requirer) | ||
|
|
||
| threads = 10.times.map do | ||
| Thread.new { loader.load_const('Faker', :Gadget) } | ||
| end | ||
|
|
||
| threads.each(&:join) | ||
|
|
||
| actual_files = requirer.loaded_files.map do |loaded| | ||
| loaded.match(/fixtures\/(?<path>.*)/)[:path] | ||
| end.compact | ||
|
|
||
| assert_equal actual_files.uniq, actual_files | ||
| end | ||
|
|
||
| def test_raises_on_unknown_const | ||
| non_existent_paths = ['faker/non_existent', 'faker/default/non_existent'] | ||
| requirer = FakeRequirer.new(failing_paths: non_existent_paths) | ||
|
|
||
| loader = lazy_loader(requirer: requirer) | ||
|
|
||
| assert_raises(LoadError) { loader.load_const('Faker', :NonExistent) } | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Faker | ||
| class Default | ||
| # rubocop:disable Lint/EmptyClass | ||
| class Widget | ||
| end | ||
| # rubocop:enable Lint/EmptyClass | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Faker | ||
| # rubocop:disable Lint/EmptyClass | ||
| class Gadget | ||
| end | ||
| # rubocop:enable Lint/EmptyClass | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Faker | ||
| class Games | ||
| # rubocop:disable Lint/EmptyClass | ||
| class DnD | ||
| end | ||
| # rubocop:enable Lint/EmptyClass | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,9 @@ | |
| # rubocop:disable Security/Eval,Style/EvalWithLocation | ||
| class TestDeterminism < Test::Unit::TestCase | ||
| def setup | ||
| # TODO: can we expose loader? | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or maybe add a |
||
| Faker.instance_variable_get(:@loader).send(:eager_load!) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 😬 |
||
|
|
||
| @all_methods = all_methods.freeze | ||
| @first_run = [] | ||
| end | ||
|
|
@@ -14,7 +17,7 @@ | |
| Faker::Config.random = Random.new(42) | ||
|
|
||
| @all_methods.each_index do |index| | ||
| store_result @all_methods[index] | ||
|
Check failure on line 20 in test/test_determinism.rb
|
||
| end | ||
|
|
||
| @first_run.freeze | ||
|
|
@@ -94,6 +97,7 @@ | |
| Time | ||
| TvShows | ||
| Music | ||
| Loader | ||
| VERSION | ||
| ] | ||
| end | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this might cause a deadlock? maybe a Monitor is a better fit?
https://ruby-doc.org/stdlib-2.5.3/libdoc/monitor/rdoc/Monitor.html