nodemcu-firmware/tests/test-mqtt.expect

449 lines
13 KiB
Plaintext
Executable File

#!/usr/bin/env expect
# Walk a NodeMCU device through some basic MQTT functionality tests.
#
# Requires `openssl` and `mosquitto` host-side; tested only on Linux. Spawns a
# mosquitto broker using the configuration files next to this script, but can
# be told to use another (see -brokerhost, -brokertcp, -brokerssl, -mqttuser, -mqttpass).
#
# A typical invocation looks like:
# export NODEMCU_TESTTMP=/tmp/nodemcu
# ./preflight-tls.sh
# TCLLIBPATH=./expectnmcu ./test-mqtt.expect -serial /dev/ttyUSB3 -wifi "$(cat wificmd)"
#
# where the file `wificmd` contains something like
# wifi.setmode(wifi.STATION); wifi.sta.config({...}); wifi.sta.connect()
# where the ... is filled in with the local network's configuration. All on
# one line, tho', so that the script just gets one prompt back.
#
# For debugging the test itself, it may be useful to invoke expect with -d,
# which will give a great deal of diagnostic information about the expect state
# machine's internals:
# TCLLIBPATH=./expectnmcu expect -d ./mqtt-test.expect ...
if { [info exists ::env(NODEMCU_TESTTMP)] } {
set tdir $::env(NODEMCU_TESTTMP)
} else {
send_user "==> Specify NODEMCU_TESTTMP in environment <=="
exit 1
}
package require expectnmcu::core
namespace import expectnmcu::core::send_exp_prompt
namespace import expectnmcu::core::send_exp_prompt_c
namespace import expectnmcu::core::send_exp_res_prompt
package require expectnmcu::net
package require tcltest
package require cmdline
set cmd_parameters {
{ serial.arg "/dev/ttyUSB0" "Set the serial interface name" }
{ wifi.arg "" "Command to run to bring up the network" }
{ ip.arg "" "My IP address (will guess if not given)" }
{ brokerhost.arg "" "Broker IP address (mine if not given)" }
{ brokertcp.arg "1883" "Broker TCP port" }
{ brokerssl.arg "8883" "Broker SSL-over-TCP port" }
{ mqttuser.arg "" "MQTT username for testing" }
{ mqttpass.arg "" "MQTT password for testing" }
{ tcfg.arg "" "Specify key/value pairs for tcltest config" }
}
set cmd_usage "- A NodeMCU MQTT test program"
if {[catch {array set cmdopts [cmdline::getoptions ::argv $cmd_parameters $cmd_usage]}]} {
send_user [cmdline::usage $cmd_parameters $cmd_usage]
exit 0
}
proc onexit {} {
uplevel 1 {if {[info exists sub_sid]} {
close -i ${sub_sid}
}}
uplevel 1 {if {[info exists broker_sid]} {
exec "kill" [exp_pid -i ${broker_sid}]
close -i ${broker_sid}
}}
}
exit -onexit onexit
::tcltest::configure -verbose pste
foreach {k v} [split ${cmdopts(tcfg)}] { ::tcltest::configure ${k} ${v} }
# Boot the board
set victim [::expectnmcu::core::connect ${cmdopts(serial)}]
send_user "\n===> Machine has booted <===\n"
# Connect the board to the network
if {0 < [string length ${cmdopts(wifi)}]} {
send_exp_prompt ${victim} ${cmdopts(wifi)}
}
set victimip [::expectnmcu::net::waitwifista ${victim}]
send_user "\n===> Victim IP address ${victimip} <===\n"
if {0 < [string length ${cmdopts(ip)}]} {
set myip ${cmdopts(ip)}
} else {
set myip [::expectnmcu::net::guessmyip ${victimip}]
}
send_user "\n===> I am ${myip} <===\n"
if {0 == [string length ${cmdopts(brokerhost)}]} {
if {0 < [string length ${cmdopts(mqttuser)}]} {
send_user "MQTT user with self-managed broker doesn't make sense\n"
exit 2
}
if {0 < [string length ${cmdopts(mqttpass)}]} {
send_user "MQTT password with self-managed broker doesn't make sense\n"
exit 2
}
set cmdopts(brokerhost) ${myip}
set cmdopts(mqttuser) "nmcutest"
set cmdopts(mqttpass) "nmcutest"
spawn "./test-mqtt.mosquitto.sh"
set broker_sid ${spawn_id}
# Wait for mosquitto to come online; it announces opening its listeners
for {set i 0} {${i} < 2} {incr i} {
expect {
-i ${broker_sid} "listen socket on port ${cmdopts(brokertcp)}" { }
-i ${broker_sid} "listen socket on port ${cmdopts(brokerssl)}" { }
-i ${broker_sid} "Error" {
send_user "===> Broker error! <==="
exit 1
}
}
}
sleep 1
}
# Locally, spawn a MQTT client to listen for messages. We expect to see all
# the messages we generate, both on the host and the device under test, show
# up here, and no others.
spawn "mosquitto_sub" "-v" "-t" "nmcutest/#" "-q" "2" \
"-h" "${cmdopts(brokerhost)}" "-P" "${cmdopts(brokertcp)}" \
"-u" "${cmdopts(mqttuser)}" "-P" "${cmdopts(mqttpass)}"
set sub_sid ${spawn_id}
proc publish [list msg {topic "nmcutest/host"} {qos 2} [list acksid ${sub_sid} ] ] {
upvar 1 cmdopts cmdopts
exec "mosquitto_pub" "-t" "${topic}" "-m" "${msg}" "-q" "${qos}" \
"-h" "${cmdopts(brokerhost)}" "-P" "${cmdopts(brokertcp)}" \
"-u" "${cmdopts(mqttuser)}" "-P" "${cmdopts(mqttpass)}"
expect {
-timeout 2
timeout { return 0 }
-i ${acksid} -re "${topic} ${msg}\[\r\n\]" { return 1 }
}
}
# Create some helper functions on the DUT
send_exp_prompt ${victim} "function mkcb(str, id) return function(...) print(str, id, ...) end end"
# Ready the DUT by creating an insecure mqtt client to our broker
send_exp_prompt ${victim} "mqct = mqtt.Client(\"nmcutest\", 10, \"${cmdopts(mqttuser)}\", \"${cmdopts(mqttpass)}\")"
send_exp_prompt ${victim} "mqct:lwt(\"nmcutest/lwt\", \"lwt\", 2, 0)"
send_exp_prompt ${victim} "mqct:on(\"offline\" , mkcb(\"OFFL\", 1))"
send_exp_prompt ${victim} "mqct:on(\"puback\" , mkcb(\"PUBL\", 1))"
send_exp_prompt ${victim} "mqct:on(\"suback\" , mkcb(\"SUBA\", 1))"
send_exp_prompt ${victim} "mqct:on(\"unsuback\", mkcb(\"UNSA\", 1))"
send_exp_prompt ${victim} "mqct:on(\"message\" , mkcb(\"MESG\", 1))"
send_exp_prompt ${victim} "mqct:on(\"overflow\", mkcb(\"MOVR\", 1))"
send_exp_prompt ${victim} "mqct:connect(\"${cmdopts(brokerhost)}\", ${cmdopts(brokertcp)}, false, mkcb(\"CONN\",1), mkcb(\"CFAI\",1))"
expect {
-i ${victim} -re "CONN\t1\[^\n]*\n" { }
-i ${victim} -re "CFAI\t1\[^\n]*\n" {
send_user "\n===> MQTT connection failed, bailing out <===\n"
exit 1
}
timeout {
send_user "\n===> MQTT connection timed out, bailing out <===\n"
exit 1
}
}
# Set some default expect handlers.
expect_after {
-i ${sub_sid} "nmcutest/lwt" { return "lwt" }
timeout { return "timeout" }
eof { return "eof" }
}
# Proc to wait around for the device to heartbeat. Note that we
# are mostly waiting for the above expect_acter's lwt trigger!
proc check_pulse { victim } {
# Timeout is 1.5x keepalive, as required by spec
expect {
-timeout 15
timeout { return "ok" }
# Pass through any debugging chatter
-i ${victim} -re ".+" { exp_continue -continue_timer }
}
}
# {{{
::tcltest::test basic_wait_after_connect {
Wait to ensure that our client is sending keepalives
} -body {
set res [eval check_pulse ${victim} ]
# Help ensure any debugging output gets logged as part of this test.
send_exp_prompt ${victim} ""
return ${res}
} -result "ok"
# }}}
# {{{
::tcltest::test basic_publish_qos1 {
Basic publish test, QoS 1
} -body {
set res 0
send_exp_prompt ${victim} "mqct:publish(\"nmcutest/nmcu\", \"4567\", 1, 0)"
expect {
-i ${sub_sid} "nmcutest/nmcu 4567" { incr res }
}
expect {
-i ${victim} -re "PUBL\t1" { incr res }
}
return ${res}
} -result 2
# }}}
# {{{
::tcltest::test basic_publish_qos2 {
Basic publish test, QoS 2
} -body {
set res 0
send_exp_prompt ${victim} "mqct:publish(\"nmcutest/nmcu\", \"1234\", 2, 0)"
expect {
-i ${sub_sid} "nmcutest/nmcu 1234" { incr res }
}
expect {
-i ${victim} -re "PUBL\t1" { incr res }
}
return ${res}
} -result 2
# }}}
# {{{
::tcltest::test basic_publish_double_qos2 {
Double-tap publish test, QoS 2
} -body {
send_exp_prompt ${victim} \
"mqct:publish(\"nmcutest/nmcu\", \"1357\", 2, 0); mqct:publish(\"nmcutest/nmcu\", \"0246\", 2, 0)"
for {set i 0} {${i} < 4} {incr i} {
expect {
-i ${sub_sid} "nmcutest/nmcu 1357" { }
-i ${sub_sid} "nmcutest/nmcu 0246" { }
-i ${victim} "PUBL\t1" { }
}
}
return ${i}
} -result 4
# }}}
# {{{
::tcltest::test basic_wait_after_pub {
Wait to ensure that our client is still heartbeating after publishing
} -body {
set res [eval check_pulse ${victim} ]
send_exp_prompt ${victim} ""
return ${res}
} -result "ok"
# }}}
# {{{
::tcltest::test basic_subscribe_qos1 {
Sub test, QoS 1
} -body {
set res 0
send_exp_prompt ${victim} "mqct:subscribe(\"nmcutest/host\", 1)"
expect {
-i ${victim} "SUBA\t1" { incr res }
}
if { [ publish "12345" ] == 1 } { incr res }
expect {
-i ${victim} -re "MESG\t1\tuserdata:\[^\t]*\tnmcutest/host\t12345" { incr res }
}
send_exp_prompt ${victim} "mqct:unsubscribe(\"nmcutest/host\")"
expect {
-i ${victim} "UNSA\t1" { incr res }
}
return ${res}
} -result 4
# }}}
# {{{
::tcltest::test basic_wait_after_sub {
Wait to ensure that our client is still heartbeating after subscribing
} -body {
set res [eval check_pulse ${victim} ]
send_exp_prompt ${victim} ""
return ${res}
} -result "ok"
# }}}
# {{{
::tcltest::test overflow {
Message Overflow test
} -body {
set res 0
send_exp_prompt ${victim} "mqct:subscribe(\"nmcutest/host\", 1)"
expect {
-i ${victim} "SUBA\t1" { incr res }
}
if { [ publish [string repeat "A" 2000] ] == 1 } { incr res }
expect {
-i ${victim} -re "MOVR\t1\tuserdata:\[^\t]*\tnmcutest/host\tA*\[\r\n]" { incr res }
}
send_exp_prompt ${victim} "mqct:unsubscribe(\"nmcutest/host\")"
expect {
-i ${victim} "UNSA\t1" { incr res }
}
return ${res}
} -result 4
# }}}
# {{{
::tcltest::test un-re-sub {
Messages not received after unsubscription
} -body {
set res 0
# We are not presently subscribed
if { [ publish "54321" ] == 1 } { incr res }
expect {
-timeout 2
timeout { incr res }
-i ${victim} -re "MESG\t1\tuserdata:\[^\t]*\tnmcutest/host\t54321" { return "fail" }
}
# Now subscribe and resend
send_exp_prompt ${victim} \
"mqct:subscribe({\[\"nmcutest/host\"\]=2,\[\"nmcutest/host2\"\]=2})"
expect {
-i ${victim} "SUBA\t1" { incr res }
}
exec "mosquitto_pub" "-t" "nmcutest/host" "-m" "09876" "-q" "2" \
"-h" "${cmdopts(brokerhost)}" "-P" "${cmdopts(brokertcp)}" \
"-u" "${cmdopts(mqttuser)}" "-P" "${cmdopts(mqttpass)}"
expect {
-i ${sub_sid} -re "nmcutest/host 09876\[\r\n\]" { incr res }
}
expect {
-i ${victim} -re "MESG\t1\tuserdata:\[^\t]*\tnmcutest/host\t09876" { incr res }
}
return ${res}
} -result 5
# }}}
send_user "\n===> Graceful disconnect <===\n"
send_exp_prompt ${victim} "mqct:close()"
expect {
-timeout 5
-i ${victim} "OFFL\t1" { }
timeout {
send_user "\n===> Failed to hang up <===\n"
exit 1
}
}
# {{{
::tcltest::test close_no_lwt {
Ensure that close() does not send a LWT
} -body {
expect {
-timeout 2
-i ${sub_sid} -re "nmcutest/lwt" { return "fail" }
timeout { return "ok" }
}
} -result "ok"
# }}}
# {{{
::tcltest::test tls_bad_cert {
TLS connection, wrong certificate
} -body {
set cert [open "${tdir}/tmp-ec384r1.crt"]
send_exp_prompt_c ${victim} "tls.cert.verify(function(ix) return ix == 1 and \[\["
while { [gets ${cert} line] >= 0 } {
send_exp_prompt_c ${victim} $line
}
send_exp_prompt ${victim} "]] end)"
close ${cert}
send_exp_prompt ${victim} "tls.cert.auth(false)"
send_exp_prompt ${victim} "tls.setDebug(2)"
send_exp_prompt ${victim} "mqct:connect(\"${cmdopts(brokerhost)}\", ${cmdopts(brokerssl)}, true, mkcb(\"CONN\",2), mkcb(\"CFAI\",2))"
expect {
-i ${victim} -re "CFAI\t2\[^\n]*\n" { return "cfai" }
-i ${victim} -re "CONN\t2\[^\n]*\n" { return "conn" }
}
} -result "cfai"
# }}}
set cert [open "${tdir}/tmp-ec256v1.crt"]
send_exp_prompt_c ${victim} "tls.cert.verify(function(ix) return ix == 1 and \[\["
while { [gets ${cert} line] >= 0 } {
send_exp_prompt_c ${victim} $line
}
send_exp_prompt ${victim} "]] end)"
close ${cert}
send_exp_prompt ${victim} "tls.cert.auth(false)"
send_exp_prompt ${victim} "mqct:connect(\"${cmdopts(brokerhost)}\", ${cmdopts(brokerssl)}, true, mkcb(\"CONN\",3), mkcb(\"CFAI\",3))"
expect {
-i ${victim} -re "CONN\t3\[^\n]*\n" { }
-i ${victim} -re "CFAI\t3\[^\n]*\n" {
send_user "\n===> Unable to reconnect over SSL <===\n"
exit 1
}
timeout {
send_user "\n===> Timeout while reconnecting over SSL <===\n"
exit 1
}
}
# {{{
::tcltest::test hangup_lwt {
Disconnect wifi and wait for everyone to notice
} -body {
set res 0
send_exp_prompt ${victim} "wifi.sta.disconnect()"
# This one is more or less immediate, because the connection is actively
# torn down ESP-side.
expect {
-timeout 4
-i ${victim} -re "OFFL\t1" { incr res }
}
expect {
-timeout 30
-i ${sub_sid} -re "nmcutest/lwt" { incr res }
}
return ${res}
} -result 2
# }}}
###############################################################################
send_exp_prompt ${victim} "collectgarbage()"
::tcltest::cleanupTests