添加单元测试,修复部分错误

This commit is contained in:
2026-03-21 11:13:13 +08:00
parent a5eb303c4b
commit a4196e53f9
14 changed files with 555 additions and 4246 deletions

48
test/CMakeLists.txt Normal file
View File

@ -0,0 +1,48 @@
cmake_minimum_required(VERSION 3.14)
project(LuaCjApiTest)
set(CMAKE_CXX_STANDARD 17)
# ==================== 核心修改 ====================
# 1. 使用 add_subdirectory 直接包含 lib 源码目录
# 这会自动继承 lib 中定义的 luacjapi 目标及其依赖路径
# 无需手动检查文件是否存在CMake 会处理整个依赖链
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../lib ${CMAKE_BINARY_DIR}/lib_build)
# ==================== 引入 Google Test ====================
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
GIT_SHALLOW ON
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
# ==================== 配置测试目标 ====================
add_executable(test_lua_cj_api test_lua_cj_api.cpp)
# 2. 包含头文件路径
# 注意:由于 lib 项目使用的是旧式 include_directories这里需要显式添加
# 或者更好的做法是修改 lib/CMakeLists.txt 使用 target_include_directories
target_include_directories(test_lua_cj_api PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../lib
${CMAKE_CURRENT_SOURCE_DIR}/../lib/lua
)
# 3. 链接库
# 直接链接 lib 项目导出的 luacjapi 目标。
# CMake 会自动处理 RPATH、库搜索路径和传递依赖如 lua、m 等)
target_link_libraries(test_lua_cj_api PRIVATE
luacjapi # 来自 lib 项目
GTest::gtest_main
pthread
dl
m
)
# ==================== 注册测试 ====================
enable_testing()
include(GoogleTest)
gtest_discover_tests(test_lua_cj_api)

321
test/test_lua_cj_api.cpp Normal file
View File

@ -0,0 +1,321 @@
#include <gtest/gtest.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/stat.h>
#include <string>
#include <fstream>
#include <streambuf>
#include <stdexcept> // 新增:用于异常处理
extern "C" {
#include "lua_cj_api.h"
#include "errors.h"
}
// 辅助函数:创建临时目录
static std::string create_temp_dir() {
// 修正点:
// 1. mkdtemp 要求模板字符串最后6个字符必须是 'XXXXXX'
// 2. 使用 /tmp 目录确保路径存在,避免 "test/" 目录不存在导致的失败
char tmpl[] = "/tmp/lua_cj_test_XXXXXX";
char *dir = mkdtemp(tmpl);
// 修正点:检查 mkdtemp 是否成功,防止传入 NULL 给 std::string 导致崩溃
if (dir == nullptr) {
perror("mkdtemp failed");
throw std::runtime_error("Failed to create temp directory");
}
return std::string(dir);
}
// 全局计数器,用于验证回调是否被调用
static int callback_invoke_count = 0;
// 模拟输入/输出回调
static int mock_callback() {
callback_invoke_count++;
return 0;
}
class LuaCjApiTest : public ::testing::Test {
protected:
void* runner;
std::string temp_dir;
void SetUp() override {
temp_dir = create_temp_dir();
callback_invoke_count = 0; // 重置计数器
// 修改:传入 temp_dir 作为 pathio确保 I/O 重定向路径有效
// 仓颉层封装中 pathio 和 pkgpath 都是可配置的,这里测试基础情况
runner = init_lua_runner(temp_dir.c_str(), NULL, mock_callback, mock_callback);
ASSERT_NE(runner, nullptr) << "init_lua_runner failed, errno=" << get_errno();
}
void TearDown() override {
if (runner) {
free_lua(runner);
runner = nullptr;
}
std::string cmd = "rm -rf " + temp_dir;
system(cmd.c_str());
}
void write_lua(const std::string& filename, const std::string& content) {
std::string path = temp_dir + "/" + filename;
FILE* f = fopen(path.c_str(), "w");
ASSERT_NE(f, nullptr);
fprintf(f, "%s", content.c_str());
fclose(f);
}
// 辅助函数:读取文件内容
std::string read_file(const std::string& filename) {
std::string path = temp_dir + "/" + filename;
std::ifstream t(path);
std::string str((std::istreambuf_iterator<char>(t)),
std::istreambuf_iterator<char>());
return str;
}
};
// 基础生命周期测试
TEST_F(LuaCjApiTest, InitFree) {
SUCCEED();
}
// 测试加载库
TEST_F(LuaCjApiTest, LoadLib) {
write_lua("mylib.lua", "function foo() return 42 end");
int ret = load_lib(runner, (temp_dir + "/mylib.lua").c_str(), "mylib");
EXPECT_EQ(ret, 0);
}
// 测试加载不存在的文件
TEST_F(LuaCjApiTest, LoadLibFileNotFound) {
int ret = load_lib(runner, "/no/such/file.lua", "bad");
EXPECT_EQ(ret, -1);
EXPECT_EQ(get_errno(), NAPI_LOAD_FILE_ERROR);
}
// 测试超过最大库数量限制
TEST_F(LuaCjApiTest, LoadLibOverflow) {
for (int i = 0; i < MAX_LUA_LIB; ++i) {
char name[20];
snprintf(name, sizeof(name), "lib%d", i);
std::string fname = std::string(name) + ".lua";
write_lua(fname, "return {}");
std::string path = temp_dir + "/" + fname;
int ret = load_lib(runner, path.c_str(), name);
EXPECT_EQ(ret, 0);
}
write_lua("extra.lua", "return {}");
std::string path = temp_dir + "/extra.lua";
int ret = load_lib(runner, path.c_str(), "extra");
EXPECT_EQ(ret, -1);
EXPECT_EQ(get_errno(), NAPI_LUALIB_LOAD_OVER_STACK);
}
// 测试卸载库
TEST_F(LuaCjApiTest, UnloadLib) {
write_lua("mylib.lua", "function foo() end");
std::string path = temp_dir + "/mylib.lua";
ASSERT_EQ(load_lib(runner, path.c_str(), "mylib"), 0);
int ret = unload_lib(runner, "mylib");
EXPECT_EQ(ret, 0);
}
// 测试卸载未加载的库
TEST_F(LuaCjApiTest, UnloadLibNotFound) {
int ret = unload_lib(runner, "nosuch");
EXPECT_EQ(ret, -1);
EXPECT_EQ(get_errno(), NAPI_UNLOADLIB_FAIL);
}
// 测试运行脚本
TEST_F(LuaCjApiTest, RunScript) {
write_lua("test.lua", "return 'hello'");
int ret = run(runner, (temp_dir + "/test.lua").c_str(), nullptr);
EXPECT_EQ(ret, 0);
char* res = getresult(runner);
EXPECT_STREQ(res, "hello");
}
// 测试带参数的脚本
TEST_F(LuaCjApiTest, RunScriptWithArg) {
// 修正点Lua 中 ... 不能作为变量名。
// 使用 select(1, ...) 或者直接 return ...
// 这里使用 "return (...)" 返回第一个参数
write_lua("test.lua", "return (...)");
int ret = run(runner, (temp_dir + "/test.lua").c_str(), "world");
EXPECT_EQ(ret, 0);
char* res = getresult(runner);
EXPECT_STREQ(res, "world");
}
// 测试脚本语法错误
TEST_F(LuaCjApiTest, RunScriptSyntaxError) {
write_lua("bad.lua", "for i=1,10 print(i) end");
int ret = run(runner, (temp_dir + "/bad.lua").c_str(), NULL);
EXPECT_EQ(ret, -1);
EXPECT_EQ(get_errno(), NAPI_SCRIPT_ERROR);
char* res = getresult(runner);
EXPECT_NE(strstr(res, "'do' expected"), nullptr);
}
// 测试脚本运行时错误
TEST_F(LuaCjApiTest, RunScriptRuntimeError) {
write_lua("bad.lua", "error('oops')");
int ret = run(runner, (temp_dir + "/bad.lua").c_str(), nullptr);
EXPECT_EQ(ret, -1);
EXPECT_EQ(get_errno(), NAPI_SCRIPT_ERROR);
char* res = getresult(runner);
EXPECT_NE(strstr(res, "oops"), nullptr);
}
// 测试 cleanup 重置状态
TEST_F(LuaCjApiTest, Cleanup) {
write_lua("set.lua", "g = 123");
ASSERT_EQ(run(runner, (temp_dir + "/set.lua").c_str(), nullptr), 0);
int ret = cleanup(runner);
EXPECT_EQ(ret, 0);
write_lua("get.lua", "return tostring(g or 'nil')");
ret = run(runner, (temp_dir + "/get.lua").c_str(), nullptr);
EXPECT_EQ(ret, 0);
char* res = getresult(runner);
EXPECT_STREQ(res, "nil");
}
// 测试跨调用数据传递
TEST_F(LuaCjApiTest, CrossCallDataPassing) {
write_lua("set.lua", "data = {1,2,3}");
ASSERT_EQ(run(runner, (temp_dir + "/set.lua").c_str(), nullptr), 0);
write_lua("get.lua", "return table.concat(data, ',')");
ASSERT_EQ(run(runner, (temp_dir + "/get.lua").c_str(), nullptr), 0);
char* res = getresult(runner);
EXPECT_STREQ(res, "1,2,3");
}
// 测试多次加载卸载后栈索引正确
TEST_F(LuaCjApiTest, MultipleLoadUnload) {
for (int i = 0; i < 5; ++i) {
char name[20];
snprintf(name, sizeof(name), "L%d", i);
std::string fname = std::string(name) + ".lua";
write_lua(fname, "return " + std::to_string(i));
std::string path = temp_dir + "/" + fname;
ASSERT_EQ(load_lib(runner, path.c_str(), name), 0);
}
for (int i = 4; i >= 0; --i) {
char name[20];
snprintf(name, sizeof(name), "L%d", i);
ASSERT_EQ(unload_lib(runner, name), 0);
for (int j = 0; j < i; ++j) {
char libname[20], scriptname[20];
snprintf(libname, sizeof(libname), "L%d", j);
snprintf(scriptname, sizeof(scriptname), "check%d.lua", j);
std::string script = "return require '" + std::string(libname) + "'";
write_lua(scriptname, script);
int ret = run(runner, (temp_dir + "/" + scriptname).c_str(), nullptr);
EXPECT_EQ(ret, -1) << "Failed for lib " << libname << " after unloading " << name;
if (ret == 0) {
char expected[10];
snprintf(expected, sizeof(expected), "%d", j);
EXPECT_STREQ(getresult(runner), expected);
}
}
}
}
// ==================== 新增测试:针对仓颉封装覆盖的功能 ====================
// 测试 I/O 重定向Print
TEST_F(LuaCjApiTest, RedirectPrint) {
write_lua("print_test.lua", "print('Hello Cangjie')");
int ret = run(runner, (temp_dir + "/print_test.lua").c_str(), nullptr);
EXPECT_EQ(ret, 0);
// 验证:回调函数应该被调用
EXPECT_GT(callback_invoke_count, 0) << "Output callback should be invoked by print";
// 验证temp_dir 下应生成 output 文件
std::string content = read_file("output");
EXPECT_NE(content.find("Hello Cangjie"), std::string::npos);
}
// 测试 I/O 重定向io.write
TEST_F(LuaCjApiTest, RedirectIoWrite) {
write_lua("write_test.lua", "io.write('Data from lua')");
int ret = run(runner, (temp_dir + "/write_test.lua").c_str(), nullptr);
EXPECT_EQ(ret, 0);
EXPECT_GT(callback_invoke_count, 0) << "Output callback should be invoked by io.write";
std::string content = read_file("output");
EXPECT_NE(content.find("Data from lua"), std::string::npos);
}
// 测试 I/O 重定向io.read
TEST_F(LuaCjApiTest, RedirectIoRead) {
// 准备:模拟外部向 input 文件写入数据
std::string input_file = temp_dir + "/input";
FILE* f = fopen(input_file.c_str(), "w");
ASSERT_NE(f, nullptr);
fprintf(f, "MockInputData");
fclose(f);
// Lua 脚本读取输入并返回
write_lua("read_test.lua", "local s = io.read('*a'); return s");
int ret = run(runner, (temp_dir + "/read_test.lua").c_str(), nullptr);
EXPECT_EQ(ret, 0);
// 验证:输入回调被调用
EXPECT_GT(callback_invoke_count, 0) << "Input callback should be invoked by io.read";
// 验证:结果正确
char* res = getresult(runner);
EXPECT_STREQ(res, "MockInputData");
}
// 测试 pkgpath 配置是否生效
TEST_F(LuaCjApiTest, PackagePathConfig) {
// 创建子目录存放库
std::string lib_dir = temp_dir + "/mypkgs";
mkdir(lib_dir.c_str(), 0755);
// 在子目录创建库
std::string lib_path = lib_dir + "/mylib.lua";
FILE* f = fopen(lib_path.c_str(), "w");
fprintf(f, "return 'LoadedViaPkgPath'");
fclose(f);
// 销毁旧 runner
free_lua(runner);
runner = nullptr;
// 创建新 runner配置 pkgpath
std::string pkg_path_pattern = lib_dir + "/?.lua";
runner = init_lua_runner(temp_dir.c_str(), pkg_path_pattern.c_str(), mock_callback, mock_callback);
ASSERT_NE(runner, nullptr);
// 脚本直接 require不指定路径
write_lua("main.lua", "return require 'mylib'");
int ret = run(runner, (temp_dir + "/main.lua").c_str(), nullptr);
EXPECT_EQ(ret, 0) << "Require failed, maybe pkgpath not set correctly. Errno: " << get_errno();
char* res = getresult(runner);
EXPECT_STREQ(res, "LoadedViaPkgPath");
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}