dev-utils 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,253 @@
1
+
2
+ !header dev-utils/test main
3
+
4
+ h1. Unit Test Organisation
5
+
6
+ !toc
7
+
8
+ <div class="planned">
9
+
10
+ (Everything in this section is _planned_ functionality.)
11
+
12
+ h2. Introduction
13
+
14
+ Unit testing can be a pain to organise.
15
+ * You have to tell your test code where to find the library code that it's
16
+ testing.
17
+ * You need a mechanism for running all your tests at once, but would also like
18
+ to be able to run just one, or a few.
19
+ * If the tests need data, you need to organise that as well. Sometimes you
20
+ need to work on a copy of the data so you don't muck it up for the next
21
+ test run.
22
+
23
+ All of these organisational hassles are dealt with by the @dev-utils/test@
24
+ package. It provides a method for running your test suite (specifying which
25
+ tests if you like), which manages the load path. It also provides a method
26
+ for accessing test data in a flexible way. These only work if you follow some
27
+ conventions (detailed below). So the final benefit provided by
28
+ @dev-utils/test@ is a documented approach to managing the unit testing
29
+ part of any project.
30
+
31
+ The whole point of @dev-utils/test@ is to collect a certain unit testing
32
+ organisation methodology in one place, and make that reusable across projects.
33
+ This is better than having to roll your own on each new project, usually
34
+ without a clear strategy.
35
+
36
+ This document contains:
37
+ * the rules for using @dev-utils/test@ with your project;
38
+ * the @dev-utils/test@ approach in detail;
39
+ * the ground rules to follow so @dev-utils/test@ can work with your project;
40
+ * configuration options in case you want to vary some things;
41
+ * information about the complete sample project included in the distribution.
42
+
43
+ p(comment). I'll probably forget about a "complete sample project". It should
44
+ be easy enough to use without that. Perhaps refer to a live project that uses
45
+ it.
46
+
47
+
48
+ h2. A @dev-utils/test@ Project: Summary
49
+
50
+ Relative to your project's root directory:
51
+
52
+ * Library code goes under @lib/@. So when a unit test does @require
53
+ 'projX/init'@, it will be @lib/projX/init.rb@ that is loaded.
54
+
55
+ * Unit test files are named, for example, @tc_projX.rb@, and go in @test/@ or
56
+ a subdirectory.
57
+
58
+ * You have a master test file, @test/TEST.rb@, which loads and runs some or
59
+ all of your unit tests. Writing this file is extremely easy, as
60
+ @dev-utils/test@ does all the hard work.
61
+
62
+ ** Running @ruby test/TEST.rb@ is the *only* way to run your unit tests.
63
+
64
+ ** This program gives you options over which tests are run and how much output
65
+ is given.
66
+
67
+ * Test data (files containing data that are used for unit tests) is located in
68
+ @test/DATA@ or a subdirectory. Unit tests use a method called @load_data@
69
+ to access this test data.
70
+
71
+ h3. Using Rake to Run Tests
72
+
73
+ Pursuant to the running of unit tests via @test/TEST.rb@, here is a Rake task
74
+ you can set up to run your tests for you.
75
+
76
+ <pre>
77
+ desc "Run unit tests"
78
+ task :test do
79
+ exec "ruby test/TEST.rb #{ARGV.join(' ')}"
80
+ end
81
+ </pre>
82
+
83
+ If that is in your Rakefile, and the Rakefile is in the project's root
84
+ directory, then you can run @rake test@ from any directory in the project, and
85
+ your unit tests will run. There are various command-line options you can give
86
+ to @test/TEST.rb@, as the next section will detail.
87
+
88
+
89
+ h2. A @dev-utils/test@ Project: Detail
90
+
91
+ To make use of @dev-utils/test@, this is how you operate. All your
92
+ unit tests are coded in files like @tc_<i>foo</i>.rb@ in the @test@
93
+ directory, or subdirectories. There is one master test file
94
+ @test/TEST.rb@, which will load and run all of the unit test files.
95
+ Thanks to this package, however, writing that file is easy.
96
+
97
+ This is what happens when you run @ruby test/TEST.rb --help@:
98
+
99
+ <pre style="font-size:10pt"> Welcome to the dev-utils/test automatic unit test runner. Here are the
100
+ options:
101
+
102
+ -q quiet mode
103
+ -Q very quiet mode
104
+ -v verbose mode (default)
105
+ -s run unit tests in separate test suites
106
+ -h show help
107
+ str1 str2 ... run only those tests whose filenames contain str1 or
108
+ str2 or ...
109
+
110
+ For example, to run only those tests whose filenames include "remote" or
111
+ "db", and to run a separate suite for each test file, and to run in quiet
112
+ mode:
113
+
114
+ ruby test/TEST.rb -q remote db -s
115
+ </pre>
116
+
117
+ As the developer, you don't need to do any programming to make this happen;
118
+ you just need to create @test/TEST.rb@, in which you call one method.
119
+ This is what that file looks like:
120
+
121
+ <pre>
122
+ require 'rubygems'
123
+ require 'dev-utils/test'
124
+
125
+ DevUtils::Test.load_tests(__FILE__, *ARGV)
126
+ </pre>
127
+
128
+ As a sneak peek behind the scenes, @load_tests@ uses <code>__FILE__</code> to
129
+ orient itself, using default values to locate the @lib@ directory (so it can
130
+ manipulate the Ruby load path), and the @DATA@ directory (so calls to
131
+ @load_data@ will work).
132
+
133
+ So far, we have covered two of the three organisational hassles listed in the
134
+ introduction. The remaining one is managing test data. This is handled by
135
+ the @load_data@ method.
136
+
137
+ All your test data is contained in files underneath the @test/DATA@
138
+ directory. You use the path to those files (relative to @test/DATA@)
139
+ to identify the data file you want to load. Simple enough. So a unit test
140
+ for an email reader might look like this:
141
+
142
+ <pre>
143
+ def test_sender_extraction
144
+ input = Test.load_data('bulk/emails.txt')
145
+ output = EmailParser.parse(input)
146
+ assert_equal(...)
147
+ end
148
+ </pre>
149
+
150
+ In that case, the data in the @test/DATA/bulk/emails.txt@ file was served up
151
+ as a String. But @load_data@ is more flexible than that. It defaults to
152
+ returning a String, but can return a File (opened for reading) or a Pathname
153
+ instead. Also, it can make a copy of the file (to a temporary place) and
154
+ point you to that.
155
+
156
+ So a unit test that is modifying an SQLite database might look like this:
157
+
158
+ <pre>
159
+ def test_database_modification
160
+ dbfile = Test.load_data('customers3.db', :pathname, :copy)
161
+ init_system(dbfile)
162
+ ...
163
+ end
164
+ </pre>
165
+
166
+ The above code uses <i>@Test@</i> instead of <i>@DevUtils::Test@</i>
167
+ because it is assumed that an @include DevUtils@ was performed in the
168
+ test class.
169
+
170
+
171
+ h3. Consequences
172
+
173
+ The consequence of writing unit tests using @dev-utils/test@ is that they
174
+ cannot be run directly. Unit tests are written _assuming_ that they can find
175
+ the right library file in the load path. Without being run through
176
+ @test/TEST.rb@, this assumption does not (necessarily) hold.
177
+
178
+ And if a unit test calls @load_data@ without the initialization step that
179
+ @load_tests@ provides (or without explicit initialization - see below), then
180
+ that data will not be found.
181
+
182
+
183
+ h2. Ground Rules
184
+
185
+ <b><i>The information here has been effectively stated in the Summary section
186
+ above. Get rid of this.</i></b>
187
+
188
+ In order to benefit from these unit testing features, you must follow some
189
+ conventions in the way you organise your project and write your code. If your
190
+ project is called @projectX@, then this is how it should be organised:
191
+
192
+ * The unit tests themselves are named @tc_<i>foo</i>.rb@, and are
193
+ located under the @projectX/test@ directory. The name of that
194
+ directory doesn't matter, but we're assuming that for the following points.
195
+
196
+ * The master test runner is @projectX/test/TEST.rb@. Unit tests are
197
+ *always* run through this program. It contains the code shown in the
198
+ previous section.
199
+
200
+ * The library code goes in @projectX/lib/...@. Observing this
201
+ convention means that @load_tests@ knows where to find the library code that
202
+ is being tested.
203
+
204
+ * If the tests make use of data files, those files are located in the
205
+ @projectX/test/DATA@ directory. This way, the default will work and
206
+ the @load_tests@ method above will set the data directory correctly.
207
+
208
+ * ...
209
+
210
+
211
+ h2. Configurations
212
+
213
+ If you want the test data directory to be something other than
214
+ @test/DATA@, or you want the lib directory to be something other than
215
+ @lib@ (both relative to the root directory of the project), then the
216
+ following methods are available to you:
217
+ * @DevUtils::Test.project_dir = ...@
218
+ * @DevUtils::Test.set_project_dir_from(path, project_name)@
219
+ * @DevUtils::Test.lib_dir = ...@
220
+ * @DevUtils::Test.test_dir = ...@
221
+ * @DevUtils::Test.data_dir = ...@
222
+
223
+ If you want to specify any of them, then you *must* specify the project
224
+ directory first, as all the others are interpreted relative to that. Also,
225
+ these directories must be specified _before_ you call @load_tests@. Really,
226
+ the only appropriate place for any of this is @test/TEST.rb@.
227
+
228
+ p. @set_project_dir_from@ is the easier way to set the project directly. If you
229
+ use @project_dir=@ instead, _make sure_ you specify an absolute path (_I think
230
+ that requirement still stands..._).
231
+
232
+ Here is an example of a @unit-tests/run_tests.rb@ file with full
233
+ customisation. The name of the project is @projectX@.
234
+
235
+ <pre>
236
+ require 'rubygems'
237
+ require 'dev-utils/test'
238
+ include DevUtils
239
+
240
+ Test.set_project_dir_from(__FILE__, "projectX")
241
+ Test.lib_dir = "."
242
+ Test.test_dir = "unit-tests"
243
+ Test.data_dir = "etc/test-data"
244
+
245
+ Test.load_tests(__FILE__, *ARGV)
246
+ </pre>
247
+
248
+
249
+ h2. A Complete Sample Project
250
+
251
+ p(comment). Not bloody likely.
252
+
253
+ </div>
@@ -0,0 +1,187 @@
1
+ #
2
+ # = generate.rb, for the ruby-journal project.
3
+ #
4
+ # Documents are written in Textile and converted to HTML with RedCloth.
5
+ #
6
+ # However, RedCloth only generates HTML _fragments_. We need to generate HTML _documents_,
7
+ # with <tt><head></tt> information (especially CSS), etc.
8
+ #
9
+ # The simple function +generate_document+ does this.
10
+ #
11
+
12
+ require 'rubygems'
13
+ require 'redcloth'
14
+ require 'extensions/io'
15
+ require 'celsoft.com/template'
16
+
17
+ #
18
+ # Takes the Textile file from +path+ (relative to ~/Projects/dev-utils) and generates an HTML
19
+ # file in the appropriate place, which is the <tt>build/www</tt> directory, and with an +html+
20
+ # instead of +textile+ extension.
21
+ #
22
+ # The HTML document will contain an inline stylesheet.
23
+ #
24
+ # The first "h1." tag in the document is used as the title of the HTML page.
25
+ #
26
+ # Special lines !header and !toc are expanded.
27
+ #
28
+ def generate_document(input_path, output_dir)
29
+ output_path = File.join(output_dir, "#{File.basename(input_path)}".sub(/\.textile/, ".html"))
30
+ raw_text = File.read(input_path)
31
+ #trace {'input_path'}
32
+ #trace { 'raw_text.length' }
33
+ input_text = process_text(raw_text)
34
+ red_cloth_text = RedCloth.new(input_text)
35
+ red_cloth_text.fold_lines = true
36
+ html_fragment = red_cloth_text.to_html
37
+ html = html_wrap(html_fragment)
38
+ File.write(output_path, html)
39
+ #File.write(output_path + '.tmp', input_text)
40
+ end
41
+
42
+ #
43
+ # Expand any meta-markers like !header and !toc.
44
+ #
45
+ def process_text(text)
46
+ require 'shellwords'
47
+ text.gsub(/^!(.*)\s*$/) {
48
+ command, *args = Shellwords.shellwords($1.strip)
49
+ case command
50
+ when 'header' then header(*args)
51
+ when 'toc'
52
+ _toc = toc(text)
53
+ _toc
54
+ else
55
+ raise "Unknown command embedded in textile input: #{command}"
56
+ end
57
+ }
58
+ end
59
+
60
+ CSS_FILE = 'etc/doc/textile.css'
61
+
62
+ #
63
+ # Wraps the given HTML fragment in head, body, etc., to produce a proper document.
64
+ #
65
+ # It points to a hardcoded stylesheet.
66
+ #
67
+ def html_wrap(fragment)
68
+ require 'extensions/string'
69
+ title = fragment.scan(%r{<h1>(.*?)</h1>}).first || []
70
+ title = title.first || "(no title)"
71
+ return %{
72
+ <html>
73
+ <head>
74
+ <title>#{title}</title>
75
+ <style type="text/css">
76
+ #{File.read(CSS_FILE).tabto(6)}
77
+ </style>
78
+ </head>
79
+ <body>
80
+ #{fragment}
81
+ </body>
82
+ </html>
83
+ }.tabto(0)
84
+ end
85
+
86
+ def header(*args)
87
+ require_arg = args.shift or raise ArgumentError
88
+ require_text = "%(small-title)<code>require '#{require_arg}'</code>%"
89
+ if args.shift == 'main'
90
+ main_page_link = '%(mylink)"main page":index.html%'
91
+ else
92
+ main_page_link = ''
93
+ end
94
+ %!table{width:100%}.\n| #{require_text} |>. #{main_page_link} |\n\n!
95
+ end
96
+
97
+ TOC_TEMPLATE = Template::Document.new
98
+ TOC_TEMPLATE.load %!
99
+ <table class="toc_table">
100
+ <tr>
101
+ <td>
102
+ <span class="header">Links</span><br/>
103
+ <ul id="links">
104
+ ${each links}
105
+ <li><a href="${var href}">${var name}</a></li>
106
+ ${end}
107
+ </ul>
108
+ </td>
109
+ <td>
110
+ <span class="header">Contents</span><br/>
111
+ <ul id="contents">
112
+ ${each contents}
113
+ <li><a href="#\${var anchor}">${var heading}</a>
114
+ ${if subheadings}
115
+ <ul>
116
+ ${each subheadings}
117
+ <li><a href="#\${var anchor}">${var heading}</a></li>
118
+ ${end}
119
+ </ul>
120
+ </li>
121
+ ${end}
122
+ ${end}
123
+ </ul>
124
+ </td>
125
+ </tr>
126
+ </table>
127
+ !
128
+
129
+ Heading = Struct.new(:level, :heading, :anchor)
130
+
131
+ # Returns the HTML for a table containing links (from links.dat) and the contents
132
+ # of this page, derived from the given text. h2 and h3 markup elements are taken
133
+ # to be first- and second-level headings in the text. The text itself is modified
134
+ # so that those headings have anchors on them.
135
+ def toc(text)
136
+ # Form links: [ { 'name' => 'Synopsis', 'href' => 'Synopsis.html' }, ... ]
137
+ links = links().map { |name, href|
138
+ Hash[ 'name', name, 'href', href ]
139
+ }
140
+
141
+ # Form contents: [
142
+ # { 'heading' => 'Introduction', 'anchor' => 'Introduction', 'subheadings' => [ { ... } ] },
143
+ # ...
144
+ # ]
145
+ regex = /^h([234])\.\s+(.*?)$/
146
+ headings = text.grep(regex).map { |line|
147
+ line =~ regex
148
+ level, heading = $1.to_i - 1, $2.strip
149
+ anchor = heading.gsub(/[^\s\w]/, '')
150
+ #Hash[ 'level', level, 'heading', heading, 'anchor', anchor ]
151
+ Heading.new(level, heading, anchor)
152
+ }
153
+ contents = []
154
+ headings.each do |h|
155
+ case h.level
156
+ when 1
157
+ contents << Hash['heading', h.heading, 'anchor', h.anchor]
158
+ when 2
159
+ (contents.last['subheadings'] ||= []) << Hash['heading', h.heading, 'anchor', h.anchor]
160
+ end
161
+ end
162
+
163
+ # Place anchors into document. This changes the input text.
164
+ text.gsub!(regex) {
165
+ match = Regexp.last_match
166
+ heading = $2.strip
167
+ anchor = heading.gsub(/[^\s\w]/, '')
168
+ match[0].chomp + %{ <a name="#{anchor}"/>\n}
169
+ }
170
+
171
+ # Run it through the template.
172
+ TOC_TEMPLATE.data = { 'links' => links, 'contents' => contents }
173
+ TOC_TEMPLATE.output
174
+ end
175
+
176
+ # Return [ [name, href], [name, href], ... ].
177
+ def links
178
+ @links ||=
179
+ begin
180
+ links_file = Pathname.new(__FILE__).dirname + 'links.dat'
181
+ File.readlines(links_file).grep(/^\w/).map { |line|
182
+ line =~ %r{^(.*?)/(.*)$} or raise "Bad data in links.dat."
183
+ [$1.strip, $2.strip]
184
+ }
185
+ end
186
+ end
187
+
@@ -0,0 +1,110 @@
1
+ !header dev-utils nomain
2
+
3
+ h1. The Ruby dev-utils Project
4
+
5
+ !toc
6
+
7
+ h2. About dev-utils
8
+
9
+ p. @dev-utils@ provides utilites to assist the process of developing Ruby
10
+ programs. At the moment, the target areas are debugging and unit testing
11
+ (planned).
12
+
13
+ Version 1.0 was released on 2004-10-08 as a RubyGem. RPA and tarball
14
+ installation will be provided shortly in 1.0.1. Throughout this
15
+ documentation, there are references to planned features. These will be
16
+ released in 2.0, for which there is no estimated completion date.
17
+
18
+ h3. Debugging
19
+
20
+ With @dev-utils/debug@ you can:
21
+
22
+ * Escape to an IRB session from a running program.
23
+
24
+ <pre style="margin-left:5em">
25
+ breakpoint
26
+ breakpoint 'Person#name' # Identify it when it happens.
27
+ breakpoint { @name } # Default return value.
28
+ </pre>
29
+
30
+ * Access a no-config logfile for debugging.
31
+
32
+ <pre style="margin-left:5em">
33
+ debug 'Database connection established' # Look in ./debug.log
34
+ </pre>
35
+
36
+ * Trace expressions in that logfile.
37
+
38
+ <pre style="margin-left:5em">
39
+ trace 'x + y'
40
+ trace 'Process.pid'
41
+ trace 'names', :pp # Pretty-print.
42
+ trace 'page_structure', :yaml # YAML representation.
43
+ </pre>
44
+
45
+ * %(planned)Find the difference between complex objects (planned).%
46
+
47
+ For more information, see "Debugging Aids":DebuggingAids.html.
48
+
49
+ <div class="planned">
50
+
51
+ h3. Unit Testing (Planned)
52
+
53
+ With @dev-utils/test@ you can:
54
+ * Run some or all of your unit tests, automatically finding the library code.
55
+ * Load test data from a separate data directory.
56
+
57
+ For more information, see "Unit Test Organisation":UnitTestOrganisation.html.
58
+
59
+ </div>
60
+
61
+
62
+ h2. Internet Links
63
+
64
+ |{width:25ex}. Home page: | "http://dev-utils.rubyforge.org":home |
65
+ | Project page: | "http://rubyforge.org/projects/dev-utils":project |
66
+ | Download: | "http://rubyforge.org/frs/?group_id=270":download |
67
+ | API Documentation: | "http://dev-utils.rubyforge.org/api":api |
68
+
69
+ [home]http://dev-utils.rubyforge.org
70
+ [project]http://rubyforge.org/projects/dev-utils
71
+ [download]http://rubyforge.org/frs/?group_id=270
72
+ [api]http://dev-utils.rubyforge.org/api
73
+
74
+
75
+ h2. Administrivia
76
+
77
+ h3. Credits
78
+
79
+ The breakpoint-related code was contributed unwittingly by Joel VanderWerf and
80
+ Florian Gross. The rest of the code is by Gavin Sinclair.
81
+
82
+ h3. Notes
83
+
84
+ p. @dev-utils@ depends on "@extensions@":http://extensions.rubyforge.org, version
85
+ 0.5 or greater.
86
+
87
+ Runnable examples are located in the @examples@ directory of the distribution.
88
+
89
+ h3. License
90
+
91
+ Copyright (c) 2004, Gavin Sinclair.
92
+
93
+ Permission is hereby granted, free of charge, to any person obtaining a copy
94
+ of this software and associated documentation files (the "Software"), to deal
95
+ in the Software without restriction, including without limitation the rights
96
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
97
+ copies of the Software, and to permit persons to whom the Software is
98
+ furnished to do so, subject to the following conditions:
99
+
100
+ The above copyright notice and this permission notice shall be included in all
101
+ copies or substantial portions of the Software.
102
+
103
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
104
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
105
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
106
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
107
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
108
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
109
+ SOFTWARE.
110
+