exsys 1.1 → 1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97086399976474a8e6d5c41ca473a6b2bd385dfe65bc1a1655301510b93ce68d
4
- data.tar.gz: f14819b8e580dcec1374b6ea1360628b3980c6dd7a8ea7aeb6f87ee406de312b
3
+ metadata.gz: a4be13bb30c8ab3697d13b8b3c9f92fd398ffbd2d1f3cbd817470b3da313e521
4
+ data.tar.gz: 2b0059f98f7d2efe29789fdeb362faf6e4c63a5218cbc57d22593d978d42153f
5
5
  SHA512:
6
- metadata.gz: 3f70740de8575d2e1982b890fd33f65bc200181e73f7854435233dc325642cefd9b10103e7edacf936f0fbae6b8de8329ab97a979072713a83f49bc17ea623f6
7
- data.tar.gz: 57c4f9a96bb7cf8de062101b72720bc76350ef0098081bb036632a7f3672975630a8d8eb14aee2a1837f9e283dc0e13a1ec31fbc43a71882521ef6daa1851911
6
+ metadata.gz: 2438a668ebc64b1236ea4d3001f4c78c4682ca3754d7214c6e50d48aa55163ca78cec57496e1ab51af010079dc2e89cf8f5127bdd8be5240d85268cf1a2946df
7
+ data.tar.gz: 4e0005f569dff091a466f44161836ba256304450b5c7499461e7e3758fa00425e167a5a988e13cca4c8967c943483d8916983adfbf0682472ee02d73580fecd0
data/README.md CHANGED
@@ -61,7 +61,7 @@ dev=/dev/ttyU0
61
61
  exsys-usb -d ${dev} on # All on
62
62
  exsys-usb -d ${dev} off # All off
63
63
  exsys-usb -d ${dev} on 1 2 # Only turn on port 1 and 2
64
- exsys-usb -d ${dev} toggle 3, 5 # Toggle port 3 and 5
64
+ exsys-usb -d ${dev} toggle 3 5 # Toggle port 3 and 5
65
65
  exsys-usb -d ${dev} set 3:on 5:off # Turn on port 3, turn off port 5
66
66
  exsys-usb -d ${dev} -D false set 3:on # Turn on port 3, all others off
67
67
  exsys-usb -d ${dev} -c on 1 # Turn on port 1, and save to flash
@@ -99,18 +99,18 @@ A port state in `set` is written `PORT:STATE`, where `STATE` is one of
99
99
 
100
100
  ### Options
101
101
 
102
- | Option | Meaning |
103
- | :-------------------- | :-------------------------------------------- |
104
- | `-d`, `--device=DEV` | Serial line to the hub (required, but see |
105
- | | `discover`) |
106
- | `-p`, `--password=STR`| Hub password; defaults to `pass` |
107
- | `-c`, `--commit` | Also write the new state to flash |
108
- | `-y`, `--yes` | Mean a destructive action |
109
- | `-v`, `--verbose` | Report the port states after a change |
110
- | `-D`, `--default=BOOL`| State for the ports `set` does not name |
111
- | `--debug[=FILE]` | Trace the serial exchange to stderr, or FILE |
112
- | `-V`, `--version` | Print the library version |
113
- | `-h`, `--help` | Print the usage |
102
+ | Option | Meaning |
103
+ | :------------------------ | :------------------------------------------- |
104
+ | `-d`, `--device=DEV` | Serial line to the hub (required, but see |
105
+ | | `discover`) |
106
+ | `-p`, `--password=STRING` | Hub password; defaults to `pass` |
107
+ | `-c`, `--commit` | Also write the new state to flash |
108
+ | `-y`, `--yes` | Mean a destructive action |
109
+ | `-v`, `--[no-]verbose` | Report the port states after a change |
110
+ | `-D`, `--default=BOOLEAN` | State for the ports `set` does not name |
111
+ | `--debug[=FILE]` | Trace the serial exchange to stderr, or FILE |
112
+ | `-V`, `--version` | Print the library version |
113
+ | `-h`, `--help` | Print the usage |
114
114
 
115
115
  The debug trace shows every frame sent and received, with the password
116
116
  blanked out; when it is written to a file, that file is created
@@ -128,8 +128,8 @@ exsys-usb -d ${dev} -v on 3 # switch, then report
128
128
  ### Exit status
129
129
 
130
130
  `0` when the command was carried out, `1` otherwise -- a malformed
131
- argument, a port outside 1..16, an unreachable serial line, or a
132
- command the hub refused. The error goes to stderr, so a script can
131
+ argument, a port the hub does not have, an unreachable serial line, or
132
+ a command the hub refused. The error goes to stderr, so a script can
133
133
  rely on the status:
134
134
 
135
135
  ~~~sh
@@ -149,7 +149,7 @@ exsys-usb discover
149
149
 
150
150
  ~~~text
151
151
  /dev/ttyUSB0 A50285BI 1-1.2.4.4
152
- /dev/ttyUSB1 - 1-1.3
152
+ /dev/ttyUSB1 - 1-1.3
153
153
  ~~~
154
154
 
155
155
  One line per adapter: the device to pass to `-d`, the FT232's own
@@ -207,7 +207,8 @@ Linux from 1 — so a path names a socket on the machine that reported
207
207
  it and does not travel to another.
208
208
 
209
209
  Discovery reads `/sys/class/tty` through `udevadm` on Linux and
210
- `dev.uftdi` through `sysctl` on FreeBSD; any other platform raises
210
+ `dev.uftdi` with `dev.uhub` -- the adapters and the tree above them --
211
+ through `sysctl` on FreeBSD; any other platform raises
211
212
  rather than answering an empty list, an empty list being a claim that
212
213
  nothing is attached.
213
214
 
@@ -248,8 +249,8 @@ hub.get(:off) # => [ 2, 4, ... ]
248
249
  you say every port. An empty list is refused rather than read as
249
250
  "all": `hub.off(*ports)` with an empty `ports` is the very same call as
250
251
  `hub.off`, so a computed list that came back empty would otherwise
251
- switch all sixteen. The command line is unaffected -- naming no port
252
- there still means every port.
252
+ switch every port on the hub. The command line is unaffected -- naming
253
+ no port there still means every port.
253
254
 
254
255
  Switching is a read-modify-write, and the library holds the serial
255
256
  line -- locked -- across the whole exchange, so two processes driving
@@ -287,7 +288,9 @@ new one; nothing in the library can notice the swap.
287
288
  above. The keyword is required: it is the one operation here that
288
289
  nothing undoes, and the one most easily reached by misunderstanding.
289
290
  It was called `restore` up to 0.6; the old name now raises rather than
290
- run.
291
+ run. The hub's password goes back to `pass`, and the object follows it
292
+ there, so it stays usable afterwards -- but a later run of the tool
293
+ against that hub must drop its `-p`.
291
294
 
292
295
  Sessions nest, so the methods above stay correct when called inside
293
296
  one, and a session belongs to the thread that opened it: another thread
data/Rakefile CHANGED
@@ -1,9 +1,24 @@
1
1
  require 'bundler'
2
+ require 'rake/clean'
2
3
  require 'rake/testtask'
3
4
  require 'yard'
4
5
 
5
6
  Bundler::GemHelper.install_tasks
6
7
 
8
+ # What a build leaves behind.
9
+ #
10
+ # CLEAN is the intermediate that is rebuilt from the sources every
11
+ # time: yard's object database. CLOBBER adds the products themselves
12
+ # -- rendered documentation, packaged gems -- which `rake yard` and
13
+ # `rake build` put back.
14
+ #
15
+ # Every entry is a path this repository's own tasks write, and every
16
+ # one of them is in .gitignore. Nothing is listed by a wildcard that
17
+ # could reach further than that: clobber is a delete, and a list that
18
+ # grows to match somebody's files is how a clean target eats work.
19
+ CLEAN.include('.yardoc', '_yardoc')
20
+ CLOBBER.include('doc', 'rdoc', 'pkg', 'coverage', '*.gem')
21
+
7
22
  Rake::TestTask.new do |t|
8
23
  t.test_files = FileList['test/test_*.rb']
9
24
  t.verbose = true
@@ -13,7 +28,7 @@ end
13
28
  task :default => :test
14
29
 
15
30
  YARD::Rake::YardocTask.new do |t|
16
- t.files = [ 'lib/**/*.rb', 'ext/ucl.c' ]
31
+ t.files = [ 'lib/**/*.rb' ]
17
32
  t.options = [ '-m', 'markdown' ]
18
33
  t.stats_options = [ '--list-undoc' ]
19
34
  end
@@ -220,6 +220,13 @@ class ManagedUSB
220
220
  return nil unless pnp.is_a?(Hash)
221
221
  return nil unless pnp[:vendor] == "0x#{CTRL_VENDOR}" &&
222
222
  pnp[:product] == "0x#{CTRL_PRODUCT}"
223
+ # No ttyname, no candidate. An adapter whose tty is
224
+ # not named yet has no line to hand anybody, and the
225
+ # answer this used to give was the WORST one available:
226
+ # '/dev/tty' + '' is /dev/tty, the caller's own
227
+ # controlling terminal, which a caller taking it for a
228
+ # hub would open and write command frames to.
229
+ return nil if Discovery.nonempty(dev[:ttyname]).nil?
223
230
  { :device => '/dev/tty' + dev[:ttyname].to_s,
224
231
  :serial => Discovery.nonempty(pnp[:sernum]),
225
232
  :usb_path => self.usb_path(dev, tree) }
@@ -255,6 +262,7 @@ class ManagedUSB
255
262
  bus = nil
256
263
  ports = []
257
264
  rooted = false
265
+ seen = {}
258
266
  while dev
259
267
  loc = dev[:'%location']
260
268
  unless loc.is_a?(Hash) && loc[:port]
@@ -263,7 +271,17 @@ class ManagedUSB
263
271
  end
264
272
  bus ||= loc[:bus]
265
273
  ports.unshift(loc[:port])
266
- dev = tree[dev[:'%parent'].to_s]
274
+ parent = dev[:'%parent'].to_s
275
+ # A tree is what this walks, and a %parent chain
276
+ # that returns to a device already on the way up is
277
+ # not one. No kernel prints that, but this parses
278
+ # whatever it is handed, and the answer without the
279
+ # guard is not a wrong path -- it is an unbounded
280
+ # loop, which on a bench tool is a command that
281
+ # never returns and never says why.
282
+ break if seen[parent]
283
+ seen[parent] = true
284
+ dev = tree[parent]
267
285
  end
268
286
  return nil unless rooted && bus && !ports.empty?
269
287
  "#{bus}-#{ports.join('.')}"
@@ -48,6 +48,8 @@ class ManagedUSB
48
48
  PASSWORD = 'pass'.freeze # @!visibility private
49
49
  PORTS = 1.upto(16).to_a.freeze # @!visibility private
50
50
  ALL = :all # every port, said explicitly
51
+ # The shapes {#get} will answer in; see it for what each one is.
52
+ TYPES = [ :ports, :on_off, :on, :off ].freeze
51
53
  TRUE_LIST = [ 1, :on, :ON, :true, :TRUE, :t, :T, true ].freeze # @!visibility private
52
54
  FALSE_LIST = [ 0, :off, :OFF, :false, :FALSE, :f, :F, false ].freeze # @!visibility private
53
55
 
@@ -135,15 +137,15 @@ class ManagedUSB
135
137
 
136
138
  # Set state for the specified ports
137
139
  #
138
- # Port specification can have one of the folling format
140
+ # Port specification can have one of the following format
139
141
  #
140
142
  # 1. hash of port values: { 1 => :on, 2 => :off, ...}
141
143
  # 2. hash of port states: { :on => [1, 3], :off => 4 }
142
144
  #
143
145
  # In the case 1. the state values can be specified by
144
- #
145
- # * True: 1, :on, :ON, :true, :TRUE, true
146
- # * False: 0, :off, :OFF, :false, :FALSE, false
146
+ #
147
+ # * True: 1, :on, :ON, :true, :TRUE, :t, :T, true
148
+ # * False: 0, :off, :OFF, :false, :FALSE, :f, :F, false
147
149
  #
148
150
  # The port states that are not specified will acquire the
149
151
  # value specified by the default parameter (nil being the
@@ -258,7 +260,16 @@ class ManagedUSB
258
260
  # * off : [ 1, 2, 3, ... ]
259
261
  #
260
262
  # @param type [:ports, :on_off, :on, :off] Type of returned value
263
+ # @raise [ArgumentError] for a type outside {TYPES}
261
264
  def get(type = :ports)
265
+ # Checked before the line is opened. An unknown type is a
266
+ # caller's typo and nothing the hub can answer, so spending a
267
+ # GP and a ?Q on it before saying so helps nobody.
268
+ unless TYPES.include?(type)
269
+ raise ArgumentError, "unknown type: #{type.inspect} " \
270
+ "(expected one of #{TYPES.inspect})"
271
+ end
272
+
262
273
  h = session do
263
274
  v = _get
264
275
  ports.reduce({}) {|acc, obj|
@@ -281,7 +292,10 @@ class ManagedUSB
281
292
  when :off
282
293
  h.reject {|_,v| v }.keys
283
294
  else
284
- raise ArgumentError
295
+ # Unreachable: TYPES is checked on the way in. Here so
296
+ # that a type added to that list and not to this case says
297
+ # so, rather than answering nil.
298
+ raise Error, "no reader for #{type.inspect}"
285
299
  end
286
300
  end
287
301
 
@@ -297,6 +311,8 @@ class ManagedUSB
297
311
  # power-on by itself. Confirmed against the vendor's own cusba
298
312
  # tool, whose /D issues the same RD command and documents it as
299
313
  # "restore to factory default settings".
314
+ # @note The password this object holds follows the hub's back to
315
+ # {PASSWORD}, so it stays usable afterwards.
300
316
  # @param confirm [Boolean] must be true; the keyword is the point
301
317
  # @raise [ArgumentError] when not confirmed
302
318
  def factory_reset(confirm: false)
@@ -305,7 +321,15 @@ class ManagedUSB
305
321
  'factory_reset drops every port and resets the ' \
306
322
  'password, and nothing undoes it; pass confirm: true'
307
323
  end
308
- action('RD', @password, secrets: [ @password ]).then { self }
324
+ action('RD', @password, secrets: [ @password ])
325
+ # RD puts the hub's password back to the default, so the one
326
+ # this object was holding is now the wrong one. Forgetting it
327
+ # here is what keeps the NEXT command from being refused by a
328
+ # hub that did exactly what it was told: without this, every
329
+ # later call on a hub that had a password raises E01, and
330
+ # nothing on the wire says why.
331
+ @password = PASSWORD.ljust(8)
332
+ self
309
333
  end
310
334
 
311
335
  # @deprecated Renamed to {#factory_reset} in 1.0.
@@ -407,13 +431,18 @@ class ManagedUSB
407
431
  def normalize(dataset, default)
408
432
  keys = dataset.keys
409
433
  if (keys - ports).empty?
410
- dataset = dataset.transform_values do |v|
411
- case v
412
- when * TRUE_LIST then true
413
- when *FALSE_LIST then false
414
- when nil
415
- else raise ArgumentError
416
- end
434
+ # to_h rather than transform_values, so that a value that
435
+ # is not a state can name the port it was given for: with
436
+ # sixteen of them, the offending value alone is not enough
437
+ # to find the typo by.
438
+ dataset = dataset.to_h do |k, v|
439
+ [ k, case v
440
+ when * TRUE_LIST then true
441
+ when *FALSE_LIST then false
442
+ when nil
443
+ else raise ArgumentError,
444
+ "not a port state: #{k} => #{v.inspect}"
445
+ end ]
417
446
  end
418
447
  elsif (keys - [:on, :off]).empty?
419
448
  on = Array(dataset[:on ])
@@ -428,7 +457,9 @@ class ManagedUSB
428
457
  dataset = on .to_h {|k| [k, true ] }
429
458
  .merge(off.to_h {|k| [k, false ] })
430
459
  else
431
- raise ArgumentError
460
+ raise ArgumentError,
461
+ 'dataset is neither { port => state } nor ' \
462
+ "{ :on/:off => ports }: #{keys.inspect}"
432
463
  end
433
464
 
434
465
  unless default.nil?
@@ -537,7 +568,20 @@ class ManagedUSB
537
568
  (serial.gets("\n") || '').chomp.tap do |data|
538
569
  @debug&.puts "--> #{data}"
539
570
  if check && data[0] != 'G'
540
- raise Error, data[1..-1]
571
+ # Exx is the hub saying no, and xx is what it
572
+ # said. Anything else is NOT a code, and must
573
+ # not be reported as one: data[1..-1] of ''
574
+ # -- which is what a silent hub and a timed-out
575
+ # read both look like -- is nil, and an Error
576
+ # raised with nil carries the class name as its
577
+ # message and nothing else. Of a one-character
578
+ # reply it is '', an error with no message at
579
+ # all. Either way the operator is told the
580
+ # command failed and not one thing more, at
581
+ # exactly the moment the line went quiet.
582
+ code = data.match(/\AE(\S+)\z/)
583
+ raise Error, code ? code[1]
584
+ : "unexpected reply: #{data.inspect}"
541
585
  end
542
586
  end
543
587
  end
data/lib/exsys/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module ExSYS
2
- VERSION = '1.1' # Version
2
+ VERSION = '1.2' # Version
3
3
  end
@@ -148,6 +148,35 @@ class TestDiscovery < Minitest::Test
148
148
  assert_equal '0x0403', tree['uftdi0'][:'%pnpinfo'][:vendor]
149
149
  end
150
150
 
151
+ # '/dev/tty' + '' is /dev/tty -- the caller's own controlling
152
+ # terminal. An adapter whose tty is not named yet has no line to
153
+ # offer, and offering that one would have a caller open the
154
+ # operator's screen and write hub command frames into it.
155
+ def test_an_adapter_with_no_tty_yet_is_not_a_candidate
156
+ t = D::FreeBSD.parse(<<~SYSCTL)
157
+ dev.uhub.0.%location=
158
+ dev.uhub.0.%parent=usbus1
159
+ dev.uftdi.0.%location=bus=1 hubaddr=1 port=2 devaddr=3
160
+ dev.uftdi.0.%parent=uhub0
161
+ dev.uftdi.0.%pnpinfo=vendor=0x0403 product=0x6001 sernum="AL03GD7X"
162
+ SYSCTL
163
+ assert_nil D::FreeBSD.candidate(t['uftdi0'], t)
164
+ end
165
+
166
+ # A %parent chain that comes back on itself is not a tree. No
167
+ # kernel prints one; this parses whatever it is handed, and the
168
+ # answer without a guard is not a wrong path but a command that
169
+ # never returns.
170
+ def test_a_parent_chain_that_loops_terminates
171
+ t = D::FreeBSD.parse(<<~SYSCTL)
172
+ dev.uhub.1.%location=bus=1 hubaddr=1 port=1 devaddr=2
173
+ dev.uhub.1.%parent=uhub2
174
+ dev.uhub.2.%location=bus=1 hubaddr=2 port=2 devaddr=3
175
+ dev.uhub.2.%parent=uhub1
176
+ SYSCTL
177
+ assert_nil D::FreeBSD.usb_path(t['uhub1'], t)
178
+ end
179
+
151
180
  def test_nothing_attached_is_an_empty_list_not_an_error
152
181
  assert_empty freebsd('')
153
182
  end
@@ -134,10 +134,25 @@ class TestManagedUSB < Minitest::Test
134
134
  assert_match(/overlap/, err.message)
135
135
  end
136
136
 
137
+ # Each refusal names what it refused. These used to be bare
138
+ # ArgumentErrors, whose message was the class name and which left a
139
+ # caller with sixteen ports to find the typo among by hand.
137
140
  def test_set_refuses_a_mixed_or_unknown_notation
138
- assert_raises(ArgumentError) { @usb.set({ :on => [ 1 ], 2 => :off }) }
139
- assert_raises(ArgumentError) { @usb.set({ :bogus => [ 1 ] }) }
140
- assert_raises(ArgumentError) { @usb.set({ 1 => :perhaps }) }
141
+ err = assert_raises(ArgumentError) {
142
+ @usb.set({ :on => [ 1 ], 2 => :off })
143
+ }
144
+ assert_match(/neither/, err.message)
145
+ assert_match(/:on/, err.message)
146
+
147
+ err = assert_raises(ArgumentError) { @usb.set({ :bogus => [ 1 ] }) }
148
+ assert_match(/:bogus/, err.message)
149
+
150
+ err = assert_raises(ArgumentError) {
151
+ @usb.set({ 1 => true, 7 => :perhaps })
152
+ }
153
+ assert_match(/not a port state/, err.message)
154
+ assert_match(/7/, err.message)
155
+ assert_match(/:perhaps/, err.message)
141
156
  end
142
157
 
143
158
  ## get ###############################################################
@@ -178,8 +193,17 @@ class TestManagedUSB < Minitest::Test
178
193
  assert_equal [ 3, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ], @usb.get(:on)
179
194
  end
180
195
 
196
+ # Refused by name, and before the line is opened: an unknown type
197
+ # is a typo and nothing the hub can answer, so it used to cost a GP
198
+ # and a ?Q and then raise a bare ArgumentError whose message was
199
+ # the class name.
181
200
  def test_get_refuses_an_unknown_type
182
- assert_raises(ArgumentError) { @usb.get(:bogus) }
201
+ @usb.port_count # the one-off ?Q, out of the way
202
+ @hub.log.clear
203
+ err = assert_raises(ArgumentError) { @usb.get(:bogus) }
204
+ assert_match(/unknown type/, err.message)
205
+ assert_match(/:bogus/, err.message)
206
+ assert_empty @hub.log, 'a typo must not reach the hub'
183
207
  end
184
208
 
185
209
  ## Flash and reset ###################################################
@@ -353,6 +377,24 @@ class TestManagedUSB < Minitest::Test
353
377
  assert_equal FakeHub::DEFAULT_PASSWORD, @hub.password
354
378
  end
355
379
 
380
+ # Regression: RD puts the hub's password back to the default, and
381
+ # this object went on holding the old one. Every later command was
382
+ # then refused by the hub that had just done what it was told --
383
+ # E01, with nothing on the wire saying why. Only reachable on a hub
384
+ # whose password is NOT the default, which is why the test above,
385
+ # running on a default-password hub, could not see it.
386
+ def test_factory_reset_forgets_the_password_the_hub_dropped
387
+ UART.hub = hub = FakeHub.new(password: 's3cret'.ljust(8))
388
+ usb = ExSYS::ManagedUSB.new('/dev/null', 's3cret')
389
+
390
+ usb.factory_reset(confirm: true)
391
+ usb.on(2)
392
+
393
+ assert_equal [ 2 ], hub.ports_on
394
+ assert_equal 'SPpass 0200FFFF', hub.log.last,
395
+ 'the frame must carry the password the hub now has'
396
+ end
397
+
356
398
  def test_on_with_commit_writes_through_to_flash
357
399
  @usb.on(3, commit: true)
358
400
  @usb.on(4)
@@ -440,6 +482,38 @@ class TestManagedUSB < Minitest::Test
440
482
  assert_raises(ExSYS::ManagedUSB::Error) { @usb.get }
441
483
  end
442
484
 
485
+ # Regression, and the same defect as the one above on the other
486
+ # path: a command whose reply is CHECKED -- commit, reset,
487
+ # factory_reset, password, and the SP behind every switch -- read
488
+ # the reply as an Exx code without first establishing it was one.
489
+ # data[1..-1] of '' is nil, and an Error raised with nil carries the
490
+ # class name as its message: a silent hub, which is also what a
491
+ # timed-out read looks like, reported itself as
492
+ # 'exsys-usb: ExSYS::ManagedUSB::Error' and nothing more.
493
+ def test_a_silent_hub_says_so_on_a_checked_command
494
+ @hub.silent = true
495
+ err = assert_raises(ExSYS::ManagedUSB::Error) { @usb.commit }
496
+ assert_match(/unexpected reply/, err.message)
497
+ end
498
+
499
+ # ... and of a one-character reply it is '', an error with no
500
+ # message at all. Anything that is not an Exx is reported as what
501
+ # arrived, not as a code the hub never sent.
502
+ def test_a_reply_too_short_to_be_a_code_is_not_read_as_one
503
+ @hub.garbage = 'X'
504
+ err = assert_raises(ExSYS::ManagedUSB::Error) { @usb.commit }
505
+ assert_match(/unexpected reply/, err.message)
506
+ assert_match(/"X"/, err.message)
507
+ end
508
+
509
+ # The Exx path itself still answers with the code alone, which is
510
+ # what the wrong-password test above reads.
511
+ def test_a_refusal_on_a_checked_command_is_still_just_its_code
512
+ @hub.garbage = 'E42'
513
+ err = assert_raises(ExSYS::ManagedUSB::Error) { @usb.commit }
514
+ assert_equal '42', err.message
515
+ end
516
+
443
517
  ## The serial line ###################################################
444
518
 
445
519
  # Regression: a read-modify-write used to close the line between the
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exsys
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.1'
4
+ version: '1.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stéphane D'Alu