yanglint TEST tcltest package integration

The script for the 'except' tool actually uses the tcl language.
And the tcltest package can be used for unit testing. Tests for
yanglint in interactive mode and non-interactive mode can be quite
similar, so naturally tcl scripts can be used for non-interactive mode
testing as well. Because of this, testing using the shunit2 framework
has been removed.
diff --git a/tools/lint/tests/interactive/all.tcl b/tools/lint/tests/interactive/all.tcl
new file mode 100644
index 0000000..b22a5ab
--- /dev/null
+++ b/tools/lint/tests/interactive/all.tcl
@@ -0,0 +1,15 @@
+package require tcltest
+
+# Hook to determine if any of the tests failed.
+# Sets a global variable exitCode to 1 if any test fails otherwise it is set to 0.
+proc tcltest::cleanupTestsHook {} {
+    variable numTests
+    set ::exitCode [expr {$numTests(Failed) > 0}]
+}
+
+if {[info exists ::env(TESTS_DIR)]} {
+    tcltest::configure -testdir "$env(TESTS_DIR)/interactive"
+}
+
+tcltest::runAllTests
+exit $exitCode
diff --git a/tools/lint/tests/interactive/clear.test b/tools/lint/tests/interactive/clear.test
new file mode 100644
index 0000000..5a247c9
--- /dev/null
+++ b/tools/lint/tests/interactive/clear.test
@@ -0,0 +1,10 @@
+source [expr {[info exists ::env(TESTS_DIR)] ? "$env(TESTS_DIR)/interactive/ly.tcl" : "ly.tcl"}]
+
+test clear_ietf_yang_library {clear --yang-library} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+    # add models
+    ly_cmd "clear -y"
+    ly_cmd "list" "I ietf-yang-library"
+}}
+
+cleanupTests
diff --git a/tools/lint/tests/interactive/completion.test b/tools/lint/tests/interactive/completion.test
new file mode 100644
index 0000000..633fabb
--- /dev/null
+++ b/tools/lint/tests/interactive/completion.test
@@ -0,0 +1,44 @@
+source [expr {[info exists ::env(TESTS_DIR)] ? "$env(TESTS_DIR)/interactive/ly.tcl" : "ly.tcl"}]
+
+variable ly_cleanup {
+    ly_ignore
+    ly_exit
+}
+
+test completion_hints_ietf_ip {Completion and hints for ietf-ip.yang} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+    ly_cmd "add $::env(YANG_MODULES_DIR)/ietf-ip.yang"
+
+    # completion and hint
+    ly_completion "print -f info -P " "print -f info -P /ietf-"
+
+    set hints {"/ietf-yang-schema-mount:schema-mounts" "/ietf-interfaces:interfaces" "/ietf-interfaces:interfaces-state"}
+    ly_hint "" "print -f info -P /ietf-" $hints
+
+    # double completion
+    ly_completion "i" "print -f info -P /ietf-interfaces:interfaces"
+    ly_completion "/" "print -f info -P /ietf-interfaces:interfaces/interface"
+
+    # a lot of hints
+    set hints {"/ietf-interfaces:interfaces/interface"
+        "/ietf-interfaces:interfaces/interface/name" "/ietf-interfaces:interfaces/interface/description"
+        "/ietf-interfaces:interfaces/interface/type" "/ietf-interfaces:interfaces/interface/enabled"
+        "/ietf-interfaces:interfaces/interface/link-up-down-trap-enable"
+        "/ietf-interfaces:interfaces/interface/ietf-ip:ipv4" "/ietf-interfaces:interfaces/interface/ietf-ip:ipv6"
+    }
+    ly_hint "" "print -f info -P /ietf-interfaces:interfaces/interface" $hints
+
+    # double tab
+    ly_completion "/i" "print -f info -P /ietf-interfaces:interfaces/interface/ietf-ip:ipv"
+    ly_completion "4" "print -f info -P /ietf-interfaces:interfaces/interface/ietf-ip:ipv4"
+    set hints { "/ietf-interfaces:interfaces/interface/ietf-ip:ipv4" "/ietf-interfaces:interfaces/interface/ietf-ip:ipv4/enabled"
+        "/ietf-interfaces:interfaces/interface/ietf-ip:ipv4/forwarding" "/ietf-interfaces:interfaces/interface/ietf-ip:ipv4/mtu"
+        "/ietf-interfaces:interfaces/interface/ietf-ip:ipv4/address" "/ietf-interfaces:interfaces/interface/ietf-ip:ipv4/neighbor"
+    }
+    ly_hint "\t" "print -f info -P /ietf-interfaces:interfaces/interface/ietf-ip:ipv" $hints
+
+    # no more completion
+    ly_completion "/e" "print -f info -P /ietf-interfaces:interfaces/interface/ietf-ip:ipv4/enabled "
+}}
+
+cleanupTests
diff --git a/tools/lint/tests/interactive/feature.test b/tools/lint/tests/interactive/feature.test
new file mode 100644
index 0000000..32f14d7
--- /dev/null
+++ b/tools/lint/tests/interactive/feature.test
@@ -0,0 +1,8 @@
+source [expr {[info exists ::env(TESTS_DIR)] ? "$env(TESTS_DIR)/interactive/ly.tcl" : "ly.tcl"}]
+
+test feature_all {feature --all} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+    ly_cmd "feature -a" "yang:\r\n\t(none)\r\n\r\nietf-yang-schema-mount:\r\n\t(none)\r\n" -ex
+}}
+
+cleanupTests
diff --git a/tools/lint/tests/interactive/list.test b/tools/lint/tests/interactive/list.test
new file mode 100644
index 0000000..ab59a32
--- /dev/null
+++ b/tools/lint/tests/interactive/list.test
@@ -0,0 +1,34 @@
+source [expr {[info exists ::env(TESTS_DIR)] ? "$env(TESTS_DIR)/interactive/ly.tcl" : "ly.tcl"}]
+namespace import uti::regex_xml_elements uti::regex_json_pairs
+
+set modules {ietf-yang-library ietf-inet-types}
+
+test list_basic {basic test} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+    ly_cmd "list" "ietf-yang-types"
+}}
+
+test list_format_xml {list --format xml} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+    ly_cmd "clear -y"
+    ly_cmd "list -f xml" [regex_xml_elements $modules "name"]
+}}
+
+test list_format_json {list --format json} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+    ly_cmd "clear -y"
+    ly_cmd "list -f json" [regex_json_pairs $modules "name"]
+}}
+
+test list_ietf_yang_library {Error due to missing ietf-yang-library} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+    ly_cmd_err "list -f xml" "Module \"ietf-yang-library\" is not implemented."
+}}
+
+test list_bad_format {Error due to bad format} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+    ly_cmd "clear -y"
+    ly_cmd_err "list -f csv" "Unknown output format csv"
+}}
+
+cleanupTests
diff --git a/tools/lint/tests/interactive/ly.tcl b/tools/lint/tests/interactive/ly.tcl
new file mode 100644
index 0000000..623398f
--- /dev/null
+++ b/tools/lint/tests/interactive/ly.tcl
@@ -0,0 +1,171 @@
+package require Expect
+
+source [expr {[info exists ::env(TESTS_DIR)] ? "$env(TESTS_DIR)/common.tcl" : "../common.tcl"}]
+
+# set the timeout to 1 second
+set timeout 1
+# prompt of yanglint
+set prompt "> "
+# turn off dialog between expect and yanglint
+log_user 0
+
+variable ly_setup {
+    spawn $::env(YANGLINT)
+    ly_skip_warnings
+}
+
+variable ly_cleanup {
+    ly_exit
+}
+
+# detection on eof and timeout will be on every expect command
+expect_after {
+    eof {
+        global error_head
+        error "$error_head unexpected termination"
+    } timeout {
+        global error_head
+        error "$error_head timeout"
+    }
+}
+
+# Run commands from command line
+tcltest::loadTestedCommands
+
+# namespace of internal functions
+namespace eval ly::private {}
+
+# Skip no dir and/or no history warnings and prompt.
+proc ly_skip_warnings {} {
+    global prompt
+    expect -re "(YANGLINT.*)*$prompt" {}
+}
+
+# Send command 'cmd' to the process, then check output string by 'pattern'.
+# Parameter cmd is a string of arguments.
+# Parameter pattern is a regex or an exact string to match. If is not specified, only prompt assumed afterwards.
+# It must not contain a prompt. There can be an '$' character at the end of the pattern, in which case the regex
+# matches the characters before the prompt.
+# Parameter 'opt' can contain:
+#   -ex     has a similar meaning to the expect command. The 'pattern' parameter is used as a simple string
+#           for exact matching of the output. So 'pattern' is not a regular expression but some characters
+#           must still be escaped, eg ][.
+proc ly_cmd {cmd {pattern ""} {opt ""}} {
+    global prompt
+
+    send -- "${cmd}\r"
+    expect -- "${cmd}\r\n"
+
+    if { $pattern eq "" } {
+        # command without output
+        expect ^$prompt
+        return
+    }
+
+    # definition of an expression that matches failure
+    set failure_pattern "\r\n${prompt}$"
+
+    if { $opt eq "" && [string index $pattern end] eq "$"} {
+        # check output by regular expression
+        # It was explicitly specified how the expression should end.
+        set pattern [string replace $pattern end end]
+        expect {
+            -re "${pattern}\r\n${prompt}$" {}
+            -re $failure_pattern {
+                error "unexpected output:\n$expect_out(buffer)"
+            }
+        }
+    } elseif { $opt eq "" } {
+        # check output by regular expression
+        expect {
+            -re "${pattern}.*\r\n${prompt}$" {}
+            -re $failure_pattern {
+                error "unexpected output:\n$expect_out(buffer)"
+            }
+        }
+    } elseif { $opt eq "-ex" } {
+        # check output by exact matching
+        expect {
+            -ex "${pattern}\r\n${prompt}" {}
+            -re $failure_pattern {
+                error "unexpected output:\n$expect_out(buffer)"
+            }
+        }
+    } else {
+        global error_head
+        error "$error_head unrecognized value of parameter 'opt'"
+    }
+}
+
+# Send command 'cmd' to the process, expect error header and then check output string by 'pattern'.
+# Parameter cmd is a string of arguments.
+# Parameter pattern is a regex. It must not contain a prompt.
+proc ly_cmd_err {cmd pattern} {
+    global prompt
+
+    send -- "${cmd}\r"
+    expect -- "${cmd}\r\n"
+
+    expect {
+        -re "YANGLINT\\\[E\\\]: .*${pattern}.*\r\n${prompt}$" {}
+        -re "libyang\\\[\[0-9]+\\\]: .*${pattern}.*\r\n${prompt}$" {}
+        -re "\r\n${prompt}$" {
+            error "unexpected output:\n$expect_out(buffer)"
+        }
+    }
+}
+
+# Send command 'cmd' to the process, expect warning header and then check output string by 'pattern'.
+# Parameter cmd is a string of arguments.
+# Parameter pattern is a regex. It must not contain a prompt.
+proc ly_cmd_wrn {cmd pattern} {
+    global prompt
+
+    send -- "${cmd}\r"
+    expect -- "${cmd}\r\n"
+
+    expect {
+        -re "YANGLINT\\\[W\\\]: .*${pattern}.*\r\n${prompt}$" {}
+        -re "\r\n${prompt}$" {
+            error "unexpected output:\n$expect_out(buffer)"
+        }
+    }
+}
+
+# Whatever is written is sent, output is ignored and then another prompt is expected.
+# Parameter cmd is optional and any output is ignored.
+proc ly_ignore {{cmd ""}} {
+    global prompt
+
+    send "${cmd}\r"
+    expect -re "$prompt$"
+}
+
+# Send a completion request and check if the anchored regex output matches.
+proc ly_completion {input output} {
+    global prompt
+
+    send -- "${input}\t"
+    # expecting echoing input, output and 10 terminal control characters
+    expect -re "^${input}\r> ${output}.*\r.*$"
+}
+
+# Send a completion request and check if the anchored regex hint options match.
+proc ly_hint {input prev_input hints} {
+    set output {}
+    foreach i $hints {
+        # each element might have some number of spaces and CRLF around it
+        append output "${i} *(?:\\r\\n)?"
+    }
+
+    send -- "${input}\t"
+    # expecting the hints, previous input from which the hints were generated
+    # and some number of terminal control characters
+    expect -re "^\r\n${output}\r> ${prev_input}.*\r.*$"
+}
+
+# Send 'exit' and wait for eof.
+proc ly_exit {} {
+    send "exit\r"
+    expect eof
+}