rdx 0.9.0.pre

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 (122) hide show
  1. checksums.yaml +7 -0
  2. data/.rdx +20 -0
  3. data/README +19 -0
  4. data/bin/rdx +7 -0
  5. data/examples/minimal/.rdx +8 -0
  6. data/examples/minimal/README +10 -0
  7. data/examples/minimal/lib/other_conventions.rb +64 -0
  8. data/examples/minimal/lib/the_basics.rb +94 -0
  9. data/examples/minimal/lib/using_directives.rb +66 -0
  10. data/examples/minimal/rakefile +27 -0
  11. data/examples/ruby-2.0.0-p0/README +7 -0
  12. data/examples/ruby-2.0.0-p0/install/core/.rdx +6 -0
  13. data/examples/ruby-2.0.0-p0/install/core/README +19 -0
  14. data/examples/ruby-2.0.0-p0/install/core/Rakefile +61 -0
  15. data/examples/ruby-2.0.0-p0/install/core/diffs/array.c.diff +166 -0
  16. data/examples/ruby-2.0.0-p0/install/core/diffs/bignum.c.diff +11 -0
  17. data/examples/ruby-2.0.0-p0/install/core/diffs/class.c.diff +36 -0
  18. data/examples/ruby-2.0.0-p0/install/core/diffs/compar.c.diff +11 -0
  19. data/examples/ruby-2.0.0-p0/install/core/diffs/complex.c.diff +301 -0
  20. data/examples/ruby-2.0.0-p0/install/core/diffs/cont.c.diff +65 -0
  21. data/examples/ruby-2.0.0-p0/install/core/diffs/dir.c.diff +147 -0
  22. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/re.rdoc.diff +328 -0
  23. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/security.rdoc.diff +8 -0
  24. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/standard_library.rdoc.diff +0 -0
  25. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/syntax.rdoc.diff +0 -0
  26. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/syntax/assignment.rdoc.diff +160 -0
  27. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/syntax/calling_methods.rdoc.diff +130 -0
  28. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/syntax/control_expressions.rdoc.diff +254 -0
  29. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/syntax/exceptions.rdoc.diff +0 -0
  30. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/syntax/literals.rdoc.diff +54 -0
  31. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/syntax/methods.rdoc.diff +157 -0
  32. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/syntax/miscellaneous.rdoc.diff +91 -0
  33. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/syntax/modules_and_classes.rdoc.diff +161 -0
  34. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/syntax/precedence.rdoc.diff +8 -0
  35. data/examples/ruby-2.0.0-p0/install/core/diffs/doc/syntax/refinements.rdoc.diff +146 -0
  36. data/examples/ruby-2.0.0-p0/install/core/diffs/encoding.c.diff +276 -0
  37. data/examples/ruby-2.0.0-p0/install/core/diffs/enum.c.diff +281 -0
  38. data/examples/ruby-2.0.0-p0/install/core/diffs/enumerator.c.diff +479 -0
  39. data/examples/ruby-2.0.0-p0/install/core/diffs/error.c.diff +143 -0
  40. data/examples/ruby-2.0.0-p0/install/core/diffs/eval.c.diff +47 -0
  41. data/examples/ruby-2.0.0-p0/install/core/diffs/eval_jump.c.diff +23 -0
  42. data/examples/ruby-2.0.0-p0/install/core/diffs/file.c.diff +752 -0
  43. data/examples/ruby-2.0.0-p0/install/core/diffs/gc.c.diff +195 -0
  44. data/examples/ruby-2.0.0-p0/install/core/diffs/hash.c.diff +84 -0
  45. data/examples/ruby-2.0.0-p0/install/core/diffs/iseq.c.diff +354 -0
  46. data/examples/ruby-2.0.0-p0/install/core/diffs/load.c.diff +53 -0
  47. data/examples/ruby-2.0.0-p0/install/core/diffs/marshal.c.diff +98 -0
  48. data/examples/ruby-2.0.0-p0/install/core/diffs/math.c.diff +110 -0
  49. data/examples/ruby-2.0.0-p0/install/core/diffs/numeric.c.diff +103 -0
  50. data/examples/ruby-2.0.0-p0/install/core/diffs/object.c.diff +295 -0
  51. data/examples/ruby-2.0.0-p0/install/core/diffs/pack.c.diff +18 -0
  52. data/examples/ruby-2.0.0-p0/install/core/diffs/parse.y.diff +23 -0
  53. data/examples/ruby-2.0.0-p0/install/core/diffs/proc.c.diff +155 -0
  54. data/examples/ruby-2.0.0-p0/install/core/diffs/random.c.diff +126 -0
  55. data/examples/ruby-2.0.0-p0/install/core/diffs/range.c.diff +49 -0
  56. data/examples/ruby-2.0.0-p0/install/core/diffs/rational.c.diff +312 -0
  57. data/examples/ruby-2.0.0-p0/install/core/diffs/re.c.diff +207 -0
  58. data/examples/ruby-2.0.0-p0/install/core/diffs/ruby.c.diff +21 -0
  59. data/examples/ruby-2.0.0-p0/install/core/diffs/signal.c.diff +67 -0
  60. data/examples/ruby-2.0.0-p0/install/core/diffs/sprintf.c.diff +29 -0
  61. data/examples/ruby-2.0.0-p0/install/core/diffs/string.c.diff +73 -0
  62. data/examples/ruby-2.0.0-p0/install/core/diffs/struct.c.diff +20 -0
  63. data/examples/ruby-2.0.0-p0/install/core/diffs/time.c.diff +691 -0
  64. data/examples/ruby-2.0.0-p0/install/core/diffs/transcode.c.diff +435 -0
  65. data/examples/ruby-2.0.0-p0/install/core/diffs/variable.c.diff +62 -0
  66. data/examples/ruby-2.0.0-p0/install/core/diffs/vm_backtrace.c.diff +164 -0
  67. data/examples/ruby-2.0.0-p0/install/core/diffs/vm_eval.c.diff +99 -0
  68. data/examples/ruby-2.0.0-p0/install/core/diffs/vm_method.c.diff +17 -0
  69. data/examples/ruby-2.0.0-p0/install/core/diffs/vm_trace.c.diff +393 -0
  70. data/examples/ruby-2.0.0-p0/install/stdlib/.rdx +6 -0
  71. data/examples/ruby-2.0.0-p0/install/stdlib/README +19 -0
  72. data/examples/ruby-2.0.0-p0/install/stdlib/Rakefile +53 -0
  73. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/abbrev.rb.diff +77 -0
  74. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/base64.rb.diff +42 -0
  75. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/benchmark.rb.diff +144 -0
  76. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/cmath.rb.diff +52 -0
  77. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/forwardable.rb.diff +150 -0
  78. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/mathn.rb.diff +58 -0
  79. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/matrix.rb.diff +657 -0
  80. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/observer.rb.diff +31 -0
  81. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/optparse.rb.diff +147 -0
  82. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/ostruct.rb.diff +78 -0
  83. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/prime.rb.diff +52 -0
  84. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/pstore.rb.diff +110 -0
  85. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/scanf.rb.diff +100 -0
  86. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/securerandom.rb.diff +144 -0
  87. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/set.rb.diff +637 -0
  88. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/shellwords.rb.diff +66 -0
  89. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/singleton.rb.diff +37 -0
  90. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/tempfile.rb.diff +104 -0
  91. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/thread.rb.diff +38 -0
  92. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/time.rb.diff +140 -0
  93. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/tmpdir.rb.diff +52 -0
  94. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/uri.rb.diff +39 -0
  95. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/uri/common.rb.diff +237 -0
  96. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/weakref.rb.diff +36 -0
  97. data/examples/ruby-2.0.0-p0/install/stdlib/diffs/lib/yaml/store.rb.diff +27 -0
  98. data/examples/ruby-2.0.0-p0/rakefile +165 -0
  99. data/lib/rdx.rb +331 -0
  100. data/lib/rdx/assertions.rb +484 -0
  101. data/lib/rdx/binding.rb +151 -0
  102. data/lib/rdx/code_object.rb +598 -0
  103. data/lib/rdx/comment.rb +338 -0
  104. data/lib/rdx/convention.rb +1174 -0
  105. data/lib/rdx/directive.rb +1432 -0
  106. data/lib/rdx/example.rb +679 -0
  107. data/lib/rdx/generator.rb +112 -0
  108. data/lib/rdx/generator/rdoc.rb +1006 -0
  109. data/lib/rdx/options.rb +359 -0
  110. data/lib/rdx/plain_text.rb +65 -0
  111. data/lib/rdx/reporter.rb +421 -0
  112. data/lib/rdx/ruby_lex.rb +324 -0
  113. data/lib/rdx/runner.rb +309 -0
  114. data/lib/rdx/source_file.rb +94 -0
  115. data/lib/rdx/specification.rb +194 -0
  116. data/lib/rdx/statement.rb +248 -0
  117. data/lib/rdx/store.rb +119 -0
  118. data/lib/rdx/task.rb +361 -0
  119. data/lib/rdx/text.rb +688 -0
  120. data/lib/rdx/version.rb +15 -0
  121. data/rakefile +64 -0
  122. metadata +203 -0
@@ -0,0 +1,331 @@
1
+ #
2
+ # = RDX: Ruby Documentation example eXecutor
3
+ #
4
+ # The aim of RDX is to join the worlds of documentation and testing, whose overlap in examples.
5
+ # Relying on the RDoc documentation generator tool and Minitest RDX parses source files,
6
+ # generates documentation, extracts examples from comments and executes them as tests.
7
+ #
8
+ # == Why RDX?
9
+ #
10
+ # The whole application is built on these four cornerstones:
11
+ # * Document Well and Right!
12
+ #
13
+ # The library should be well documented. The examples can be the most direct way
14
+ # to explain and show the use of the library - if these have been unit tested and work fine.
15
+ # * DRY (Don't Repeat Yourself)!
16
+ #
17
+ # We want to avoid - honestly, we should hate - writing the same things more than once.
18
+ # It's too tedious to translate back and forth all the examples between documentation and testing.
19
+ # * Join coupled parts!
20
+ #
21
+ # Source, documentation and tests are different sides of the same thing - the programmer's idea -.
22
+ # When some parts are tightly coupled, the more these are close the more straightforward the eventual
23
+ # fix is. Documentation tools unify documentation with the source, RDX tries to take the next step.
24
+ # * Be Consistent!
25
+ #
26
+ # Most scripts of the Ruby Core and many of the Standard Library already have the examples
27
+ # written in a conformed and accepted way. It was this coherency that allowed (and induced)
28
+ # me to write RDX: a tool can easily automate tasks if they fit well into a general scheme.
29
+ #
30
+ # == How RDX works
31
+ #
32
+ # In order to achieve its goal it first deals with the documentation tool
33
+ # (RDoc is the most commonly used, so currently only its support is built-in) asking to keep
34
+ # track of examples. These are parsed and subdivided into statements: the comment at
35
+ # their end can eventually become an expectation. The tests are build according to
36
+ # conventions and directives.
37
+ #
38
+ # The *conventions*, heart of RDX, define the rules to accept the ending comments and process the code
39
+ # (for example the historical hash-rocket notation, <tt>#=></tt>, is implemented in Convention::result_eval:
40
+ # it evaluates both the statement and the expectation, then compares those results through +assert_equal+).
41
+ # See more details in the RDX::Convention class.
42
+ #
43
+ # While conventions affect single statements, the *directives* operate on a larger scale.
44
+ # Perhaps we may want to not execute an example, to interpret one as the output of the
45
+ # previous, to locally change some of the assertions methods, to run it in a temporary directory,
46
+ # to simulate a bash environment, and so on...
47
+ # See the RDX::Directive class for more information about them.
48
+ #
49
+ # Both the conventions and directives can be either built-in because of their popularity in the
50
+ # Ruby Community or defined for convenience by the user.
51
+ #
52
+ # Once these tests are built, the only thing left to do is to simply run them.
53
+ # As a bonus, if everything is correct, RDX's signature is added to those made by the generator
54
+ # tool: the user of our library now can know that the examples showed enjoy a high level of trust.
55
+ # All this with a single run, either from the command line or through rake.
56
+ #
57
+ # == Conclusions
58
+ #
59
+ # When RDX has finished we can see the report of our tests, as if we had manually written all of
60
+ # them refactoring the comments. In fact, it's even better: if an example has something wrong - if it
61
+ # doesn't work as declared - the error points us to its location in the comment, which is just above
62
+ # the source code. This makes the tracking and fixing steps inceadibly easy.
63
+ #
64
+ # Through execution, <b>RDX gives formalism to the documentation examples</b>,
65
+ # greatly increasing their strength. It gives all the section of a source file the deserved emphasis,
66
+ # transforming fictitious examples into living ones!
67
+ #
68
+ # A great point in its favour is that a developer familiar with the Ruby documentation
69
+ # (of course most of them) doesn't have to learn a new DSL or syntax, but just
70
+ # to code and document in the "standard" way, with very few alteration.
71
+ #
72
+ # == Security
73
+ #
74
+ # The point that +RDX+ evaluates the comments leads to the topic of security.
75
+ # Never run blindly RDX on libraries that haven't been written for that, since it's never
76
+ # a good idea to evaluate unknown code.
77
+ # If you want, fisrt you have to scan the files and find the examples
78
+ # (the <tt>--dry-run</tt> option may help you, though it isn't fully secure).
79
+ # Once you have found that the code is safe you can give RDX a try.
80
+ #
81
+ # However, if a library has been built RDX-compatible, you can run it
82
+ # from your machine - if you _trust_ the producer. If you don't
83
+ # you must be very careful: even a plain +require+ can be dangerous,
84
+ # with RDX you have to pay an extra attention to the comments.
85
+ #
86
+ # == Roadmap
87
+ #
88
+ # We have already talked about conventions, directives and their relative classes.
89
+ # These are probably the most intresting topics.
90
+ #
91
+ # If you want to define your own conventions and directives, see the Specification.
92
+ #
93
+ # If you want to know the available command line options, see Options.
94
+ #
95
+ # If you want to run RDX through +rake+, see Task.
96
+ #
97
+ # The role of other classes is explained in these sections.
98
+ #
99
+ # == Credits
100
+ #
101
+ # I have ideated RDX and I am developing it. <br>
102
+ # I think this is the best way I have to thank Yukihiro "Matz" Matsumoto
103
+ # for the beatiful language he has designed.
104
+ #
105
+ # Maurizio Destefanis
106
+ #
107
+ module RDX
108
+
109
+ # :stopdoc:
110
+
111
+ require 'stringio'
112
+ require 'tempfile'
113
+ require 'pathname'
114
+
115
+ ROOT = File.dirname(__FILE__).freeze
116
+
117
+ require 'rdx/version'
118
+
119
+ VERSION_HEADER = "RDX version: #{VERSION}".freeze
120
+
121
+ HOMEPAGE = 'https://rubygems.org/gems/rdx'.freeze
122
+
123
+ SIGNATURE = "Examples tested with <a href='#{HOMEPAGE}'>RDX</a> #{VERSION}".freeze
124
+
125
+
126
+ Error = Class.new RuntimeError
127
+
128
+ # Raised when invalid code is processed.
129
+ class ParseError < RuntimeError
130
+ attr_accessor :line_no
131
+ def initialize msg=nil, line_no=nil
132
+ super msg
133
+ @line_no = line_no
134
+ end
135
+ end
136
+
137
+ class Assertion < Exception
138
+ end
139
+
140
+ class << self
141
+ attr_accessor :backtrace_filter
142
+ end
143
+ class BacktraceFilter # :nodoc:
144
+ def filter bt
145
+ return ['No backtrace'] if bt.empty?
146
+ return bt.take_while{ |line| !line.start_with?(RDX::ROOT) } unless RDX.options.debug
147
+ # We are not debugging launching tools (eg: rake)
148
+ last_index = bt.rindex{ |line| line.start_with?(RDX::ROOT) } || bt.size-1
149
+ bt.take last_index+1
150
+ end
151
+ end
152
+ self.backtrace_filter = BacktraceFilter.new
153
+ def self.filter_backtrace bt
154
+ backtrace_filter.filter bt
155
+ end
156
+
157
+ def self.runner
158
+ Runner.current
159
+ end
160
+
161
+ def self.options
162
+ Options.current
163
+ end
164
+
165
+ def self.generator
166
+ Generator.current
167
+ end
168
+
169
+ def self.active?
170
+ Runner.active?
171
+ end
172
+
173
+ def self.passed?
174
+ Runner.passed?
175
+ end
176
+
177
+ def self.run(argv)
178
+ Runner.run(argv)
179
+ end
180
+
181
+ # :startdoc:
182
+
183
+ #
184
+ # Contains various useful stuff.
185
+ #
186
+ module Util
187
+
188
+ module_function
189
+
190
+ # :stopdoc:
191
+
192
+ def runner
193
+ RDX.runner
194
+ end
195
+
196
+ def options
197
+ RDX.options
198
+ end
199
+
200
+ def generator
201
+ RDX.generator
202
+ end
203
+
204
+ # :startdoc:
205
+
206
+ #
207
+ # Returns the number of seconds elapsed during the execution of the given block.
208
+ #
209
+ def measure_time
210
+ t0 = Time.now
211
+ yield
212
+ return Time.now - t0
213
+ end
214
+
215
+ #
216
+ # Run the given block with the current directory set to a temporary one.
217
+ #
218
+ def in_tmpdir *args
219
+ tmpdir = Dir.mktmpdir *args
220
+ begin
221
+ result = nil
222
+ Dir.chdir tmpdir do
223
+ result = yield(tmpdir)
224
+ end
225
+ result
226
+ ensure
227
+ FileUtils.remove_entry tmpdir, true
228
+ end
229
+ end
230
+
231
+ #
232
+ # Changes <tt>$stdout</tt> and <tt>$stderr</tt> to the given +IO+ streams _new_out_ and _new_err_
233
+ # during the execution of the block.
234
+ # If +nil+ is provided that stream will be discarded;
235
+ # if +false+ is given that stream will not be changed.
236
+ #
237
+ def change_ostreams new_out, new_err
238
+ new_out = StringIO.new if new_out.nil?
239
+ new_err = StringIO.new if new_err.nil?
240
+ begin
241
+ orig_stdout,$stdout = $stdout,new_out if new_out
242
+ orig_stderr,$stderr = $stderr,new_err if new_err
243
+ yield
244
+ ensure
245
+ $stdout = orig_stdout if new_out
246
+ $stderr = orig_stderr if new_err
247
+ end
248
+ end
249
+
250
+ #
251
+ # Captures <tt>$stdout</tt> and <tt>$stderr</tt> produced by the block into strings and returns them.
252
+ # If _stdout_ or _stderr_ is +nil+ or +false+ that stream will not be captured.
253
+ #
254
+ def capture_ostreams stdout=true, stderr=true, &blk
255
+ stdout = stdout ? StringIO.new : false
256
+ stderr = stderr ? StringIO.new : false
257
+ change_ostreams(stdout,stderr,&blk)
258
+ return [stdout && stdout.string, stderr && stderr.string]
259
+ end
260
+
261
+ #
262
+ # Changes <tt>$stdin</tt> to the given +String+ _in_str_
263
+ # during the execution of the block.
264
+ #
265
+ def change_stdin in_str
266
+ stdin = StringIO.new(in_str.to_str)
267
+ begin
268
+ orig_stdin,$stdin = $stdin,stdin
269
+ yield
270
+ ensure
271
+ $stdin = orig_stdin
272
+ end
273
+ end
274
+
275
+ def restore_instance_method klass, meth # :nodoc:
276
+ name = meth.name
277
+ owner = meth.owner
278
+ unless owner == klass
279
+ klass.__send__ :remove_method, name
280
+ else
281
+ klass.__send__ :define_method, name, meth
282
+ end
283
+ end
284
+
285
+ def restore_singleton_method receiver, orig_singleton_method # :nodoc:
286
+ receiver_singleton_class = class<<receiver; self; end
287
+ restore_instance_method receiver_singleton_class, orig_singleton_method.unbind
288
+ end
289
+
290
+ def patch_singleton_method receiver, name, new_method # :nodoc:
291
+ name = name.to_sym
292
+ new_method = new_method.to_proc
293
+ orig_method = receiver.method(name)
294
+ receiver.define_singleton_method name do |*args,&blk|
295
+ new_method.call(orig_method,*args,&blk)
296
+ end
297
+ begin
298
+ yield
299
+ ensure
300
+ restore_singleton_method receiver, orig_method
301
+ end
302
+ end
303
+
304
+ end
305
+
306
+ gem 'minitest'
307
+ gem 'rdoc', '= 4.0.0'
308
+
309
+ require 'rdx/runner'
310
+
311
+ autoload :Options, 'rdx/options'
312
+ autoload :Store, 'rdx/store'
313
+ autoload :Generator, 'rdx/generator'
314
+ autoload :Task, 'rdx/task'
315
+
316
+ autoload :Convention, 'rdx/convention'
317
+ autoload :Directive, 'rdx/directive'
318
+ autoload :Specification, 'rdx/specification'
319
+
320
+ autoload :Assertions, 'rdx/assertions'
321
+ autoload :Binding, 'rdx/binding'
322
+ autoload :SourceFile, 'rdx/source_file'
323
+ autoload :Comment, 'rdx/comment'
324
+ autoload :PlainText, 'rdx/plain_text'
325
+ autoload :Example, 'rdx/example'
326
+ autoload :Statement, 'rdx/statement'
327
+
328
+ autoload :Text, 'rdx/text'
329
+ autoload :RubyLex, 'rdx/ruby_lex'
330
+
331
+ end
@@ -0,0 +1,484 @@
1
+
2
+ module RDX
3
+
4
+ #
5
+ # RDX uses the assertions of the standard testing framework of ruby, +Minitest+.
6
+ # This module adapt its functionality to RDX's needs.
7
+ # The most important new features are:
8
+ # * #assert_equal_numeric: more powerful than +assert_equal+ if +Numeric+ are involved;
9
+ # * #assert_string_match: perfect when comparing an actual +String+ with an expected +String+ or +Regexp+;
10
+ # * #assert_msg_raised: checks the exception message in alternative of or along to the exception class;
11
+ # * #assert_same_type: expected and actual should be "similar";
12
+ # * #assert_*_but family: it's possible to do some normalization before the assertion.
13
+ #
14
+ #-- rdx
15
+ # :rdx: setup
16
+ # include RDX::Assertions
17
+ #
18
+ module Assertions
19
+
20
+ begin
21
+ gem 'minitest', '>= 5.0'
22
+ require 'minitest/assertions'
23
+ rescue Gem::LoadError
24
+ gem 'minitest'
25
+ require 'minitest/unit'
26
+ ::Minitest = ::MiniTest unless Object.const_defined?(:Minitest) # For minitest < 2.12
27
+ end
28
+
29
+ include Minitest::Assertions
30
+
31
+ extend self
32
+
33
+ PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException, SystemExit] # :nodoc:
34
+
35
+ # Doesn't count assertions (done in CodeObject::Runnable#trace_assertion).
36
+ def assert test, msg = nil
37
+ return true if test
38
+ msg ||= "Failed assertion, no message given."
39
+ msg = msg.call if Proc === msg
40
+ raise Assertion, msg
41
+ end
42
+
43
+ #
44
+ # Same as +assert_equal+, but uses #normalize_values (passing _normalize_.+to_proc+) before the comparison.
45
+ #
46
+ # act = "a string"
47
+ # exp = "A string"
48
+ # assert_equal exp, act # fails!
49
+ # assert_equal_but :capitalize, exp, act # passes
50
+ #
51
+ def assert_equal_but normalize, exp, act, msg=nil
52
+ exp,act = normalize_values(exp,act,&normalize)
53
+ msg = message(msg,''){ diff exp, act }
54
+ assert exp==act, msg
55
+ end
56
+
57
+ # Overrided to implicitly call #normalize_values.
58
+ def assert_equal *args, &blk
59
+ assert_equal_but nil, *args, &blk
60
+ end
61
+
62
+ # This should be used in libraries, because +assert_equal+ can get patched.
63
+ alias rdx_assert_equal assert_equal # :nodoc:
64
+ private :rdx_assert_equal
65
+
66
+ #
67
+ # Fails unless _comparison_ <tt>===</tt> _target_.
68
+ #
69
+ # assert_relation 'a string', String # passes
70
+ # assert_relation 'a string', /str/ # passes
71
+ # assert_relation 3, 1..4 # passes
72
+ #
73
+ def assert_relation target, comparison, msg=nil
74
+ msg = message msg do
75
+ "Expected #{mu_pp(comparison)} === #{mu_pp(target)} to be true"
76
+ end
77
+ assert comparison===target, msg
78
+ end
79
+
80
+ #
81
+ # Fails unless _exp_ and _act_ aren't instances of the same class.
82
+ #
83
+ # assert_same_class "A string", "Another string" # passes
84
+ # assert_same_class 1, 2**64 # fails! # Fixnum and Bignum
85
+ # assert_same_class Complex(1,1), Complex(1.0,1) # passes
86
+ # assert_same_class [1], ['A'] # passes
87
+ #
88
+ def assert_same_class exp, act, msg=nil
89
+ msg = message msg do
90
+ "Expected #{mu_pp(exp)} and #{mu_pp(act)} to be instances of the same class, not #{exp.class} and #{act.class}"
91
+ end
92
+ assert exp.class==act.class, msg
93
+ end
94
+
95
+ #
96
+ # Resembles #assert_same_class but with a few differences:
97
+ # * It's more permissive with similar objects:
98
+ # assert_same_type Object.new, nil # passes # nil is the wildcard, because it's quite common
99
+ # assert_same_type 1, 2**64 # passes # Fixnum and Bignum are Integer
100
+ # assert_same_type true, false # passes # both are booleans
101
+ # * It's more strict with +Enumerable+ objects, where +assert_same_type+
102
+ # is called again with some of their elements:
103
+ # assert_same_type [1], ['A'] # fails! # the first element for non-empty arrays
104
+ # assert_same_type( {:a=>1}, {:b=>:c} ) # fails! # the first pair for non-empty hashes
105
+ # assert_same_type 1..2, 1..2.0 # fails! # begin and end for ranges
106
+ # assert_same_type [1].each, [:a].each # fails! # the first element for other enumerables
107
+ #
108
+ def assert_same_type exp, act, msg=nil
109
+ return pass if nil==exp || nil==act
110
+ if ([true,false] & [exp,act]).size==2
111
+ return pass
112
+ end
113
+ exp_class = exp.class
114
+ act_class = act.class
115
+ if ([Fixnum,Bignum] & [exp_class,act_class]).size==2
116
+ return pass
117
+ end
118
+ assert_same_class exp, act, msg
119
+ return unless Enumerable > exp_class
120
+ if Array==exp_class && !exp.empty? && !act.empty?
121
+ assert_same_type exp.first, act.first, message(msg,''){ 'First element of Array' }
122
+ elsif Hash==exp_class && !exp.empty? && !act.empty?
123
+ assert_same_type exp.first[0], act.first[0], message(msg,''){ 'First key of Hash' }
124
+ assert_same_type exp.first[1], act.first[1], message(msg,''){ 'First value of Hash' }
125
+ elsif Range==exp_class
126
+ assert_same_type exp.begin, act.begin, message(msg,''){ 'Begin of Range' }
127
+ assert_same_type exp.end, act.end, message(msg,''){ 'End of Range' }
128
+ else
129
+ assert_same_type exp.first, act.first, message(msg,''){ 'First element of Enumerable' }
130
+ end
131
+ end
132
+
133
+ #
134
+ # More suitable for +Numeric+ in contrast to +assert_equal+ because:
135
+ # * type checking is enforced according to #assert_same_type
136
+ # assert_equal 1, 1.0 # passes
137
+ # assert_equal_numeric 1, 1.0 # fails!
138
+ # assert_equal Complex(1,1), Complex(1,1.0) # passes
139
+ # assert_equal_numeric Complex(1,1), Complex(1,1.0) # fails!
140
+ # * accuracy is reduced in float comparison: #assert_in_epsilon is used
141
+ # assert_equal 1.0, 1.0+1e-10 # fails!
142
+ # assert_equal_numeric 1.0, 1.0+1e-10 # passes
143
+ # assert_equal_numeric 1.0, 1.0+1e-10, 0 # fails!
144
+ #
145
+ def assert_equal_numeric exp, act, epsilon=1e-6, msg=nil
146
+ assert_same_type exp, act, msg
147
+ case exp
148
+ when Float
149
+ assert_in_epsilon exp, act, epsilon, msg
150
+ when Complex
151
+ assert_same_type exp.real, act.real, message(msg,''){ 'Real part of Complex' }
152
+ assert_same_type exp.imag, act.imag, message(msg,''){ 'Imag part of Complex' }
153
+ assert_equal_numeric exp.real, act.real, epsilon, msg
154
+ assert_equal_numeric exp.imag, act.imag, epsilon, msg
155
+ else
156
+ rdx_assert_equal exp, act, msg
157
+ end
158
+ end
159
+
160
+ #
161
+ # Same as +assert_match+, but uses #normalize_values (passing _normalize_.+to_proc+)
162
+ # before the comparison.
163
+ #
164
+ # normalize = lambda do |arg|
165
+ # arg.respond_to?(:to_str) ? arg.to_str.strip : arg
166
+ # end
167
+ # re = /\A\w+\z/
168
+ # str = " a_word "
169
+ # assert_match re, str # fails!
170
+ # assert_match_but normalize, re, str # passes
171
+ #
172
+ def assert_match_but normalize, matcher, obj, msg=nil
173
+ matcher,obj = normalize_values matcher, obj, &normalize
174
+ msg = message msg do
175
+ "Expected #{mu_pp matcher} to match #{mu_pp obj}"
176
+ end
177
+ assert matcher =~ obj, msg
178
+ end
179
+
180
+ # Overrided to implicitly call #normalize_values.
181
+ def assert_match *args, &blk
182
+ assert_match_but nil, *args, &blk
183
+ end
184
+
185
+ #
186
+ # Same as #assert_string_match, but uses #normalize_values (passing _normalize_.+to_proc+)
187
+ # before the comparison.
188
+ #
189
+ # act = "a string"
190
+ # exp = /str$/
191
+ # normalize = lambda do |arg|
192
+ # arg.is_a?(String) ? arg.chomp('ing') : arg
193
+ # end
194
+ # assert_string_match exp, act # fails!
195
+ # assert_string_match_but normalize, exp, act # passes
196
+ #
197
+ def assert_string_match_but normalize, matcher, str, msg=nil
198
+ if matcher.respond_to?(:to_str)
199
+ assert_equal_but normalize, matcher.to_str, str.to_str, msg
200
+ else
201
+ assert_match_but normalize, matcher, str.to_str, msg
202
+ end
203
+ end
204
+
205
+ #
206
+ # Usable when _act_ is a +String+.
207
+ # If _exp_ is a +Regexp+ uses #assert_match, otherwise (including a +String+) uses #assert_equal.
208
+ #
209
+ # assert_string_match 'string', 'string' # passes
210
+ # assert_string_match 'ing', 'string' # fails!
211
+ # assert_string_match /ing/, 'string' # passes
212
+ # assert_string_match 'string', /rgxp/ # raises!
213
+ #
214
+ def assert_string_match exp, act, msg=nil
215
+ assert_string_match_but nil, exp, act, msg
216
+ end
217
+
218
+ #
219
+ # Same as +assert_output+, but uses #normalize_values (passing _normalize_.+to_proc+)
220
+ # before the comparison.
221
+ #
222
+ # assert_output 'Hey!' do puts "Hey!" end # fails! # Does the trailing newline really matter?
223
+ # assert_output_but :chomp, 'Hey!' do puts "Hey!" end # passes
224
+ # strip_and_capitalize = lambda{ |str| str.strip.capitalize }
225
+ # assert_output_but strip_and_capitalize, "A word" do puts " a word " end # passes
226
+ #
227
+ def assert_output_but normalize, exp_out=nil, exp_err=nil, out_msg='In stdout', err_msg='In stderr', &blk
228
+ act_out,act_err = capture_io(exp_out,exp_err,&blk)
229
+ assert_string_match_but normalize, exp_out, act_out, out_msg if exp_out
230
+ assert_string_match_but normalize, exp_err, act_err, err_msg if exp_err
231
+ end
232
+
233
+ # Overrided to implicitly call #normalize_values.
234
+ def assert_output *args, &blk
235
+ assert_output_but nil, *args, &blk
236
+ end
237
+
238
+ #
239
+ # Same as +assert_silent+, but uses #normalize_values (passing _normalize_.+to_proc+)
240
+ # before the comparison.
241
+ #
242
+ # assert_silent do print " " end # fails!
243
+ # assert_silent_but :strip do print " " end # passes
244
+ #
245
+ def assert_silent_but normalize, out_msg=nil, err_msg=nil, &blk
246
+ assert_output_but normalize, '', '', out_msg, err_msg, &blk
247
+ end
248
+
249
+ # Overrided to implicitly call #normalize_values.
250
+ def assert_silent *args, &blk
251
+ assert_silent_but nil, *args, &blk
252
+ end
253
+
254
+ #
255
+ # The block should raise an exception listed in _excs_, or a generic +StandardError+ if no
256
+ # exceptions are given. The last argument given can be a message containing additional failure details.
257
+ # If the raised exception isn't expected two things may happen:
258
+ # * if it's a +StandardError+ a failure will be reported:
259
+ # assert_raises(ArgumentError){ raise NameError } # fails!
260
+ # * otherwise it's re-raised, eventually reporting an error:
261
+ # assert_raises(NameError){ raise LoadError } # raises LoadError
262
+ #
263
+ # Returns the exception matched so you can check the message, attributes, etc..
264
+ #
265
+ def assert_raises *excs
266
+ msg = "#{excs.pop.to_str}.\n" if excs.last.respond_to?(:to_str)
267
+ excs << StandardError if excs.empty?
268
+ begin
269
+ yield
270
+ rescue Exception => raised
271
+ if excs.any?{ |ex| raised.kind_of? ex }
272
+ pass
273
+ return raised
274
+ elsif raised.is_a?(StandardError)
275
+ flunk exception_details(raised, "#{msg}#{mu_pp(excs)} exception expected, not")
276
+ else # Unhandled generic Exception
277
+ raise raised
278
+ end
279
+ else
280
+ flunk "#{msg}#{mu_pp(excs)} expected but nothing was raised."
281
+ end
282
+ end
283
+ alias assert_raise assert_raises
284
+
285
+ #
286
+ # The block should not raise an exception listed in _excs_, or a generic +StandardError+ if no
287
+ # exceptions are given. The last argument given can be a message containing additional failure details.
288
+ # If the raised exception isn't expected two things may happen:
289
+ # * if it's a +StandardError+ a failure will be reported:
290
+ # assert_nothing_raised(ArgumentError){ raise NameError } # fails!
291
+ # * otherwise it's re-raised, eventually reporting an error:
292
+ # assert_nothing_raised(NameError){ raise LoadError } # raises LoadError
293
+ #
294
+ def assert_nothing_raised *excs
295
+ msg = "#{excs.pop.to_str}.\n" if excs.last.respond_to?(:to_str)
296
+ excs << StandardError if excs.empty?
297
+ begin
298
+ yield
299
+ rescue Exception => raised
300
+ specified = excs.any?{ |ex| raised.kind_of? ex }
301
+ if specified || raised.is_a?(StandardError)
302
+ msg = message msg do
303
+ "Unexpected exception raised:\n#{exception_details raised}"
304
+ end
305
+ flunk msg
306
+ else # Unhandled generic Exception
307
+ raise raised
308
+ end
309
+ else
310
+ pass
311
+ end
312
+ end
313
+
314
+ #
315
+ # Same as +assert_msg_raised+, but uses #normalize_values (passing _normalize_.+to_proc+)
316
+ # before the comparison.
317
+ #
318
+ # exp_msg = "Can't modify frozen String"
319
+ # assert_msg_raised( exp_msg){ "".freeze << 'a' } # fails!
320
+ # assert_msg_raised_but(:capitalize,exp_msg){ "".freeze << 'a' } # passes
321
+ #
322
+ def assert_msg_raised_but normalize, exp_msg, *excs, &blk
323
+ whole = [false,true].include?(excs.first) ? excs.shift : true
324
+ raised = assert_raises(*excs,&blk)
325
+ if exp_msg
326
+ act_msg = raised.message
327
+ if exp_msg.respond_to?(:to_str)
328
+ exp_msg,act_msg = normalize_values(exp_msg.to_str,act_msg,&normalize)
329
+ if whole
330
+ assert_equal exp_msg, act_msg, 'Wrong exception message'
331
+ else
332
+ # Better than the message of assert_match implicitly called
333
+ msg = message 'Wrong exception message (expected should be included in actual)' do
334
+ diff exp_msg, act_msg
335
+ end
336
+ assert act_msg=~/(?<!\w)#{Regexp.escape(exp_msg)}(?!\w)/, msg if exp_msg=~/\S/
337
+ end
338
+ else
339
+ assert_match_but normalize, exp_msg, act_msg, 'Wrong message for exception'
340
+ end
341
+ end
342
+ return raised
343
+ end
344
+
345
+ #
346
+ # First uses #assert_raises passing the exceptions _excs_ and the given block.
347
+ #
348
+ # If _exp_msg_ isn't +nil+ the check of the exception message is also performed:
349
+ # this can be a +String+ (which is interpreted literally) or a +Regexp+ (which should match).
350
+ #
351
+ # If after the string message is sent the parameter +false+ the message given (expected)
352
+ # doesn't have to be exact: it can be only a part of the actual one.
353
+ #
354
+ # assert_raises{ [].freeze << 1 } # passes # but we may need more information...
355
+ # assert_msg_raised("frozen Array"){ [].freeze << 1 } # fails! # the message isn't complete
356
+ # assert_msg_raised("frozen Array",false){ [].freeze << 1 }
357
+ # # passes # the message can be only a part
358
+ # re = /number of arguments \(\d for \d\)/
359
+ # assert_msg_raised(re,ArgumentError){ 12.floor(2) } # passes
360
+ #
361
+ def assert_msg_raised exp_msg, *excs, &blk
362
+ assert_msg_raised_but nil, exp_msg, *excs, &blk
363
+ end
364
+
365
+ #
366
+ # :section: Helper methods
367
+ #
368
+
369
+ ##
370
+ # :method:
371
+ #
372
+ # Proxy to RDX::Util.capture_ostreams.
373
+ #
374
+ define_method :capture_io, RDX::Util.method(:capture_ostreams).to_proc
375
+
376
+ #
377
+ # Returns _exp_ and _act_ in an +Array+ normalized through the block given.
378
+ # This method can be overridded to modify the normalization.
379
+ #
380
+ def normalize_values exp, act, &normalize
381
+ if normalize
382
+ exp = normalize.call(exp)
383
+ act = normalize.call(act)
384
+ end
385
+ [exp,act]
386
+ end
387
+
388
+ #
389
+ # This differs from the original implementation in that hexadecimal values are not
390
+ # generalized in this step.
391
+ #--
392
+ # really escaped newlines are expanded (and not a simple backslash followed by an 'n')
393
+ #
394
+ def mu_pp_for_diff obj
395
+ mu_pp(obj).
396
+ gsub(/\\(?:n()|.)/){ |seq| $1 ? "\n" : seq }
397
+ end
398
+
399
+ # Returns details for exception _exc_.
400
+ def exception_details exc, msg=nil
401
+ [
402
+ msg.to_s,
403
+ "Class: #{exc.class}",
404
+ "Message: #{exc.message}",
405
+ "---Backtrace---",
406
+ *exc.backtrace,
407
+ "---------------",
408
+ ""
409
+ ].join "\n"
410
+ end
411
+
412
+ class << self
413
+ # The system +diff+ command used in +assert_equal+.
414
+ attr_accessor :diff
415
+ end
416
+ def self.use_default_diff
417
+ self.diff = if system("diff", __FILE__, __FILE__)
418
+ 'diff -u'
419
+ elsif system("gdiff", __FILE__, __FILE__)
420
+ 'gdiff -u'
421
+ else
422
+ nil
423
+ end
424
+ end
425
+
426
+ # This no more relies on Minitest
427
+ def diff exp, act
428
+ expect = mu_pp_for_diff exp
429
+ butwas = mu_pp_for_diff act
430
+ result = nil
431
+
432
+ need_to_diff =
433
+ Assertions.diff &&
434
+ (expect.include?("\n") ||
435
+ butwas.include?("\n") ||
436
+ expect.size > 30 ||
437
+ butwas.size > 30 ||
438
+ expect == butwas)
439
+
440
+ return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless
441
+ need_to_diff
442
+
443
+ if exp.is_a?(String) && act.is_a?(String)
444
+ # leave the quotes on their own lines, so that diff compares the real content of the string
445
+ expect.insert(1,"\n").insert(-2,"\n")
446
+ butwas.insert(1,"\n").insert(-2,"\n")
447
+ end
448
+
449
+ Tempfile.open("expect") do |a|
450
+ a.puts expect
451
+ a.flush
452
+
453
+ Tempfile.open("butwas") do |b|
454
+ b.puts butwas
455
+ b.flush
456
+
457
+ result = `#{Assertions.diff} "#{a.path}" "#{b.path}"`
458
+ result.sub!(/^\-\-\- .+/, "--- expected")
459
+ result.sub!(/^\+\+\+ .+/, "+++ actual")
460
+
461
+ if result.empty? then
462
+ klass = exp.class
463
+ result = [
464
+ "No visible difference in the #{klass}#inspect output.\n",
465
+ "You should look at the implementation of #== on ",
466
+ "#{klass} or its members.\n",
467
+ expect,
468
+ ].join
469
+ end
470
+ end
471
+ end
472
+
473
+ result
474
+ end
475
+
476
+ # No need, since parallelization does not occurr...
477
+ def synchronize
478
+ yield
479
+ end
480
+ alias _synchronize synchronize
481
+
482
+ end
483
+
484
+ end