#!/usr/bin/env expect

# Push a file to the device, run it, and watch the tests run
#
# A typical invocation looks like:
#   TCLLIBPATH=./expectnmcu ./tap-driver.expect -serial /dev/ttyUSB3 ./mispec.lua ./mispec_file.lua
#
# For debugging the driver 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 ./tap-driver.expect ...
#
# The -debug option will turn on some additional reporting from this driver program, as well.


package require expectnmcu::core
package require expectnmcu::xfer

package require cmdline
set cmd_parameters {
  { serial.arg "/dev/ttyUSB0"   "Set the serial interface name" }
  { tpfx.arg   "TAP: "          "Set the expected TAP test prefix" }
  { lfs.arg    ""               "Flash a file to LFS" }
  { noxfer                      "Do not send files, just run script" }
  { runfunc                     "Last argument is function, not file" }
  { notests                     "Don't run tests, just xfer files" }
  { nontestshim                 "Don't shim NTest when testing" }
  { debug                       "Enable debugging reporting" }
}
set cmd_usage "- A NodeMCU Lua-based-test runner"
if {[catch {array set cmdopts [cmdline::getoptions ::argv $cmd_parameters $cmd_usage]}]} {
  send_user [cmdline::usage $cmd_parameters $cmd_usage]
  send_user "\n  Additional arguments should be files be transferred\n"
  send_user "  The last file transferred will be run with `dofile`\n"
  exit 0
}

if { ${cmdopts(noxfer)} } {
  if { [ llength ${::argv} ] > 1 } {
    send_user "No point in more than one argument if noxfer given\n"
    exit 1
  }
} {
  set xfers ${::argv}

  if { ${cmdopts(runfunc)} } {
    # Last argument is command, not file to xfer
    set xfers [lreplace xfers end end]
  }

  foreach arg ${xfers} {
    if { ! [file exists ${arg}] } {
      send_user "File ${arg} does not exist\n"
      exit 1
    }
  }
}

if { ${cmdopts(lfs)} ne "" } {
  if { ! [file exists ${cmdopts(lfs)}] } {
    send_user "LFS file does not exist\n"
    exit 1
  }
}

proc sus { what } { send_user "\n===> ${what} <===\n" }
proc sui { what } { send_user "\n---> ${what} <---\n" }
proc sud { what } {
  upvar 1 cmdopts cmdopts
  if { ${cmdopts(debug)} } { send_user "\n~~~> ${what} <~~~\n" }
}

set victim [::expectnmcu::core::connect ${cmdopts(serial)}]
sus "Machine has booted"

if { ${cmdopts(lfs)} ne "" } {
  ::expectnmcu::xfer::sendfile ${victim} ${cmdopts(lfs)} "tap-driver.lfs"
  send -i ${victim} "=node.LFS.reload(\"tap-driver.lfs\")\n"
  ::expectnmcu::core::waitboot ${victim}
}

if { ! ${cmdopts(noxfer)} } {
  foreach arg ${xfers} {
    ::expectnmcu::xfer::sendfile ${victim} ${arg} [file tail ${arg}]
  }
}

set tfn [file tail [lindex ${::argv} end ] ]

if { ${cmdopts(notests)} || ${tfn} eq "" } {
  sus "No tests requested, and so operations are completed"
  exit 0
}

sus "Files transferred; running ${tfn}"

if { ! ${cmdopts(nontestshim)} } {
  ::expectnmcu::core::send_exp_prompt_c ${victim} "function ntshim(...)"
  ::expectnmcu::core::send_exp_prompt_c ${victim} "  local test = (require \"NTest\")(...)"
  ::expectnmcu::core::send_exp_prompt_c ${victim} "  test.outputhandler = require\"NTestTapOut\""
  ::expectnmcu::core::send_exp_prompt_c ${victim} "  return test"
  ::expectnmcu::core::send_exp_prompt   ${victim} "end"
} else {
  sui "Not shimming NTest output; test must report its own TAP messages"
}

# ntshim may be nil at this point if -nontestshim was given; that's fine
if { ${cmdopts(runfunc)} } {
  send -i ${victim} "[ lindex ${::argv} end ](ntshim)\n"
  expect -i ${victim} -re "\\(ntshim\\)\[\r\n\]+" { }
} else {
  send -i ${victim} "assert(loadfile(\"${tfn}\"))(ntshim)\n"
  expect -i ${victim} -re "assert\\(loadfile\\(\"${tfn}\"\\)\\)\\(ntshim\\)\[\r\n\]+" { }
}

set tpfx ${cmdopts(tpfx)}
set toeol "\[^\n\]*(?=\n)"

# Wait for the test to start and tell us how many
# success lines we should expect
set ntests 0
set timeout 10
expect {
  -i ${victim} -re "${tpfx}1\\.\\.(\\d+)(?=\r?\n)" {
    global ntests
    set ntests $expect_out(1,string)
  }
  -i ${victim} -re "${tpfx}Bail out!${toeol}" {
    sus "Bail out before start"
    exit 2
  }
  -i ${victim} -re ${::expectnmcu::core::panicre} {
    sus "Panic!"
    exit 2
  }
  # A prefixed line other than a plan (1..N) or bailout means we've not got
  # a plan.  Leave ${ntests} at 0 and proceed to run the protocol.
  -i ${victim} -notransfer -re "${tpfx}${toeol}" { }
  # -i ${victim} -ex "\n> " {
  #   sus "Prompt before start!"
  #   exit 2
  # }
  # Consume other outputs and discard as if they were comments
  # This must come as the last pattern that looks at input
  -i ${victim} -re "(?p).${toeol}" { exp_continue }
  timeout {
    send_user "Failure: time out getting started\n"
    exit 2
  }
}

if { ${ntests} == 0 } {
  sus "System did not report plan; will look for summary at end"
} else {
  sus "Expecting ${ntests} test results"
}

set timeout 60
set exitwith 0
set failures 0
for {set this 1} {${ntests} == 0 || ${this} <= ${ntests}} {incr this} {
  expect {
    -i ${victim} -re "${tpfx}#${toeol}" {
      sud "Harness got comment: ${expect_out(buffer)}"
      exp_continue
    }
    -i ${victim} -re "${tpfx}ok (\\d+)\\D${toeol}" {
      sud "Harness acknowledge OK! ${this} ${expect_out(1,string)}"
      set tid ${expect_out(1,string)}
      if { ${tid} != "" && ${tid} != ${this} } {
        sui "WARNING: Test reporting misaligned at ${this} (got ${tid})"
      }
    }
    -i ${victim} -re "${tpfx}ok #${toeol}" {
      sud "Harness acknowledge anonymous ok! ${this}"
    }
    -i ${victim} -re "${tpfx}not ok (\\d+)\\D${toeol}" {
      sud "Failure in simulation after ${this} ${expect_out(1,string)}"
      set tid ${expect_out(1,string)}
      if { ${tid} != "" && ${tid} != ${this} } {
        sui "WARNING: Test reporting misaligned at ${this}"
      }
      set exitwith [expr max(${exitwith},1)]
      incr failures
    }
    -i ${victim} -re "${tpfx}not ok #${toeol}" {
      sud "Failure (anonymous) in simulation after ${this}"
      set exitwith [expr max(${exitwith},1)]
      incr failures
    }
    -i ${victim} -re "${tpfx}Bail out!${toeol}" {
      sus "Bail out after ${this} tests"
      exit 2
    }
    -i ${victim} -re "${tpfx}POST 1\\.\\.(\\d+)(?=\r?\n)" {
      # A post-factual plan; this must be the end of testing
      global ntests
      set ntests ${expect_out(1,string)}
      if { ${ntests} != ${this} } {
        sus "Postfix plan claimed ${ntests} but we saw ${this}"
        set exitwith [expr max(${exitwith},2)]
        incr failures
      }
      # break out of for loop
      set this ${ntests}
    }
    -i ${victim} -re "${tpfx}${toeol}" {
        sus "TAP line not understood!"
        exit 2
    }
    # -i ${victim} -ex ${::expectnmcu::core::promptstr} {
    #   sus "Prompt while running tests!"
    #   exit 2
    # }
    -i ${victim} -re ${::expectnmcu::core::panicre} {
      sus "Panic!"
      exit 2
    }
    # Consume other outputs and discard as if they were comments
    # This must come as the last pattern that looks at input
    -re "(?p).${toeol}" { exp_continue }
    timeout {
      send_user "Failure: time out\n"
      exit 2
    }
  }
}

# We think we're done running tests; send a final command for synchronization
send -i ${victim} "print(\"f\",\"i\",\"n\")\n"
expect -i ${victim} -re "print\\(\"f\",\"i\",\"n\"\\)\[\r\n\]+" { }
expect {
  -i ${victim} -ex "f\ti\tn" { }

  -i ${victim} -re "${tpfx}#${toeol}" {
    sud "Harness got comment: ${expect_out(buffer)}"
    exp_continue
  }

  -i ${victim} -re "${tpfx}Bail out!${toeol}" {
    sus "Bail out after all tests finished"
    exit 2
  }
  -i ${victim} -re "${tpfx}${toeol}" {
    sus "Unexpected TAP output after tests finished"
    exit 2
  }
  -i ${victim} -re ${::expectnmcu::core::panicre} {
    sus "Panic!"
    exit 2
  }

  -re "(?p).${toeol}" { exp_continue }
  timeout {
    send_user "Failure: time out\n"
    exit 2
  }
}

if { ${exitwith} == 0 } {
  sus "All tests reported in OK"
} else {
  sus "${failures} TEST FAILURES; REVIEW LOGS"
}
exit ${exitwith}
