* fix(repair): keep RPC progress alive * fix(daemon): propagate DB open failure to FSM and add shutdown cancellation * fix(db): classify FTS5 inverted-index errors as repairable, not transient locks * fix(daemon): clean up stale corrupt files after salvage and suppress optional readiness keys * test(db_recovery): update FTS5 classification test for new transient-lock exclusion * test(gc): add garbage collector unit tests for collection, dry-run, scheduling * test(repair): expand repair manager tests with hash-verified recovery and source ordering * fix(gc): dry-run mode no longer increments blocksDeleted/bytesReclaimed * fix(mock,mcp): add atomic bool for lock consistency, move field assignments * style: fix unnecessary-copy, dead-store, and const-refable findings from Infer * fix(daemon): increase default shutdown timeout from 15s to 120s * fix(daemon): demote non-critical warnings to info (FTS5, VACUUM, WorkCoordinator timeout) * fix(db): re-enable WAL auto-checkpoint to prevent unbounded WAL growth * test(pool): add WAL auto-checkpoint and concurrency coverage for connection pool * test(pool): add exhaustion, stats, lifecycle, and read-only connection pool tests * test(pool): add tagging, WAL journal, reuse, and busy timeout tests * test(metrics): relax session snippet assertion to tolerate stub behavior * fix(test): stabilize sanitizer triage failures * fix(build): wire version and zyp deps * fix(daemon): close TSAN race clusters * fix(test): guard optional zyp dep * fix(test): address PR review findings
161 lines
5.5 KiB
C++
161 lines
5.5 KiB
C++
// CLI Add Command stdin tests
|
|
// Catch2 migration from GTest (yams-3s4 / yams-cli)
|
|
//
|
|
// Tests for add command reading from piped stdin.
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <chrono>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include "../../common/test_helpers_catch2.h"
|
|
#include <yams/compat/unistd.h>
|
|
|
|
#ifdef _WIN32
|
|
#define WIFEXITED(x) ((x) != -1)
|
|
#define WEXITSTATUS(x) (x)
|
|
#else
|
|
#include <sys/wait.h>
|
|
#endif
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace {
|
|
|
|
auto findBuiltYamsCli() -> fs::path {
|
|
std::vector<fs::path> candidates;
|
|
|
|
if (const char* buildRoot = std::getenv("MESON_BUILD_ROOT")) {
|
|
candidates.emplace_back(fs::path(buildRoot) / "tools" / "yams-cli" / "yams-cli");
|
|
}
|
|
|
|
std::vector<fs::path> bases;
|
|
if (const char* sourceRoot = std::getenv("MESON_SOURCE_ROOT")) {
|
|
bases.emplace_back(sourceRoot);
|
|
}
|
|
|
|
std::error_code ec;
|
|
auto cwd = fs::current_path(ec);
|
|
if (!ec) {
|
|
bases.push_back(cwd);
|
|
if (cwd.has_parent_path()) {
|
|
bases.push_back(cwd.parent_path());
|
|
}
|
|
if (cwd.has_parent_path() && cwd.parent_path().has_parent_path()) {
|
|
bases.push_back(cwd.parent_path().parent_path());
|
|
}
|
|
}
|
|
|
|
const std::vector<fs::path> suffixes = {
|
|
fs::path("tools") / "yams-cli" / "yams-cli",
|
|
fs::path("builddir-local") / "tools" / "yams-cli" / "yams-cli",
|
|
fs::path("builddir-asan-local") / "tools" / "yams-cli" / "yams-cli",
|
|
fs::path("builddir-ubsan-local") / "tools" / "yams-cli" / "yams-cli",
|
|
fs::path("builddir-tsan-local") / "tools" / "yams-cli" / "yams-cli",
|
|
fs::path("builddir-nosan-local") / "tools" / "yams-cli" / "yams-cli",
|
|
fs::path("builddir") / "tools" / "yams-cli" / "yams-cli",
|
|
fs::path("builddir-asan") / "tools" / "yams-cli" / "yams-cli",
|
|
fs::path("builddir-ubsan") / "tools" / "yams-cli" / "yams-cli",
|
|
fs::path("builddir-tsan") / "tools" / "yams-cli" / "yams-cli",
|
|
fs::path("builddir-nosan") / "tools" / "yams-cli" / "yams-cli",
|
|
fs::path("build") / "asan" / "tools" / "yams-cli" / "yams-cli",
|
|
fs::path("build") / "coverage" / "tools" / "yams-cli" / "yams-cli",
|
|
fs::path("build") / "release" / "tools" / "yams-cli" / "yams-cli",
|
|
fs::path("build") / "debug" / "tools" / "yams-cli" / "yams-cli",
|
|
};
|
|
|
|
for (const auto& base : bases) {
|
|
for (const auto& suffix : suffixes) {
|
|
candidates.push_back(base / suffix);
|
|
}
|
|
}
|
|
|
|
for (const auto& candidate : candidates) {
|
|
if (fs::exists(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("AddCommand - reads from piped stdin and stores content", "[cli][add][stdin][catch2]") {
|
|
#ifdef _WIN32
|
|
SKIP("Test requires Unix shell with 'cat' and pipe support");
|
|
#else
|
|
const fs::path yamsCli = findBuiltYamsCli();
|
|
REQUIRE_FALSE(yamsCli.empty());
|
|
|
|
const std::string payload = "line1\nline2\n\tindent\n";
|
|
|
|
const auto unique = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
|
|
fs::path tmp = fs::temp_directory_path() / ("yams_add_stdin_" + unique);
|
|
fs::create_directories(tmp);
|
|
|
|
// Use RAII to restore environment on exit (including exceptions/early returns)
|
|
yams::test::ScopedEnvVar configEnv("YAMS_CONFIG", (tmp / "config.toml").string());
|
|
yams::test::ScopedEnvVar dataEnv("YAMS_DATA_DIR", tmp.string());
|
|
yams::test::ScopedEnvVar nonInteractiveEnv("YAMS_NON_INTERACTIVE",
|
|
std::optional<std::string>{"1"});
|
|
yams::test::ScopedEnvVar disableDaemonEnv("YAMS_CLI_DISABLE_DAEMON_AUTOSTART",
|
|
std::optional<std::string>{"1"});
|
|
|
|
fs::path in = tmp / "stdin.txt";
|
|
{
|
|
std::ofstream o(in, std::ios::binary);
|
|
o << payload;
|
|
}
|
|
|
|
fs::path out = tmp / "out.json";
|
|
std::string cmd = "cat '" + in.string() + "' | '" + yamsCli.string() +
|
|
"' --json add - --name piped.txt > '" + out.string() + "'";
|
|
int rc = std::system(cmd.c_str());
|
|
REQUIRE(WIFEXITED(rc));
|
|
CHECK(WEXITSTATUS(rc) == 0);
|
|
|
|
// Validate JSON output
|
|
{
|
|
std::ifstream i(out);
|
|
REQUIRE(i.good());
|
|
std::string content((std::istreambuf_iterator<char>(i)), std::istreambuf_iterator<char>());
|
|
REQUIRE(!content.empty());
|
|
|
|
// Some invocations may prepend or append non-JSON lines; isolate the JSON object.
|
|
const auto first = content.find('{');
|
|
const auto last = content.rfind('}');
|
|
REQUIRE(first != std::string::npos);
|
|
REQUIRE(last != std::string::npos);
|
|
REQUIRE(last >= first);
|
|
content = content.substr(first, last - first + 1);
|
|
|
|
nlohmann::json j;
|
|
REQUIRE_NOTHROW(j = nlohmann::json::parse(content));
|
|
REQUIRE(j.is_object());
|
|
|
|
REQUIRE(j.contains("results"));
|
|
REQUIRE(j["results"].is_array());
|
|
REQUIRE(!j["results"].empty());
|
|
|
|
REQUIRE(j.contains("summary"));
|
|
REQUIRE(j["summary"].is_object());
|
|
REQUIRE(j["summary"].contains("added"));
|
|
REQUIRE(j["summary"].contains("updated"));
|
|
REQUIRE(j["summary"].contains("skipped"));
|
|
REQUIRE(j["summary"].contains("failed"));
|
|
|
|
const auto& r0 = j["results"][0];
|
|
REQUIRE(r0.is_object());
|
|
REQUIRE(r0.contains("path"));
|
|
REQUIRE(r0.contains("success"));
|
|
}
|
|
|
|
// Cleanup temp directory
|
|
std::error_code ec;
|
|
fs::remove_all(tmp, ec);
|
|
#endif
|
|
}
|