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;
|
|
}
|