mocktail 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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