#!/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