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/README.md b/tools/lint/tests/README.md
new file mode 100644
index 0000000..6c51d89
--- /dev/null
+++ b/tools/lint/tests/README.md
@@ -0,0 +1,107 @@
+# yanglint testing
+
+Testing yanglint is divided into two ways.
+It is either tested in interactive mode using the tcl command 'expect' or non-interactively, classically from the command line.
+For both modes, unit testing was used using the tcl package tcltest.
+
+## How to
+
+The sample commands in this chapter using `tclsh` are called in the `interactive` or `non-interactive` directories.
+
+### How to run all yanglint tests?
+
+In the build directory designated for cmake, enter:
+
+```
+ctest -R yanglint
+```
+
+### How to run all yanglint tests that are in interactive mode?
+
+In the interactive directory, run:
+
+```
+tclsh all.tcl
+```
+
+### How to run all yanglint tests that are in non-interactive mode?
+
+In the non-interactive directory, run:
+
+```
+tclsh all.tcl
+```
+
+### How to run all unit-tests from .test file?
+
+```
+tclsh clear.test
+```
+
+or alternatively:
+
+```
+tclsh all.tcl -file clear.test
+```
+
+### How to run one unit-test?
+
+```
+tclsh clear.test -match clear_ietf_yang_library
+```
+
+or alternatively:
+
+```
+tclsh all.tcl -file clear.test -match clear_ietf_yang_library
+```
+
+### How to run unit-tests for a certain yanglint command?
+
+Test names are assumed to consist of the command name:
+
+```
+tclsh all.tcl -match clear*
+```
+
+### How do I get more detailed information about 'expect' for a certain test?
+
+In the interactive directory, run:
+
+```
+tclsh clear.test -match clear_ietf_yang_library -load "exp_internal 1"
+```
+
+### How do I get more detailed dialog between 'expect' and yanglint for a certain test?
+
+In the interactive directory, run:
+
+```
+tclsh clear.test -match clear_ietf_yang_library -load "log_user 1"
+```
+
+### How do I suppress error message from tcltest?
+
+Probably only possible to do via `-verbose ""`
+
+### How can I also debug?
+
+You can write commands `interact` and `interpreter` from 'Expect' package into some test.
+However, the most useful are the `exp_internal` and `log_user`, which can also be written directly into the test.
+See also the rlwrap tool.
+You can also use other debugging methods used in tcl programming.
+
+### Are the tests between interactive mode and non-interactive mode similar?
+
+Sort of...
+- regex \n must be changed to \r\n in the tests for interactive yanglint
+
+### I would like to add a new "ly_" function.
+
+Add it to the ly.tcl file.
+If you need to call other subfunctions in it, add them to namespace ly::private.
+
+### I would like to use function other than those prefixed with "ly_".
+
+Look in the common.tcl file in the "uti" namespace,
+which contains general tcl functions that can be used in both interactive and non-interactive tests.
diff --git a/tools/lint/tests/common.tcl b/tools/lint/tests/common.tcl
new file mode 100644
index 0000000..e432bf9
--- /dev/null
+++ b/tools/lint/tests/common.tcl
@@ -0,0 +1,96 @@
+package require tcltest
+namespace import ::tcltest::test ::tcltest::cleanupTests
+
+if { ![info exists ::env(TESTS_DIR)] } {
+    # the script is not run via 'ctest' so paths must be set
+    set ::env(TESTS_DIR) "../"
+    set ::env(YANG_MODULES_DIR) "../modules"
+    set ::env(YANGLINT) "../../../../build/yanglint"
+}
+
+# prompt of error message
+set error_prompt ">>>"
+# the beginning of error message
+set error_head "$error_prompt Check-failed"
+
+namespace eval uti {
+    namespace export *
+}
+
+# Iterate through the items in the list 'lst' and return a new list where
+# the items will have the form: <prefix><item><suffix>.
+# Parameter 'index' determines at which index it will start wrapping.
+# Parameter 'step' specifies how far the iterator must move to wrap the next item.
+proc uti::wrap_list_items {lst {prefix ""} {suffix ""} {index 0} {step 1}} {
+    # counter to track when to insert wrapper
+    set cnt $step
+    set len [llength $lst]
+
+    if {$index > 0} {
+        # copy list from interval <0;$index)
+        set ret [lrange $lst 0 [expr {$index - 1}]]
+    } else {
+        set ret {}
+    }
+
+    for {set i $index} {$i < $len} {incr i} {
+        incr cnt
+        set item [lindex $lst $i]
+        if {$cnt >= $step} {
+            # insert wrapper for item
+            set cnt 0
+            lappend ret [string cat $prefix $item $suffix]
+        } else {
+            # just copy item
+            lappend ret $item
+        }
+    }
+
+    return $ret
+}
+
+# Wrap list items with xml tags.
+# The element format is: <tag>value</tag>
+# Parameter 'values' is list of values.
+# Parameter 'tag' is the name of the searched tag.
+proc uti::wrap_to_xml {values tag {index 0} {step 1}} {
+    return [wrap_list_items $values "<$tag>" "</$tag>" $index $step]
+}
+
+# Wrap list items with json attributes.
+# The pair format is: "attribute": "value"
+# Parameter 'values' is list of values.
+# Parameter 'attribute' is the name of the searched attribute.
+proc uti::wrap_to_json {values attribute {index 0} {step 1}} {
+    return [wrap_list_items $values "\"$attribute\": \"" "\"" $index $step]
+}
+
+# Convert list to a regex (which is just a string) so that 'delim' is between items,
+# 'begin' is at the beginning of the expression and 'end' is at the end.
+proc uti::list_to_regex {lst {delim ".*"} {begin ".*"} {end ".*"}} {
+    return [string cat $begin [join $lst $delim] $end]
+}
+
+# Merge two lists into one such that the nth items are merged into one separated by a delimiter.
+# Returns a list that is the same length as 'lst1' and 'lst2'
+proc uti::blend_lists {lst1 lst2 {delim ".*"}} {
+    return [lmap a $lst1 b $lst2 {string cat $a $delim $b}]
+}
+
+# Create regex to find xml elements.
+# The element format is: <tag>value</tag>
+# Parameter 'values' is list of values.
+# Parameter 'tag' is the name of the searched tag.
+# The resulting expression looks like: ".*<tag>value1</tag>.*<tag>value2</tag>.*..."
+proc uti::regex_xml_elements {values tag} {
+    return [list_to_regex [wrap_to_xml $values $tag]]
+}
+
+# Create regex to find json pairs.
+# The pair format is: "attribute": "value"
+# Parameter 'values' is list of values.
+# Parameter 'attribute' is the name of the searched attribute.
+# The resulting expression looks like: ".*\"attribute\": \"value1\".*\"attribute\": \"value2\".*..."
+proc uti::regex_json_pairs {values attribute} {
+    return [list_to_regex [wrap_to_json $values $attribute]]
+}
diff --git a/tools/lint/tests/expect/common.exp b/tools/lint/tests/expect/common.exp
deleted file mode 100644
index dea0c3f..0000000
--- a/tools/lint/tests/expect/common.exp
+++ /dev/null
@@ -1,232 +0,0 @@
-# detect the path to the yanglint binary
-if { [info exists ::env(YANGLINT)] } {
-    set yanglint "$env(YANGLINT)"
-} else {
-    set yanglint "../../../../build/yanglint"
-}
-
-# detect the path to the examples
-if { [info exists ::env(CURRENT_SOURCE_DIR)] } {
-    set yang_models "$env(CURRENT_SOURCE_DIR)/tests/models"
-} else {
-    set yang_models "../models"
-}
-
-# set the variable used to print the error message
-if { ![info exists error_verbose] } {
-    set error_verbose 1
-}
-
-# prompt of yanglint
-set prompt "> "
-# prompt of error message
-set error_prompt ">>>"
-# the beginning of error message
-set error_head "$error_prompt Check-failed"
-# set the timeout to 1 second
-set timeout 1
-
-# detection on eof and timeout will be on every expect command
-expect_after {
-    eof {
-        global error_head
-        send_error "\n$error_head unexpected termination.\n"
-        exit 1
-    } timeout {
-        global error_head
-        send_error "\n$error_head timeout.\n"
-        exit 1
-    }
-}
-
-# Internal function. Print error message and exit script with an error return value.
-proc check_failed {pattern output} {
-    global error_verbose
-    global error_prompt
-    global error_head
-
-    set frame [info frame 1]
-    set line [dict get $frame line]
-    set file [lindex [split [dict get $frame file] /] end]
-    switch $error_verbose {
-        0 {}
-        1 { send_error "\n$error_head in $file on line $line\n" }
-        2 { send_error "\n$error_head in $file on line $line, output is:\n$output\n" }
-        3 {
-            send_error "\n$error_head in $file on line $line, expecting:\n$pattern\n"
-            send_error "$error_prompt but the output is:\n$output\n"
-        }
-        default { send_error "\n$error_head unrecognized entry \"$error_verbose\" in error_verbose variable.\n" }
-    }
-    close
-    wait
-    exit 1
-}
-
-# Iterate through the items in the list 'lst' and return a new list where
-# the items will have the form: <prefix><item><suffix>.
-# Parameter 'index' determines at which index it will start wrapping.
-# Parameter 'step' specifies how far the iterator must move to wrap the next item.
-proc wrap_list_items {lst {prefix ""} {suffix ""} {index 0} {step 1}} {
-    # counter to track when to insert wrapper
-    set cnt $step
-    set len [llength $lst]
-
-    if {$index > 0} {
-        # copy list from interval <0;$index)
-        set ret [lrange $lst 0 [expr {$index - 1}]]
-    } else {
-        set ret {}
-    }
-
-    for {set i $index} {$i < $len} {incr i} {
-        incr cnt
-        set item [lindex $lst $i]
-        if {$cnt >= $step} {
-            # insert wrapper for item
-            set cnt 0
-            lappend ret [string cat $prefix $item $suffix]
-        } else {
-            # just copy item
-            lappend ret $item
-        }
-    }
-
-    return $ret
-}
-
-# Wrap list items with xml tags.
-# The element format is: <tag>value</tag>
-# Parameter 'values' is list of values.
-# Parameter 'tag' is the name of the searched tag.
-proc wrap_to_xml {values tag {index 0} {step 1}} {
-    return [wrap_list_items $values "<$tag>" "</$tag>" $index $step]
-}
-
-# Wrap list items with json attributes.
-# The pair format is: "attribute": "value"
-# Parameter 'values' is list of values.
-# Parameter 'attribute' is the name of the searched attribute.
-proc wrap_to_json {values attribute {index 0} {step 1}} {
-    return [wrap_list_items $values "\"$attribute\": \"" "\"" $index $step]
-}
-
-# Convert list to a regex (which is just a string) so that 'delim' is between items,
-# 'begin' is at the beginning of the expression and 'end' is at the end.
-proc list_to_regex {lst {delim ".*"} {begin ".*"} {end ".*"}} {
-    return [string cat $begin [join $lst $delim] $end]
-}
-
-# Merge two lists into one such that the nth items are merged into one separated by a delimiter.
-# Returns a list that is the same length as 'lst1' and 'lst2'
-proc blend_lists {lst1 lst2 {delim ".*"}} {
-    return [lmap a $lst1 b $lst2 {string cat $a $delim $b}]
-}
-
-# Create regex to find xml elements.
-# The element format is: <tag>value</tag>
-# Parameter 'values' is list of values.
-# Parameter 'tag' is the name of the searched tag.
-# The resulting expression looks like: ".*<tag>value1</tag>.*<tag>value2</tag>.*..."
-proc regex_xml_contains_elements {values tag} {
-    return [list_to_regex [wrap_list_items $values "<$tag>" "</$tag>"]]
-}
-
-# Create regex to find json pairs.
-# The pair format is: "attribute": "value"
-# Parameter 'values' is list of values.
-# Parameter 'attribute' is the name of the searched attribute.
-# The resulting expression looks like: ".*\"attribute\": \"value1\".*\"attribute\": \"value2\".*..."
-proc regex_json_contains_pairs {values attribute} {
-    return [list_to_regex [wrap_list_items $values "\"$attribute\": \"" "\""]]
-}
-
-# skip no dir and/or no history warnings and prompt
-proc skip_warnings {} {
-    global prompt
-    expect -re "(YANGLINT.*)*$prompt" {}
-}
-
-# Send command 'cmd' to the process, then check output string by 'pattern'.
-# The parameter 'pattern' should not contain prompt.
-# If 'pattern' is not specified, only the prompt assumed afterwards.
-# 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 command {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 "" } {
-        # check output by regular expression
-        expect {
-            -re "^${pattern}\r\n${prompt}$" {}
-            -indices -re $failure_pattern {
-                # Pattern does not match the output. Print error and exit the script.
-                check_failed $pattern $expect_out(1,string)
-            }
-        }
-    } elseif { $opt eq "-ex" } {
-        # check output by exact matching
-        expect {
-            -ex "${pattern}\r\n${prompt}" {}
-            -indices -re $failure_pattern {
-                # Pattern does not match the output. Print error and exit the script.
-                check_failed $pattern $expect_out(1,string)
-            }
-        }
-    } else {
-        global error_head
-        send_error "\n$error_head unrecognized value of parameter 'opt'.\n"
-        exit 1
-    }
-}
-
-# whatever is written is sent, output is ignored and then another prompt is expected
-proc next_prompt {} {
-    global prompt
-
-    send "\r"
-    expect -re "$prompt$"
-}
-
-# send a completion request and check if the anchored regex output matches
-proc expect_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 expect_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 send_exit {} {
-    send "exit\r"
-    expect eof
-}
diff --git a/tools/lint/tests/expect/completion.exp b/tools/lint/tests/expect/completion.exp
deleted file mode 100755
index e263c09..0000000
--- a/tools/lint/tests/expect/completion.exp
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/expect -f
-
-source [expr {[info exists ::env(CURRENT_SOURCE_DIR)] ? "$env(CURRENT_SOURCE_DIR)/tests/expect/common.exp" : "common.exp"}]
-
-spawn $yanglint
-skip_warnings
-
-command "clear -ii"
-command "add ${yang_models}/ietf-ip.yang"
-
-expect_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"}
-expect_hint "" "print -f info -P /ietf-" $hints
-
-expect_completion "i" "print -f info -P /ietf-interfaces:interfaces"
-expect_completion "/" "print -f info -P /ietf-interfaces:interfaces/interface"
-
-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"}
-expect_hint "" "print -f info -P /ietf-interfaces:interfaces/interface" $hints
-
-expect_completion "/i" "print -f info -P /ietf-interfaces:interfaces/interface/ietf-ip:ipv"
-expect_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"
-}
-expect_hint "\t" "print -f info -P /ietf-interfaces:interfaces/interface/ietf-ip:ipv" $hints
-
-expect_completion "/e" "print -f info -P /ietf-interfaces:interfaces/interface/ietf-ip:ipv4/enabled "
-
-next_prompt
-send_exit
diff --git a/tools/lint/tests/expect/feature.exp b/tools/lint/tests/expect/feature.exp
deleted file mode 100755
index 4c77436..0000000
--- a/tools/lint/tests/expect/feature.exp
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/usr/bin/expect -f
-
-source [expr {[info exists ::env(CURRENT_SOURCE_DIR)] ? "$env(CURRENT_SOURCE_DIR)/tests/expect/common.exp" : "common.exp"}]
-
-spawn $yanglint
-skip_warnings
-
-command "feature -a" "yang:\r\n\t(none)\r\n\r\nietf-yang-schema-mount:\r\n\t(none)\r\n" -ex
-
-send_exit
diff --git a/tools/lint/tests/expect/list.exp b/tools/lint/tests/expect/list.exp
deleted file mode 100755
index 0d4b2ce..0000000
--- a/tools/lint/tests/expect/list.exp
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/usr/bin/expect -f
-
-source [expr {[info exists ::env(CURRENT_SOURCE_DIR)] ? "$env(CURRENT_SOURCE_DIR)/tests/expect/common.exp" : "common.exp"}]
-
-spawn $yanglint
-skip_warnings
-
-# default loaded models
-command "list" "List of the loaded models:\r
-    i ietf-yang-metadata@2016-08-05\r
-    I yang@2022-06-16\r
-    i ietf-inet-types@2013-07-15\r
-    i ietf-yang-types@2013-07-15\r
-    I ietf-yang-schema-mount@2019-01-14\r
-    i ietf-yang-structure-ext@2020-06-17" -ex
-
-# add models for ietf-yang-library
-command "clear -y"
-
-# check that the new models has been added
-command "list" "List of the loaded models:\r
-    i ietf-yang-metadata@2016-08-05\r
-    I yang@2022-06-16\r
-    i ietf-inet-types@2013-07-15\r
-    i ietf-yang-types@2013-07-15\r
-    I ietf-yang-schema-mount@2019-01-14\r
-    i ietf-yang-structure-ext@2020-06-17\r
-    I ietf-datastores@2018-02-14\r
-    I ietf-yang-library@2019-01-04" -ex
-
-# list --format
-# print xml format
-set modules {complete yang ietf-yang-schema-mount
-    ietf-datastores ietf-yang-library ietf-yang-metadata
-    ietf-inet-types ietf-yang-types ietf-yang-structure-ext
-}
-command "list -f xml" [list_to_regex [wrap_to_xml $modules "name"]]
-# print json format
-command "list -f json" [list_to_regex [wrap_to_json $modules "name"]]
-
-# remove all the loaded modules
-command "clear"
-command "list" "List of the loaded models:\r
-    i ietf-yang-metadata@2016-08-05\r
-    I yang@2022-06-16\r
-    i ietf-inet-types@2013-07-15\r
-    i ietf-yang-types@2013-07-15\r
-    I ietf-yang-schema-mount@2019-01-14\r
-    i ietf-yang-structure-ext@2020-06-17" -ex
-
-# error message when using --format without ietf-yang-library
-command "list -f xml" "libyang\[0\]: Module \"ietf-yang-library\" is not implemented.\r
-YANGLINT\[E\]: Getting context info (ietf-yang-library data) failed.\
-If the YANG module is missing or not implemented, use an option to add it internally." -ex
-
-send_exit
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
+}
diff --git a/tools/lint/tests/models/ietf-interfaces.yang b/tools/lint/tests/modules/ietf-interfaces.yang
similarity index 100%
rename from tools/lint/tests/models/ietf-interfaces.yang
rename to tools/lint/tests/modules/ietf-interfaces.yang
diff --git a/tools/lint/tests/models/ietf-ip.yang b/tools/lint/tests/modules/ietf-ip.yang
similarity index 100%
rename from tools/lint/tests/models/ietf-ip.yang
rename to tools/lint/tests/modules/ietf-ip.yang
diff --git a/tools/lint/tests/non-interactive/all.tcl b/tools/lint/tests/non-interactive/all.tcl
new file mode 100644
index 0000000..998c03a
--- /dev/null
+++ b/tools/lint/tests/non-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)/non-interactive"
+}
+
+tcltest::runAllTests
+exit $exitCode
diff --git a/tools/lint/tests/non-interactive/list.test b/tools/lint/tests/non-interactive/list.test
new file mode 100644
index 0000000..69f42da
--- /dev/null
+++ b/tools/lint/tests/non-interactive/list.test
@@ -0,0 +1,22 @@
+source [expr {[info exists ::env(TESTS_DIR)] ? "$env(TESTS_DIR)/non-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 {} {
+    ly_cmd "-l" "ietf-yang-types"
+} {}
+
+test list_format_xml {list --format xml} {
+    ly_cmd "-y -f xml -l" [regex_xml_elements $modules "name"]
+} {}
+
+test list_format_json {list --format json} {
+    ly_cmd "-y -f json -l" [regex_json_pairs $modules "name"]
+} {}
+
+test list_bad_format {Error due to bad format} {
+    ly_cmd_err "-f csv -l" "Unknown output format csv"
+} {}
+
+cleanupTests
diff --git a/tools/lint/tests/non-interactive/ly.tcl b/tools/lint/tests/non-interactive/ly.tcl
new file mode 100644
index 0000000..7cdfde8
--- /dev/null
+++ b/tools/lint/tests/non-interactive/ly.tcl
@@ -0,0 +1,94 @@
+source [expr {[info exists ::env(TESTS_DIR)] ? "$env(TESTS_DIR)/common.tcl" : "../common.tcl"}]
+
+# namespace of internal functions
+namespace eval ly::private {
+    namespace export *
+}
+
+# Run the process with arguments.
+# Parameter cmd is a string with arguments.
+# Parameter wrn is a flag. Set to 1 if stderr should be ignored.
+# Returns a pair where the first is the return code and the second is the output.
+proc ly::private::ly_exec {cmd {wrn ""}} {
+    try {
+        set results [exec -- $::env(YANGLINT) {*}$cmd]
+        set status 0
+    } trap CHILDSTATUS {results options} {
+        # return code is not 0
+        set status [lindex [dict get $options -errorcode] 2]
+    } trap NONE results {
+        if { $wrn == 1 } {
+            set status 0
+        } else {
+            error "return code is 0 but something was written to stderr:\n$results\n"
+        }
+    } trap CHILDKILLED {results options} {
+        set status [lindex [dict get $options -errorcode] 2]
+        error "process was killed: $status"
+    }
+    list $status $results
+}
+
+# Internal function.
+# Check the output with pattern.
+# Parameter pattern is a regex or an exact string to match.
+# Parameter msg is the output to check.
+# Parameter 'opt' is optional. If contains '-ex', then the 'pattern' parameter is
+# used as a simple string for exact matching of the output.
+proc ly::private::output_check {pattern msg {opt ""}} {
+    if { $opt eq "" } {
+        expr {![regexp -- $pattern $msg]}
+    } elseif { $opt eq "-ex" } {
+        expr {![string equal "$pattern" $msg]}
+    } else {
+        global error_head
+        error "$error_head unrecognized value of parameter 'opt'"
+    }
+}
+
+# Execute yanglint with arguments and expect success.
+# Parameter cmd is a string of arguments.
+# Parameter pattern is a regex or an exact string to match.
+# Parameter 'opt' is optional. If contains '-ex', then the 'pattern' parameter is
+# used as a simple string for exact matching of the output.
+proc ly_cmd {cmd {pattern ""} {opt ""}} {
+    namespace import ly::private::*
+    lassign [ly_exec $cmd] rc msg
+    if { $rc != 0 } {
+        error "unexpected return code $rc:\n$msg\n"
+    }
+    if { $pattern ne "" && [output_check $pattern $msg $opt] } {
+        error "unexpected output:\n$msg\n"
+    }
+    return
+}
+
+# Execute yanglint with arguments and expect error.
+# Parameter cmd is a string of arguments.
+# Parameter pattern is a regex.
+proc ly_cmd_err {cmd pattern} {
+    namespace import ly::private::*
+    lassign [ly_exec $cmd] rc msg
+    if { $rc == 0 } {
+        error "unexpected return code $rc"
+    }
+    if { [output_check $pattern $msg] } {
+        error "unexpected output:\n$msg\n"
+    }
+    return
+}
+
+# Execute yanglint with arguments, expect warning in stderr but success.
+# Parameter cmd is a string of arguments.
+# Parameter pattern is a regex.
+proc ly_cmd_wrn {cmd pattern} {
+    namespace import ly::private::*
+    lassign [ly_exec $cmd 1] rc msg
+    if { $rc != 0 } {
+        error "unexpected return code $rc:\n$msg\n"
+    }
+    if { [output_check $pattern $msg] } {
+        error "unexpected output:\n$msg\n"
+    }
+    return
+}
diff --git a/tools/lint/tests/shunit2/feature.sh b/tools/lint/tests/shunit2/feature.sh
deleted file mode 100755
index fb2ee88..0000000
--- a/tools/lint/tests/shunit2/feature.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env bash
-
-testFeature() {
-	models=( "iana-if-type@2014-05-08.yang" "ietf-netconf@2011-06-01.yang" "ietf-netconf-with-defaults@2011-06-01.yang"
-	 		"sm.yang" "ietf-interfaces@2014-05-08.yang" "ietf-netconf-acm@2018-02-14.yang" "ietf-origin@2018-02-14.yang"
-	 		"ietf-ip@2014-06-16.yang" "ietf-restconf@2017-01-26.yang" )
-	features=( " -F iana-if-type:"
-			  " -F ietf-netconf:writable-running,candidate,confirmed-commit,rollback-on-error,validate,startup,url,xpath"
-			  " -F ietf-netconf-with-defaults:" " -F sm:" " -F ietf-interfaces:arbitrary-names,pre-provisioning,if-mib"
-			  " -F ietf-netconf-acm:" " -F ietf-origin:" " -F ietf-ip:ipv4-non-contiguous-netmasks,ipv6-privacy-autoconf"
-			  " -F ietf-restconf:" )
-
-	for i in ${!models[@]}; do
-		output=`${YANGLINT} -f feature-param ${YANG_MODULES_DIR}/${models[$i]}`
-		assertEquals "Unexpected features of module ${models[$i]}." "${features[$i]}" "${output}"
-	done
-}
-
-. shunit2
diff --git a/tools/lint/tests/shunit2/list.sh b/tools/lint/tests/shunit2/list.sh
deleted file mode 100755
index d64503a..0000000
--- a/tools/lint/tests/shunit2/list.sh
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env bash
-
-LIST_BASE="List of the loaded models:
-    i ietf-yang-metadata@2016-08-05
-    I yang@2022-06-16
-    i ietf-inet-types@2013-07-15
-    i ietf-yang-types@2013-07-15
-    I ietf-yang-schema-mount@2019-01-14
-    i ietf-yang-structure-ext@2020-06-17"
-
-testListEmptyContext() {
-  output=`${YANGLINT} -l`
-  assertEquals "Unexpected list of modules in empty context." "${LIST_BASE}" "${output}"
-}
-
-testListAllImplemented() {
-  LIST_BASE_ALLIMPLEMENTED="List of the loaded models:
-    I ietf-yang-metadata@2016-08-05
-    I yang@2022-06-16
-    I ietf-inet-types@2013-07-15
-    I ietf-yang-types@2013-07-15
-    I ietf-yang-schema-mount@2019-01-14
-    I ietf-yang-structure-ext@2020-06-17"
-  output=`${YANGLINT} -lii`
-  assertEquals "Unexpected list of modules in empty context with -ii." "${LIST_BASE_ALLIMPLEMENTED}" "${output}"
-}
-
-. shunit2