andyw8-seeing_is_believing 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +60 -0
  3. data/.gitignore +19 -0
  4. data/.rspec +2 -0
  5. data/Gemfile +2 -0
  6. data/README.md +70 -0
  7. data/Rakefile +88 -0
  8. data/appveyor.yml +32 -0
  9. data/bin/seeing_is_believing +7 -0
  10. data/docs/example.gif +0 -0
  11. data/docs/frog-brown.png +0 -0
  12. data/docs/sib-streaming.gif +0 -0
  13. data/features/deprecated-flags.feature +91 -0
  14. data/features/errors.feature +155 -0
  15. data/features/examples.feature +423 -0
  16. data/features/flags.feature +852 -0
  17. data/features/regression.feature +898 -0
  18. data/features/support/env.rb +102 -0
  19. data/features/xmpfilter-style.feature +471 -0
  20. data/lib/seeing_is_believing/binary/align_chunk.rb +47 -0
  21. data/lib/seeing_is_believing/binary/align_file.rb +24 -0
  22. data/lib/seeing_is_believing/binary/align_line.rb +25 -0
  23. data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +56 -0
  24. data/lib/seeing_is_believing/binary/annotate_every_line.rb +52 -0
  25. data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +179 -0
  26. data/lib/seeing_is_believing/binary/comment_lines.rb +36 -0
  27. data/lib/seeing_is_believing/binary/commentable_lines.rb +126 -0
  28. data/lib/seeing_is_believing/binary/config.rb +455 -0
  29. data/lib/seeing_is_believing/binary/data_structures.rb +58 -0
  30. data/lib/seeing_is_believing/binary/engine.rb +161 -0
  31. data/lib/seeing_is_believing/binary/format_comment.rb +79 -0
  32. data/lib/seeing_is_believing/binary/interline_align.rb +57 -0
  33. data/lib/seeing_is_believing/binary/remove_annotations.rb +113 -0
  34. data/lib/seeing_is_believing/binary/rewrite_comments.rb +62 -0
  35. data/lib/seeing_is_believing/binary.rb +73 -0
  36. data/lib/seeing_is_believing/code.rb +139 -0
  37. data/lib/seeing_is_believing/compatibility.rb +28 -0
  38. data/lib/seeing_is_believing/debugger.rb +32 -0
  39. data/lib/seeing_is_believing/error.rb +17 -0
  40. data/lib/seeing_is_believing/evaluate_by_moving_files.rb +195 -0
  41. data/lib/seeing_is_believing/event_stream/consumer.rb +221 -0
  42. data/lib/seeing_is_believing/event_stream/events.rb +193 -0
  43. data/lib/seeing_is_believing/event_stream/handlers/debug.rb +61 -0
  44. data/lib/seeing_is_believing/event_stream/handlers/record_exit_events.rb +26 -0
  45. data/lib/seeing_is_believing/event_stream/handlers/stream_json_events.rb +23 -0
  46. data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +41 -0
  47. data/lib/seeing_is_believing/event_stream/producer.rb +178 -0
  48. data/lib/seeing_is_believing/hard_core_ensure.rb +58 -0
  49. data/lib/seeing_is_believing/hash_struct.rb +206 -0
  50. data/lib/seeing_is_believing/result.rb +89 -0
  51. data/lib/seeing_is_believing/safe.rb +112 -0
  52. data/lib/seeing_is_believing/swap_files.rb +90 -0
  53. data/lib/seeing_is_believing/the_matrix.rb +97 -0
  54. data/lib/seeing_is_believing/version.rb +3 -0
  55. data/lib/seeing_is_believing/wrap_expressions.rb +265 -0
  56. data/lib/seeing_is_believing/wrap_expressions_with_inspect.rb +19 -0
  57. data/lib/seeing_is_believing.rb +69 -0
  58. data/seeing_is_believing.gemspec +84 -0
  59. data/spec/binary/alignment_specs.rb +27 -0
  60. data/spec/binary/comment_lines_spec.rb +852 -0
  61. data/spec/binary/config_spec.rb +831 -0
  62. data/spec/binary/engine_spec.rb +114 -0
  63. data/spec/binary/format_comment_spec.rb +210 -0
  64. data/spec/binary/marker_spec.rb +71 -0
  65. data/spec/binary/remove_annotations_spec.rb +342 -0
  66. data/spec/binary/rewrite_comments_spec.rb +106 -0
  67. data/spec/code_spec.rb +233 -0
  68. data/spec/debugger_spec.rb +45 -0
  69. data/spec/evaluate_by_moving_files_spec.rb +204 -0
  70. data/spec/event_stream_spec.rb +762 -0
  71. data/spec/hard_core_ensure_spec.rb +120 -0
  72. data/spec/hash_struct_spec.rb +514 -0
  73. data/spec/seeing_is_believing_spec.rb +1094 -0
  74. data/spec/sib_spec_helpers/version.rb +17 -0
  75. data/spec/spec_helper.rb +26 -0
  76. data/spec/spec_helper_spec.rb +16 -0
  77. data/spec/wrap_expressions_spec.rb +1013 -0
  78. metadata +340 -0
@@ -0,0 +1,120 @@
1
+ require 'spec_helper'
2
+ require 'seeing_is_believing/hard_core_ensure'
3
+
4
+ RSpec.describe SeeingIsBelieving::HardCoreEnsure do
5
+ def call(options)
6
+ described_class.new(options).call
7
+ end
8
+
9
+ it "raises an argument error if it doesn't get a code proc" do
10
+ expect { call ensure: -> {} }.to raise_error ArgumentError, "Must pass the :code key"
11
+ end
12
+
13
+ it "raises an argument error if it doesn't get an ensure proc" do
14
+ expect { call code: -> {} }.to raise_error ArgumentError, "Must pass the :ensure key"
15
+ end
16
+
17
+ it "raises an argument error if it gets any other keys" do
18
+ expect { call code: -> {}, ensure: -> {}, other: 123 }.to \
19
+ raise_error ArgumentError, "Unknown key: :other"
20
+
21
+ expect { call code: -> {}, ensure: -> {}, other1: 123, other2: 456 }.to \
22
+ raise_error ArgumentError, "Unknown keys: :other1, :other2"
23
+ end
24
+
25
+ it 'invokes the code and returns the value' do
26
+ expect(call(code: -> { :result }, ensure: -> {})).to eq :result
27
+ end
28
+
29
+ it 'invokes the ensure after the code' do
30
+ seen = []
31
+ call code: -> { seen << :code }, ensure: -> { seen << :ensure }
32
+ expect(seen).to eq [:code, :ensure]
33
+ end
34
+
35
+ it 'invokes the ensure even if an exception is raised' do
36
+ ensure_invoked = false
37
+ expect do
38
+ call code: -> { raise Exception, 'omg!' }, ensure: -> { ensure_invoked = true }
39
+ end.to raise_error Exception, 'omg!'
40
+ expect(ensure_invoked).to eq true
41
+ end
42
+
43
+ def ruby(program)
44
+ child = ChildProcess.build RbConfig.ruby,
45
+ '-I', File.expand_path('../lib', __dir__),
46
+ '-r', 'seeing_is_believing/hard_core_ensure',
47
+ '-e', program
48
+ child.duplex = true
49
+ outread, outwrite = IO.pipe
50
+ errread, errwrite = IO.pipe
51
+ child.io.stdout = outwrite
52
+ child.io.stderr = errwrite
53
+ child.start
54
+ outwrite.close
55
+ errwrite.close
56
+ yield child, outread
57
+ ensure
58
+ errread && !errread.closed? && expect(errread.read).to(be_empty)
59
+ outread.close unless outread.closed?
60
+ end
61
+
62
+
63
+ # for fuck sake, I can't get Windows to let me trap the interrupt
64
+ it 'invokes the code even if an interrupt is sent and there is a default handler', windows: false do
65
+ program = <<-RUBY
66
+ trap("INT") do
67
+ puts %(CUSTOM-HANDLER)
68
+ exit
69
+ end
70
+ SeeingIsBelieving::HardCoreEnsure.new(
71
+ code: -> { puts %(CODE); $stdout.flush; sleep },
72
+ ensure: -> { puts %(ENSURE) },
73
+ ).call
74
+ RUBY
75
+ ruby program do |ps, psout|
76
+ expect(psout.gets.chomp).to eq "CODE"
77
+ # is_alive = ChildProcess::Windows::Lib.alive?(ps.pid)
78
+ Process.kill 'INT', ps.pid
79
+
80
+ expect(psout.gets.chomp).to eq "ENSURE"
81
+ expect(psout.gets.chomp).to eq "CUSTOM-HANDLER"
82
+ end
83
+ end
84
+
85
+ it 'invokes the code even if an interrupt is sent and interrupts are set to ignore', windows: false do
86
+ # empty string isn't documented, but it causes ignore too
87
+ # https://github.com/ruby/ruby/blob/256d8c9ecffbcd8f4fe7562b866fcd55f1d445e7/signal.c#L1128-L1129
88
+ ignore_handlers = ['IGNORE', 'SIG_IGN', '']
89
+
90
+ ignore_handlers.each do |handler|
91
+ program = <<-RUBY
92
+ trap %(INT), #{handler.inspect}
93
+ SeeingIsBelieving::HardCoreEnsure.new(
94
+ code: -> {
95
+ puts %(CODE1)
96
+ $stdout.flush
97
+ gets
98
+ puts %(CODE2)
99
+ },
100
+ ensure: -> { puts %(ENSURE) },
101
+ ).call
102
+ RUBY
103
+ ruby program do |ps, psout|
104
+ expect(psout.gets).to eq "CODE1\n" # we're in the code block
105
+ Process.kill 'INT', ps.pid # should be ignored
106
+
107
+ # note that if we don't check this, the pipe on the next line may beat the signal
108
+ # to the process leading to nondeterministic printing
109
+ expect(ps).to be_alive
110
+
111
+ ps.io.stdin.puts "wake up!"
112
+ ps.wait
113
+ expect(ps.exit_code).to eq 0
114
+ expect(psout.gets).to eq "CODE2\n"
115
+ expect(psout.gets).to eq "ENSURE\n"
116
+ expect(psout.gets).to eq nil
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,514 @@
1
+ require 'seeing_is_believing/hash_struct'
2
+
3
+ RSpec.describe SeeingIsBelieving::HashStruct do
4
+ let(:klass) { described_class.anon }
5
+
6
+ def eq!(expected, actual, *message)
7
+ expect(actual).to eq(expected), *message
8
+ end
9
+
10
+ def neq!(expected, actual, *message)
11
+ expect(actual).to_not eq(expected), *message
12
+ end
13
+
14
+ def raises!(*exception_class_and_matcher, &block)
15
+ expect(&block).to raise_error(*exception_class_and_matcher)
16
+ end
17
+
18
+ def include!(needle, haystack)
19
+ expect(haystack).to include needle
20
+ end
21
+
22
+ def ninclude!(needle, haystack)
23
+ expect(haystack).to_not include needle
24
+ end
25
+
26
+ describe 'declaration' do
27
+ describe 'attributes' do
28
+ specify 'can be individually declared, providing a default value or init block using .attribute' do
29
+ eq! 1, klass.attribute(:a, 1 ).new.a
30
+ eq! 2, klass.attribute(:b) { 2 }.new.b
31
+ end
32
+
33
+ specify 'the block value is not cached' do
34
+ klass.attribute(:a, "a" ).new.a << "-modified"
35
+ eq! "a-modified", klass.new.a
36
+
37
+ klass.attribute(:b) { "b" }.new.b << "-modified"
38
+ eq! "b", klass.new.b
39
+ end
40
+
41
+ specify 'can be group declared with a default value using .attributes(hash)' do
42
+ klass.attributes a: 1, b: 2
43
+ eq! 1, klass.new.a
44
+ eq! 2, klass.new.b
45
+ end
46
+
47
+ specify 'can omit a default if they are initialized with one' do
48
+ klass.attribute :a
49
+ eq! 1, klass.new(a: 1).a
50
+ raises!(ArgumentError, /:a/) { klass.new }
51
+ end
52
+
53
+ specify 'can group declare uninitialized attributes with .attributes(*names)' do
54
+ klass.attributes(:a, :b)
55
+ eq! 1, klass.new(a: 1, b: 2).a
56
+ raises!(ArgumentError, /:b/) { klass.new a: 1 }
57
+ end
58
+ end
59
+
60
+ describe 'predicates are attributes which' do
61
+ specify 'can be individually declared with .predicate' do
62
+ eq! 1, klass.predicate(:a, 1 ).new.a
63
+ eq! 2, klass.predicate(:b) { 2 }.new.b
64
+ end
65
+ specify 'can be group declared with .predicates' do
66
+ klass.predicates a: 1, b: 2
67
+ eq! 1, klass.new.a
68
+ eq! 2, klass.new.b
69
+ end
70
+ specify 'can omit a default if they are initialized with one' do
71
+ klass.predicate :a
72
+ eq! true, klass.new(a: 1).a?
73
+ raises!(ArgumentError, /:a/) { klass.new }
74
+ end
75
+ specify 'can group declare uninitialized attributes with .attributes(*names)' do
76
+ klass.predicates(:a, :b)
77
+ eq! true, klass.new(a: 1, b: 2).a?
78
+ raises!(ArgumentError, /:b/) { klass.new a: 1 }
79
+ end
80
+ end
81
+
82
+ describe 'conflicts' do
83
+ it 'raises if you double-declare an attribute' do
84
+ klass.attribute :a, 1
85
+ raises!(ArgumentError) { klass.attribute :a, 2 }
86
+ raises!(ArgumentError) { klass.predicate :a, 3 }
87
+ raises!(ArgumentError) { klass.attributes a: 4 }
88
+ raises!(ArgumentError) { klass.predicates a: 5 }
89
+ eq! 1, klass.new.a
90
+
91
+ klass.predicate :b, 1
92
+ raises!(ArgumentError) { klass.attribute :b, 2 }
93
+ raises!(ArgumentError) { klass.predicate :b, 3 }
94
+ raises!(ArgumentError) { klass.attributes b: 4 }
95
+ raises!(ArgumentError) { klass.predicates b: 5 }
96
+ eq! 1, klass.new.b
97
+ end
98
+ end
99
+
100
+ describe '.attribute / .attributes / .predicate / .predicates' do
101
+ specify 'raise if a key is not a symbol (you shouldn\'t be dynamically creating this class with strings)' do
102
+ raises!(ArgumentError) { klass.attribute 'a', 1 }
103
+ raises!(ArgumentError) { klass.predicate 'b', 1 }
104
+ raises!(ArgumentError) { klass.attributes 'c' => 1 }
105
+ raises!(ArgumentError) { klass.predicates 'd' => 1 }
106
+ end
107
+ end
108
+ end
109
+
110
+ describe 'anonymous subclasses try to be generally terse and useful to be valid replacements over Struct' do
111
+ specify '.anon / .for / .for? return a subclass of HashStruct' do
112
+ klass = described_class.anon
113
+ neq! described_class, klass
114
+ eq! described_class, klass.superclass
115
+
116
+ klass = described_class.for
117
+ neq! described_class, klass
118
+ eq! described_class, klass.superclass
119
+
120
+ klass = described_class.for?
121
+ neq! described_class, klass
122
+ eq! described_class, klass.superclass
123
+ end
124
+
125
+ specify '.anon / .for / .for? take blocks which get class_eval\'d' do
126
+ klass = described_class.anon { def a; 1 end }
127
+ eq! 1, klass.new.a
128
+
129
+ klass = described_class.for { def b; 2 end }
130
+ eq! 2, klass.new.b
131
+
132
+ klass = described_class.for? { def c; 3 end }
133
+ eq! 3, klass.new.c
134
+ end
135
+
136
+ specify '.for? passes its args to .predicates' do
137
+ klass = described_class.for?(:a, b: 3)
138
+ eq! 1, klass.new(a: 1).a
139
+ eq! 3, klass.new(a: 1).b
140
+ eq! true, klass.new(a: 1).b?
141
+ eq! false, klass.new(a: 1, b: nil).b?
142
+ end
143
+
144
+ specify '.for passes its args to .attributes' do
145
+ klass = described_class.for(:a, b: 3)
146
+ eq! 1, klass.new(a: 1).a
147
+ eq! 3, klass.new(a: 1).b
148
+ raises!(NoMethodError) { klass.new(a: 1).b? }
149
+ end
150
+
151
+ specify 'subclasses retain their parents attributes without them mixing' do
152
+ parent = klass.for(a: 1)
153
+ child = parent.for(b: 2)
154
+ eq! 1, child.new.a
155
+ eq! 2, child.new.b
156
+ eq! 1, parent.new.a
157
+ raises!(NoMethodError) { parent.new.b }
158
+ end
159
+
160
+ specify 'subclasses can override their parents attributes' do
161
+ c1 = klass.for(a: 1, b: 1, c: 1)
162
+ c2 = c1.for(a: 2, b: 2)
163
+ c3 = c2.for(a: 3)
164
+ eq! 3, c3.new.a
165
+ eq! 2, c3.new.b
166
+ eq! 1, c3.new.c
167
+
168
+ eq! 2, c2.new.a
169
+ eq! 2, c2.new.b
170
+ eq! 1, c2.new.c
171
+
172
+ eq! 1, c1.new.a
173
+ eq! 1, c1.new.b
174
+ eq! 1, c1.new.c
175
+
176
+ raises!(ArgumentError) { c2.attribute :a } # still can't redefine their own
177
+ end
178
+
179
+ specify '.inspect returns HashStruct.anon when it does not have a name' do
180
+ expect(klass.anon.inspect).to eq 'HashStruct.anon'
181
+
182
+ named_class = klass.anon
183
+ allow(named_class).to receive(:name).and_return("SomeClass")
184
+ expect(named_class.inspect).to eq 'SomeClass'
185
+ end
186
+ end
187
+
188
+
189
+ describe 'use' do
190
+ describe 'initialization' do
191
+ it 'sets all values to their defaults, calling the init blocks at that time, with the instance' do
192
+ calls = []
193
+ klass.attribute(:a) { calls << :a; 1 }.attribute(:b, 2).attributes(c: 3)
194
+ .predicate(:d) { calls << :d; 4 }.predicate(:e, 5).predicates(f: 6)
195
+ .attribute(:g) { |i| i.b + i.c }
196
+ .predicate(:h) { |i| i.e + i.f }
197
+ eq! [], calls
198
+ instance = klass.new
199
+ eq! [:a, :d], calls
200
+ eq! 1, instance.a
201
+ eq! 2, instance.b
202
+ eq! 3, instance.c
203
+ eq! 4, instance.d
204
+ eq! 5, instance.e
205
+ eq! 6, instance.f
206
+ eq! 5, instance.g
207
+ eq! 11, instance.h
208
+ eq! [:a, :d], calls
209
+ end
210
+ it 'accepts a hash of any declard attribute overrides' do
211
+ instance = klass.attributes(a: 1, b: 2).new(a: 3)
212
+ eq! 3, instance.a
213
+ eq! 2, instance.b
214
+ end
215
+ it 'accepts string and symbol keys' do
216
+ instance = klass.attributes(a: 1, b: 2).new(a: 3, 'b' => 4)
217
+ eq! 3, instance.a
218
+ eq! 4, instance.b
219
+ end
220
+ it 'raises if initialized with attributes it doesn\'t know' do
221
+ klass.attribute :a, 1
222
+ raises!(KeyError) { klass.new b: 2 }
223
+ end
224
+ it 'raises an ArgumentError if all its values aren\'t initialized between defaults and init hash' do
225
+ klass.attribute :a, 1
226
+ klass.attribute :b
227
+ klass.new(b: 1)
228
+ raises!(ArgumentError) { klass.new }
229
+ end
230
+ it 'won\'t raise until after a provided block is invoked' do
231
+ klass.attributes(:a, :b)
232
+ eq! 1, klass.new(b: 1) { |i| i.a = 1 }.a
233
+ eq! nil, klass.new(b: 1) { |i| i.a = nil }.a
234
+ raises!(ArgumentError) { klass.new {} }
235
+ klass.new(b: 2) { |instance|
236
+ raises!(ArgumentError) { instance.a }
237
+ raises!(ArgumentError) { instance[:a] }
238
+ instance.a = 1
239
+ eq! 1, instance.a
240
+ eq! 1, instance[:a]
241
+ eq! 2, instance.b
242
+ }
243
+ end
244
+ it 'gives you a helpful message when you pass it a non-enumerable argument (ie when used to normal Struct)' do
245
+ klass.attributes(a: 1)
246
+ expect { klass.new 1 }.to raise_error ArgumentError, /\b1\b/
247
+ end
248
+ end
249
+
250
+ describe '#[] / #[]=' do
251
+ specify 'get/set an attribute using string or symbol' do
252
+ instance = klass.attribute(:a, 1).new
253
+ eq! 1, instance[:a]
254
+ eq! 1, instance['a']
255
+ instance[:a] = 2
256
+ eq! 2, instance[:a]
257
+ eq! 2, instance['a']
258
+ instance['a'] = 3
259
+ eq! 3, instance[:a]
260
+ eq! 3, instance['a']
261
+ end
262
+ specify 'raise if given a key that is not an attribute' do
263
+ instance = klass.attribute(:a, 1).new
264
+ instance[:a]
265
+ raises!(KeyError) { instance[:b] }
266
+
267
+ instance[:a] = 2
268
+ raises!(KeyError) { instance[:b] = 2 }
269
+ end
270
+ end
271
+
272
+ describe '#fetch' do
273
+ let(:instance) { klass.attributes(a: :value).new }
274
+ it 'returns the key if it exists' do
275
+ eq! :value, instance.fetch(:a)
276
+ end
277
+ it 'accepts a second argument, which it just ignores' do
278
+ eq! :value, instance.fetch(:a, :default)
279
+ end
280
+ it 'raises a KeyError if the key doesn\'t exist, regardless of the second argument or a default block -- point of this is that you know what\'s in the hashes' do
281
+ raises!(KeyError) { instance.fetch(:b, :default) }
282
+ raises!(KeyError) { instance.fetch(:b) { :default } }
283
+ end
284
+ end
285
+
286
+ describe 'setter, getter, predicate' do
287
+ specify '#<attr> gets the attribute' do
288
+ eq! 1, klass.attribute(:a, 1).new.a
289
+ end
290
+ specify '#<attr>= sets the attribute' do
291
+ instance = klass.attribute(:a, 1).new
292
+ eq! 1, instance.a
293
+ instance.a = 2
294
+ eq! 2, instance.a
295
+ end
296
+ specify '#<attr>? is an additional predicate getter' do
297
+ klass.attribute(:a, 1).attributes(b: 2)
298
+ .predicate(:c, 3).predicates(d: 4)
299
+ instance = klass.new
300
+ raises!(NoMethodError) { instance.a? }
301
+ raises!(NoMethodError) { instance.b? }
302
+ instance.c?
303
+ instance.d?
304
+ end
305
+ specify '#<attr>? returns true or false based on what the value would do in a conditional' do
306
+ instance = klass.predicates(nil: nil, false: false, true: true, object: Object.new).new
307
+ eq! false, instance.nil?
308
+ eq! false, instance.false?
309
+ eq! true, instance.true?
310
+ eq! true, instance.object?
311
+ end
312
+ end
313
+
314
+ # include a fancy inspect with optional color?, optional width? tabular format?
315
+ describe 'inspection' do
316
+ class Example < described_class
317
+ attributes a: 1, b: "c"
318
+ end
319
+ it 'inspects prettily' do
320
+ eq! '#<HashStruct Example: {a: 1, b: "c"}>', Example.new.inspect
321
+ klass.attributes(c: /d/)
322
+ eq! '#<HashStruct.anon: {c: /d/}>', klass.new.inspect
323
+ end
324
+ end
325
+
326
+ describe 'pretty printed' do
327
+ require 'pp'
328
+
329
+ def pretty_inspect(attrs)
330
+ klass.for(attrs.keys.map &:intern)
331
+ .new(attrs)
332
+ .pretty_inspect
333
+ .chomp
334
+ end
335
+
336
+ class EmptySubclass < SeeingIsBelieving::HashStruct
337
+ end
338
+ it 'begins with instantiation of the class or HashStruct.anon { ... }.new(' do
339
+ eq! "HashStruct.anon { ... }.new()", klass.new.pretty_inspect.chomp
340
+ eq! "EmptySubclass.new()", EmptySubclass.new.pretty_inspect.chomp
341
+ end
342
+
343
+ it "uses 1.9 hash syntax" do
344
+ include! "key:", pretty_inspect(key: :value)
345
+ ninclude! "=>", pretty_inspect(key: :value)
346
+ end
347
+ it "puts the key/value pairs inline when they are short" do
348
+ include! "(key: :value)", pretty_inspect(key: :value)
349
+ end
350
+ it "puts the key/value pairs on their own indented line, when they are long" do
351
+ include! "(\n #{"k"*30}: \"#{"v"*30}\"\n)",
352
+ pretty_inspect("k"*30 => "v"*30)
353
+ end
354
+ it "indents the key/value pairs" do
355
+ attrs = {"a"*30 => "b"*30,
356
+ "c"*30 => "d"*30}
357
+ include! "(\n"\
358
+ " #{"a"*30}: \"#{"b"*30}\",\n"\
359
+ " #{"c"*30}: \"#{"d"*30}\"\n"\
360
+ ")",
361
+ pretty_inspect(attrs)
362
+ end
363
+ it "breaks the value onto an indented next line when long" do
364
+ include! " #{"a"*50}:\n \"#{"b"*50}\"",
365
+ pretty_inspect("a"*50 => "b"*50)
366
+ end
367
+ it "pretty prints the value" do
368
+ ary = [*1..25]
369
+ include! " k:\n [#{ary.map { |n| " #{n}"}.join(",\n").lstrip}]",
370
+ pretty_inspect(k: ary)
371
+ end
372
+ end
373
+
374
+ describe '#to_hash / #to_h' do
375
+ it 'returns a dup\'d Ruby hash of the internal attributes' do
376
+ klass.attributes(a: 1, b: 2)
377
+ eq!({a: 1, b: 3}, klass.new(b: 3).to_hash)
378
+ eq!({a: 3, b: 2}, klass.new(a: 3).to_h)
379
+
380
+ instance = klass.new
381
+ instance.to_h[:a] = :injected
382
+ eq!({a: 1, b: 2}, instance.to_h)
383
+ end
384
+ end
385
+
386
+ describe 'merge' do
387
+ before { klass.attributes(a: 1, b: 2, c: 3) }
388
+
389
+ it 'returns a new instance with the merged values overriding its own' do
390
+ merged = klass.new(b: -2).merge c: -3
391
+ eq! klass, merged.class
392
+ eq!({a: 1, b: -2, c: -3}, merged.to_h)
393
+ end
394
+
395
+ it 'does not modify the LHS or RHS' do
396
+ instance = klass.new b: -2
397
+ merge_hash = {c: -3}
398
+ instance.merge merge_hash
399
+ eq!({a: 1, b: -2, c: 3}, instance.to_h)
400
+ eq!({c: -3}, merge_hash)
401
+ end
402
+ end
403
+
404
+ describe 'enumerability' do
405
+ it 'is enumerable, iterating over each attribute(as symbol)/value pair' do
406
+ klass.attributes(a: 1, b: 2)
407
+ eq! [[:a, 1], [:b, 2]], klass.new.to_a
408
+ eq! "a1b2", klass.new.each.with_object("") { |(k, v), s| s << "#{k}#{v}" }
409
+ end
410
+ end
411
+
412
+ describe 'keys/values' do
413
+ specify 'keys returns an array of symbols of all its attributes' do
414
+ eq! [:a, :b], klass.attributes(a: 1, b: 2).new(b: 3).keys
415
+ end
416
+ specify 'values returns an array of symbol values' do
417
+ eq! [1, 3], klass.attributes(a: 1, b: 2).new(b: 3).values
418
+ end
419
+ end
420
+
421
+ describe '#key? / #has_key? / #include? / #member?' do
422
+ specify 'return true iff the key (symbolic or string) is an attribute' do
423
+ instance = klass.attributes(a: 1, b: nil, c: false).new
424
+ [:key?, :has_key?, :include?, :member?].each do |predicate|
425
+ [:a, :b, :c, 'a', 'b', 'c'].each do |key|
426
+ eq! true, instance.__send__(predicate, key), "#{instance.inspect}.#{predicate}(#{key.inspect}) returned false"
427
+ end
428
+ eq! false, instance.__send__(predicate, :d)
429
+ eq! false, instance.__send__(predicate, 'd')
430
+ eq! false, instance.__send__(predicate, /b/)
431
+ end
432
+ end
433
+ end
434
+
435
+ describe '#==' do
436
+ it 'is true if the RHS\'s to_h has the same key/value pairs' do
437
+ instance1 = described_class.for(a: 1, b: 2).new
438
+ instance2 = described_class.for(a: 1, b: 2).new
439
+ instance3 = described_class.for(a: 1, c: 2).new
440
+ eq! instance1, instance1
441
+ eq! instance1, instance2
442
+ eq! instance1, {a: 1, b: 2}
443
+ instance2.b = 3
444
+ neq! instance1, instance2
445
+ neq! instance1, instance3
446
+ neq! instance1, {a: 1}
447
+ neq! instance1, {a: 1, b: 2, c: 1}
448
+ eq! false, 1.respond_to?(:to_h)
449
+ eq! false, 1.respond_to?(:to_hash)
450
+ neq! instance1, 1
451
+ end
452
+ end
453
+
454
+ specify '#eql? is an alias of #==' do
455
+ instance1 = described_class.for(a: 1, b: 2).new
456
+ instance2 = described_class.for(a: 1, b: 2).new
457
+ expect(instance1).to eql instance2
458
+ end
459
+
460
+ specify '#hash is the same as Hash#hash' do
461
+ instance = described_class.for(a: 1, b: 2).new
462
+ eq! instance.hash, {a: 1, b: 2}.hash
463
+ end
464
+
465
+ it 'can be used in set methods, e.g. as a hash key' do
466
+ instance1 = described_class.for(a: 1, b: 2).new
467
+ instance2 = described_class.for(a: 1, b: 2).new
468
+ eq! [], [instance1] - [instance2]
469
+ eq! [], [instance2] - [instance1]
470
+ eq! [], [instance1] - [{a: 1, b: 2}]
471
+ eq! [], [{a: 1, b: 2}] - [instance1]
472
+ eq! [instance1], [instance1, instance2].uniq
473
+ end
474
+
475
+ specify 'accepts nil as a value (common edge case)' do
476
+ klass.attributes default_is_nil: nil, default_is_1: 1
477
+
478
+ # default init block
479
+ instance = klass.new
480
+ eq! nil, instance.default_is_nil
481
+ eq! nil, instance[:default_is_nil]
482
+
483
+ # overridden on initialization
484
+ instance = klass.new default_is_1: nil
485
+ eq! nil, instance.default_is_1
486
+ eq! nil, instance[:default_is_1]
487
+
488
+ # set with setter
489
+ instance = klass.new
490
+ instance.default_is_1 = nil
491
+ eq! nil, instance.default_is_1
492
+ eq! nil, instance[:default_is_1]
493
+
494
+ # set with []= and symbol
495
+ instance = klass.new
496
+ instance[:default_is_1] = nil
497
+ eq! nil, instance.default_is_1
498
+ eq! nil, instance[:default_is_1]
499
+
500
+ # set with []= and string
501
+ instance = klass.new
502
+ instance['default_is_1'] = nil
503
+ eq! nil, instance.default_is_1
504
+ eq! nil, instance[:default_is_1]
505
+
506
+ # set after its been set to nil
507
+ instance = klass.new
508
+ instance[:default_is_nil] = nil
509
+ instance[:default_is_nil] = nil
510
+ instance.default_is_nil = nil
511
+ instance.default_is_nil = nil
512
+ end
513
+ end
514
+ end