Bryton 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +23 -0
  3. data/lib/bryton/runner.rb +459 -0
  4. data/lib/bryton/tools.rb +394 -0
  5. metadata +45 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 515b6e28901538ec80a86cae04da951149a4c5e726d57869c86386ca34e8928f
4
+ data.tar.gz: c28f2fe9fb9962eef207fecdca9853e9b3489e0144a15d938bb38fbc784b7f47
5
+ SHA512:
6
+ metadata.gz: 49faf782a0a7b691a854fea4211e1038fca60df54c7000a0cae5bbbb298ba75530ba8688f2db878e1709e9b5dc53fbf2f7d7352812ec65461d4bb9d04a3405a7
7
+ data.tar.gz: 219c8f0a8106fb9715ecd05375d78346d7a2530b721680beb70ac80eb465669defb13ad9b76e96a8b51fd135dedcdb786468cd0b3fd7b66235e36835184b5501
data/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # Bryton
2
+
3
+ Bryton is a file based testing protocol. Each file contains one or more tests.
4
+ The results of the tests are reported in STDOUT as a JSON Object.
5
+
6
+ Bryton is not ready for use yet. Just sharing online for a few collaborators.
7
+
8
+ ## Install
9
+
10
+ ```
11
+ gem install bryton
12
+ ```
13
+
14
+ ## Author
15
+
16
+ Mike O'Sullivan
17
+ mike@idocs.com
18
+
19
+ ## History
20
+
21
+ | version | date | notes |
22
+ |----------|--------------|-----------------|
23
+ | 1.0 | May 16, 2023 | Initial upload. |
@@ -0,0 +1,459 @@
1
+ #!/usr/bin/ruby -w
2
+ require 'timeout'
3
+ require 'json'
4
+ require 'fso'
5
+ require 'hash/digger'
6
+ require 'talk-to-me'
7
+
8
+
9
+ #===============================================================================
10
+ # Bryton
11
+ #
12
+ module Bryton
13
+ end
14
+ #
15
+ # Bryton
16
+ #===============================================================================
17
+
18
+
19
+ # load bryton xeme classes
20
+ require 'bryton/xeme'
21
+
22
+
23
+ #===============================================================================
24
+ # Bryton::FSO
25
+ # Just initializing namespace.
26
+ #
27
+ module Bryton::FSO
28
+ end
29
+ #
30
+ # Bryton::FSO
31
+ #===============================================================================
32
+
33
+
34
+ #===============================================================================
35
+ # Bryton::FSO::Dir
36
+ #
37
+ class Bryton::FSO::Dir < FSO::Dir
38
+ attr_reader :xeme
39
+ attr_accessor :stop_on_fail
40
+ attr_accessor :verbose
41
+ attr_accessor :run_tests
42
+
43
+ #---------------------------------------------------------------------------
44
+ # classes
45
+ #
46
+ def self.classes
47
+ return {
48
+ 'dir'=>Bryton::FSO::Dir,
49
+ 'file'=>Bryton::FSO::File
50
+ };
51
+ end
52
+ #
53
+ # classes
54
+ #---------------------------------------------------------------------------
55
+
56
+
57
+ #---------------------------------------------------------------------------
58
+ # initialize
59
+ #
60
+ def initialize(p_path='./')
61
+ super(p_path)
62
+ @settings = nil
63
+ @children = nil
64
+ @stop_on_fail = false
65
+ end
66
+ #
67
+ # initialize
68
+ #---------------------------------------------------------------------------
69
+
70
+
71
+ #---------------------------------------------------------------------------
72
+ # settings
73
+ #
74
+ def settings
75
+ # cache bryton.json if necessary
76
+ if not @settings
77
+ settings_path = "#{path}/bryton.json"
78
+ @settings = {'active'=>true}
79
+
80
+ # slurp in settings file
81
+ if ::File.exist?(settings_path)
82
+ explicit = ::File.read(settings_path)
83
+ explicit = JSON.parse(explicit)
84
+ @settings = @settings.merge(explicit)
85
+ end
86
+ end
87
+
88
+ # return
89
+ return @settings
90
+ end
91
+ #
92
+ # settings
93
+ #---------------------------------------------------------------------------
94
+
95
+
96
+ #---------------------------------------------------------------------------
97
+ # meta
98
+ #
99
+ def meta
100
+ return settings['meta'] || {}
101
+ end
102
+ #
103
+ # meta
104
+ #---------------------------------------------------------------------------
105
+
106
+
107
+ #---------------------------------------------------------------------------
108
+ # children
109
+ #
110
+ def children(opts={})
111
+ # cache @children if it's not already defined
112
+ if not @children
113
+ chdir do
114
+ @children = []
115
+
116
+ # special case: files element exists but is false
117
+ if settings.has_key?('files') and (not settings['files'])
118
+ return @children
119
+ end
120
+
121
+ # build list of child files
122
+ children_explicit opts
123
+ children_implicit super(), opts
124
+ end
125
+ end
126
+
127
+ # return
128
+ return @children
129
+ end
130
+ #
131
+ # children
132
+ #---------------------------------------------------------------------------
133
+
134
+
135
+ #---------------------------------------------------------------------------
136
+ # children_explicit
137
+ #
138
+ def children_explicit(opts)
139
+ explicits = settings['files'] || {}
140
+
141
+ explicits.each do |child_path, child_settings|
142
+ if child_settings
143
+ if child = self.class.existing(child_path)
144
+ if child.settings['active']
145
+ @children.push child
146
+ end
147
+ else
148
+ raise 'non-existent-file-in-list: ' + child_path
149
+ end
150
+ end
151
+ end
152
+ end
153
+ #
154
+ # children_explicit
155
+ #---------------------------------------------------------------------------
156
+
157
+
158
+ #---------------------------------------------------------------------------
159
+ # children_implicit
160
+ # skips dev.* files
161
+ #
162
+ def children_implicit(kids, opts)
163
+ # early exit: if filter is true, don't import implicit children
164
+ settings['listed-only'] and return
165
+
166
+ kids.each do |child|
167
+ if not child_names.include?(child.name)
168
+ unless child.name.match(/\Adev\./mu)
169
+ if child.dir?
170
+ @children.push child
171
+ elsif child.executable?
172
+ @children.push child
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ #
179
+ # children_implicit
180
+ #---------------------------------------------------------------------------
181
+
182
+
183
+ #---------------------------------------------------------------------------
184
+ # child_names
185
+ #
186
+ def child_names
187
+ return @children.map{|child| child.name}
188
+ end
189
+ #
190
+ # child_names
191
+ #---------------------------------------------------------------------------
192
+
193
+
194
+ #---------------------------------------------------------------------------
195
+ # run
196
+ #
197
+ def run(opts={})
198
+ opts = {'nested'=>0}.merge(opts)
199
+ xeme = Xeme.new()
200
+
201
+ # verbosify
202
+ unless opts['first']
203
+ TTM.puts title(opts)
204
+ end
205
+
206
+ # operate in own directory
207
+ chdir() do
208
+ TTM.indent('skip'=>opts.delete('first')) do
209
+ run_children xeme, opts
210
+ run_commands xeme, opts
211
+ end
212
+ end
213
+
214
+ # At this point, all tests in the have been run. Attempt to succeed.
215
+ xeme.try_succeed
216
+
217
+ # if set as not ready, fail anyway
218
+ if xeme.success?
219
+ if meta.has_key?('ready') and (not meta['ready'])
220
+ xeme['success-but-not-ready'] = true
221
+ xeme.fail
222
+ end
223
+ end
224
+
225
+ # return xeme
226
+ return xeme
227
+ end
228
+ #
229
+ # run
230
+ #---------------------------------------------------------------------------
231
+
232
+
233
+ #---------------------------------------------------------------------------
234
+ # run_children
235
+ #
236
+ def run_children(xeme, opts)
237
+ children.each do |child|
238
+ if child.executable? or child.dir? and child.settings['active']
239
+ send_opts = opts.clone
240
+ send_opts['nested'] += 1
241
+
242
+ # run child, add to nested if any results
243
+ if nest = child.run(send_opts)
244
+ xeme['nested'].push nest
245
+
246
+ if nest.failure? and @stop_on_fail
247
+ return
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
253
+ #
254
+ # run_children
255
+ #---------------------------------------------------------------------------
256
+
257
+
258
+ #---------------------------------------------------------------------------
259
+ # run_commands
260
+ #
261
+ def run_commands(xeme, opts)
262
+ commands = settings['commands'] || []
263
+ commands.empty? and return
264
+
265
+ # load EzCapture
266
+ require 'ezcapture'
267
+
268
+ # loop through commands
269
+ commands.each do |cmd|
270
+ nest = run_command(cmd)
271
+ xeme['nested'].push nest
272
+
273
+ # exit if error
274
+ if nest.failure? and @stop_on_fail
275
+ return
276
+ end
277
+ end
278
+ end
279
+ #
280
+ # run_commands
281
+ #---------------------------------------------------------------------------
282
+
283
+
284
+ #---------------------------------------------------------------------------
285
+ # run_command
286
+ #
287
+ def run_command(cmd)
288
+ capture = EzCapture.new(*cmd)
289
+ xeme = Xeme.last_line(capture.stdout)
290
+
291
+ # if no stdout, return failure xeme
292
+ if not xeme
293
+ xeme = Xeme.new()
294
+ xeme.error 'did-not-get-xeme-in-stdout'
295
+ return xeme
296
+ end
297
+
298
+ # return
299
+ return xeme
300
+ end
301
+ #
302
+ # run_command
303
+ #---------------------------------------------------------------------------
304
+
305
+
306
+ #---------------------------------------------------------------------------
307
+ # title
308
+ # Determines the string that should be displayed when this file is listed
309
+ # when verbose setting is true.
310
+ #
311
+ def title(opts)
312
+ # if defined name for this directory
313
+ if explicit = settings.digger('meta', 'title')
314
+ return explicit
315
+
316
+ # else return the name of the directory
317
+ else
318
+ return name
319
+ end
320
+ end
321
+ #
322
+ # title
323
+ #---------------------------------------------------------------------------
324
+ end
325
+ #
326
+ # Bryton::FSO::Dir
327
+ #===============================================================================
328
+
329
+
330
+ #===============================================================================
331
+ # Bryton::FSO::File
332
+ #
333
+ class Bryton::FSO::File < FSO::File
334
+ #---------------------------------------------------------------------------
335
+ # classes
336
+ #
337
+ def self.classes
338
+ return {
339
+ 'file' => Bryton::FSO::File,
340
+ 'dir' => Bryton::FSO::Dir
341
+ };
342
+ end
343
+ #
344
+ # classes
345
+ #---------------------------------------------------------------------------
346
+
347
+
348
+ #---------------------------------------------------------------------------
349
+ # initialize
350
+ #
351
+ def initialize(*opts)
352
+ super(*opts)
353
+ @children = nil
354
+ @settings = nil
355
+ end
356
+ #
357
+ # initialize
358
+ #---------------------------------------------------------------------------
359
+
360
+
361
+ #---------------------------------------------------------------------------
362
+ # settings
363
+ #
364
+ def settings()
365
+ # cache if necessary
366
+ if not @settings
367
+ default = {'active'=>true}
368
+ explicit = dir.settings.digger('files', name)
369
+
370
+ # If no explicit definition of the file, initialize to empty hash.
371
+ if explicit.nil?
372
+ explicit = {}
373
+
374
+ # If explicit is defined but is not a hash, the non-hash value is
375
+ # assumed to be the "active" value.
376
+ elsif not explicit.is_a?(Hash)
377
+ explicit = {'active'=>explicit}
378
+ end
379
+
380
+ # by this point explicit is a hash
381
+
382
+ # merge explicit and default
383
+ @settings = default.merge(explicit)
384
+ end
385
+
386
+ # return
387
+ return @settings
388
+ end
389
+ #
390
+ # settings
391
+ #---------------------------------------------------------------------------
392
+
393
+
394
+ #---------------------------------------------------------------------------
395
+ # run
396
+ #
397
+ def run(opts={})
398
+ # If the file is not active, do nothing - don't even print the name.
399
+ if not settings['active']
400
+ return nil
401
+ end
402
+
403
+ # verbosify
404
+ if opts['verbose']
405
+ TTM.puts title(opts)
406
+
407
+ # message before running
408
+ if before = @settings.digger('messages', 'before')
409
+ TTM.indent do
410
+ TTM.puts before
411
+ end
412
+ end
413
+ end
414
+
415
+ # run and return xeme if necessary
416
+ unless opts['list_only']
417
+ return run_test(opts)
418
+ end
419
+ end
420
+ #
421
+ # run
422
+ #---------------------------------------------------------------------------
423
+
424
+
425
+ #---------------------------------------------------------------------------
426
+ # run_test
427
+ #
428
+ def run_test(opts)
429
+ capture = execute()
430
+ xeme = Xeme.last_line(capture.stdout)
431
+
432
+ # if no stdout, return failure xeme
433
+ if not xeme
434
+ xeme = Xeme.new()
435
+ xeme.error 'did-not-get-xeme-in-stdout'
436
+ return xeme
437
+ end
438
+
439
+ # return
440
+ return xeme
441
+ end
442
+ #
443
+ # run_test
444
+ #---------------------------------------------------------------------------
445
+
446
+
447
+ #---------------------------------------------------------------------------
448
+ # title
449
+ #
450
+ def title(opts)
451
+ return name
452
+ end
453
+ #
454
+ # title
455
+ #---------------------------------------------------------------------------
456
+ end
457
+ #
458
+ # Bryton::FSO::File
459
+ #===============================================================================
@@ -0,0 +1,394 @@
1
+ require 'xeme'
2
+ require 'talk-to-me'
3
+
4
+
5
+ #===============================================================================
6
+ # Bryton
7
+ #
8
+ module Bryton
9
+ # instance properties
10
+ @exit_on_fail = false
11
+ @ready = true
12
+ @keep_count = true
13
+ @line_comments = false
14
+
15
+
16
+ #---------------------------------------------------------------------------
17
+ # accessors
18
+ #
19
+ class << self
20
+ attr_accessor :exit_on_fail
21
+ attr_accessor :ready
22
+ attr_accessor :keep_count
23
+ attr_accessor :xeme
24
+ attr_accessor :line_comments
25
+ end
26
+ #
27
+ # accessors
28
+ #---------------------------------------------------------------------------
29
+
30
+
31
+ #---------------------------------------------------------------------------
32
+ # accessor methods
33
+ #
34
+ def self.verbose
35
+ return TTM.io ? true : false
36
+ end
37
+
38
+ def self.verbose=(bool)
39
+ if bool
40
+ TTM.io = STDERR
41
+ else
42
+ TTM.io = nil
43
+ end
44
+ end
45
+ #
46
+ # accessor methods
47
+ #---------------------------------------------------------------------------
48
+
49
+
50
+ #---------------------------------------------------------------------------
51
+ # reset
52
+ #
53
+ def self.reset()
54
+ self.verbose= true
55
+ @xeme = Xeme.new()
56
+ @xeme['count'] = 0
57
+ end
58
+
59
+ reset()
60
+ #
61
+ # reset
62
+ #---------------------------------------------------------------------------
63
+
64
+
65
+ #---------------------------------------------------------------------------
66
+ # pause_counting
67
+ #
68
+ def self.pause_counting
69
+ hold_keep_count = @keep_count
70
+ @keep_count = false
71
+ yield
72
+ ensure
73
+ @keep_count = hold_keep_count
74
+ end
75
+ #
76
+ # pause_counting
77
+ #---------------------------------------------------------------------------
78
+
79
+
80
+ #---------------------------------------------------------------------------
81
+ # increment_count
82
+ #
83
+ def self.increment_count
84
+ if @keep_count
85
+ @xeme['count'] += 1
86
+ end
87
+ end
88
+ #
89
+ # increment_count
90
+ #---------------------------------------------------------------------------
91
+
92
+
93
+ #---------------------------------------------------------------------------
94
+ # succeed
95
+ #
96
+ def self.succeed
97
+ if not @ready
98
+ @xeme.error 'not-ready'
99
+ end
100
+
101
+ # set success to true
102
+ @xeme.try_succeed()
103
+
104
+ # output
105
+ puts xeme.to_json()
106
+ end
107
+ #
108
+ # succeed
109
+ #---------------------------------------------------------------------------
110
+
111
+
112
+ #---------------------------------------------------------------------------
113
+ # line_error
114
+ #
115
+ def self.line_error(level=1)
116
+ loc = caller_locations[level]
117
+ error = @xeme.error('line-' + loc.lineno.to_s)
118
+
119
+ # get line comment if there is one
120
+ line_comment error, loc
121
+
122
+ # exit if exiting on first fail
123
+ maybe_exit_on_fail()
124
+ end
125
+ #
126
+ # line_error
127
+ #---------------------------------------------------------------------------
128
+
129
+
130
+ #---------------------------------------------------------------------------
131
+ # line_comment
132
+ #
133
+ def self.line_comment(error, loc)
134
+ @line_comments or return
135
+
136
+ # KLUDGE: I don't understand why loc.lineno and the line number are two
137
+ # apart.
138
+ idx = 2
139
+
140
+ # read file
141
+ File.foreach(loc.path) do |line|
142
+ if idx == loc.lineno
143
+ if line.sub!(/\A\s*\#\#\s*/mu, '')
144
+ if line.match(/\S/mu)
145
+ line = line.sub(/\s+\z/mu, '')
146
+ error['comment'] = line
147
+ end
148
+ end
149
+
150
+ # we're done
151
+ return
152
+ end
153
+
154
+ # increment current line number
155
+ idx += 1
156
+ end
157
+ end
158
+ #
159
+ # line_comment
160
+ #---------------------------------------------------------------------------
161
+
162
+
163
+ #---------------------------------------------------------------------------
164
+ # maybe_exit_on_fail
165
+ #
166
+ def self.maybe_exit_on_fail
167
+ if @exit_on_fail
168
+ puts xeme.to_json()
169
+ exit
170
+ end
171
+ end
172
+ #
173
+ # maybe_exit_on_fail
174
+ #---------------------------------------------------------------------------
175
+
176
+
177
+ ### tests
178
+
179
+
180
+ #---------------------------------------------------------------------------
181
+ # pass, fail
182
+ #
183
+ def self.pass
184
+ Bryton.increment_count()
185
+ end
186
+
187
+ def self.fail
188
+ increment_count()
189
+ line_error()
190
+ end
191
+ #
192
+ # pass, fail
193
+ #---------------------------------------------------------------------------
194
+
195
+
196
+ #---------------------------------------------------------------------------
197
+ # structure comparisons
198
+ #
199
+ def self.compare(should, is)
200
+ TTM.hrm
201
+ return Bryton::StructureComp.compare(should, is)
202
+ end
203
+ #
204
+ # structure comparisons
205
+ #---------------------------------------------------------------------------
206
+
207
+
208
+ #-------------------------------------------------------------------------------
209
+ # isa
210
+ #
211
+ def self.isa(obj, clss)
212
+ Bryton.increment_count()
213
+
214
+ # success
215
+ if obj.is_a?(clss)
216
+ return true
217
+
218
+ # failure
219
+ else
220
+ Bryton.line_error(2)
221
+ return false
222
+ end
223
+ end
224
+ #
225
+ # isa
226
+ #-------------------------------------------------------------------------------
227
+ end
228
+ #
229
+ # Bryton
230
+ #===============================================================================
231
+
232
+
233
+ #===============================================================================
234
+ # Bryton::ExceptionTest
235
+ #
236
+ class Bryton::ExceptionTest
237
+ attr_accessor :err_class
238
+
239
+
240
+ #---------------------------------------------------------------------------
241
+ # initialize
242
+ #
243
+ def initialize
244
+ @err_class = nil
245
+ end
246
+ #
247
+ # initialize
248
+ #---------------------------------------------------------------------------
249
+
250
+
251
+ #---------------------------------------------------------------------------
252
+ # exec
253
+ #
254
+ def exec
255
+ Bryton.increment()
256
+ success = false
257
+ e = nil
258
+
259
+ # Run the block. If it runs successfully then it fails the test.
260
+ begin
261
+ yield
262
+ success = true
263
+
264
+ # verbosify
265
+ TTM.puts 'block-did-not-fail'
266
+
267
+ # We want to get to this rescue block because that means the block
268
+ # failed, which is what it's supposed to do.
269
+ rescue => e
270
+ # verbosify if necessary
271
+
272
+ TTM.puts "error class: #{e.class.to_s}"
273
+ TTM.puts "error: #{e}"
274
+
275
+ # if error id, add that
276
+ if e.respond_to?('id')
277
+ TTM.puts "error id: #{e}"
278
+ end
279
+
280
+
281
+ # check error class
282
+ if @err_class
283
+ if not e.class == @err_class
284
+ if @verbose
285
+ puts "error class was #{e.class.to_s} but should have been #{@err_class.to_s}"
286
+ end
287
+
288
+ Bryton.line_error(2)
289
+ end
290
+ end
291
+ end
292
+
293
+ # if yield succeeded, then an exception did not occur
294
+ if success
295
+ Bryton.line_error()
296
+ end
297
+
298
+ # return error
299
+ return e
300
+ end
301
+ #
302
+ # exec
303
+ #---------------------------------------------------------------------------
304
+ end
305
+ #
306
+ # Bryton::ExceptionTest
307
+ #===============================================================================
308
+
309
+
310
+ #===============================================================================
311
+ # Bryton::StructureComp
312
+ #
313
+ module Bryton::StructureComp
314
+ #-------------------------------------------------------------------------------
315
+ # array_comp
316
+ #
317
+ def self.array_comp(should, is, opts={'recurse'=>true})
318
+ should.each_with_index do |should_el, idx|
319
+ element_comp should_el, is[idx], opts
320
+ end
321
+ end
322
+ #
323
+ # array_comp
324
+ #-------------------------------------------------------------------------------
325
+
326
+
327
+ #-------------------------------------------------------------------------------
328
+ # hash_comp
329
+ #
330
+ def self.hash_comp(should, is, opts={})
331
+ Bryton.increment()
332
+ opts = {'recurse'=>true}.merge(opts)
333
+
334
+
335
+ # early exit: is is null
336
+ defined is
337
+ is or return
338
+
339
+ # is a hash
340
+ isa is, Hash
341
+ is.is_a?(Hash) or return
342
+
343
+ # if not a hash, return
344
+ if not is.is_a?(Hash)
345
+ return false
346
+ end
347
+
348
+ # should be of same length
349
+ eq should.length, is.length
350
+
351
+ # loop through keys
352
+ should.keys.each do |should_k|
353
+ element_comp should[should_k], is[should_k], opts
354
+ end
355
+ end
356
+ #
357
+ # hash_comp
358
+ #-------------------------------------------------------------------------------
359
+
360
+
361
+ #-------------------------------------------------------------------------------
362
+ # compare
363
+ #
364
+ def self.compare(should, is, opts)
365
+ Bryton.increment_count()
366
+
367
+ # should be same classes
368
+ if not Bryton.isa(is, should.class)
369
+ return
370
+ end
371
+
372
+ # pause counting and recurse
373
+ Bryton.pause_counting do
374
+ # if should element is a hash
375
+ if should.is_a?(Hash)
376
+ return hash_comp(should, is, opts)
377
+
378
+ # if should is an array
379
+ elsif should.is_a?(Array)
380
+ return array_comp(should, is, opts)
381
+
382
+ # if should is a string
383
+ elsif should.is_a?(String)
384
+ return Bryon.eq(should, is)
385
+ end
386
+ end
387
+ end
388
+ #
389
+ # compare
390
+ #-------------------------------------------------------------------------------
391
+ end
392
+ #
393
+ # Bryton::StructureComp
394
+ #===============================================================================
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: Bryton
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Mike O'Sullivan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-05-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: File based testing protocol.
14
+ email: mike@idocs.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - README.md
20
+ - lib/bryton/runner.rb
21
+ - lib/bryton/tools.rb
22
+ homepage: https://github.com/mikosullivan/bryton
23
+ licenses:
24
+ - MIT
25
+ metadata: {}
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ required_rubygems_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubygems_version: 3.1.2
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: Bryton
45
+ test_files: []