diff --git a/.gitignore b/.gitignore index e4ea543..75e99b0 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ tests/unit/test_help_text tests/unit/test_manual_text tests/unit/test_support_text tests/unit/test_cli_text +tests/unit/test_tntctl_text tests/unit/test_ratelimit diff --git a/Makefile b/Makefile index fdbec19..bb1828f 100644 --- a/Makefile +++ b/Makefile @@ -20,12 +20,12 @@ SRC_DIR = src INC_DIR = include OBJ_DIR = obj -SOURCES = $(filter-out $(SRC_DIR)/tntctl.c,$(wildcard $(SRC_DIR)/*.c)) +SOURCES = $(filter-out $(SRC_DIR)/tntctl.c $(SRC_DIR)/tntctl_text.c,$(wildcard $(SRC_DIR)/*.c)) OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o) DEPS = $(OBJECTS:.o=.d) $(CTL_OBJECTS:.o=.d) TARGET = tnt CTL_TARGET = tntctl -CTL_OBJECTS = $(OBJ_DIR)/tntctl.o $(OBJ_DIR)/exec_catalog.o $(OBJ_DIR)/common.o $(OBJ_DIR)/i18n.o +CTL_OBJECTS = $(OBJ_DIR)/tntctl.o $(OBJ_DIR)/tntctl_text.o $(OBJ_DIR)/exec_catalog.o $(OBJ_DIR)/common.o $(OBJ_DIR)/i18n.o TARGETS = $(TARGET) $(CTL_TARGET) PREFIX ?= /usr/local diff --git a/README.md b/README.md index 7adf3e0..d2018c9 100644 --- a/README.md +++ b/README.md @@ -324,6 +324,7 @@ TNT/ │ ├── exec_catalog.c # SSH exec command matching, usage, and argument shape │ ├── exec.c # SSH exec command dispatch │ ├── tntctl.c # local wrapper around the SSH exec interface +│ ├── tntctl_text.c # tntctl help and option text │ ├── ssh_server.c # SSH server implementation │ ├── bootstrap.c # SSH authentication and session bootstrap │ ├── chat_room.c # chat room logic diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fef20c3..9a0fe78 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased ### Added +- Added a dedicated `tntctl_text` module with unit coverage for local + `tntctl` help and validation diagnostics. - Documented the stable SSH exec interface contract, including exit statuses and JSON field shapes for package tests, scripts, and future `tntctl` work. - Documented `messages.log` v1 as the stable TNT 1.x persisted history format, diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 872d6d4..0024080 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -40,6 +40,7 @@ make check ``` main.c → entry point, signal handling cli_text.c → startup CLI text +tntctl_text.c → tntctl local help and diagnostics command_catalog.c → COMMAND-mode command metadata, usage, and argument shape commands.c → COMMAND-mode command dispatch exec_catalog.c → SSH exec command matching, usage, and argument shape diff --git a/docs/QUICKREF.md b/docs/QUICKREF.md index ca3f015..65809b0 100644 --- a/docs/QUICKREF.md +++ b/docs/QUICKREF.md @@ -66,6 +66,7 @@ MAINTENANCE STRUCTURE src/main.c entry, signals src/cli_text.c startup CLI text + src/tntctl_text.c tntctl local help and diagnostics src/command_catalog.c command metadata, usage, argument shape src/ssh_server.c SSH listener and server setup src/bootstrap.c SSH auth/session bootstrap diff --git a/include/tntctl_text.h b/include/tntctl_text.h new file mode 100644 index 0000000..ed8c385 --- /dev/null +++ b/include/tntctl_text.h @@ -0,0 +1,28 @@ +#ifndef TNTCTL_TEXT_H +#define TNTCTL_TEXT_H + +#include "common.h" + +typedef enum { + TNTCTL_TEXT_USAGE, + TNTCTL_TEXT_INVALID_PORT, + TNTCTL_TEXT_INVALID_LOGIN, + TNTCTL_TEXT_INVALID_HOST_KEY_MODE, + TNTCTL_TEXT_INVALID_KNOWN_HOSTS, + TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT, + TNTCTL_TEXT_MISSING_HOST, + TNTCTL_TEXT_INVALID_HOST, + TNTCTL_TEXT_LOGIN_HOST_CONFLICT, + TNTCTL_TEXT_UNKNOWN_COMMAND, + TNTCTL_TEXT_INVALID_REMOTE_COMMAND, + TNTCTL_TEXT_DESTINATION_TOO_LONG, + TNTCTL_TEXT_INVALID_DESTINATION, + TNTCTL_TEXT_OUT_OF_MEMORY, + TNTCTL_TEXT_HOST_KEY_OPTION_TOO_LONG, + TNTCTL_TEXT_KNOWN_HOSTS_OPTION_TOO_LONG, + TNTCTL_TEXT_COUNT +} tntctl_text_id_t; + +const char *tntctl_text(ui_lang_t lang, tntctl_text_id_t id); + +#endif /* TNTCTL_TEXT_H */ diff --git a/src/tntctl.c b/src/tntctl.c index 20f4590..47dbb0c 100644 --- a/src/tntctl.c +++ b/src/tntctl.c @@ -1,6 +1,7 @@ #include "common.h" #include "exec_catalog.h" #include "i18n.h" +#include "tntctl_text.h" #include #include @@ -8,113 +9,6 @@ #include #include -typedef enum { - TNTCTL_TEXT_USAGE, - TNTCTL_TEXT_INVALID_PORT, - TNTCTL_TEXT_INVALID_LOGIN, - TNTCTL_TEXT_INVALID_HOST_KEY_MODE, - TNTCTL_TEXT_INVALID_KNOWN_HOSTS, - TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT, - TNTCTL_TEXT_MISSING_HOST, - TNTCTL_TEXT_INVALID_HOST, - TNTCTL_TEXT_LOGIN_HOST_CONFLICT, - TNTCTL_TEXT_UNKNOWN_COMMAND, - TNTCTL_TEXT_INVALID_REMOTE_COMMAND, - TNTCTL_TEXT_DESTINATION_TOO_LONG, - TNTCTL_TEXT_INVALID_DESTINATION, - TNTCTL_TEXT_OUT_OF_MEMORY, - TNTCTL_TEXT_HOST_KEY_OPTION_TOO_LONG, - TNTCTL_TEXT_KNOWN_HOSTS_OPTION_TOO_LONG, - TNTCTL_TEXT_COUNT -} tntctl_text_id_t; - -static const i18n_string_t tntctl_text_catalog[TNTCTL_TEXT_COUNT] = { - [TNTCTL_TEXT_USAGE] = I18N_STRING( - "Usage: tntctl [options] host command [args...]\n" - "\n" - "Options:\n" - " -p, --port PORT SSH port (default: 2222)\n" - " -l, --login USER SSH login name for exec identity\n" - " --host-key-checking MODE\n" - " OpenSSH host-key mode: yes, accept-new, no\n" - " --known-hosts FILE OpenSSH known_hosts file\n" - " -V, --version Print version and exit\n" - " -h, --help Print this help and exit\n" - "\n" - "Commands mirror the TNT SSH exec interface: health, stats, users,\n" - "tail, dump, post, help, and exit.\n", - "用法: tntctl [options] host command [args...]\n" - "\n" - "选项:\n" - " -p, --port PORT SSH 端口 (默认: 2222)\n" - " -l, --login USER SSH 登录名,用作 exec 身份\n" - " --host-key-checking MODE\n" - " OpenSSH 主机密钥模式: yes, accept-new, no\n" - " --known-hosts FILE OpenSSH known_hosts 文件\n" - " -V, --version 输出版本并退出\n" - " -h, --help 输出此帮助并退出\n" - "\n" - "命令对应 TNT SSH exec 接口: health, stats, users,\n" - "tail, dump, post, help 和 exit.\n" - ), - [TNTCTL_TEXT_INVALID_PORT] = I18N_STRING( - "invalid port", "端口无效" - ), - [TNTCTL_TEXT_INVALID_LOGIN] = I18N_STRING( - "invalid login", "登录名无效" - ), - [TNTCTL_TEXT_INVALID_HOST_KEY_MODE] = I18N_STRING( - "invalid host-key checking mode", "主机密钥检查模式无效" - ), - [TNTCTL_TEXT_INVALID_KNOWN_HOSTS] = I18N_STRING( - "invalid known_hosts path", "known_hosts 路径无效" - ), - [TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT] = I18N_STRING( - "unknown option: %s", "未知选项: %s" - ), - [TNTCTL_TEXT_MISSING_HOST] = I18N_STRING( - "missing host", "缺少 host" - ), - [TNTCTL_TEXT_INVALID_HOST] = I18N_STRING( - "invalid host", "host 无效" - ), - [TNTCTL_TEXT_LOGIN_HOST_CONFLICT] = I18N_STRING( - "use either --login or user@host, not both", - "只能使用 --login 或 user@host 之一" - ), - [TNTCTL_TEXT_UNKNOWN_COMMAND] = I18N_STRING( - "unknown or missing command", "未知命令或缺少命令" - ), - [TNTCTL_TEXT_INVALID_REMOTE_COMMAND] = I18N_STRING( - "invalid or too-long command", "命令无效或过长" - ), - [TNTCTL_TEXT_DESTINATION_TOO_LONG] = I18N_STRING( - "destination too long", "目标地址过长" - ), - [TNTCTL_TEXT_INVALID_DESTINATION] = I18N_STRING( - "invalid destination", "目标地址无效" - ), - [TNTCTL_TEXT_OUT_OF_MEMORY] = I18N_STRING( - "out of memory", "内存不足" - ), - [TNTCTL_TEXT_HOST_KEY_OPTION_TOO_LONG] = I18N_STRING( - "host-key option too long", "主机密钥选项过长" - ), - [TNTCTL_TEXT_KNOWN_HOSTS_OPTION_TOO_LONG] = I18N_STRING( - "known_hosts option too long", "known_hosts 选项过长" - ) -}; -typedef char tntctl_text_catalog_must_cover_enum[ - sizeof(tntctl_text_catalog) / sizeof(tntctl_text_catalog[0]) == - TNTCTL_TEXT_COUNT ? 1 : -1]; - -static const char *tntctl_text(ui_lang_t lang, tntctl_text_id_t id) { - if (id < 0 || id >= TNTCTL_TEXT_COUNT) { - return ""; - } - return i18n_string(tntctl_text_catalog[id], lang); -} - static void print_usage(FILE *stream, ui_lang_t lang) { fputs(tntctl_text(lang, TNTCTL_TEXT_USAGE), stream); } diff --git a/src/tntctl_text.c b/src/tntctl_text.c new file mode 100644 index 0000000..a0090b7 --- /dev/null +++ b/src/tntctl_text.c @@ -0,0 +1,89 @@ +#include "tntctl_text.h" + +#include "i18n.h" + +static const i18n_string_t text_catalog[TNTCTL_TEXT_COUNT] = { + [TNTCTL_TEXT_USAGE] = I18N_STRING( + "Usage: tntctl [options] host command [args...]\n" + "\n" + "Options:\n" + " -p, --port PORT SSH port (default: 2222)\n" + " -l, --login USER SSH login name for exec identity\n" + " --host-key-checking MODE\n" + " OpenSSH host-key mode: yes, accept-new, no\n" + " --known-hosts FILE OpenSSH known_hosts file\n" + " -V, --version Print version and exit\n" + " -h, --help Print this help and exit\n" + "\n" + "Commands mirror the TNT SSH exec interface: health, stats, users,\n" + "tail, dump, post, help, and exit.\n", + "用法: tntctl [options] host command [args...]\n" + "\n" + "选项:\n" + " -p, --port PORT SSH 端口 (默认: 2222)\n" + " -l, --login USER SSH 登录名,用作 exec 身份\n" + " --host-key-checking MODE\n" + " OpenSSH 主机密钥模式: yes, accept-new, no\n" + " --known-hosts FILE OpenSSH known_hosts 文件\n" + " -V, --version 输出版本并退出\n" + " -h, --help 输出此帮助并退出\n" + "\n" + "命令对应 TNT SSH exec 接口: health, stats, users,\n" + "tail, dump, post, help 和 exit.\n" + ), + [TNTCTL_TEXT_INVALID_PORT] = I18N_STRING( + "invalid port", "端口无效" + ), + [TNTCTL_TEXT_INVALID_LOGIN] = I18N_STRING( + "invalid login", "登录名无效" + ), + [TNTCTL_TEXT_INVALID_HOST_KEY_MODE] = I18N_STRING( + "invalid host-key checking mode", "主机密钥检查模式无效" + ), + [TNTCTL_TEXT_INVALID_KNOWN_HOSTS] = I18N_STRING( + "invalid known_hosts path", "known_hosts 路径无效" + ), + [TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT] = I18N_STRING( + "unknown option: %s", "未知选项: %s" + ), + [TNTCTL_TEXT_MISSING_HOST] = I18N_STRING( + "missing host", "缺少 host" + ), + [TNTCTL_TEXT_INVALID_HOST] = I18N_STRING( + "invalid host", "host 无效" + ), + [TNTCTL_TEXT_LOGIN_HOST_CONFLICT] = I18N_STRING( + "use either --login or user@host, not both", + "只能使用 --login 或 user@host 之一" + ), + [TNTCTL_TEXT_UNKNOWN_COMMAND] = I18N_STRING( + "unknown or missing command", "未知命令或缺少命令" + ), + [TNTCTL_TEXT_INVALID_REMOTE_COMMAND] = I18N_STRING( + "invalid or too-long command", "命令无效或过长" + ), + [TNTCTL_TEXT_DESTINATION_TOO_LONG] = I18N_STRING( + "destination too long", "目标地址过长" + ), + [TNTCTL_TEXT_INVALID_DESTINATION] = I18N_STRING( + "invalid destination", "目标地址无效" + ), + [TNTCTL_TEXT_OUT_OF_MEMORY] = I18N_STRING( + "out of memory", "内存不足" + ), + [TNTCTL_TEXT_HOST_KEY_OPTION_TOO_LONG] = I18N_STRING( + "host-key option too long", "主机密钥选项过长" + ), + [TNTCTL_TEXT_KNOWN_HOSTS_OPTION_TOO_LONG] = I18N_STRING( + "known_hosts option too long", "known_hosts 选项过长" + ) +}; +typedef char text_catalog_must_cover_enum[ + sizeof(text_catalog) / sizeof(text_catalog[0]) == TNTCTL_TEXT_COUNT ? 1 : -1]; + +const char *tntctl_text(ui_lang_t lang, tntctl_text_id_t id) { + if (id < 0 || id >= TNTCTL_TEXT_COUNT) { + return ""; + } + return i18n_string(text_catalog[id], lang); +} diff --git a/tests/unit/Makefile b/tests/unit/Makefile index c6667c6..25bc8b5 100644 --- a/tests/unit/Makefile +++ b/tests/unit/Makefile @@ -16,6 +16,7 @@ MESSAGE_LOG_SRC = ../../src/message_log.c COMMON_SRC = ../../src/common.c COMMAND_CATALOG_SRC = ../../src/command_catalog.c CLI_TEXT_SRC = ../../src/cli_text.c +TNTCTL_TEXT_SRC = ../../src/tntctl_text.c CHAT_ROOM_SRC = ../../src/chat_room.c HISTORY_VIEW_SRC = ../../src/history_view.c I18N_SRC = ../../src/i18n.c @@ -26,7 +27,7 @@ HELP_TEXT_SRC = ../../src/help_text.c MANUAL_TEXT_SRC = ../../src/manual_text.c RATELIMIT_SRC = ../../src/ratelimit.c -TESTS = test_utf8 test_message test_chat_room test_history_view test_i18n test_system_message test_command_catalog test_exec_catalog test_help_text test_manual_text test_cli_text test_ratelimit +TESTS = test_utf8 test_message test_chat_room test_history_view test_i18n test_system_message test_command_catalog test_exec_catalog test_help_text test_manual_text test_cli_text test_tntctl_text test_ratelimit .PHONY: all clean run @@ -65,6 +66,9 @@ test_manual_text: test_manual_text.c $(MANUAL_TEXT_SRC) $(COMMAND_CATALOG_SRC) $ test_cli_text: test_cli_text.c $(CLI_TEXT_SRC) $(COMMON_SRC) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +test_tntctl_text: test_tntctl_text.c $(TNTCTL_TEXT_SRC) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + test_ratelimit: test_ratelimit.c $(RATELIMIT_SRC) $(COMMON_SRC) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) @@ -102,6 +106,9 @@ run: all @echo "=== Running CLI Text Tests ===" ./test_cli_text @echo "" + @echo "=== Running tntctl Text Tests ===" + ./test_tntctl_text + @echo "" @echo "=== Running Rate Limit Tests ===" ./test_ratelimit diff --git a/tests/unit/test_tntctl_text.c b/tests/unit/test_tntctl_text.c new file mode 100644 index 0000000..b303ad5 --- /dev/null +++ b/tests/unit/test_tntctl_text.c @@ -0,0 +1,53 @@ +/* Unit tests for tntctl local help and diagnostic text */ + +#include "../../include/tntctl_text.h" +#include +#include +#include + +#define TEST(name) static void test_##name() +#define RUN_TEST(name) do { \ + printf("Running %s... ", #name); \ + test_##name(); \ + printf("✓\n"); \ + tests_passed++; \ +} while(0) + +static int tests_passed = 0; + +TEST(usage_matches_language) { + const char *en = tntctl_text(UI_LANG_EN, TNTCTL_TEXT_USAGE); + const char *zh = tntctl_text(UI_LANG_ZH, TNTCTL_TEXT_USAGE); + + assert(strstr(en, "Usage: tntctl [options] host command [args...]") != NULL); + assert(strstr(en, "--host-key-checking MODE") != NULL); + assert(strstr(en, "health, stats, users") != NULL); + assert(strstr(zh, "用法: tntctl [options] host command [args...]") != NULL); + assert(strstr(zh, "OpenSSH 主机密钥模式") != NULL); + assert(strstr(zh, "health, stats, users") != NULL); +} + +TEST(errors_match_language) { + assert(strcmp(tntctl_text(UI_LANG_EN, TNTCTL_TEXT_INVALID_PORT), + "invalid port") == 0); + assert(strcmp(tntctl_text(UI_LANG_ZH, TNTCTL_TEXT_INVALID_PORT), + "端口无效") == 0); + assert(strcmp(tntctl_text(UI_LANG_EN, TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT), + "unknown option: %s") == 0); + assert(strcmp(tntctl_text(UI_LANG_ZH, TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT), + "未知选项: %s") == 0); + assert(strcmp(tntctl_text((ui_lang_t)99, TNTCTL_TEXT_INVALID_PORT), + "invalid port") == 0); + assert(strcmp(tntctl_text(UI_LANG_EN, + (tntctl_text_id_t)TNTCTL_TEXT_COUNT), "") == 0); +} + +int main(void) { + printf("Running tntctl text unit tests...\n\n"); + + RUN_TEST(usage_matches_language); + RUN_TEST(errors_match_language); + + printf("\n✓ All %d tests passed!\n", tests_passed); + return 0; +}