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 +4 -4
- data/README.md +23 -20
- data/Rakefile +16 -1
- data/lib/exsys/discovery.rb +19 -1
- data/lib/exsys/managed-usb.rb +59 -15
- data/lib/exsys/version.rb +1 -1
- data/test/test_discovery.rb +29 -0
- data/test/test_managed_usb.rb +78 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4be13bb30c8ab3697d13b8b3c9f92fd398ffbd2d1f3cbd817470b3da313e521
|
|
4
|
+
data.tar.gz: 2b0059f98f7d2efe29789fdeb362faf6e4c63a5218cbc57d22593d978d42153f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
103
|
-
|
|
|
104
|
-
| `-d`, `--device=DEV`
|
|
105
|
-
|
|
|
106
|
-
| `-p`, `--password=
|
|
107
|
-
| `-c`, `--commit`
|
|
108
|
-
| `-y`, `--yes`
|
|
109
|
-
| `-v`, `--verbose`
|
|
110
|
-
| `-D`, `--default=
|
|
111
|
-
| `--debug[=FILE]`
|
|
112
|
-
| `-V`, `--version`
|
|
113
|
-
| `-h`, `--help`
|
|
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
|
|
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 -
|
|
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`
|
|
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
|
|
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'
|
|
31
|
+
t.files = [ 'lib/**/*.rb' ]
|
|
17
32
|
t.options = [ '-m', 'markdown' ]
|
|
18
33
|
t.stats_options = [ '--list-undoc' ]
|
|
19
34
|
end
|
data/lib/exsys/discovery.rb
CHANGED
|
@@ -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
|
-
|
|
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('.')}"
|
data/lib/exsys/managed-usb.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 ])
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
data/test/test_discovery.rb
CHANGED
|
@@ -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
|
data/test/test_managed_usb.rb
CHANGED
|
@@ -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) {
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|