bryton-lite 1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +296 -0
  3. data/lib/bryton/lite.rb +427 -0
  4. metadata +44 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3774eb36e39cd0c1e23784ff1c9b6972aee2cd3228f162183f1b45c7c51ba348
4
+ data.tar.gz: d12041455299d8b4f19cf83975991168bc6792ff07c227c8c6b5d49e95d34789
5
+ SHA512:
6
+ metadata.gz: 34cca96d0ce490c263a1e22be5978bb708832940dd0ad7c187f57bfe3195eaa930376957a46c57a61e403284895f8f129786267957c5bc067b45bf0ecc297fc8
7
+ data.tar.gz: 445ccdbca8dbdba5faf901e2bbcba4eed21214a6e85f8a3fe6376d858b6896752f8dfe2a38cf866678f460243e0075a581770b234fb67623500fa240d7cd50d2
data/README.md ADDED
@@ -0,0 +1,296 @@
1
+ # bryton-lite
2
+ Bare bones Ruby implementation of the Bryton testing protocol
3
+
4
+ ## Install
5
+
6
+ The usual:
7
+
8
+ ```sh
9
+ gem install bryton-lite
10
+ ```
11
+
12
+ ## Overview
13
+
14
+ Bryton is a file-based testing protocol. The point of Bryton is to allow you to
15
+ write your tests without constraints on how they need to be organized. All your
16
+ test scripts have to do is output a JSON object as the last line of STDOUT. At
17
+ a minimum, the JSON object should have a "success" element set to true or false:
18
+
19
+ ```json
20
+ {"success":true}
21
+ {"success":false}
22
+ ```
23
+
24
+ Bryton allows you to start simple, with just a few tests scripts that you can
25
+ run manually to see the results. As you progress to automated testing you can
26
+ use a Bryton runner to run all your tests.
27
+
28
+ A Bryton runner (i.e. the routine running the tests) calls each executable file
29
+ in a directory, collecting the results. Those executables can be written in any
30
+ language, Ruby or otherwise. It also recurses into subdirectories. The runner
31
+ then reports the results back to you in either a human readable or machine
32
+ readable format.
33
+
34
+ Bryton (which is currently under development) will be a full featured testing
35
+ system with a large array of options. Bryton::Lite only implements a few of the
36
+ features. In fact, Bryton::Lite is to be used for testing Bryton.
37
+
38
+
39
+ ## Use
40
+
41
+ This gem has two modules, one for building tests and one for running tests. The
42
+ two modules don't depend on each other. You can use one or the other or both.
43
+
44
+ ### Bryton::Lite::Tests
45
+
46
+ #### Basic example
47
+
48
+ Bryton is designed to be simple. You don't need any special tools to implement
49
+ the basic protocol. For example, consider the following script:
50
+
51
+ ```ruby
52
+ #!/usr/bin/ruby -w
53
+ require 'json'
54
+
55
+ # some test
56
+ def some_test()
57
+ return true
58
+ end
59
+
60
+ # results hash
61
+ results = {}
62
+
63
+ # run a test
64
+ results['success'] = some_test()
65
+
66
+ # output results
67
+ puts JSON.generate(results)
68
+ ```
69
+
70
+ Notice that you don't even need this gem to run that script. Bryton is designed
71
+ to be simple and easy to implement. That test outputs a JSON object as either
72
+ `{"success":true}` or `{"success":false}`. Now let's take a look at a script
73
+ in which a test fails.
74
+
75
+ ```ruby
76
+ #!/usr/bin/ruby -w
77
+ require 'json'
78
+
79
+ # some test
80
+ def some_test()
81
+ return false
82
+ end
83
+
84
+ # results hash
85
+ results = {}
86
+
87
+ # run a test
88
+ if some_test()
89
+ results['success'] = true
90
+ else
91
+ results['errors'] ||= []
92
+ results['errors'].push({'id'=>'some_test_failure'})
93
+ end
94
+
95
+ # output results
96
+ puts JSON.generate(results)
97
+ ```
98
+
99
+ In this example one of the tests fails. The script could have simply set
100
+ `success` to false, but instead it gives a little more information by creating
101
+ the `errors` array and adding a message to it. Notice that `success` is never
102
+ explicitly set. That's because the presence of an error implies failure.
103
+
104
+ #### Bryton::Lite::Tests.assert
105
+
106
+ Bryton::Lite::Tests provides several tools for building and outputting test
107
+ results. Consider this script:
108
+
109
+ ```ruby
110
+ #!/usr/bin/ruby -w
111
+ require 'bryton/lite'
112
+
113
+ # some test
114
+ def some_test
115
+ return false
116
+ end
117
+
118
+ # test a function
119
+ Bryton::Lite::Tests.assert some_test()
120
+
121
+ # done
122
+ Bryton::Lite::Tests.try_succeed
123
+ Bryton::Lite::Tests.done
124
+ ```
125
+
126
+ In this test, we create a function that, in this case, always returns false.
127
+ Then we use Bryton::Lite::Tests.assert to check the result of that function.
128
+ If the test fails, then an error is added to the output hash.
129
+
130
+ Next we call Bryton::Lite::Tests.try_succeed. That function marks the test
131
+ script as successful, but only if there are not errors. Remember that a test run
132
+ is only considered successful if the output explicitly sets `success` as true.
133
+
134
+
135
+ Finally, we call Bryton::Lite::Tests.done, which outputs the the results and
136
+ exits. Bryton::Lite::Tests.done should be called as the last line of your
137
+ script.
138
+
139
+ Here's the output for that run.
140
+
141
+ ```json
142
+ {"errors":[{"line":12,"file":"./basic.rb"}]}
143
+ ```
144
+
145
+ By default, `assert` notes the file name and line number of the error. You can
146
+ also add an id for the error:
147
+
148
+ ```ruby
149
+ Bryton::Lite::Tests.assert some_test(), 'running some_test()'
150
+ ```
151
+
152
+ which outputs
153
+
154
+ ```json
155
+ {"errors":[{"line":11,"file":"./id.rb","id":"running some_test()"}]}
156
+ ```
157
+
158
+ If you want to manually add information to the error, use a do block. `assert`
159
+ yields the error hash:
160
+
161
+ ```ruby
162
+ Bryton::Lite::Tests.assert(some_test()) do |error|
163
+ error['notes'] = 'failure of some_test'
164
+ end
165
+ ```
166
+
167
+ which outputs a JSON object with whatever you added:
168
+
169
+ ```json
170
+ {"errors":[{"line":12,"file":"./block.rb","notes":"failure of some_test"}]}
171
+ ```
172
+
173
+ #### Bryton::Lite::Tests.fail
174
+
175
+ `fail` works much like `assert`, but it unconditionally adds an error. `fail`
176
+ has syntax similar to `assert`.
177
+
178
+ ```ruby
179
+ Bryton::Lite::Tests.fail() do |error|
180
+ error['notes'] = 'failed db test'
181
+ end
182
+ ```
183
+
184
+ which outputs JSON like this:
185
+
186
+ ```json
187
+ {"errors":[{"line":6,"file":"./no-id.rb","notes":"failed db test"}]}
188
+ ```
189
+
190
+ ### Bryton::Lite::Runner
191
+
192
+ Bryton::Lite::Runner runs all the tests in a directory tree. The full Bryton
193
+ protocol will allow you to pick and choose which tests to run and what to do
194
+ on success or failure. Bryton::Lite::Runner just implements the basic concept of
195
+ running all the tests.
196
+
197
+ To run tests, your script should go to the root of the directory where you have
198
+ your test files. Then just run `Bryton::Lite::Runner.run`.
199
+
200
+ ```ruby
201
+ #!/usr/bin/ruby -w
202
+ require 'bryton/lite'
203
+
204
+ Dir.chdir '../tests'
205
+ Bryton::Lite::Runner.run
206
+ puts Bryton::Lite::Runner.success?
207
+ ```
208
+
209
+ If you just want to know the success or failure of the tests, output
210
+ `Bryton::Lite::Runner.success?`. If you want more information, you can output
211
+ all the results from the entire test run:
212
+
213
+ ```ruby
214
+ puts JSON.pretty_generate(Bryton::Lite::Tests.hsh)
215
+ ```
216
+
217
+ Notice that we use `Bryton::Lite::Tests.hsh` to get the results. The runner
218
+ stores results with Bryton::Lite::Tests because the run itself is a test. There
219
+ are main three elements to a results hash.
220
+
221
+ | key | data type | explanation |
222
+ |---------|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
223
+ | success | true, false, or nil | Indicates success of failure of a test. Nil is considered false. |
224
+ | file | hash | Hash of information about the fiule being tested. Should at least contain the path to the file relative to the root directory of the test. If `dir` is true then the file is a directory. |
225
+ | errors | array | Each element of the array give details about an error. If any elements are present in the errors array then `success` should not be true. |
226
+ | nested | array | Array of nested test results. Every nested result must be successful or the entire test run is considered to have failed. |
227
+
228
+ As with any hash, you can add your own custom elements.
229
+
230
+ Here is a sample output from a script run. (JSON doesn't allow comments, but
231
+ I've added some here for clarity.
232
+
233
+ ```javascript
234
+ // root directory for tests
235
+ {
236
+ // indicates that the test run failed
237
+ "success": false,
238
+
239
+ // file information about this directory
240
+ "file": {
241
+ "path": ".",
242
+ "dir": true
243
+ },
244
+
245
+ // nested list of subtests
246
+ // directories always have an array of nested tests
247
+ "nested": [
248
+ {
249
+ "file": {
250
+ "path": "./load-tests",
251
+ "dir": true
252
+ },
253
+
254
+ "nested": [
255
+ {
256
+ // indicates that this file test failed
257
+ "success": false,
258
+
259
+ // file information about ./load-tests/crash.rb
260
+ "file": {
261
+ "path": "./load-tests/crash.rb"
262
+ },
263
+
264
+ // array of error messages
265
+ "errors": [
266
+ {
267
+ // indicates that execution of the script failed
268
+ "id": "execution-failed",
269
+
270
+ // STDERR from the execution
271
+ "stderr": "./crash.rb:5:in `/': divided by 0 (ZeroDivisionError)\n\tfrom ./crash.rb:5:in `<main>'\n"
272
+ }
273
+ ]
274
+ },
275
+
276
+ {
277
+ // indicates that the script succeeded
278
+ "success": true,
279
+
280
+ // file information
281
+ "file": {
282
+ "path": "./load-tests/success.rb"
283
+ }
284
+ }
285
+ ]
286
+ },
287
+
288
+ {
289
+ "success": true,
290
+ "file": {
291
+ "path": "./test-a.rb"
292
+ }
293
+ }
294
+ ]
295
+ }
296
+ ```
@@ -0,0 +1,427 @@
1
+ require 'open3'
2
+ require 'json'
3
+
4
+
5
+ #===============================================================================
6
+ # Bryton, Bryton::Lite
7
+ #
8
+
9
+ # Not much here. Just initializing the Bryton namespace.
10
+
11
+ module Bryton
12
+ end
13
+
14
+ # Not much here either. Just initializing the Bryton::Lite namespace.
15
+
16
+ module Bryton::Lite
17
+ end
18
+ #
19
+ # Bryton, Bryton::Lite
20
+ #===============================================================================
21
+
22
+
23
+ #===============================================================================
24
+ # Bryton::Lite::Runner
25
+ #
26
+
27
+ # Runs tests in the current directory tree.
28
+
29
+ module Bryton::Lite::Runner
30
+ #---------------------------------------------------------------------------
31
+ # run
32
+ #
33
+
34
+ # Run the tests. If you pass in a block, each test result is yielded with
35
+ # the path to the file and the results hash.
36
+
37
+ def self.run(&block)
38
+ Bryton::Lite::Tests.reset
39
+ dir('.', &block)
40
+ Bryton::Lite::Tests.try_succeed
41
+ Bryton::Lite::Tests.reorder_results
42
+ end
43
+ #
44
+ # run
45
+ #---------------------------------------------------------------------------
46
+
47
+
48
+ #---------------------------------------------------------------------------
49
+ # dir
50
+ #
51
+
52
+ # Runs the tests in a directory. Don't call this method directly.
53
+
54
+ def self.dir(path, &block)
55
+ entries = Dir.entries('./')
56
+ entries = entries.reject {|entry| entry.match(/\A\.+\z/mu)}
57
+
58
+ # note file and path
59
+ Bryton::Lite::Tests['file'] = {}
60
+ Bryton::Lite::Tests['file']['path'] = path
61
+ Bryton::Lite::Tests['file']['dir'] = true
62
+
63
+ # loop through entries
64
+ entries.each do |entry|
65
+ entry_path = "#{path}/#{entry}"
66
+
67
+ # if directory, recurse into it
68
+ if File.directory?(entry)
69
+ Dir.chdir(entry) do
70
+ Bryton::Lite::Tests.nest do
71
+ self.dir(entry_path, &block)
72
+ end
73
+ end
74
+
75
+ # elsif the file is executable then execute it
76
+ elsif File.executable?(entry)
77
+ results = execute(entry, entry_path)
78
+ Bryton::Lite::Tests.add_to_nest results
79
+
80
+ if block_given?
81
+ yield entry_path, results
82
+ end
83
+ end
84
+ end
85
+ end
86
+ #
87
+ # dir
88
+ #---------------------------------------------------------------------------
89
+
90
+
91
+ #---------------------------------------------------------------------------
92
+ # execute
93
+ #
94
+
95
+ # Executes a file, parses the results, and returns the results. Don't call
96
+ # this method directly.
97
+
98
+ def self.execute(name, path)
99
+ # init
100
+ rv = {}
101
+ rv['file'] = {}
102
+ rv['file']['path'] = path
103
+
104
+ # execute script
105
+ cap = Bryton::Lite::Runner::Capture.new("./#{name}")
106
+
107
+ # if script crashed
108
+ if not cap.success?
109
+ rv['success'] = false
110
+ rv['errors'] ||= []
111
+
112
+ # build error
113
+ error = {'id'=>'execution-failed'}
114
+ error['id'] = 'execution-failed'
115
+ error['stderr'] = cap.stderr
116
+
117
+ # add error and return
118
+ rv['errors'].push(error)
119
+ return rv
120
+
121
+ # if we get a non-blank line, attempt to parse it as JSON
122
+ elsif non_blank = self.last_non_blank(cap.stdout)
123
+ begin
124
+ # retrieve and parse results
125
+ results = JSON.parse( non_blank )
126
+ rv = rv.merge(results)
127
+
128
+ # ensure that if there are errors, success is false
129
+ if rv['errors'] and rv['errors'].any?
130
+ rv['success'] = false
131
+ end
132
+
133
+ # return
134
+ return rv
135
+ rescue
136
+ rv['success'] = false
137
+ rv['errors'] = []
138
+ rv['errors'].push({'id'=>'invalid-json'})
139
+ return rv
140
+ end
141
+
142
+ # did not get non-blank line
143
+ else
144
+ rv['success'] = false
145
+ rv['errors'] = []
146
+ rv['errors'].push({'id'=>'no-results'})
147
+ return rv
148
+ end
149
+ end
150
+ #
151
+ # execute
152
+ #---------------------------------------------------------------------------
153
+
154
+
155
+ #---------------------------------------------------------------------------
156
+ # last_non_blank
157
+ #
158
+
159
+ # Finds the last non-blank line in STDOUT from the file execution. Don't
160
+ # call this method directly.
161
+
162
+ def self.last_non_blank(str)
163
+ str.lines.reverse.each do |line|
164
+ if line.match(/\S/mu)
165
+ return line
166
+ end
167
+ end
168
+
169
+ # didn't find non-blank
170
+ return nil
171
+ end
172
+ #
173
+ # last_non_blank
174
+ #---------------------------------------------------------------------------
175
+
176
+
177
+ #---------------------------------------------------------------------------
178
+ # success?
179
+ #
180
+
181
+ # Returns the success or failure of the test run.
182
+
183
+ def self.success?
184
+ return Bryton::Lite::Tests.success?
185
+ end
186
+ #
187
+ # success?
188
+ #---------------------------------------------------------------------------
189
+ end
190
+ #
191
+ # Bryton::Lite::Runner
192
+ #===============================================================================
193
+
194
+
195
+ #===============================================================================
196
+ # Bryton::Lite::Runner::Capture
197
+ # class for capturing the output of a script
198
+ #
199
+
200
+ # Executes the given command and captures the results.
201
+
202
+ class Bryton::Lite::Runner::Capture
203
+ # Executes the command with Open3.capture3 and holds on to the results.
204
+ def initialize(*cmd)
205
+ @results = Open3.capture3(*cmd)
206
+ end
207
+
208
+ # Returns the content of STDOUT from the execution.
209
+ # @return [String]
210
+ def stdout
211
+ return @results[0]
212
+ end
213
+
214
+ # Returns the content of STDERR from the execution.
215
+ # @return [String]
216
+ def stderr
217
+ return @results[1]
218
+ end
219
+
220
+ # Returns the status of execution.
221
+ # @return [Process::Status]
222
+ def status
223
+ return @results[2]
224
+ end
225
+
226
+ # Returns the success or failure of the execution.
227
+ # @return [Boolean]
228
+ def success?
229
+ return status.success?
230
+ end
231
+ end
232
+ #
233
+ # Bryton::Lite::Runner::Capture
234
+ #===============================================================================
235
+
236
+
237
+ #===============================================================================
238
+ # Bryton::Lite::Tests
239
+ #
240
+
241
+ # Utilities for use in the test script.
242
+
243
+ module Bryton::Lite::Tests
244
+ # accessors
245
+ class << self
246
+ # @return [Hash]
247
+ attr_reader :hsh
248
+ end
249
+
250
+ # reset
251
+ def self.reset()
252
+ @hsh = {}
253
+ end
254
+
255
+ # call reset
256
+ reset()
257
+
258
+ # Get the element with the given key.
259
+ def self.[](k)
260
+ return @hsh[k]
261
+ end
262
+
263
+ # Set the element with the given key and value.
264
+ def self.[]=(k, v)
265
+ return @hsh[k] = v
266
+ end
267
+
268
+ # Set to success. Raises an exception if any errors exist. You probably want
269
+ # to use try_succeed() instead.
270
+ def self.succeed
271
+ if not allow_success?
272
+ raise 'cannot-set-to-success'
273
+ end
274
+
275
+ return @hsh['success'] = true
276
+ end
277
+
278
+ # Returns true if ['success'] is true, false otherwise. Always returns true
279
+ # or false.
280
+ # @return [Boolean]
281
+ def self.success?
282
+ return @hsh['success'] ? true : false
283
+ end
284
+
285
+ # Try to set to success, but will not do so if there are errors or if the
286
+ # test is already set as fail. Does not raise an exception if the success
287
+ # cannot be set to true.
288
+ # @return [Boolean]
289
+ def self.try_succeed
290
+ return @hsh['success'] = allow_success?()
291
+ end
292
+
293
+ # Test if try_succeed can set success. You probably don't need to call this
294
+ # method directly.
295
+ # @return [Boolean]
296
+ def self.allow_success?
297
+ return allow_success_recurse(@hsh)
298
+ end
299
+
300
+ # Tests each nested result. If any of the nested results are set to failure,
301
+ # return false. Otherwise returns true.
302
+ def self.allow_success_recurse(test_hsh)
303
+ # if any errors
304
+ if test_hsh['errors'] and test_hsh['errors'].any?
305
+ return false
306
+ end
307
+
308
+ # recurse into nested elements
309
+ if test_hsh['nested']
310
+ test_hsh['nested'].each do |child|
311
+ if not allow_success_recurse(child)
312
+ return false
313
+ end
314
+ end
315
+ end
316
+
317
+ # at this point it should be successful
318
+ return true
319
+ end
320
+
321
+ # Returns the errors array, creating it if necessary.
322
+ def self.errors
323
+ @hsh['errors'] ||= []
324
+ return @hsh['errors']
325
+ end
326
+
327
+ # Creates (if necessary) an array with the key "nested". Creates a results
328
+ # hash that is nested in that array. In the do block,
329
+ # Bryton::Lite::Tests.hsh is set to that child results hash.
330
+ def self.nest()
331
+ @hsh['nested'] ||= []
332
+ child = {}
333
+ @hsh['nested'].push child
334
+ hold_hsh = @hsh
335
+ @hsh = child
336
+
337
+ begin
338
+ yield
339
+ ensure
340
+ @hsh = hold_hsh
341
+ end
342
+ end
343
+
344
+ # Created the "nested" array and adds the given results to that array
345
+ def self.add_to_nest(child)
346
+ @hsh['nested'] ||= []
347
+ @hsh['nested'].push child
348
+ end
349
+
350
+ # done
351
+ def self.done
352
+ try_succeed()
353
+ reorder_results()
354
+ STDOUT.puts JSON.generate(@hsh)
355
+ exit
356
+ end
357
+
358
+ # to_json
359
+ def self.to_json
360
+ return JSON.generate(@hsh)
361
+ end
362
+
363
+ # assert
364
+ def self.assert(bool, id=nil, level=0, &block)
365
+ if not bool
366
+ fail id, 1, &block
367
+ end
368
+ end
369
+
370
+ # Mark the test as failed.
371
+ def self.fail(id=nil, level=0, &block)
372
+ loc = caller_locations[level]
373
+
374
+ # create error object
375
+ error = {}
376
+ error['line'] = loc.lineno
377
+ id and error['id'] = id
378
+
379
+ # add to errors
380
+ @hsh['errors'] ||= []
381
+ @hsh['errors'].push error
382
+
383
+ # if block
384
+ if block_given?
385
+ yield error
386
+ end
387
+ end
388
+
389
+ # reorder_results
390
+ # This method is for improving the readability of a result by putting the
391
+ # "success" element first and "nested" last.
392
+ def self.reorder_results
393
+ @hsh = reorder_results_recurse(@hsh)
394
+ end
395
+
396
+ # reorder_results_recurse
397
+ # This method rcurses through test results, putting "success" first and
398
+ # "nested" last.
399
+ def self.reorder_results_recurse(old_hsh)
400
+ new_hsh = {}
401
+ nested = old_hsh.delete('nested')
402
+
403
+ # add success if it exists
404
+ if old_hsh.has_key?('success')
405
+ new_hsh['success'] = old_hsh.delete('success')
406
+ end
407
+
408
+ # add every other element
409
+ new_hsh = new_hsh.merge(old_hsh)
410
+
411
+ # add nested last
412
+ if nested
413
+ new_hsh['nested'] = nested
414
+
415
+ # recurse through nested results
416
+ new_hsh['nested'].map! do |child|
417
+ reorder_results_recurse child
418
+ end
419
+ end
420
+
421
+ # return reformatted hash
422
+ return new_hsh
423
+ end
424
+ end
425
+ #
426
+ # Bryton::Lite::Tests
427
+ #===============================================================================
metadata ADDED
@@ -0,0 +1,44 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bryton-lite
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ platform: ruby
6
+ authors:
7
+ - Mike O'Sullivan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-05-18 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Bare bones implementation of the Bryton testing protocol
14
+ email: mike@idocs.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - README.md
20
+ - lib/bryton/lite.rb
21
+ homepage: https://github.com/mikosullivan/bryton-lite
22
+ licenses:
23
+ - MIT
24
+ metadata: {}
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ requirements: []
40
+ rubygems_version: 3.1.2
41
+ signing_key:
42
+ specification_version: 4
43
+ summary: Bryton::Lite
44
+ test_files: []