aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan Bracco <href@random.sh>2022-03-25 15:44:44 +0000
committerJordan Bracco <href@random.sh>2022-03-25 15:44:44 +0000
commitbabd144393c0cbfdc28fb475190d9a552070b240 (patch)
tree927edd0818be95134ece7b819311b0269b3c5fdc
parentInitial PoC (diff)
slowly but surely (maybe)HEADmain
-rwxr-xr-xcloyster239
-rw-r--r--commands/freebsd/_freebsd.sh21
-rwxr-xr-xcommands/freebsd/ifconfig/list-interfaces-names (renamed from commands/ifconfig/interfaces-names)0
-rwxr-xr-xcommands/freebsd/jails/list (renamed from commands/jails/list)0
-rw-r--r--commands/freebsd/netgraph/.mock.ngctl.list.l.txt380
-rw-r--r--commands/freebsd/netgraph/_netgraph.sh7
-rwxr-xr-xcommands/freebsd/netgraph/list-nodes111
-rw-r--r--lib/os.sh19
-rw-r--r--lib/test.sh179
-rw-r--r--lib/test/asserts.sh68
-rw-r--r--lib/test/output.sh130
-rw-r--r--lib/vars.sh5
m---------vendor/shellspec0
13 files changed, 1095 insertions, 64 deletions
diff --git a/cloyster b/cloyster
index 18f72ca..16b1c23 100755
--- a/cloyster
+++ b/cloyster
@@ -6,6 +6,9 @@ hostname="${HOSTNAME:-$(hostname)}"
_executing=
_exec_error=
_script_name=$(basename "$0" | cut -d '.' -f 1)
+CLOYSTER_SCRIPT_ROOT=$(dirname "$0" | realpath)
+_testing=
+_silent=
# is_true VARIABLE
# return 0 if VARIABLE is "y", "yes", "t", "true", "1". Otherwise returns 1.
@@ -42,11 +45,14 @@ _debug() {
fi
}
+# Shadowed in the command subshell
_execution_error() {
_debug "fun=_execution_error() executing=${_executing}"
>&2 jo error="$(jo type=execution "$@")"
}
+. ./lib/vars.sh
+
. ./lib/dependencies.sh
ensure_command_exists jq
ensure_command_exists bc
@@ -60,21 +66,7 @@ fi
. ./lib/identifiers.sh
. ./lib/datetime.sh
-## getopts
-input_decode=
-output_encode=
-stdin=
-while getopts :Bbs _opt; do
- case "${_opt}" in
- b) input_decode="base64";;
- B) output_encode="base64";;
- s) stdin=yes;;
- *) _execution_error error=invalid_option option="${_opt}";;
- esac
-done
-shift $((OPTIND - 1))
-
-if is_true "${stdin}" || [ "${1}" = "-" ]; then
+if is_true "${stdin}" || [ "${1}" = "\-" ]; then
if [ "${1}" = "-" ]; then shift; fi
stdin=yes
_exec="STDIN"
@@ -90,6 +82,42 @@ else
fi
fi
+## getopts
+input_decode=
+output_encode=
+stdin=
+while getopts :BbsTqVh _opt; do
+ case "${_opt}" in
+ b) input_decode="base64";;
+ B) output_encode="base64";;
+ s) stdin=yes;;
+ T) _testing=yes;;
+ q) _silent=yes;;
+ V)
+ echo "cloyster vXXX??"
+ exit 0
+ ;;
+ h)
+ if [ -n "{_exec}" ] && [ ! "${_exec}" = "stdin" ]; then
+ echo "${_exec}: cloyster command"
+ echo "${_exec} [exec_options] [command json arguments]"
+ else
+ echo "cloyster help:"
+ fi
+ echo "execution options:"
+ echo -e " -s, -\texecute command from stdin"
+ echo -e " -q\tquiet (do not copy stdout/stderr to stderr)"
+ echo -e " -b\tdecode input from base64"
+ echo -e " -B\tencode output to base64"
+ echo -e " -T\trun tests"
+ echo -e " -V\tcloyster version"
+ exit 0
+ ;;
+ *) _execution_error error=invalid_option option="${_opt}";;
+ esac
+done
+shift $((OPTIND - 1))
+
_args_json=
if [ ! "${#}" = "0" ]; then
_args_json="$(jo -d. $@)"
@@ -113,49 +141,61 @@ json_unquote() {
echo "$1" | jq -r '.'
}
-_trap_exit() {
- _debug "TRAPPED EXIT >:( $@ executing? ${_executing}"
- if is_true "${_executing}"; then _exec_post "TRAPEXIT"; fi
- _debug "TRAPPED EXIT >:( $@ executing? ${_executing}"
- #rm "${_tmp_base}"* > /dev/null 2>&1 || true
- ( _json_loggers_stop ) > /dev/null 2>&1
- _debug "$(jobs)"
- _debug "bye"
- if is_true "${_executing}"; then exit 1; else exit 0; fi
+_cloyster_trap_exit() {
+ if is_true "${_executing}"; then
+ _debug "_cloyster_trap_exit: ignoring: is executing"
+ else
+ _debug "_cloyster_trap_exit: bye"
+ ( _json_loggers_stop ) > /dev/null 2>&1 || true
+ ( _cloyster_delete_tmp_dir ) > /dev/null 2>&1 || true
+ fi
}
-#trap _trap_exit EXIT
+trap _cloyster_trap_exit EXIT
_exec_name=$(basename "$_exec")
-_tmp_base="/tmp/${_script_name}@${_exec_name}@$(make_random_identifier 16)"
-_stdout_dest="${_tmp_base}_stdout.log"
-_stderr_dest="${_tmp_base}_stderr.log"
-_cloyster_dest="${_tmp_base}_cloyster.jsonstream"
-_output_dest="${_tmp_base}_output.jsonstream"
-_cntrl_dest="${_tmp_base}_cntrl"
+_tmp_dir="/tmp/${_script_name}@${_exec_name}@$(make_random_identifier 16)"
+_stdout_dest="${_tmp_dir}/stdout.log"
+_stderr_dest="${_tmp_dir}/stderr.log"
+_cloyster_dest="${_tmp_dir}/cloyster.jsonstream"
+_output_dest="${_tmp_dir}/output.jsonstream"
+_cntrl_dest="${_tmp_dir}/return"
_tmp_files="${_stdout_dest} ${_stderr_dest} ${_output_dest} ${_cloyster_dest} ${_cntrl_dest}"
-_debug "tmp_files: ${_tmp_files}"
+_debug "tmp_dir: ${_tmp_dir} tmp_files: ${_tmp_files}"
+mkdir "${_tmp_dir}"
for _f in ${_tmp_files}; do
if ! touch "${_f}"; then _execution_error error=tmpfile_creation_failed tmpfile="${_f}"; fi
done
+_cloyster_delete_tmp_dir() {
+ if [ -n "${_tmp_dir}" ] && ! is_true "$_tmp_dir_deleted" && [ -d "${_tmp_dir}" ]; then
+ _debug "Cleaning ${_tmp_dir}"
+ rm -rfx "${_tmp_dir}"
+ _tmp_dir_deleted=yes
+ else
+ _debug "Tmpdir already cleaned!"
+ fi
+}
+
# _logger_pretty_check NAME ISO806ZDATE LINE...
_logger_pretty_echo() {
- _name=$1
- _date=$2
- shift 2
- _line=$@
- _time=$(echo "$_date" | cut -d 'T' -f 2)
- _bold=$(tput md)
- _format_rst=$(tput me)
- _color=$(tput AF 3)
- _time_color=$(tput AF 59)
- case "${_name}" in
- stdout) _color=$(tput AF 2);;
- stderr) _color=$(tput AF 1);;
- cloyster) _color=$(tput AF 111);;
- esac
- >&2 echo -e "${_time_color}${_time} ${_color}${_bold}[${_name}]${_format_rst} ${_line}"
+ if ! is_true "${_silent}"; then
+ _name=$1
+ _date=$2
+ shift 2
+ _line=$@
+ _time=$(echo "$_date" | cut -d 'T' -f 2)
+ _bold=$(tput md)
+ _format_rst=$(tput me)
+ _color=$(tput AF 3)
+ _time_color=$(tput AF 59)
+ case "${_name}" in
+ stdout) _color=$(tput AF 2);;
+ stderr) _color=$(tput AF 1);;
+ cloyster) _color=$(tput AF 111);;
+ esac
+ >&2 echo -e "${_time_color}${_time} ${_color}${_bold}[${_name}]${_format_rst} ${_line}"
+ fi
}
_logger_pids=""
@@ -165,8 +205,9 @@ _json_logger() {
_format=$3
_combined_log=${4:-${_output_dest}}
_json_logger_job() {
+ trap - EXIT
_debug "_json_logger ${_name} ${_log}"
- tail -F "${_log}" | while read -r _line; do
+ stdbuf -oL tail -F "${_log}" | while read -r _line; do
_date=$(date_now_iso_utc)
case "${_format}" in
text)
@@ -181,24 +222,28 @@ _json_logger() {
esac
done
}
- $(_json_logger_job) &
+ _json_logger_job &
_logger_pids="${_logger_pids} $!"
}
_json_loggers_stop() {
- for tailpid in $(ps auxww | grep "tail -F ${_tmp_base}" | grep -v grep | awk '{ print $2 }'); do
+ if ! is_true "$_json_loggers_stop_done"; then
+ for tailpid in $(ps auxww | grep "tail -F ${_tmp_dir}" | grep -v grep | awk '{ print $2 }'); do
kill -s PIPE "${tailpid}" || true
- done
- for pid in ${_logger_pids}; do
+ done
+ for pid in ${_logger_pids}; do
kill "${pid}" > /dev/null 2>&1 || true
wait "${pid}" 2>/dev/null || true;
- done
+ done
+ _logger_pids=
+ _json_loggers_stop_done=yes
+ fi
+ #sleep 4
}
_json_logger "cloyster" "${_cloyster_dest}" "rawjson"
_json_logger "stdout" "${_stdout_dest}" "text"
_json_logger "stderr" "${_stderr_dest}" "text"
_exec_post() {
- trap - EXIT
_arg=$1
_executing=
_debug "_exec_post $@"
@@ -210,7 +255,7 @@ _exec_post() {
cat ${_output_dest} | jq -sr '. | @json' > "${_output_dest}.json"
eval $(cat $_cntrl_dest)
_format_output
- rm "${_tmp_base}"* || true
+ _cloyster_delete_tmp_dir
if [ ! "${_arg}" = "TRAPEXIT" ]; then exit "${_exec_exit_status:-1}"; fi
}
_format_output() {
@@ -253,24 +298,100 @@ _exec__put_return() {
echo "_exec_${_key}=${_value}" >> ${_cntrl_dest}
}
+_cloyster_exec() {
_debug "executing: command=${_exec} args=${@}"
_exec_pre
if is_true "$stdin"; then
eval "$(cat -)" 2> "${_stderr_dest}" > "${_stdout_dest}"
else
SCRIPT="${_exec}"
+ if [ "${_exec}" = "stdin" ]; then
+ CLOYSTER_COMMAND=stdin
+ CLOYSTER_COMMAND_FILE=/dev/null
+ CLOYSTER_COMMAND_DIR=/dev/null
+ _debug "Running: stdin"
+ else
+ CLOYSTER_COMMAND="$(basename "${_exec}")"
+ CLOYSTER_COMMAND_DIR="$(realpath "$(dirname "${_exec}")")"
+ CLOYSTER_COMMAND_FILE="${CLOYSTER_COMMAND_DIR}/${CLOYSTER_COMMAND}"
+ _debug "Running: ${CLOYSTER_COMMAND} (${CLOYSTER_COMMAND_FILE})"
+ fi
+ CLOYSTER_TMP_DIR="${_tmp_dir}"
+
+ _cloyster_test_tests=
+
# shellcheck source=/dev/null
- eval "$(
+ (
trap _exec__trap_exit EXIT
_execution_error() {
_exec__put_return "error" "$( jo type=cmd $@)" "json"
exit 2
}
- return_result() {
+
+ include_cmd_dir() {
+ if [ "${CLOYSTER_COMMAND}" = "stdin" ]; then
+ _execution_error function=include_cmd_dir error=include_cmd_dir_with_stdin msg="cannot be used with stdin commands"
+ exit 1
+ else
+ if [ -z "${1}" ]; then _execution_error function=include_cmd_dir error=missing_argument argument=file; exit 1; fi
+ _debug "Including ${CLOYSTER_COMMAND_DIR}/${1}"
+ . "${CLOYSTER_COMMAND_DIR}/${1}" 2>"${_stderr_dest}" 1>"${_stdout_dest}"
+ fi
+ }
+
+ return_result() {
_exec__put_return "result" "$1" "json"
exit 0
}
- . "${SCRIPT}" 2>"${_stderr_dest}" 1>"${_stdout_dest}"
- )" || true
+
+ return_error() {
+ _exec__put_return "error" "$(jo type=cmd "$@")" "json"
+ exit 1
+ }
+
+ alias RUN="{ runcmd() {"
+ alias ENDR="}; }"
+
+
+. "${CLOYSTER_SCRIPT_ROOT}/lib/test.sh"
+cloyster_test_init
+# if is_true "${_testing}"; then
+ _test() {
+ id=$(make_random_identifier 12)
+ export _cloyster_test_tests="${_cloyster_test_tests} ${id}"
+ setvar "_cloyster_test_tests_${id}__name" "$1"
+ shift
+ setvar "_cloyster_test_tests_${id}__args" "$*"
+ alias TEST="{ setvar \"_cloyster_test_tests_${id}__line\" \$LINENO; _test_${id}() {"
+ alias ENDT="}; unalias TEST; unalias ENDT; }"
+ }
+# else
+# _test() {
+# alias TEST="{ __cloyster_test_disabled() {"
+# alias ENDT="}; unalias TEST; unalias ENDT; }"
+# }
+# fi
+
+ cd "${CLOYSTER_TMP_DIR}"
+
+ _debug "Including command ${CLOYSTER_COMMAND_FILE}"
+ . "${CLOYSTER_COMMAND_FILE}" 2>"${_stderr_dest}" 1>"${_stdout_dest}"
+
+ if [ "$_executing" = "y" ]; then
+# if command -v "runcmd"; then
+ if is_true "${_testing}"; then
+ cloyster_test_run
+ else
+ _debug "runcmd!"
+ runcmd "$@" 2>"${_stderr_dest}" 1>"${_stdout_dest}"
+ fi
+# else
+# _execution_error error=no_run_function
+# fi
+ fi
+ ) || true
fi
_exec_post
+}
+
+_cloyster_exec
diff --git a/commands/freebsd/_freebsd.sh b/commands/freebsd/_freebsd.sh
new file mode 100644
index 0000000..76871f7
--- /dev/null
+++ b/commands/freebsd/_freebsd.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+freebsd() {
+ if [ ! "$(os_get "id")" = "freebsd" ]; then exit 1; fi
+}
+
+freebsd_ensure() {
+ _os_type=os_get "id"
+ if [ ! "${_os_type}" = "freebsd" ]; then
+ _execution_error error=unsupported_operating_system expected_os_type="freebsd" os_type="${_os_type}"
+ fi
+}
+
+freebsd_ensure_kernel_module_loaded() {
+ _kmod="${1}"
+ if [ -z "${_kmod}" ]; then _execution_error function=freebsd_ensure_kernel_module_loaded error=missing_argument arg=kmod; fi
+ if ! kldstat -q -m "${_kmod}" > /dev/null 2>&1; then
+ _execution_error error=missing_dependency dependency=kernel_module kernel_module="${_kmod}"
+ fi
+}
+
diff --git a/commands/ifconfig/interfaces-names b/commands/freebsd/ifconfig/list-interfaces-names
index f058404..f058404 100755
--- a/commands/ifconfig/interfaces-names
+++ b/commands/freebsd/ifconfig/list-interfaces-names
diff --git a/commands/jails/list b/commands/freebsd/jails/list
index b7e009e..b7e009e 100755
--- a/commands/jails/list
+++ b/commands/freebsd/jails/list
diff --git a/commands/freebsd/netgraph/.mock.ngctl.list.l.txt b/commands/freebsd/netgraph/.mock.ngctl.list.l.txt
new file mode 100644
index 0000000..90a8957
--- /dev/null
+++ b/commands/freebsd/netgraph/.mock.ngctl.list.l.txt
@@ -0,0 +1,380 @@
+There are 66 total nodes:
+ Name: pub Type: ether ID: 00000001 Num hooks: 2
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ upper pubbridge bridge 00000038 link1
+ lower pubbridge bridge 00000038 link0
+
+ Name: igb1 Type: ether ID: 00000002 Num hooks: 0
+
+ Name: ng0_beautte Type: eiface ID: 00000202 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link2
+
+ Name: tinc Type: ether ID: 00000003 Num hooks: 2
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ upper tincbridge bridge 0000002b link1
+ lower tincbridge bridge 0000002b link0
+
+ Name: local Type: eiface ID: 00000006 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link0
+
+ Name: ng1_front Type: eiface ID: 00000107 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link9
+
+ Name: ng0_opa Type: eiface ID: 00000407 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link23
+
+ Name: localbridge Type: bridge ID: 0000000a Num hooks: 25
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ link25 ng0_barrel eiface 0000047f ether
+ link24 ng0_portsdev eiface 00000442 ether
+ link23 ng0_opa eiface 00000407 ether
+ link22 ng0_kraui eiface 000003ce ether
+ link21 ngeth48 eiface 000003b1 ether
+ link20 ng0_kratos_admi eiface 0000037b ether
+ link19 ng0_enki_test2 eiface 00000347 ether
+ link18 ng0_enki_test1 eiface 00000315 ether
+ link17 ng0_grafana eiface 000002e5 ether
+ link16 ng0_ca eiface 000002b7 ether
+ link15 ng0_deterior eiface 000002a0 ether
+ link14 ng0_vpn_as43069 eiface 0000026a ether
+ link2 ng0_beautte eiface 00000202 ether
+ link13 ng0_mgmt eiface 000001a0 ether
+ link12 ng0_sade eiface 0000016c ether
+ link11 ng0_loki eiface 00000147 ether
+ link10 ng0_sso eiface 00000124 ether
+ link9 ng0_front eiface 000000f7 ether
+ link8 ng0_net eiface 000000c1 ether
+ link7 ng0_matrix eiface 000000a5 ether
+ link6 ng0_git eiface 0000008b ether
+ link5 ng0_vpn eiface 00000068 ether
+ link4 ng0_piss eiface 00000048 ether
+ link3 ng0_bender eiface 00000023 ether
+ link0 local eiface 00000006 ether
+
+ Name: as43069 Type: eiface ID: 0000000d Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether as43069bridge bridge 00000011 link0
+
+ Name: as43069bridge Type: bridge ID: 00000011 Num hooks: 4
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ link4 ng2_vpn_as43069 eiface 0000028a ether
+ link3 ng2_beautte eiface 0000021b ether
+ link2 ng2_net eiface 000000d8 ether
+ link0 as43069 eiface 0000000d ether
+
+ Name: ng1_beautte Type: eiface ID: 00000211 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link8
+
+ Name: ng2_front Type: eiface ID: 00000113 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether pubbridge bridge 00000038 link5
+
+ Name: ng0_enki_test1 Type: eiface ID: 00000315 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link18
+
+ Name: ng2_beautte Type: eiface ID: 0000021b Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether as43069bridge bridge 00000011 link3
+
+ Name: ng0_bender Type: eiface ID: 00000023 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link3
+
+ Name: ng1_opa Type: eiface ID: 00000423 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link21
+
+ Name: ng0_sso Type: eiface ID: 00000124 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link10
+
+ Name: tincbridge Type: bridge ID: 0000002b Num hooks: 24
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ link23 ng1_barrel eiface 0000049d ether
+ link22 ng1_portsdev eiface 0000045f ether
+ link21 ng1_opa eiface 00000423 ether
+ link20 ng1_kraui eiface 000003e9 ether
+ link19 ng1_kratos_admi eiface 00000395 ether
+ link18 ng1_enki_test2 eiface 00000360 ether
+ link17 ng1_enki_test1 eiface 0000032d ether
+ link16 ng1_grafana eiface 000002fc ether
+ link15 ng1_ca eiface 000002cd ether
+ link14 ng1_vpn_as43069 eiface 0000027f ether
+ link8 ng1_beautte eiface 00000211 ether
+ link13 ng1_mgmt eiface 000001b4 ether
+ link12 ng1_sade eiface 0000017f ether
+ link11 ng1_loki eiface 00000159 ether
+ link10 ng1_sso eiface 00000135 ether
+ link9 ng1_front eiface 00000107 ether
+ link7 ng1_net eiface 000000cf ether
+ link6 ng1_matrix eiface 000000b2 ether
+ link5 ng1_git eiface 00000097 ether
+ link4 ng1_vpn eiface 00000073 ether
+ link3 ng1_piss eiface 00000052 ether
+ link2 ng1_bender eiface 00000030 ether
+ link1 tinc ether 00000003 upper
+ link0 tinc ether 00000003 lower
+
+ Name: ng1_enki_test1 Type: eiface ID: 0000032d Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link17
+
+ Name: ng1_bender Type: eiface ID: 00000030 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link2
+
+ Name: ng1_sso Type: eiface ID: 00000135 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link10
+
+ Name: pubbridge Type: bridge ID: 00000038 Num hooks: 8
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ link7 ng2_mgmt eiface 000001c2 ether
+ link6 ng2_sade eiface 0000018c ether
+ link5 ng2_front eiface 00000113 ether
+ link4 ng2_vpn eiface 0000007e ether
+ link3 ng2_piss eiface 0000005c ether
+ link2 ng2_bender eiface 0000003d ether
+ link1 pub ether 00000001 upper
+ link0 pub ether 00000001 lower
+
+ Name: ng2_bender Type: eiface ID: 0000003d Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether pubbridge bridge 00000038 link2
+
+ Name: ng0_portsdev Type: eiface ID: 00000442 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link24
+
+ Name: ng0_loki Type: eiface ID: 00000147 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link11
+
+ Name: ng0_enki_test2 Type: eiface ID: 00000347 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link19
+
+ Name: ng0_piss Type: eiface ID: 00000048 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link4
+
+ Name: ng1_piss Type: eiface ID: 00000052 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link3
+
+ Name: ng1_loki Type: eiface ID: 00000159 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link11
+
+ Name: ng2_piss Type: eiface ID: 0000005c Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether pubbridge bridge 00000038 link3
+
+ Name: ng1_portsdev Type: eiface ID: 0000045f Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link22
+
+ Name: ng1_enki_test2 Type: eiface ID: 00000360 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link18
+
+ Name: ng0_vpn Type: eiface ID: 00000068 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link5
+
+ Name: ng0_vpn_as43069 Type: eiface ID: 0000026a Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link14
+
+ Name: ng0_sade Type: eiface ID: 0000016c Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link12
+
+ Name: ng1_vpn Type: eiface ID: 00000073 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link4
+
+ Name: ng0_kratos_admi Type: eiface ID: 0000037b Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link20
+
+ Name: ng2_vpn Type: eiface ID: 0000007e Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether pubbridge bridge 00000038 link4
+
+ Name: ng0_barrel Type: eiface ID: 0000047f Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link25
+
+ Name: ng1_sade Type: eiface ID: 0000017f Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link12
+
+ Name: ng1_vpn_as43069 Type: eiface ID: 0000027f Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link14
+
+ Name: ng2_vpn_as43069 Type: eiface ID: 0000028a Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether as43069bridge bridge 00000011 link4
+
+ Name: ng0_git Type: eiface ID: 0000008b Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link6
+
+ Name: ng2_sade Type: eiface ID: 0000018c Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether pubbridge bridge 00000038 link6
+
+ Name: ng1_kratos_admi Type: eiface ID: 00000395 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link19
+
+ Name: ng1_git Type: eiface ID: 00000097 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link5
+
+ Name: ng1_barrel Type: eiface ID: 0000049d Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link23
+
+ Name: ng0_mgmt Type: eiface ID: 000001a0 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link13
+
+ Name: ng0_deterior Type: eiface ID: 000002a0 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link15
+
+ Name: ngeth57 Type: eiface ID: 000004a3 Num hooks: 0
+
+ Name: ng0_matrix Type: eiface ID: 000000a5 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link7
+
+ Name: ngeth48 Type: eiface ID: 000003b1 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link21
+
+ Name: ng1_matrix Type: eiface ID: 000000b2 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link6
+
+ Name: ngctl39192 Type: socket ID: 000004b4 Num hooks: 0
+
+ Name: ng1_mgmt Type: eiface ID: 000001b4 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link13
+
+ Name: ng0_ca Type: eiface ID: 000002b7 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link16
+
+ Name: ng0_net Type: eiface ID: 000000c1 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link8
+
+ Name: ng2_mgmt Type: eiface ID: 000001c2 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether pubbridge bridge 00000038 link7
+
+ Name: ng1_ca Type: eiface ID: 000002cd Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link15
+
+ Name: ng0_kraui Type: eiface ID: 000003ce Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link22
+
+ Name: ng1_net Type: eiface ID: 000000cf Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link7
+
+ Name: ng2_net Type: eiface ID: 000000d8 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether as43069bridge bridge 00000011 link2
+
+ Name: ng0_grafana Type: eiface ID: 000002e5 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link17
+
+ Name: ng1_kraui Type: eiface ID: 000003e9 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link20
+
+ Name: ng0_front Type: eiface ID: 000000f7 Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether localbridge bridge 0000000a link9
+
+ Name: ng1_grafana Type: eiface ID: 000002fc Num hooks: 1
+ Local hook Peer name Peer type Peer ID Peer hook
+ ---------- --------- --------- ------- ---------
+ ether tincbridge bridge 0000002b link16
diff --git a/commands/freebsd/netgraph/_netgraph.sh b/commands/freebsd/netgraph/_netgraph.sh
new file mode 100644
index 0000000..d30ff76
--- /dev/null
+++ b/commands/freebsd/netgraph/_netgraph.sh
@@ -0,0 +1,7 @@
+include_cmd_dir "../_freebsd.sh"
+
+netgraph_prerun() {
+ freebsd_ensure
+ freebsd_ensure_kernel_module_loaded netgraph
+ ensure_command_exists ngctl
+}
diff --git a/commands/freebsd/netgraph/list-nodes b/commands/freebsd/netgraph/list-nodes
new file mode 100755
index 0000000..4ebcd63
--- /dev/null
+++ b/commands/freebsd/netgraph/list-nodes
@@ -0,0 +1,111 @@
+#!./cloyster
+include_cmd_dir '_netgraph.sh'
+
+awkk=$(cat <<AWK
+BEGIN {
+ RS="#";
+ FS="|";
+}
+{ print $1; }
+AWK
+)
+
+_netgraph_list_reformat() {
+ _input="${1}"
+ echo "${_input}" | \
+ tail -n +2 | \
+ grep -v "Local hook" | \
+ grep -v "\----------" | \
+ sed -e 's:^ ::g' \
+ -e 's:Name\: ::g' -e 's:Type\: ::g' -e 's:ID\: ::g' -e 's:Num hooks\: ::g' | \
+ tr '\n' ':' | \
+ sed -e "s/::/#/g" | \
+ awk '{gsub(/ {2,}/, "|")}; $1' | \
+ sed -e "s/|$//g" -e "s/|:$//g" | \
+ FS='[:]' awk -F'[:]' "${awkk}"
+ #) || fail "awk exited: $?"
+}
+
+_netgraph_list_json=
+_netgraph_list_to_json() {
+ if [ -z "${_netgraph_list_json}" ]; then
+ _input="${1}"
+ _ifs=$IFS
+ _json=$(jo nodes="{}")
+ for _line in ${_input}; do
+ _at=0; _node=; _nodename=
+ IFS=:
+ for _row in ${_line}; do
+ _at=$((_at + 1))
+ IFS='|'
+ set -- ""
+ _hook=;
+ for _field in ${_row}; do set -- "${@}" "${_field}"; done
+ if [ "${_at}" = "1" ]; then
+ _nodename="${2}"
+ _node=$(jo node="${_nodename}" type="${3}" id="${4}" links_count="${5}" hooks=[])
+ else
+ _hook=$(jo hook="${2}" peer_name="${3}" peer_type="${4}" peer_id="${5}" peer_hook="${6}")
+ _node=$(echo "$_node" | jo -f - hooks[]="${_hook}")
+ fi
+ done
+ _json=$(echo "$_json" | jo -d. -f - "nodes.${_nodename}=${_node}")
+ done
+ IFS=$_ifs
+ _netgraph_list_json="${_json}"
+ else
+ "${_netgraph_list_json}"
+ fi
+ echo "${_netgraph_list_json}"
+}
+
+RUN
+ netgraph_prerun
+ list="$(ngctl list -l)"
+ reformatted=$(_netgraph_list_reformat "${list}")
+ json=$(_netgraph_list_to_json "${reformatted}")
+ return_result "${json}"
+ENDR
+
+_test "awk"
+TEST
+ input="$(cat "${CLOYSTER_COMMAND_DIR}/.mock.ngctl.list.l.txt")"
+ assert_exited $? 0 "could not read mock data file"
+ assert "test -n \"${input}\""
+ (echo "$input" | grep "There are 66 total nodes" >/dev/null)
+ assert_exited $? 0 "mock input does not contains banner"
+ (echo "$input" | grep "ng0_" >/dev/null)
+ assert_exited $? 0 "mock input does not contains ng0_"
+
+ reformatted=$(_netgraph_list_reformat "${input}")
+
+ (echo "$reformatted" | grep "There are 66 total nodes" >/dev/null); s=$?
+ assert_exited "$s" 1 "reformat output contains banner"
+
+ (echo "$reformatted" | grep "ng0" > /dev/null); s=$?
+ assert_exited $s 0 "reformat output does not contains ng0"
+
+ json=$(_netgraph_list_to_json "${reformatted}")
+
+ (echo "${json}" | jq >/dev/null)
+ assert_exited $? 0 "json couldn't get parsed by jq"
+
+ # shellcheck disable=SC2034
+ count=$(echo "${json}" | jq '[.nodes[].node] | length')
+ assert_exited $? 0 "json query failed"
+ assert "echo \"\${count}\"" "67" "should contain 67 nodes"
+
+# assert "$(echo ${json}) | jq '[.nodes[].node] | length'" "67" "should contain 67 nodes"
+ENDT
+
+_test "runs on freebsd" .tag:integration .if:freebsd .skip:FIXME
+TEST
+ # ( /bin/su -l $(whoami) /bin/sh ${CLOYSTER_COMMAND_FILE} )
+ exit 1
+ENDT
+
+_test "doesnt run on freebsd" .tag:integration .not:freebsd .refute
+TEST
+ ( ${CLOYSTER_COMMAND_FILE} )
+ENDT
+
diff --git a/lib/os.sh b/lib/os.sh
index 211bfaa..2f5e1b5 100644
--- a/lib/os.sh
+++ b/lib/os.sh
@@ -5,8 +5,8 @@ export JSON_OS=
if [ -f /etc/os-release ]; then
. /etc/os-release
- _os_type="${NAME}"
- _os_name="${ID}"
+ _os_type="${ID}"
+ _os_name="${NAME}"
_os_pretty_name="${PRETTY_NAME}"
_os_version="${VERSION}"
_os_version_id="${VERSION_ID}"
@@ -15,9 +15,18 @@ else
uname=$(uname | tr '[:upper:]' '[:lower:]')
case "${uname}" in
darwin)
- _os_type="macos"
- >&2 echo "${0}: error: macos not done yet!"
- exit 1;;
+ _os_type="darwin"
+ _vers=$(sw_vers | awk '{print $2}')
+ if [ "$?" = "0" ]; then
+ _os_name=$(echo "${_vers}" | tail -n +1 | head -n 1)
+ _os_version=$(echo "${_vers}" | tail -n +2 | head -n 1)
+ _os_pretty_name="${_os_name} ${_os_version}"
+ _os_version_id="${_os_version}"
+ _os_supported=1
+ else
+ >&2 echo "cloyster/os_detect.sh: error: unknown darwin system: '${uname}'"
+ fi
+ ;;
*)
_os_type="${uname}"
>&2 echo "cloyster/os_detect.sh: error: unknown operating system: '${uname}'"
diff --git a/lib/test.sh b/lib/test.sh
new file mode 100644
index 0000000..1da470b
--- /dev/null
+++ b/lib/test.sh
@@ -0,0 +1,179 @@
+. "${CLOYSTER_SCRIPT_ROOT}/lib/test/output.sh"
+
+cloyster_test_varname() {
+ if [ -n "${1}" ]; then echo "_cloyster_test_${1}"; else echo "_cloyster_test"; fi
+}
+
+cloyster_test_init() {
+ export _cloyster_test_tests_counter_count=0
+ export _cloyster_test_tests_counter_success=0
+ export _cloyster_test_tests_counter_skipped=0
+ export _cloyster_test_tests_counter_failed=0
+ export _cloyster_test_tests_counter_ignored=0
+ export _cloyster_test_tests_failed=
+ export _cloyster_test_tests_skipped=
+ export _cloyster_test_tests_success=
+ export _cloyster_test_tests_ignored=
+ export _cloyster_test_tests=
+ export _cloyster_test_groups=
+ export _cloyster_test_init=y
+}
+
+cloyster_test_add_shellcheck() {
+ ignore=
+ if ! command -v shellcheck >/dev/null; then ign=".ignore:SHELLCHECK_REQUIRED"; fi
+ _cloyster__test__shellcheck() {
+ shellcheck -e SC1008,SC2239 "${CLOYSTER_COMMAND_FILE}"
+ }
+ cloyster_test_add -a shellcheck shellcheck "_cloyster__test__shellcheck" "${ignore}"
+}
+
+cloyster_test_add() {
+ append=
+ while getopts :a _opt; do case "${_opt}" in
+ a) append=y;;
+ esac; done; shift $((OPTIND - 1))
+ id=$1
+ name=$2
+ fun=$3
+ args=$4
+ if [ -z "${id}" ]; then return 1; fi
+ if [ -z "${name}" ]; then return 1; fi
+ list=$(cloyster_test_varname "tests")
+ var=$(cloyster_test_varname "tests_${id}")
+ if [ -n "$(readvar "$var")" ]; then
+ echo "ERROR: A test '${id}' has already been defined"
+ return 1
+ fi
+ if is_true "${append}"; then _cloyster_test_tests="${name} ${_cloyster_test_tests}"; else _cloyster_test_tests="${_cloyster_test_tests} ${name}"; fi
+ setvar "${var}" "${var}"
+ setvar "${var}__name" "${name}"
+ setvar "${var}__fun" "${fun}"
+ setvar "${var}__args" "${args}"
+}
+
+cloyster_test_run() {
+ cloyster_test_log "${_green}${_bold}⚡⚡⚡ Running cloyster tests for ${_bold}${_yellow}${CLOYSTER_COMMAND}${_rst} ⚡⚡⚡"
+ cloyster_test_log "${_darkgray} (did you knew that #!/bin/sh is twenty years older than emojis ?)${_rst}\n"
+ cloyster_test_add_shellcheck #|| s=$?; _error "Failed to lol $s"; exit 1
+
+ incr() {
+ var="_cloyster_test_tests_counter_${1}"
+ val=$(($(readvar "$var") + 1))
+ setvar "$var" "$val"
+ }
+
+ _echo_block_init
+ all_start_us=$(posix_time_microseconds)
+ for id in ${_cloyster_test_tests}; do
+ incr "count"
+ name=$(readvar "_cloyster_test_tests_${id}__name")
+ args=$(readvar "_cloyster_test_tests_${id}__args")
+ line=$(readvar "_cloyster_test_tests_${id}__line")
+ fun=$(readvar "_cloyster_test_tests_${id}__fun")
+ fun="${fun:-"_test_${id}"}"
+ testlog="${_tmp_dir}/test_${id}.log"
+ _debug "Running test '${name}' line=${line} fun=${fun} args=${args}"
+ refute=; skip=; tags=; status=; precond_status=; ignore=; fatal=;
+ skip_reason=; ignore_reason= fatal_reason=
+ for arg in $args; do
+ tag=$(echo "$arg" | cut -d ":" -f 1)
+ targ=$(echo "$arg" | sed "s/^${tag}://g")
+ case $tag in
+ .refute) refute=y;;
+ .skip) skip=y; skip_reason=${targ:-.skip};;
+ .ignore) ignore=y; ignore_reason=${targ:-.ignore};;
+ .fatal) fatal=y; fatal_reason=${targ:-.fatal};;
+ .tag) if [ -n "${targ}" ]; then tags="${tags} ${targ}"; fi;;
+ *) ;;
+ esac
+ done
+
+## -- Preconditions
+if [ ! "$skip" = "y" ]; then
+ (
+ preconds=$(
+ for arg in $args; do
+ cond=$(echo "$arg" | cut -d ":" -f 2,100)
+ case $arg in
+ .if:*) if $($cond); then miss=y; break; fi;;
+ .not:*) if ! $($cond); then miss=y; break; fi;;
+ *) ;;
+ esac
+ done
+ if [ "${miss}" = "y" ]; then
+ echo "$(tput AF 105)Precondition failed: ${arg}" >> "${testlog}"
+ exit 2
+ fi
+ )
+ precond_status=$?
+ exit $precond_status
+ ) 2>> "${testlog}" >> "${testlog}"
+ precond_status=$?
+else
+ echo "$(tput AF 105)${skip_reason}" >> "${testlog}"
+fi
+
+# -- TEST SUBSHELL
+if [ "${precond_status}" = "0" ] && [ ! "${skip}" = "y" ]; then
+(
+. "${CLOYSTER_SCRIPT_ROOT}/lib/test/asserts.sh"
+_assert_reset_count
+start_us=$(posix_time_microseconds)
+(
+ "$fun"
+ s=$?
+ asserts=$(_assert_count)
+ if [ ! "${asserts}" = "0" ]; then echo "☀️ Passed ${asserts} assertions."; fi
+ exit $s
+)
+status=$?
+end_us=$(posix_time_microseconds)
+duration=$(bc -e "$end_us - $start_us")
+if is_true "$refute" && [ "${status}" = "0" ]; then echo "⚠️ Exited with code ${_bold}${status}${_rst} (refuting, expected >0)"; fi
+if is_true "$refute" && [ ! "${status}" = "0" ]; then echo "🌗 Exited with code ${status} (refuting)"; fi
+if ! is_true "$refute" && [ ! "${status}" = "0" ]; then echo "⚠️ Exited with code ${_bold}${status}${_rst} (expected 0)"; fi
+echo "$(tput AF 59)Ran in ${duration}ms${_rst}"
+exit $status
+) 2>> "${testlog}" >> "${testlog}"
+status=$?
+fi
+
+## -- TEST FINISHED
+
+ cmpstatus="$status"
+ if is_true "$refute"; then
+ if [ "$status" = "0" ]; then cmpstatus=1; else cmpstatus=0; fi
+ fi
+ if [ "$precond_status" = "0" ] && [ "$cmpstatus" = "0" ]; then
+ _echo_block success
+ _cloyster_test_tests_success="${_cloyster_test_tests_success}${id} "
+ incr "success"
+ elif [ "$precond_status" = "2" ] || [ "$skip" = "y" ]; then
+ _echo_block skipped
+ _cloyster_test_tests_skipped="${_cloyster_test_tests_skipped} ${id}"
+ incr "skipped"
+ else
+ if [ ! "$precond_status" = "0" ]; then
+ err="precondition_error:${precond_status}"
+ else
+ err="${status}"
+ fi
+ _action=failed
+ if is_true "${ignore}"; then _action="ignored"; fi
+ _echo_block "${_action}"
+ setvar "_cloyster_test_tests_${id}__status" "$err"
+ setvar "_cloyster_test_tests_${id}__ignore" "$ignore"
+ setvar "_cloyster_test_tests_${_action}" "$(readvar "_cloyster_test_tests_${_action}") ${id}"
+ incr "${_action}"
+ fi
+
+ done
+ _echo_block_flush
+ all_end_us=$(posix_time_microseconds)
+ duration=$(bc -e "$all_end_us - $all_start_us")
+
+ _test_output_results
+ echo ""
+ _test_output_overview
+}
diff --git a/lib/test/asserts.sh b/lib/test/asserts.sh
new file mode 100644
index 0000000..b1f538e
--- /dev/null
+++ b/lib/test/asserts.sh
@@ -0,0 +1,68 @@
+#!/bin/sh
+
+fail() {
+ message=${1:-"fail called without message"}
+ echo -e " ${red}fail:${_rst} ${reason}"
+ exit 1
+}
+
+_assert_warn=
+
+assert_warn() {
+ if ! is_true "${_assert_warn}"; then _assert_warn=y; fi
+}
+
+assert_exited() {
+ status=${1}
+ expected=${2:-0}
+ message=${3:-"exit status"}
+ assert_compare "=" "$status" "$expected" "$message"
+}
+
+assert_compare() {
+ expression=$1
+ left=$2
+ right=$3
+ message=$4
+ #cloyster_test_log "assert_compare 'test \"$left\" $expression \"$right\" && echo \"OK\" || echo \"$right\"'"
+ assert "test \"$left\" $expression \"$right\" && echo \"\$?\" || echo \"\$?\"" "0" "${message}"
+}
+
+assert() {
+ assertion=$1
+ expected=$2
+ message=${3:-assertion}
+ status=
+ assertion_eval="$(eval $(echo "$1"))" && status=0 || status=$?; true
+ cloyster_test_log "assert: assertion={$(echo "$1" | head -c 30 | sed "s:\\n: :g")} expected=${expected} evaluated=${assertion_eval} status=${status}"
+ if [ ! "$status" = "0" ]; then
+ _assert_failed "exited: ${status}"
+ _assert_exit
+ elif [ ! "$assertion_eval" = "$expected" ]; then
+ _assert_failed "${message}"
+ echo -e "${_bold}Expected:${_rst} ${expected}\n${_bold}Got :${_rst} ${assertion_eval}"
+ _assert_exit
+ fi
+ _test_asserts_counter=$(( ${_test_asserts_counter:-0} + 1 ))
+ _assert_warn=
+}
+
+_assert_count() {
+ echo "$_test_asserts_counter"
+}
+_assert_reset_count() {
+ _test_asserts_counter=0
+}
+_assert_failed() {
+ name=$1
+ reason=$2; if [ -n "${reason}" ]; then reason=" ${reason}"; fi
+ line=; if [ -n "${_assert_line}" ]; then line=" at line ${_bold}${_assert_line}"; fi
+ emoji="💥"; text=
+ if is_true "$_assert_warn"; then emoji="🔥"; text=" (WARNING)"; fi
+ echo -e "${emoji} $(tput AF 214)Assertion ${_test_asserts_counter} failed${text}${line}: ${_bold}$name${_rst}${reason}"
+}
+
+_assert_exit() {
+ if ! is_true "$_assert_warn"; then exit 99; fi
+ _assert_warn=
+}
diff --git a/lib/test/output.sh b/lib/test/output.sh
new file mode 100644
index 0000000..4b09685
--- /dev/null
+++ b/lib/test/output.sh
@@ -0,0 +1,130 @@
+#!/bin/sh
+
+ _bold=$(tput md)
+ _rst=$(tput me)
+ _red=$(tput AF 202)
+ _redb=$(tput AF 202)
+ _green=$(tput AF 2)
+ _yellow=$(tput AF 3)
+ _blue=$(tput AF 111)
+ _darkgray=$(tput AF 8)
+ _gray=$(tput AF 102)
+ _orange=$(tput AF 214)
+
+cloyster_test_log() {
+# >&2 echo -e "${_ctx}[${_logcontext}]${_rst} $@${_rst}"
+ >&2 echo -e "$@${_rst}"
+ }
+ cloyster_test_debug() {
+ if is_true "${DEBUG}"; then cloyster_test_log "🔵 ${_blue}${@}"; fi
+ }
+ cloyster_test_info() {
+ cloyster_test_log "👍 ${@}"
+ }
+ cloyster_test_warn() {
+ cloyster_test_log "⚠️ ${_bold}${_yellow}${@}"
+ }
+ cloyster_test_error() {
+ cloyster_test_log "❌ ${_redb}${@}"
+ }
+
+
+_echo_block=
+_echo_block_per_line=25
+_echo_block_count=
+_echo_block_init() {
+ _echo_block=y
+ _echo_block_count=1
+}
+_echo_block() {
+ if is_true "${_echo_block}"; then
+ _echo_block_count=$((${_echo_block_count} + 1))
+ case $1 in
+ success) e="🟩";;
+ skipped) e="🟦";;
+ failed) e="🟥";;
+ ignored) e="🟧";;
+ *) e="⬛";;
+ esac
+ if [ "${_echo_block_count}" -le "${_echo_block_per_line}" ]; then
+ echo -n "${e}"
+ else
+ _echo_block_count=1
+ echo "${e}"
+ fi
+ fi
+}
+_echo_block_flush() {
+ if is_true "${_echo_block}"; then
+ _echo_block=
+ _echo_block_count=
+ echo ""
+ fi
+}
+
+_test_output_overview() {
+ if [ -n "${_cloyster_test_tests_failed}" ]; then
+ color="${_redb}"
+ accent="${_bold}${_redb}"
+ emoji="❌"
+ title="FAILED"
+ else
+ color="${_green}"
+ accent="${_bold}${_green}"
+ emoji="✅"
+ title="PASSED"
+ fi
+
+ cloyster_test_log "${emoji} ${_bold}${_yellow}${CLOYSTER_COMMAND}${_rst} ${color}tests${_rst} ${accent}${title}${_rst}"
+ _greenb=$(tput AF 154)
+ _gray=$(tput AF 8)
+ echo -n "${emoji} ${_bold}${_cloyster_test_tests_counter_count} tests${_rst}, ${_bold}${_greenb}${_cloyster_test_tests_counter_success} ok${_rst}"
+ if [ ! "${_cloyster_test_tests_counter_failed}" = "0" ]; then
+ echo -n ", ${_bold}${_redb}${_cloyster_test_tests_counter_failed} failed${_rst}"
+ fi
+ if [ ! "${_cloyster_test_tests_counter_ignored}" = "0" ]; then
+ echo -n ", ${_bold}${_orange}${_cloyster_test_tests_counter_ignored} ignored${_rst}"
+ fi
+ if [ ! "${_cloyster_test_tests_counter_skipped}" = "0" ]; then
+ echo -n ", ${_bold}${_blue}${_cloyster_test_tests_counter_skipped} skipped${_rst}"
+ fi
+ echo -n " ${_gray}in ${duration}ms${_rst}"
+ echo -e "${_rst}"
+}
+
+_test_output_results() {
+ list=${1:-"success skipped ignored failed"}
+ for t in ${list}; do _test_output_results_one "${t}"; done
+}
+
+_test_output_results_one() {
+ key=$1
+ cnt=0
+ count=$(readvar "_cloyster_test_tests_counter_${key}")
+
+ case $key in
+ success) color="${_green}"; type="SUCCESS"; emoji="👍"; logcolor=$(tput AF 64);;
+ skipped) color="${_blue}"; type="SKIPPED"; emoji="🙈"; logcolor=$(tput AF 111);;
+ failed) color="${_red}"; type="FAILED"; emoji="😡"; logcolor=$(tput AF 181);;
+ ignored) color="${_orange}"; type="IGNORED"; emoji="🥲"; logcolor=$(tput 130);;
+ *) color="${_yellow}"; type="TEST"; emoji="💩"; logcolor=${_yellow};;
+ esac
+
+ for id in $(readvar "_cloyster_test_tests_${key}"); do
+ cnt=$(( $cnt + 1 ))
+ name=$(readvar "_cloyster_test_tests_${id}__name")
+ args=$(readvar "_cloyster_test_tests_${id}__args")
+ lineno=$(readvar "_cloyster_test_tests_${id}__line")
+ status=$(readvar "_cloyster_test_tests_${id}__status")
+ args_s=; lineno_s=; status_s=; line=
+ args_s=" [${args}]"
+ if [ -n "${lineno}" ] && [ ! "${lineno}" = "0" ]; then lineno_s=" L#${lineno}"; fi
+ if [ -n "${status}" ]; then status_s=" !${status}"; fi
+ line="\n${emoji} ${_bold}${color}${type} ${cnt}/${count}:${_rst} ${_bold}${name}${_rst}"
+ line="${line}${_gray}${args_s}${_darkgray}${lineno_s}${status_s}${_rst}"
+ echo -e "${line}"
+ cat "${_tmp_dir}/test_${id}.log" | while read -r line; do
+ echo -e " ${logcolor}> ${line}${_rst}"
+ done
+ done
+}
diff --git a/lib/vars.sh b/lib/vars.sh
new file mode 100644
index 0000000..4ae2ee7
--- /dev/null
+++ b/lib/vars.sh
@@ -0,0 +1,5 @@
+readvar() {
+ local var=$1;
+ if [ ! "$var" ]; then _debug "readvar: missing arg: var"; exit 1; fi
+ eval "echo \$$var"
+}
diff --git a/vendor/shellspec b/vendor/shellspec
new file mode 160000
+Subproject 22a80af088c939e03599821b7e9721b51cb1bd8