499 lines
19 KiB
C
499 lines
19 KiB
C
/* test_config.c - 配置默认值 / 环境变量 / 配置文件覆盖 */
|
||
#include "config.h"
|
||
#include "util.h"
|
||
#include "i18n.h"
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <limits.h>
|
||
#include <sys/stat.h>
|
||
|
||
static int failures = 0;
|
||
static int checks = 0;
|
||
|
||
#define CHECK(cond, ...) \
|
||
do { \
|
||
checks++; \
|
||
if (!(cond)) { \
|
||
failures++; \
|
||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||
printf(__VA_ARGS__); \
|
||
printf("\n"); \
|
||
} \
|
||
} while (0)
|
||
|
||
static void unset_all(void)
|
||
{
|
||
const char *vars[] = {
|
||
"LIGENT_API_KEY", "DEEPSEEK_API_KEY", "OPENAI_API_KEY",
|
||
"LIGENT_BASE_URL", "LIGENT_MODEL", "LIGENT_SYSTEM",
|
||
"LIGENT_TEMPERATURE", "LIGENT_MAX_TOKENS", "LIGENT_TIMEOUT",
|
||
"LIGENT_MAX_STEPS", "LIGENT_STREAM", "LIGENT_TOOLS",
|
||
"OPENAI_BASE_URL", "LIGENT_CONFIG",
|
||
};
|
||
for (size_t i = 0; i < sizeof vars / sizeof vars[0]; i++)
|
||
unsetenv(vars[i]);
|
||
}
|
||
|
||
static void test_defaults(void)
|
||
{
|
||
puts("默认值");
|
||
unset_all();
|
||
LigentConfig c;
|
||
config_defaults(&c);
|
||
CHECK(strcmp(c.model, "deepseek-flash") == 0,
|
||
"默认模型应为 deepseek-flash,实际 %s", c.model);
|
||
CHECK(strcmp(c.base_url, "https://api.deepseek.com/v1") == 0,
|
||
"默认 base_url 应为 DeepSeek,实际 %s", c.base_url);
|
||
CHECK(c.api_key && c.api_key[0] == '\0', "默认 api_key 应为空");
|
||
CHECK(c.stream == 1, "默认应开启流式");
|
||
CHECK(c.use_tools == 1, "默认应启用工具");
|
||
/* 默认不限往返次数:0(或负数)= 不限,只有正数才是上限 */
|
||
CHECK(c.max_steps == 0, "默认应不限往返次数,实际 %d", c.max_steps);
|
||
config_free(&c);
|
||
}
|
||
|
||
static void test_env_key(void)
|
||
{
|
||
puts("环境变量取 key");
|
||
unset_all();
|
||
LigentConfig c;
|
||
config_defaults(&c);
|
||
|
||
/* 没有任何 key */
|
||
config_apply_env(&c);
|
||
CHECK(c.api_key[0] == '\0', "无环境变量时 key 应为空");
|
||
|
||
/* 只设 DEEPSEEK_API_KEY:默认就应读它 */
|
||
setenv("DEEPSEEK_API_KEY", "sk-deepseek", 1);
|
||
config_apply_env(&c);
|
||
CHECK(strcmp(c.api_key, "sk-deepseek") == 0,
|
||
"应读 $DEEPSEEK_API_KEY,实际 [%s]", c.api_key);
|
||
|
||
/* LIGENT_API_KEY 作为显式覆盖优先 */
|
||
setenv("LIGENT_API_KEY", "sk-explicit", 1);
|
||
config_apply_env(&c);
|
||
CHECK(strcmp(c.api_key, "sk-explicit") == 0,
|
||
"LIGENT_API_KEY 应覆盖,实际 [%s]", c.api_key);
|
||
|
||
/* OPENAI_API_KEY 只在端点确实是 OpenAI 时才兜底 ——
|
||
* 把 OpenAI 的密钥发给 DeepSeek 的端点是没有意义的 */
|
||
unset_all();
|
||
setenv("OPENAI_API_KEY", "sk-openai", 1);
|
||
/* 模拟"配置文件没写 api_key":必须先清掉上一步留下的值 */
|
||
free(c.api_key);
|
||
c.api_key = xstrdup("");
|
||
config_apply_env(&c);
|
||
CHECK(c.api_key[0] == '\0', "DeepSeek 端点不应兜底到 OPENAI_API_KEY");
|
||
|
||
free(c.base_url);
|
||
c.base_url = xstrdup("https://api.openai.com/v1");
|
||
config_apply_env(&c);
|
||
CHECK(strcmp(c.api_key, "sk-openai") == 0,
|
||
"OpenAI 端点应兜底到 OPENAI_API_KEY,实际 %s", c.api_key);
|
||
|
||
unset_all();
|
||
config_free(&c);
|
||
}
|
||
|
||
static void test_env_others(void)
|
||
{
|
||
puts("环境变量其他项");
|
||
unset_all();
|
||
LigentConfig c;
|
||
config_defaults(&c);
|
||
setenv("LIGENT_MODEL", "my-model", 1);
|
||
setenv("LIGENT_MAX_STEPS", "7", 1);
|
||
setenv("LIGENT_STREAM", "0", 1);
|
||
setenv("LIGENT_TOOLS", "false", 1);
|
||
setenv("LIGENT_THINKING", "off", 1);
|
||
setenv("LIGENT_REASONING_EFFORT", "max", 1);
|
||
config_apply_env(&c);
|
||
CHECK(strcmp(c.model, "my-model") == 0, "LIGENT_MODEL 应生效");
|
||
CHECK(c.max_steps == 7, "LIGENT_MAX_STEPS 应生效");
|
||
CHECK(c.stream == 0, "LIGENT_STREAM=0 应关闭流式");
|
||
CHECK(c.use_tools == 0, "LIGENT_TOOLS=false 应关闭工具");
|
||
CHECK(c.thinking == THINKING_OFF, "LIGENT_THINKING=off 应关闭思考");
|
||
CHECK(c.reasoning_effort && strcmp(c.reasoning_effort, "max") == 0,
|
||
"LIGENT_REASONING_EFFORT=max 应生效");
|
||
unset_all();
|
||
config_free(&c);
|
||
|
||
/* 环境变量是临时开关:写错了应当被忽略,而不是把程序拦下来 */
|
||
config_defaults(&c);
|
||
setenv("LIGENT_THINKING", "随便写的", 1);
|
||
setenv("LIGENT_REASONING_EFFORT", "超高", 1);
|
||
config_apply_env(&c);
|
||
CHECK(c.thinking == THINKING_AUTO, "无法识别的 LIGENT_THINKING 应被忽略");
|
||
CHECK(c.reasoning_effort == NULL, "无法识别的级别应被忽略");
|
||
unset_all();
|
||
config_free(&c);
|
||
}
|
||
|
||
static void test_thinking_parse(void)
|
||
{
|
||
puts("思考模式 / 级别解析");
|
||
int t = 123;
|
||
CHECK(config_parse_thinking("auto", &t) == 0 && t == THINKING_AUTO, "auto");
|
||
CHECK(config_parse_thinking("", &t) == 0 && t == THINKING_AUTO, "空串 = auto");
|
||
CHECK(config_parse_thinking("on", &t) == 0 && t == THINKING_ON, "on");
|
||
CHECK(config_parse_thinking("true", &t) == 0 && t == THINKING_ON, "true");
|
||
CHECK(config_parse_thinking("enabled", &t) == 0 && t == THINKING_ON, "enabled");
|
||
CHECK(config_parse_thinking("off", &t) == 0 && t == THINKING_OFF, "off");
|
||
CHECK(config_parse_thinking("false", &t) == 0 && t == THINKING_OFF, "false");
|
||
CHECK(config_parse_thinking("disabled", &t) == 0 && t == THINKING_OFF, "disabled");
|
||
CHECK(config_parse_thinking("maybe", &t) != 0, "无法识别的值应报错");
|
||
CHECK(config_parse_thinking(NULL, &t) != 0, "NULL 应报错");
|
||
|
||
static const char *const good[] = {
|
||
"minimal", "low", "medium", "high", "xhigh", "max", "ultra", NULL
|
||
};
|
||
for (size_t i = 0; good[i]; i++)
|
||
CHECK(config_effort_valid(good[i]), "%s 应是合法级别", good[i]);
|
||
CHECK(!config_effort_valid("huge"), "huge 不是合法级别");
|
||
CHECK(!config_effort_valid(""), "空串不是合法级别");
|
||
CHECK(!config_effort_valid(NULL), "NULL 不是合法级别");
|
||
CHECK(strstr(config_effort_list(), "ultra") != NULL, "展示串应含全部级别");
|
||
CHECK(config_thinking_name(THINKING_AUTO) != NULL, "THINKING_AUTO 有说明");
|
||
CHECK(config_thinking_name(THINKING_ON) != NULL, "THINKING_ON 有说明");
|
||
CHECK(config_thinking_name(THINKING_OFF) != NULL, "THINKING_OFF 有说明");
|
||
}
|
||
|
||
static void test_file_thinking(void)
|
||
{
|
||
puts("配置文件里的 thinking / reasoning_effort");
|
||
char err[256];
|
||
LigentConfig c;
|
||
|
||
/* 布尔写法 */
|
||
char t1[] = "/tmp/ligent_think_XXXXXX";
|
||
int fd = mkstemp(t1);
|
||
if (fd < 0) { CHECK(0, "mkstemp 失败"); return; }
|
||
const char *j1 = "{\"thinking\":false,\"reasoning_effort\":\"medium\"}";
|
||
ssize_t w = write(fd, j1, strlen(j1));
|
||
close(fd);
|
||
CHECK(w == (ssize_t)strlen(j1), "写临时配置失败");
|
||
config_defaults(&c);
|
||
CHECK(config_load_file(&c, t1, err, sizeof err) == 0, "加载失败: %s", err);
|
||
CHECK(c.thinking == THINKING_OFF, "thinking:false 应为关闭");
|
||
CHECK(c.reasoning_effort && strcmp(c.reasoning_effort, "medium") == 0,
|
||
"reasoning_effort 应被读入");
|
||
config_free(&c);
|
||
unlink(t1);
|
||
|
||
/* 字符串写法 */
|
||
char t2[] = "/tmp/ligent_think_XXXXXX";
|
||
fd = mkstemp(t2);
|
||
if (fd >= 0) {
|
||
const char *j2 = "{\"thinking\":\"on\"}";
|
||
w = write(fd, j2, strlen(j2));
|
||
close(fd);
|
||
CHECK(w == (ssize_t)strlen(j2), "写临时配置失败");
|
||
config_defaults(&c);
|
||
CHECK(config_load_file(&c, t2, err, sizeof err) == 0, "加载失败: %s", err);
|
||
CHECK(c.thinking == THINKING_ON, "thinking:\"on\" 应为开启");
|
||
config_free(&c);
|
||
unlink(t2);
|
||
}
|
||
|
||
/* 写错了必须报错,不能静默忽略 —— 配置文件是长期存在的,静默会让人困惑 */
|
||
char t3[] = "/tmp/ligent_think_XXXXXX";
|
||
fd = mkstemp(t3);
|
||
if (fd >= 0) {
|
||
const char *j3 = "{\"thinking\":\"大概吧\"}";
|
||
w = write(fd, j3, strlen(j3));
|
||
close(fd);
|
||
CHECK(w == (ssize_t)strlen(j3), "写临时配置失败");
|
||
config_defaults(&c);
|
||
CHECK(config_load_file(&c, t3, err, sizeof err) != 0,
|
||
"非法的 thinking 应加载失败");
|
||
config_free(&c);
|
||
unlink(t3);
|
||
}
|
||
|
||
char t4[] = "/tmp/ligent_think_XXXXXX";
|
||
fd = mkstemp(t4);
|
||
if (fd >= 0) {
|
||
const char *j4 = "{\"reasoning_effort\":\"超级高\"}";
|
||
w = write(fd, j4, strlen(j4));
|
||
close(fd);
|
||
CHECK(w == (ssize_t)strlen(j4), "写临时配置失败");
|
||
config_defaults(&c);
|
||
CHECK(config_load_file(&c, t4, err, sizeof err) != 0,
|
||
"非法的 reasoning_effort 应加载失败");
|
||
config_free(&c);
|
||
unlink(t4);
|
||
}
|
||
}
|
||
|
||
static void test_file_override(void)
|
||
{
|
||
puts("配置文件覆盖 + ${VAR} 展开");
|
||
unset_all();
|
||
setenv("MY_SECRET", "sk-from-env", 1);
|
||
|
||
char tmpl[] = "/tmp/ligent_cfg_XXXXXX";
|
||
int fd = mkstemp(tmpl);
|
||
CHECK(fd >= 0, "mkstemp 失败");
|
||
if (fd < 0) return;
|
||
const char *json =
|
||
"{\"base_url\":\"http://localhost:11434/v1\","
|
||
"\"api_key\":\"${MY_SECRET}\","
|
||
"\"model\":\"qwen2.5:14b\","
|
||
"\"temperature\":0.3,"
|
||
"\"max_tokens\":4096,"
|
||
"\"max_steps\":9,"
|
||
"\"stream\":false,"
|
||
"\"tools\":false}";
|
||
ssize_t w = write(fd, json, strlen(json));
|
||
close(fd);
|
||
CHECK(w == (ssize_t)strlen(json), "写临时配置失败");
|
||
|
||
LigentConfig c;
|
||
config_defaults(&c);
|
||
char err[256];
|
||
int rc = config_load_file(&c, tmpl, err, sizeof err);
|
||
CHECK(rc == 0, "加载配置失败: %s", err);
|
||
CHECK(strcmp(c.base_url, "http://localhost:11434/v1") == 0, "base_url 覆盖");
|
||
CHECK(strcmp(c.model, "qwen2.5:14b") == 0, "model 覆盖");
|
||
CHECK(strcmp(c.api_key, "sk-from-env") == 0,
|
||
"${MY_SECRET} 应被展开,实际 [%s]", c.api_key);
|
||
CHECK(c.temperature > 0.29 && c.temperature < 0.31, "temperature 覆盖");
|
||
CHECK(c.max_tokens == 4096, "max_tokens 覆盖");
|
||
CHECK(c.max_steps == 9, "max_steps 覆盖");
|
||
CHECK(c.stream == 0, "stream 覆盖");
|
||
CHECK(c.use_tools == 0, "tools 覆盖");
|
||
CHECK(c.config_path && strcmp(c.config_path, tmpl) == 0, "记录配置文件路径");
|
||
|
||
/* 坏配置应报错而不是静默通过。
|
||
*
|
||
* 解析器的报错跟着界面语言走(json_parse_ui),所以下面这段断语先把语言
|
||
* 钉住,再顺手把英文那份也验一遍 —— 位置信息两种语言都得有。 */
|
||
char tmpl2[] = "/tmp/ligent_bad_XXXXXX";
|
||
int fd2 = mkstemp(tmpl2);
|
||
if (fd2 >= 0) {
|
||
const char *bad = "{ not json ";
|
||
ssize_t w2 = write(fd2, bad, strlen(bad));
|
||
(void)w2;
|
||
close(fd2);
|
||
LigentConfig c2;
|
||
i18n_set_lang(LIGENT_LANG_ZH);
|
||
config_defaults(&c2);
|
||
CHECK(config_load_file(&c2, tmpl2, err, sizeof err) != 0,
|
||
"非法 JSON 应加载失败");
|
||
CHECK(err[0] != '\0', "应给出错误信息");
|
||
/* 报错必须带上文件路径与出错位置,否则用户无从下手 */
|
||
CHECK(strstr(err, tmpl2) != NULL, "错误信息应含文件路径: %s", err);
|
||
CHECK(strstr(err, "行") != NULL, "错误信息应含出错位置: %s", err);
|
||
config_free(&c2);
|
||
|
||
LigentConfig c3;
|
||
i18n_set_lang(LIGENT_LANG_EN);
|
||
config_defaults(&c3);
|
||
CHECK(config_load_file(&c3, tmpl2, err, sizeof err) != 0 &&
|
||
strstr(err, "line") != NULL,
|
||
"英文界面下报错也带位置(且是英文): %s", err);
|
||
config_free(&c3);
|
||
i18n_set_lang(LIGENT_LANG_ZH);
|
||
unlink(tmpl2);
|
||
}
|
||
|
||
config_free(&c);
|
||
unlink(tmpl);
|
||
unset_all();
|
||
}
|
||
|
||
/* 配置只认二进制所在目录:不做用户级、系统级,也不做当前目录查找 */
|
||
static void test_binary_dir_lookup(void)
|
||
{
|
||
puts("配置位置 = 二进制所在目录");
|
||
char err[256];
|
||
|
||
char *dir = config_binary_dir(NULL);
|
||
char *canon = config_default_path(NULL);
|
||
CHECK(dir && *dir, "应能解析出二进制所在目录");
|
||
CHECK(canon && str_ends_with(canon, "/ligent.json"),
|
||
"规范路径应以 /ligent.json 结尾: %s", canon ? canon : "(null)");
|
||
CHECK(canon && dir && str_starts_with(canon, dir),
|
||
"规范路径应位于二进制目录下: %s / %s", canon, dir);
|
||
|
||
/* 当前目录放一份 ligent.json,它绝不能被加载 */
|
||
char tmpl[] = "/tmp/ligent_cwd_XXXXXX";
|
||
char *tmp = mkdtemp(tmpl);
|
||
if (!tmp) { free(dir); free(canon); return; }
|
||
|
||
char *fake = xasprintf("%s/ligent.json", tmp);
|
||
FILE *f = fopen(fake, "w");
|
||
if (f) { fputs("{\"model\":\"FROM-CWD-ONLY\"}", f); fclose(f); }
|
||
|
||
char *saved = getcwd(NULL, 0);
|
||
int loaded_from_cwd = 0;
|
||
char *got_dir = NULL;
|
||
|
||
if (chdir(tmp) == 0) {
|
||
LigentConfig c;
|
||
config_init(&c, NULL, NULL, err, sizeof err);
|
||
loaded_from_cwd = (c.model && strcmp(c.model, "FROM-CWD-ONLY") == 0);
|
||
got_dir = c.config_dir ? xstrdup(c.config_dir) : NULL;
|
||
config_free(&c);
|
||
if (saved) chdir(saved);
|
||
}
|
||
|
||
CHECK(!loaded_from_cwd, "不应加载当前工作目录下的 ligent.json");
|
||
CHECK(got_dir && dir && strcmp(got_dir, dir) == 0,
|
||
"config_dir 应是二进制目录,实际 %s", got_dir ? got_dir : "(null)");
|
||
|
||
free(got_dir);
|
||
free(saved);
|
||
unlink(fake); /* 先删文件再释放路径字符串 */
|
||
free(fake);
|
||
rmdir(tmp);
|
||
free(dir);
|
||
free(canon);
|
||
}
|
||
|
||
/* 写一个临时配置文件并加载 */
|
||
static int load_temp_config(LigentConfig *c, const char *json)
|
||
{
|
||
char tmpl[] = "/tmp/ligent_prec_XXXXXX";
|
||
int fd = mkstemp(tmpl);
|
||
if (fd < 0) return -1;
|
||
ssize_t w = write(fd, json, strlen(json));
|
||
close(fd);
|
||
if (w != (ssize_t)strlen(json)) { unlink(tmpl); return -1; }
|
||
char err[256];
|
||
int rc = config_load_file(c, tmpl, err, sizeof err);
|
||
unlink(tmpl);
|
||
return rc;
|
||
}
|
||
|
||
/* 厂商专属的环境变量不能越权覆盖配置文件里写好的密钥。
|
||
* 这正是用户踩到的坑:配置写了 OpenRouter 的 key,
|
||
* shell 里残留的 DEEPSEEK_API_KEY 把它顶掉,于是 A 家的密钥发给了 B 家。 */
|
||
static void test_env_key_precedence(void)
|
||
{
|
||
puts("api_key 优先级:厂商环境变量不得越权");
|
||
unset_all();
|
||
setenv("DEEPSEEK_API_KEY", "sk-leftover-in-shell", 1);
|
||
|
||
/* 1) 配置文件写了 key => 它就是最终值,环境变量顶不掉 */
|
||
LigentConfig c;
|
||
config_defaults(&c);
|
||
CHECK(load_temp_config(&c,
|
||
"{\"base_url\":\"https://openrouter.ai/api/v1\","
|
||
"\"api_key\":\"sk-or-v1-from-file\"}") == 0, "加载配置失败");
|
||
config_apply_env(&c);
|
||
CHECK(strcmp(c.api_key, "sk-or-v1-from-file") == 0,
|
||
"配置文件里的 key 不应被残留环境变量顶掉,实际 %s", c.api_key);
|
||
config_free(&c);
|
||
|
||
/* 2) 配置文件没写 key + 端点就是 DeepSeek => 用 DEEPSEEK_API_KEY 兜底 */
|
||
config_defaults(&c);
|
||
CHECK(load_temp_config(&c, "{\"base_url\":\"https://api.deepseek.com/v1\"}") == 0,
|
||
"加载配置失败");
|
||
config_apply_env(&c);
|
||
CHECK(strcmp(c.api_key, "sk-leftover-in-shell") == 0,
|
||
"DeepSeek 端点应能用 DEEPSEEK_API_KEY 兜底,实际 %s", c.api_key);
|
||
config_free(&c);
|
||
|
||
/* 3) 配置文件没写 key + 端点是别家 => 不猜,宁可空着让用户看到明确提示 */
|
||
config_defaults(&c);
|
||
CHECK(load_temp_config(&c, "{\"base_url\":\"https://openrouter.ai/api/v1\"}") == 0,
|
||
"加载配置失败");
|
||
config_apply_env(&c);
|
||
CHECK(c.api_key[0] == '\0',
|
||
"base_url 指向别家时不应拿 DeepSeek 的密钥去猜,实际 %s", c.api_key);
|
||
config_free(&c);
|
||
|
||
/* 4) LIGENT_API_KEY 是显式覆盖,任何时候都最高 */
|
||
setenv("LIGENT_API_KEY", "sk-explicit", 1);
|
||
config_defaults(&c);
|
||
CHECK(load_temp_config(&c,
|
||
"{\"base_url\":\"https://openrouter.ai/api/v1\","
|
||
"\"api_key\":\"sk-or-v1-from-file\"}") == 0, "加载配置失败");
|
||
config_apply_env(&c);
|
||
CHECK(strcmp(c.api_key, "sk-explicit") == 0,
|
||
"LIGENT_API_KEY 应最高优先,实际 %s", c.api_key);
|
||
config_free(&c);
|
||
|
||
unset_all();
|
||
}
|
||
|
||
/* 软链接:配置目录要跟着"用户实际调用那个文件"走。
|
||
* 否则把 ligent 软链到 ~/bin 之后,配置会被悄悄挪到源文件所在的目录,
|
||
* 用户放在 ~/bin 旁边的 ligent.json 就永远读不到。 */
|
||
static void test_symlink_dir(void)
|
||
{
|
||
puts("软链接:配置目录跟着软链接走");
|
||
|
||
char self[PATH_MAX];
|
||
ssize_t n = readlink("/proc/self/exe", self, sizeof self - 1);
|
||
if (n <= 0) { puts(" (没有 /proc/self/exe,跳过)"); return; }
|
||
self[n] = '\0';
|
||
|
||
char tmpl[] = "/tmp/ligent_link_XXXXXX";
|
||
char *dir = mkdtemp(tmpl);
|
||
if (!dir) { puts(" (mkdtemp 失败,跳过)"); return; }
|
||
|
||
char *link = xasprintf("%s/ligent", dir);
|
||
if (symlink(self, link) != 0) {
|
||
puts(" (symlink 失败,跳过)");
|
||
free(link); rmdir(dir);
|
||
return;
|
||
}
|
||
|
||
/* 核心:指向软链接时取软链接所在目录,而不是源文件所在目录 */
|
||
char *got = config_binary_dir(link);
|
||
CHECK(got && strcmp(got, dir) == 0,
|
||
"应取软链接所在目录 %s,实际 %s", dir, got ? got : "(null)");
|
||
free(got);
|
||
|
||
/* 路径里的 . 应被规范化,但最后一段仍不解析 */
|
||
char *dotted = xasprintf("%s/./ligent", dir);
|
||
got = config_binary_dir(dotted);
|
||
CHECK(got && strcmp(got, dir) == 0,
|
||
"带 . 的路径应规范化,实际 %s", got ? got : "(null)");
|
||
free(got); free(dotted);
|
||
|
||
/* argv[0] 只是个名字时经 $PATH 找到的仍应是软链接本身 */
|
||
char *saved = getenv("PATH") ? xstrdup(getenv("PATH")) : NULL;
|
||
char *newpath = xasprintf("%s:%s", dir, saved ? saved : "");
|
||
setenv("PATH", newpath, 1);
|
||
got = config_binary_dir("ligent");
|
||
CHECK(got && strcmp(got, dir) == 0,
|
||
"$PATH 找到软链接时应取它所在目录,实际 %s", got ? got : "(null)");
|
||
free(got);
|
||
if (saved) setenv("PATH", saved, 1); else unsetenv("PATH");
|
||
free(saved); free(newpath);
|
||
|
||
/* 直接用真实文件时,取的还是真实目录 */
|
||
char *selfdir = xstrdup(self);
|
||
char *slash = strrchr(selfdir, '/');
|
||
if (slash) *slash = '\0';
|
||
got = config_binary_dir(self);
|
||
CHECK(got && strcmp(got, selfdir) == 0,
|
||
"真实文件应取真实目录 %s,实际 %s", selfdir, got ? got : "(null)");
|
||
free(got); free(selfdir);
|
||
|
||
unlink(link);
|
||
free(link);
|
||
rmdir(dir);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
test_defaults();
|
||
test_env_key();
|
||
test_env_others();
|
||
test_thinking_parse();
|
||
test_file_thinking();
|
||
test_file_override();
|
||
test_binary_dir_lookup();
|
||
test_env_key_precedence();
|
||
test_symlink_dir();
|
||
printf("\n%d 项检查,%d 项失败\n", checks, failures);
|
||
return failures ? 1 : 0;
|
||
}
|