mocktail 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +18 -0
  3. data/.gitignore +8 -0
  4. data/.standard.yml +1 -0
  5. data/CHANGELOG.md +3 -0
  6. data/Gemfile +10 -0
  7. data/Gemfile.lock +62 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.md +557 -0
  10. data/Rakefile +11 -0
  11. data/bin/console +35 -0
  12. data/bin/setup +8 -0
  13. data/lib/mocktail/dsl.rb +21 -0
  14. data/lib/mocktail/errors.rb +15 -0
  15. data/lib/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +16 -0
  16. data/lib/mocktail/handles_dry_call/fulfills_stubbing.rb +21 -0
  17. data/lib/mocktail/handles_dry_call/logs_call.rb +7 -0
  18. data/lib/mocktail/handles_dry_call/validates_arguments.rb +57 -0
  19. data/lib/mocktail/handles_dry_call.rb +19 -0
  20. data/lib/mocktail/handles_dry_new_call.rb +36 -0
  21. data/lib/mocktail/imitates_type/ensures_imitation_support.rb +11 -0
  22. data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +95 -0
  23. data/lib/mocktail/imitates_type/makes_double.rb +18 -0
  24. data/lib/mocktail/imitates_type.rb +19 -0
  25. data/lib/mocktail/initializes_mocktail.rb +17 -0
  26. data/lib/mocktail/matcher_presentation.rb +15 -0
  27. data/lib/mocktail/matchers/any.rb +18 -0
  28. data/lib/mocktail/matchers/base.rb +25 -0
  29. data/lib/mocktail/matchers/captor.rb +52 -0
  30. data/lib/mocktail/matchers/includes.rb +24 -0
  31. data/lib/mocktail/matchers/is_a.rb +11 -0
  32. data/lib/mocktail/matchers/matches.rb +13 -0
  33. data/lib/mocktail/matchers/not.rb +11 -0
  34. data/lib/mocktail/matchers/numeric.rb +18 -0
  35. data/lib/mocktail/matchers/that.rb +24 -0
  36. data/lib/mocktail/matchers.rb +14 -0
  37. data/lib/mocktail/records_demonstration.rb +32 -0
  38. data/lib/mocktail/registers_matcher.rb +52 -0
  39. data/lib/mocktail/registers_stubbing.rb +19 -0
  40. data/lib/mocktail/replaces_next.rb +36 -0
  41. data/lib/mocktail/replaces_type/redefines_new.rb +26 -0
  42. data/lib/mocktail/replaces_type/redefines_singleton_methods.rb +39 -0
  43. data/lib/mocktail/replaces_type.rb +26 -0
  44. data/lib/mocktail/resets_state.rb +9 -0
  45. data/lib/mocktail/share/determines_matching_calls.rb +60 -0
  46. data/lib/mocktail/share/simulates_argument_error.rb +28 -0
  47. data/lib/mocktail/value/cabinet.rb +41 -0
  48. data/lib/mocktail/value/call.rb +15 -0
  49. data/lib/mocktail/value/demo_config.rb +10 -0
  50. data/lib/mocktail/value/double.rb +11 -0
  51. data/lib/mocktail/value/matcher_registry.rb +19 -0
  52. data/lib/mocktail/value/stubbing.rb +24 -0
  53. data/lib/mocktail/value/top_shelf.rb +61 -0
  54. data/lib/mocktail/value/type_replacement.rb +11 -0
  55. data/lib/mocktail/value.rb +8 -0
  56. data/lib/mocktail/verifies_call/finds_verifiable_calls.rb +15 -0
  57. data/lib/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +10 -0
  58. data/lib/mocktail/verifies_call/raises_verification_error/stringifies_call.rb +47 -0
  59. data/lib/mocktail/verifies_call/raises_verification_error.rb +63 -0
  60. data/lib/mocktail/verifies_call.rb +29 -0
  61. data/lib/mocktail/version.rb +3 -0
  62. data/lib/mocktail.rb +63 -0
  63. data/mocktail.gemspec +31 -0
  64. metadata +107 -0
@@ -0,0 +1,32 @@
1
+ module Mocktail
2
+ class RecordsDemonstration
3
+ def record(demonstration, demo_config)
4
+ cabinet = Mocktail.cabinet
5
+ prior_call_count = Mocktail.cabinet.calls.dup.size
6
+
7
+ begin
8
+ cabinet.demonstration_in_progress = true
9
+ ValidatesArguments.optional(demo_config.ignore_arity) do
10
+ demonstration.call(Mocktail.matchers)
11
+ end
12
+ ensure
13
+ cabinet.demonstration_in_progress = false
14
+ end
15
+
16
+ if prior_call_count + 1 == cabinet.calls.size
17
+ cabinet.calls.pop
18
+ elsif prior_call_count == cabinet.calls.size
19
+ raise MissingDemonstrationError.new <<~MSG.tr("\n", " ")
20
+ `stubs` & `verify` expect an invocation of a mocked method by a passed
21
+ block, but no invocation occurred.
22
+ MSG
23
+ else
24
+ raise AmbiguousDemonstrationError.new <<~MSG.tr("\n", " ")
25
+ `stubs` & `verify` expect exactly one invocation of a mocked method,
26
+ but #{cabinet.calls.size - prior_call_count} were detected. As a
27
+ result, Mocktail doesn't know which invocation to stub or verify.
28
+ MSG
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,52 @@
1
+ module Mocktail
2
+ class RegistersMatcher
3
+ def register(matcher_type)
4
+ if invalid_type?(matcher_type)
5
+ raise InvalidMatcherError.new <<~MSG.tr("\n", " ")
6
+ Matchers must be Ruby classes
7
+ MSG
8
+ elsif invalid_name?(matcher_type)
9
+ raise InvalidMatcherError.new <<~MSG.tr("\n", " ")
10
+ #{matcher_type.name}.matcher_name must return a valid method name
11
+ MSG
12
+ elsif invalid_match?(matcher_type)
13
+ raise InvalidMatcherError.new <<~MSG.tr("\n", " ")
14
+ #{matcher_type.name}#match? must be defined as a one-argument method
15
+ MSG
16
+ elsif invalid_flag?(matcher_type)
17
+ raise InvalidMatcherError.new <<~MSG.tr("\n", " ")
18
+ #{matcher_type.name}#is_mocktail_matcher? must be defined
19
+ MSG
20
+ else
21
+ MatcherRegistry.instance.add(matcher_type)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def invalid_type?(matcher_type)
28
+ !matcher_type.is_a?(Class)
29
+ end
30
+
31
+ def invalid_name?(matcher_type)
32
+ return true unless matcher_type.respond_to?(:matcher_name)
33
+ name = matcher_type.matcher_name
34
+
35
+ !(name.is_a?(String) || name.is_a?(Symbol)) ||
36
+ name.to_sym.inspect.start_with?(":\"")
37
+ end
38
+
39
+ def invalid_match?(matcher_type)
40
+ params = matcher_type.instance_method(:match?).parameters
41
+ params.size > 1 || ![:req, :opt].include?(params.first[0])
42
+ rescue NameError
43
+ true
44
+ end
45
+
46
+ def invalid_flag?(matcher_type)
47
+ !matcher_type.instance_method(:is_mocktail_matcher?)
48
+ rescue NameError
49
+ true
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "records_demonstration"
2
+
3
+ module Mocktail
4
+ class RegistersStubbing
5
+ def initialize
6
+ @records_demonstration = RecordsDemonstration.new
7
+ end
8
+
9
+ def register(demonstration, demo_config)
10
+ Stubbing.new(
11
+ demonstration: demonstration,
12
+ demo_config: demo_config,
13
+ recording: @records_demonstration.record(demonstration, demo_config)
14
+ ).tap do |stubbing|
15
+ Mocktail.cabinet.store_stubbing(stubbing)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,36 @@
1
+ module Mocktail
2
+ class ReplacesNext
3
+ def initialize
4
+ @top_shelf = TopShelf.instance
5
+ @redefines_new = RedefinesNew.new
6
+ @imitates_type = ImitatesType.new
7
+ end
8
+
9
+ def replace(type, count)
10
+ raise UnsupportedMocktail.new("Mocktail.of_next() only supports classes") unless type.is_a?(Class)
11
+
12
+ mocktails = count.times.map { @imitates_type.imitate(type) }
13
+
14
+ @top_shelf.register_of_next_replacement!(type)
15
+ @redefines_new.redefine(type)
16
+ mocktails.reverse_each do |mocktail|
17
+ Mocktail.stubs(
18
+ ignore_extra_args: true,
19
+ ignore_block: true,
20
+ ignore_arity: true,
21
+ times: 1
22
+ ) {
23
+ type.new
24
+ }.with {
25
+ if mocktail == mocktails.last
26
+ @top_shelf.unregister_of_next_replacement!(type)
27
+ end
28
+
29
+ mocktail
30
+ }
31
+ end
32
+
33
+ mocktails.size == 1 ? mocktails.first : mocktails
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,26 @@
1
+ module Mocktail
2
+ class RedefinesNew
3
+ def initialize
4
+ @handles_dry_new_call = HandlesDryNewCall.new
5
+ end
6
+
7
+ def redefine(type)
8
+ type_replacement = TopShelf.instance.type_replacement_for(type)
9
+
10
+ if type_replacement.replacement_new.nil?
11
+ type_replacement.original_new = type.method(:new)
12
+ type.singleton_class.send(:undef_method, :new)
13
+ handles_dry_new_call = @handles_dry_new_call
14
+ type.define_singleton_method :new, ->(*args, **kwargs, &block) {
15
+ if TopShelf.instance.new_replaced?(type) ||
16
+ TopShelf.instance.of_next_registered?(type)
17
+ handles_dry_new_call.handle(type, args, kwargs, block)
18
+ else
19
+ type_replacement.original_new.call(*args, **kwargs, &block)
20
+ end
21
+ }
22
+ type_replacement.replacement_new = type.singleton_method(:new)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ module Mocktail
2
+ class RedefinesSingletonMethods
3
+ def initialize
4
+ @handles_dry_call = HandlesDryCall.new
5
+ end
6
+
7
+ def redefine(type)
8
+ type_replacement = TopShelf.instance.type_replacement_for(type)
9
+ return unless type_replacement.replacement_methods.nil?
10
+
11
+ type_replacement.original_methods = type.singleton_methods.map { |name|
12
+ type.method(name)
13
+ } - [type_replacement.replacement_new]
14
+
15
+ handles_dry_call = @handles_dry_call
16
+ type_replacement.replacement_methods = type_replacement.original_methods.map { |original_method|
17
+ type.singleton_class.send(:undef_method, original_method.name)
18
+ type.define_singleton_method original_method.name, ->(*args, **kwargs, &block) {
19
+ if TopShelf.instance.singleton_methods_replaced?(type)
20
+ handles_dry_call.handle(Call.new(
21
+ singleton: true,
22
+ double: type,
23
+ original_type: type,
24
+ dry_type: type,
25
+ method: original_method.name,
26
+ original_method: original_method,
27
+ args: args,
28
+ kwargs: kwargs,
29
+ block: block
30
+ ))
31
+ else
32
+ original_method.call(*args, **kwargs, &block)
33
+ end
34
+ }
35
+ type.singleton_method(original_method.name)
36
+ }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ require_relative "replaces_type/redefines_new"
2
+ require_relative "replaces_type/redefines_singleton_methods"
3
+
4
+ module Mocktail
5
+ class ReplacesType
6
+ def initialize
7
+ @top_shelf = TopShelf.instance
8
+ @redefines_new = RedefinesNew.new
9
+ @redefines_singleton_methods = RedefinesSingletonMethods.new
10
+ end
11
+
12
+ def replace(type)
13
+ unless type.is_a?(Class) || type.is_a?(Module)
14
+ raise UnsupportedMocktail.new("Mocktail.replace() only supports classes and modules")
15
+ end
16
+
17
+ if type.is_a?(Class)
18
+ @top_shelf.register_new_replacement!(type)
19
+ @redefines_new.redefine(type)
20
+ end
21
+
22
+ @top_shelf.register_singleton_method_replacement!(type)
23
+ @redefines_singleton_methods.redefine(type)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ module Mocktail
2
+ class ResetsState
3
+ def reset
4
+ TopShelf.instance.reset_current_thread!
5
+ Mocktail.cabinet.reset!
6
+ ValidatesArguments.enable!
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,60 @@
1
+ module Mocktail
2
+ class DeterminesMatchingCalls
3
+ def determine(real_call, demo_call, demo_config)
4
+ real_call.double == demo_call.double &&
5
+ real_call.method == demo_call.method &&
6
+
7
+ # Matcher implementation will replace this:
8
+ args_match?(real_call.args, demo_call.args, demo_config.ignore_extra_args) &&
9
+ kwargs_match?(real_call.kwargs, demo_call.kwargs, demo_config.ignore_extra_args) &&
10
+ blocks_match?(real_call.block, demo_call.block, demo_config.ignore_block)
11
+ end
12
+
13
+ private
14
+
15
+ def args_match?(real_args, demo_args, ignore_extra_args)
16
+ return true if ignore_extra_args && demo_args.empty?
17
+
18
+ (
19
+ real_args.size == demo_args.size ||
20
+ (ignore_extra_args && real_args.size >= demo_args.size)
21
+ ) &&
22
+ demo_args.each.with_index.all? { |demo_arg, i|
23
+ match?(real_args[i], demo_arg)
24
+ }
25
+ end
26
+
27
+ def kwargs_match?(real_kwargs, demo_kwargs, ignore_extra_args)
28
+ return true if ignore_extra_args && demo_kwargs.empty?
29
+
30
+ (
31
+ real_kwargs.size == demo_kwargs.size ||
32
+ (ignore_extra_args && real_kwargs.size >= demo_kwargs.size)
33
+ ) &&
34
+ demo_kwargs.all? { |key, demo_val|
35
+ real_kwargs.key?(key) && match?(real_kwargs[key], demo_val)
36
+ }
37
+ end
38
+
39
+ def blocks_match?(real_block, demo_block, ignore_block)
40
+ ignore_block ||
41
+ (real_block.nil? && demo_block.nil?) ||
42
+ (
43
+ real_block && demo_block &&
44
+ (
45
+ demo_block == real_block ||
46
+ demo_block.call(real_block)
47
+ )
48
+ )
49
+ end
50
+
51
+ def match?(real_arg, demo_arg)
52
+ if demo_arg.respond_to?(:is_mocktail_matcher?) &&
53
+ demo_arg.is_mocktail_matcher?
54
+ demo_arg.match?(real_arg)
55
+ else
56
+ demo_arg == real_arg
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,28 @@
1
+ module Mocktail
2
+ class SimulatesArgumentError
3
+ def simulate(arg_params, args, kwarg_params, kwargs)
4
+ req_args = arg_params.count { |type, _| type == :req }
5
+ opt_args = arg_params.count { |type, _| type == :opt }
6
+ rest_args = arg_params.any? { |type, _| type == :rest }
7
+ req_kwargs = kwarg_params.select { |type, _| type == :keyreq }
8
+
9
+ allowed_args = req_args + opt_args
10
+ msg = if args.size < req_args || (!rest_args && args.size > allowed_args)
11
+ expected_desc = if rest_args
12
+ "#{req_args}+"
13
+ elsif allowed_args != req_args
14
+ "#{req_args}..#{allowed_args}"
15
+ else
16
+ req_args.to_s
17
+ end
18
+
19
+ "wrong number of arguments (given #{args.size}, expected #{expected_desc}#{"; required keyword#{"s" if req_kwargs.size > 1}: #{req_kwargs.map { |_, name| name }.join(", ")}" unless req_kwargs.empty?})"
20
+
21
+ elsif !(missing_kwargs = req_kwargs.reject { |_, name| kwargs.key?(name) }).empty?
22
+ "missing keyword#{"s" if missing_kwargs.size > 1}: #{missing_kwargs.map { |_, name| name.inspect }.join(", ")}"
23
+ end
24
+
25
+ ArgumentError.new(msg)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ # The Cabinet stores all thread-local state, so anything that goes here
2
+ # is guaranteed by Mocktail to be local to the currently-running thread
3
+ module Mocktail
4
+ class Cabinet
5
+ attr_writer :demonstration_in_progress
6
+ attr_reader :calls, :stubbings
7
+
8
+ def initialize
9
+ @doubles = []
10
+ @calls = []
11
+ @stubbings = []
12
+ @demonstration_in_progress = false
13
+ end
14
+
15
+ def reset!
16
+ @calls = []
17
+ @stubbings = []
18
+ # Could cause an exception or prevent pollution—you decide!
19
+ @demonstration_in_progress = false
20
+ # note we don't reset doubles as they don't carry any
21
+ # user-meaningful state on them, and clearing them on reset could result
22
+ # in valid mocks being broken and stop working
23
+ end
24
+
25
+ def store_double(double)
26
+ @doubles << double
27
+ end
28
+
29
+ def store_call(call)
30
+ @calls << call
31
+ end
32
+
33
+ def store_stubbing(stubbing)
34
+ @stubbings << stubbing
35
+ end
36
+
37
+ def demonstration_in_progress?
38
+ @demonstration_in_progress
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ module Mocktail
2
+ class Call < Struct.new(
3
+ :singleton,
4
+ :double,
5
+ :original_type,
6
+ :dry_type,
7
+ :method,
8
+ :original_method,
9
+ :args,
10
+ :kwargs,
11
+ :block,
12
+ keyword_init: true
13
+ )
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module Mocktail
2
+ class DemoConfig < Struct.new(
3
+ :ignore_block,
4
+ :ignore_extra_args,
5
+ :ignore_arity,
6
+ :times,
7
+ keyword_init: true
8
+ )
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ module Mocktail
2
+ class Double
3
+ attr_reader :original_type, :dry_type, :dry_instance
4
+
5
+ def initialize(original_type:, dry_type:, dry_instance:)
6
+ @original_type = original_type
7
+ @dry_type = dry_type
8
+ @dry_instance = dry_instance
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ module Mocktail
2
+ class MatcherRegistry
3
+ def self.instance
4
+ @matcher_registry ||= new
5
+ end
6
+
7
+ def initialize
8
+ @matchers = {}
9
+ end
10
+
11
+ def add(matcher_type)
12
+ @matchers[matcher_type.matcher_name] = matcher_type
13
+ end
14
+
15
+ def get(name)
16
+ @matchers[name]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ module Mocktail
2
+ class Stubbing < Struct.new(
3
+ :demonstration,
4
+ :demo_config,
5
+ :satisfaction_count,
6
+ :recording,
7
+ :effect,
8
+ keyword_init: true
9
+ )
10
+
11
+ def initialize(**kwargs)
12
+ super
13
+ self.satisfaction_count ||= 0
14
+ end
15
+
16
+ def satisfied!
17
+ self.satisfaction_count += 1
18
+ end
19
+
20
+ def with(&block)
21
+ self.effect = block
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,61 @@
1
+ # The top shelf stores all cross-thread & thread-aware state, so anything that
2
+ # goes here is on its own when it comes to ensuring thread safety.
3
+ module Mocktail
4
+ class TopShelf
5
+ def self.instance
6
+ @self ||= new
7
+ end
8
+
9
+ def initialize
10
+ @type_replacements = {}
11
+ @new_registrations = {}
12
+ @of_next_registrations = {}
13
+ @singleton_method_registrations = {}
14
+ end
15
+
16
+ def type_replacement_for(type)
17
+ @type_replacements[type] ||= TypeReplacement.new(type: type)
18
+ end
19
+
20
+ def reset_current_thread!
21
+ @new_registrations[Thread.current] = []
22
+ @of_next_registrations[Thread.current] = []
23
+ @singleton_method_registrations[Thread.current] = []
24
+ end
25
+
26
+ def register_new_replacement!(type)
27
+ @new_registrations[Thread.current] ||= []
28
+ @new_registrations[Thread.current] |= [type]
29
+ end
30
+
31
+ def new_replaced?(type)
32
+ @new_registrations[Thread.current] ||= []
33
+ @new_registrations[Thread.current].include?(type)
34
+ end
35
+
36
+ def register_of_next_replacement!(type)
37
+ @of_next_registrations[Thread.current] ||= []
38
+ @of_next_registrations[Thread.current] |= [type]
39
+ end
40
+
41
+ def of_next_registered?(type)
42
+ @of_next_registrations[Thread.current] ||= []
43
+ @of_next_registrations[Thread.current].include?(type)
44
+ end
45
+
46
+ def unregister_of_next_replacement!(type)
47
+ @of_next_registrations[Thread.current] ||= []
48
+ @of_next_registrations[Thread.current] -= [type]
49
+ end
50
+
51
+ def register_singleton_method_replacement!(type)
52
+ @singleton_method_registrations[Thread.current] ||= []
53
+ @singleton_method_registrations[Thread.current] |= [type]
54
+ end
55
+
56
+ def singleton_methods_replaced?(type)
57
+ @singleton_method_registrations[Thread.current] ||= []
58
+ @singleton_method_registrations[Thread.current].include?(type)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,11 @@
1
+ module Mocktail
2
+ class TypeReplacement < Struct.new(
3
+ :type,
4
+ :original_methods,
5
+ :replacement_methods,
6
+ :original_new,
7
+ :replacement_new,
8
+ keyword_init: true
9
+ )
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ require_relative "value/cabinet"
2
+ require_relative "value/call"
3
+ require_relative "value/demo_config"
4
+ require_relative "value/double"
5
+ require_relative "value/matcher_registry"
6
+ require_relative "value/stubbing"
7
+ require_relative "value/top_shelf"
8
+ require_relative "value/type_replacement"
@@ -0,0 +1,15 @@
1
+ require_relative "../share/determines_matching_calls"
2
+
3
+ module Mocktail
4
+ class FindsVerifiableCalls
5
+ def initialize
6
+ @determines_matching_calls = DeterminesMatchingCalls.new
7
+ end
8
+
9
+ def find(recording, demo_config)
10
+ Mocktail.cabinet.calls.select { |call|
11
+ @determines_matching_calls.determine(call, recording, demo_config)
12
+ }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module Mocktail
2
+ class GathersCallsOfMethod
3
+ def gather(dry_call)
4
+ Mocktail.cabinet.calls.select { |call|
5
+ call.double == dry_call.double &&
6
+ call.method == dry_call.method
7
+ }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,47 @@
1
+ module Mocktail
2
+ class StringifiesCall
3
+ def stringify(call)
4
+ "#{call.method}#{args_to_s(call)}#{blockify(call.block)}"
5
+ end
6
+
7
+ private
8
+
9
+ def args_to_s(call)
10
+ unless (args_lists = [
11
+ argify(call.args),
12
+ kwargify(call.kwargs),
13
+ lambdafy(call.block)
14
+ ].compact).empty?
15
+ "(#{args_lists.join(", ")})"
16
+ end
17
+ end
18
+
19
+ def argify(args)
20
+ return unless args && !args.empty?
21
+ args.map(&:inspect).join(", ")
22
+ end
23
+
24
+ def kwargify(kwargs)
25
+ return unless kwargs && !kwargs.empty?
26
+ kwargs.map { |key, val| "#{key}: #{val.inspect}" }.join(", ")
27
+ end
28
+
29
+ def lambdafy(block)
30
+ return unless block&.lambda?
31
+ "&lambda[#{source_locationify(block)}]"
32
+ end
33
+
34
+ def blockify(block)
35
+ return unless block && !block.lambda?
36
+ " { Proc at #{source_locationify(block)} }"
37
+ end
38
+
39
+ def source_locationify(block)
40
+ "#{strip_pwd(block.source_location[0])}:#{block.source_location[1]}"
41
+ end
42
+
43
+ def strip_pwd(path)
44
+ path.gsub(Dir.pwd + File::SEPARATOR, "")
45
+ end
46
+ end
47
+ end