bryton-lite 1.0

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 (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: []