Differentially verified against tai.py (4890 cases incl. real pty runs). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
334 lines
17 KiB
C++
334 lines
17 KiB
C++
// Unit tests for src/shell and src/util. Real processes, a real pty, real files.
|
|
// Build and run (outputs under build/ only):
|
|
// g++ -std=c++23 -Wall -Wextra -Isrc/util -Isrc/shell -pthread tests/shell_test.cpp
|
|
// (every src/util/*.cpp except http.cpp) src/shell/*.cpp -o build/shell_test && build/shell_test
|
|
// The byte-for-byte comparison against tai.py lives in tests/shell_parity.py.
|
|
#include "ansi.h"
|
|
#include "diffrender.h"
|
|
#include "boxdata.h"
|
|
#include "glow.h"
|
|
#include "jsonvalue.h"
|
|
#include "lanes.h"
|
|
#include "livestream.h"
|
|
#include "markdown.h"
|
|
#include "pty.h"
|
|
#include "pyglob.h"
|
|
#include "runlive.h"
|
|
#include "sandbox.h"
|
|
#include "sequencematcher.h"
|
|
#include "session.h"
|
|
#include "shellio.h"
|
|
#include "shlex.h"
|
|
#include "spinner.h"
|
|
#include "targets.h"
|
|
#include "text.h"
|
|
#include "utf8.h"
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <thread>
|
|
#include <unistd.h>
|
|
using namespace detz;
|
|
using namespace detz::shell;
|
|
namespace fs = std::filesystem;
|
|
|
|
static int gFailures = 0, gChecks = 0;
|
|
#define CHECK(cond) do { ++gChecks; if (!(cond)) { ++gFailures; std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); } } while (0)
|
|
#define CHECK_EQ(a, b) do { ++gChecks; auto va = (a); auto vb = (b); if (!(va == vb)) { ++gFailures; std::printf("FAIL %s:%d: %s == %s\n", __FILE__, __LINE__, #a, #b); } } while (0)
|
|
|
|
|
|
static void TestUtf8() {
|
|
CHECK_EQ(Utf8::Len("caf\xc3\xa9"), (size_t)4);
|
|
CHECK_EQ(Utf8::Slice("a\xc3\xa9z", 1, 2), std::string("\xc3\xa9"));
|
|
CHECK_EQ(Utf8::Tail("abcdef", 2), std::string("ef"));
|
|
CHECK_EQ(Utf8::Strip(" \t x\xc2\xa0\n"), std::string("x"));
|
|
CHECK_EQ(Utf8::SplitLines("a\r\nb\rc\n").size(), (size_t)3);
|
|
CHECK_EQ(Utf8::ExpandTabs("a\tb", 8), std::string("a b"));
|
|
CHECK_EQ(Utf8::Encode(Utf8::Decode("x\xff y")), std::string("x\xef\xbf\xbd y"));
|
|
}
|
|
|
|
static void TestText() {
|
|
CHECK_EQ(Truncate("abc"), std::string("abc"));
|
|
CHECK(Truncate(std::string(3000, 'a'), 100, 100).find("2800 chars truncated") != std::string::npos);
|
|
// text[-0:] is the whole text in Python, so tail=0 keeps everything after the marker
|
|
CHECK_EQ(Truncate(std::string(3000, 'a'), 5, 0).size(), (size_t)5 + std::string("\n...[2995 chars truncated]...\n").size() + 3000);
|
|
CHECK_EQ(TruncMarker(7), std::string("\n...[truncated, full size 7 bytes]..."));
|
|
Capped c = CappedText(std::string(20001, 'x'));
|
|
CHECK(c.present && c.size == 20001 && c.text.size() == 20000 + TruncMarker(20001).size());
|
|
CHECK(!CappedText(std::nullopt).present);
|
|
CHECK_EQ(ChunkLines("a\nb\nc", 2, 1, 4000).size(), (size_t)2);
|
|
CHECK_EQ(CollapseRepeatedLines("x\nx\nx\ny"), std::string("x\n... (repeated 2 more times) ...\ny"));
|
|
auto t = SplitLeadingTags("@a @b rest @c");
|
|
CHECK(t.first.size() == 2 && t.second == "rest @c");
|
|
CHECK_EQ(HumanSize(2048), std::string("2.0 KB"));
|
|
CHECK_EQ(FormatDelay(7200), std::string("2h"));
|
|
CHECK_EQ(ParseDuration("3 m"), 180L);
|
|
CHECK_EQ(ParseDuration("x"), -1L);
|
|
CHECK_EQ(AsInt("1_0", 5), 10L);
|
|
CHECK_EQ(AsInt("1__0", 5), 5L);
|
|
CHECK_EQ(ToolNameClean("shell<|channel|>x"), std::string("shell"));
|
|
fs::path dir = fs::temp_directory_path() / "detz_shell_test_text";
|
|
fs::remove_all(dir); fs::create_directories(dir / "Sub");
|
|
std::ofstream(dir / "alpha.txt") << "x";
|
|
std::ofstream(dir / ".dot") << "x";
|
|
CHECK_EQ(ExpandFileMentions("see @alpha.txt, ok", dir.string()), "see " + (dir / "alpha.txt").string() + ", ok");
|
|
CHECK_EQ(ExpandFileMentions("mail me@alpha.txt", dir.string()), std::string("mail me@alpha.txt"));
|
|
auto cf = CompleteFiles("", dir.string());
|
|
CHECK(cf.size() == 2 && cf[0] == "Sub/" && cf[1] == "alpha.txt");
|
|
CHECK_EQ(CompleteFiles(".", dir.string()).size(), (size_t)1);
|
|
fs::remove_all(dir);
|
|
ReadCappedResult rc = ReadCapped("/nonexistent/file");
|
|
CHECK(!rc.ok);
|
|
}
|
|
|
|
static void TestAnsi() {
|
|
CHECK_EQ(AnsiText::Strip("\033[31mred\033[0m"), std::string("red"));
|
|
CHECK_EQ(AnsiText::Truncate("hello world", 5), std::string("hello\033[0m"));
|
|
CHECK_EQ(AnsiText::Truncate("hi", 5), std::string("hi"));
|
|
CHECK_EQ(AnsiText::StyleSpan("a\033[0mb", {"\033[1m"}), std::string("\033[1ma\033[0m\033[1mb\033[0m"));
|
|
auto w = AnsiText::Wrap("one two three four", 9);
|
|
CHECK(w.size() == 3 && w[0] == "one two" && w[1] == "three" && w[2] == "four");
|
|
CHECK_EQ(AnsiText::PadTinted("ab\033[0m", 5), std::string("ab \033[0m"));
|
|
}
|
|
|
|
static void TestJson() {
|
|
bool ok = false;
|
|
JsonValue v = JsonValue::Parse("{\"b\": [1, 2.5, \"x\\u00e9\"], \"a\": null, \"a\": true}", &ok);
|
|
CHECK(ok);
|
|
CHECK_EQ(v.Dump(false), std::string("{\"b\": [1, 2.5, \"x\\u00e9\"], \"a\": true}"));
|
|
CHECK_EQ(v.Dump(true), std::string("{\"a\": true, \"b\": [1, 2.5, \"x\\u00e9\"]}"));
|
|
CHECK_EQ(v.Repr(), std::string("{'b': [1, 2.5, 'x\xc3\xa9'], 'a': True}"));
|
|
JsonValue::Parse("{'a': 1}", &ok); CHECK(!ok);
|
|
JsonValue::Parse("[1,]", &ok); CHECK(!ok);
|
|
CHECK_EQ(JsonValue::Parse("123456789012345678901234567890", &ok).Dump(), std::string("123456789012345678901234567890"));
|
|
CHECK_EQ(JsonValue::Parse("1e16", &ok).Dump(), std::string("1e+16"));
|
|
}
|
|
|
|
static void TestMarkdown() {
|
|
CHECK_EQ(Markdown::Inline("a **b** c"), std::string("a \033[1mb\033[0m c"));
|
|
CHECK_EQ(Markdown::Inline("snake_case_word"), std::string("snake_case_word"));
|
|
CHECK_EQ(Markdown::Inline("`x`"), std::string("\033[36mx\033[0m"));
|
|
CHECK_EQ(Markdown::Render("plain", 40, 0), std::string("plain"));
|
|
CHECK_EQ(Markdown::Render("# T", 40, 1), std::string("\033[1m\033[36mT\033[0m"));
|
|
CHECK(Markdown::SplitRow("| a | b\\|c |").size() == 2);
|
|
CHECK(Markdown::IsSeparator("|:--|--:|"));
|
|
CHECK(!Markdown::IsSeparator("| a |"));
|
|
}
|
|
|
|
static void TestDiff() {
|
|
SequenceMatcher m({"a", "b", "c"}, {"a", "x", "c"});
|
|
auto ops = m.Opcodes();
|
|
CHECK(ops.size() == 3 && ops[1].tag == "replace");
|
|
std::string out = DiffRender::Render("f.txt", std::string("a\nb\n"), std::string("a\nc\n"), 40, false);
|
|
CHECK(out.find("\xe2\x94\x82 \xe2\x94\x80\xe2\x94\x80 f.txt (+1 -1)") == 0);
|
|
CHECK_EQ(DiffRender::Render("f", std::string("x\n"), std::string("x\n"), 40, true), std::string(""));
|
|
}
|
|
|
|
static void TestShlexGlobTargets() {
|
|
std::vector<std::string> t;
|
|
CHECK(Shlex::Split("a 'b c' \"d\\\"e\" f\\ g ''", &t));
|
|
CHECK(t.size() == 5 && t[1] == "b c" && t[2] == "d\"e" && t[3] == "f g" && t[4] == "");
|
|
CHECK(!Shlex::Split("a 'unclosed", &t));
|
|
CHECK(!Shlex::Split("trailing\\", &t));
|
|
CHECK(PyGlob::FnmatchCase("abc.txt", "a*.t?t"));
|
|
CHECK(PyGlob::FnmatchCase("b", "[a-c]"));
|
|
CHECK(!PyGlob::FnmatchCase("d", "[!a-d]"));
|
|
fs::path dir = fs::temp_directory_path() / "detz_shell_test_targets";
|
|
fs::remove_all(dir); fs::create_directories(dir / "sub");
|
|
std::ofstream(dir / "a.txt") << "x";
|
|
std::ofstream(dir / "b.txt") << "x";
|
|
std::ofstream(dir / "sub" / "c.txt") << "x";
|
|
auto p = ShellTargets::Paths("rm *.txt && echo hi > sub/c.txt", dir.string());
|
|
CHECK(p.size() == 3 && p[0] == (dir / "a.txt").string() && p[2] == (dir / "sub" / "c.txt").string());
|
|
CHECK(ShellTargets::Paths("rm 'unclosed", dir.string()).empty());
|
|
auto seg = ShellTargets::Segment({"sudo", "env", "A=1", "tee", "f", "-"});
|
|
CHECK(seg.size() == 1 && seg[0] == "f");
|
|
CHECK_EQ(ShellTargets::Segment({"truncate", "-s", "5", "f"}).size(), (size_t)1);
|
|
CHECK_EQ(ShellTargets::Segment({"dd", "if=in", "of=out"})[0], std::string("out"));
|
|
fs::remove_all(dir);
|
|
}
|
|
|
|
static void TestProcAndRun() {
|
|
// Popen error text follows Python's OSError
|
|
try { PopenOptions o; o.argv = {"/nonexistent/prog"}; Popen p(o); CHECK(false); }
|
|
catch (const OsError& e) { CHECK_EQ(std::string(e.what()), std::string("[Errno 2] No such file or directory: '/nonexistent/prog'")); }
|
|
try { PopenOptions o; o.argv = {"true"}; o.cwd = "/nonexistent/dir"; Popen p(o); CHECK(false); }
|
|
catch (const OsError& e) { CHECK_EQ(std::string(e.what()), std::string("[Errno 2] No such file or directory: '/nonexistent/dir'")); }
|
|
// subprocess.run semantics
|
|
SubprocessOptions so;
|
|
so.popen.argv = {"sh", "-c", "cat; echo err >&2; exit 4"};
|
|
so.popen.out = StdMode::Pipe; so.popen.err = StdMode::Pipe; so.input = "in-data";
|
|
CompletedProcess done = Subprocess::Run(so);
|
|
CHECK(done.returncode == 4 && done.out == "in-data" && done.err == "err\n");
|
|
SubprocessOptions slow;
|
|
slow.popen.argv = {"sleep", "5"}; slow.timeout = 0.3;
|
|
try { Subprocess::Run(slow); CHECK(false); }
|
|
catch (const TimeoutExpired& e) { CHECK_EQ(std::string(e.what()), std::string("Command '['sleep', '5']' timed out after 0.3 seconds")); }
|
|
// incremental decoding / newline translation
|
|
IncrementalUtf8 dec;
|
|
CHECK_EQ(dec.Decode("\xc3"), std::string(""));
|
|
CHECK_EQ(dec.Decode("\xa9"), std::string("\xc3\xa9"));
|
|
UniversalNewlines nl;
|
|
CHECK_EQ(nl.Feed("a\r"), std::string("a"));
|
|
CHECK_EQ(nl.Feed("\nb\rc"), std::string("\nb\nc"));
|
|
CHECK_EQ(Subprocess::Which("sh").empty(), false);
|
|
|
|
// run_live pipe path
|
|
RunLiveOptions opt;
|
|
RunResult r = RunLive::Run(Command::Shell("echo hi; echo err >&2; exit 3"), opt);
|
|
CHECK(r.code == 3 && r.output == "hi\nerr\n" && !r.timedOut);
|
|
r = RunLive::Run(Command::Shell("printf 'a\\rb\\r\\nc'"), opt);
|
|
CHECK_EQ(r.output, std::string("a\nb\nc"));
|
|
opt.timeout = 0.5;
|
|
auto t0 = std::chrono::steady_clock::now();
|
|
r = RunLive::Run(Command::Shell("echo before; sleep 5"), opt);
|
|
double took = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
|
// sh is killed but its sleep child keeps the pipe open: run_live joins the reader (up to 10s), like tai.py
|
|
CHECK(r.timedOut && r.code == -9 && r.output == "before\n" && took > 4.0 && took < 13.0);
|
|
try { RunLive::Run(Command::Argv({"/nonexistent/prog"}), opt); CHECK(false); } catch (const OsError&) { CHECK(true); }
|
|
|
|
// big output through the pipe path
|
|
opt.timeout = 60;
|
|
r = RunLive::Run(Command::Shell("seq 1 200000"), opt);
|
|
CHECK(r.code == 0 && r.output.size() == 1288895);
|
|
|
|
// pty path: prompt detection, secret, decline, wait time not counted
|
|
struct Rec : StreamSink { std::vector<std::string> lines; int pauses = 0; void Feed(const std::string& l) override { lines.push_back(l); } void Pause() override { ++pauses; } void Close(const std::string&) override {} };
|
|
Rec rec;
|
|
std::vector<std::pair<std::string, bool>> asked;
|
|
RunLiveOptions po;
|
|
po.timeout = 20; po.stream = &rec;
|
|
po.ask = [&](const std::string& text, bool secret) -> std::optional<std::string> { asked.emplace_back(text, secret); return std::string("hunter2"); };
|
|
r = RunLive::Run(Command::Shell("printf 'Password: '; read -r x; echo \"got:$x\""), po);
|
|
CHECK(r.output == "Password: hunter2\ngot:hunter2\n" && asked.size() == 1 && asked[0].first == "Password:" && !asked[0].second && rec.pauses == 1);
|
|
CHECK(rec.lines.size() == 3 && rec.lines[0].find("\xc2\xbb Password: hunter2") == 0);
|
|
asked.clear(); rec.lines.clear();
|
|
r = RunLive::Run(Command::Shell("stty -echo; printf 'Secret: '; read -r x; stty echo; echo; echo \"len:${#x}\""), po);
|
|
CHECK(asked.size() == 1 && asked[0].second && rec.lines[0] == "\xc2\xbb Secret: (hidden)\n");
|
|
asked.clear();
|
|
po.ask = [&](const std::string& text, bool secret) -> std::optional<std::string> { asked.emplace_back(text, secret); return std::nullopt; };
|
|
r = RunLive::Run(Command::Shell("printf 'Name: '; read -r x; echo \"read:$x:\""), po);
|
|
CHECK(asked.size() == 1 && r.output.find("read::\n") != std::string::npos);
|
|
po.timeout = 2;
|
|
po.ask = [&](const std::string&, bool) -> std::optional<std::string> { std::this_thread::sleep_for(std::chrono::seconds(3)); return std::string("slow"); };
|
|
r = RunLive::Run(Command::Shell("printf 'Q: '; read -r x; echo \"hi $x\""), po);
|
|
CHECK(!r.timedOut && r.output == "Q: slow\nhi slow\n");
|
|
po.ask = [&](const std::string&, bool) -> std::optional<std::string> { return std::string("x"); };
|
|
po.timeout = 1;
|
|
r = RunLive::Run(Command::Shell("echo start; sleep 6"), po);
|
|
CHECK(r.timedOut && r.code == -9);
|
|
// control sequences are stripped from pty output
|
|
po.timeout = 10;
|
|
r = RunLive::Run(Command::Shell("printf '\\033[31mred\\033[0m \\033]0;t\\007ok\\n'"), po);
|
|
CHECK_EQ(r.output, std::string("red ok\n"));
|
|
// RUN_OUTPUT_CAP bounds what comes back from the pty path
|
|
po.timeout = 60;
|
|
r = RunLive::Run(Command::Shell("seq 1 700000"), po);
|
|
CHECK(r.output.size() < 4200000 && r.output.size() > 3900000);
|
|
CHECK_EQ(RunLive::StripControl("a\033[1;2Hb\033]0;x\007c\x01"), std::string("abc"));
|
|
CHECK(RunLive::LooksLikePrompt("Password:") && RunLive::LooksLikePrompt("Continue? (y/n)") && !RunLive::LooksLikePrompt("done"));
|
|
}
|
|
|
|
static void TestBurst() {
|
|
// many concurrent runs through both paths must neither deadlock nor mix output
|
|
std::atomic<int> bad{0};
|
|
std::vector<std::thread> threads;
|
|
for (int i = 0; i < 16; ++i) {
|
|
threads.emplace_back([i, &bad] {
|
|
RunLiveOptions o; o.timeout = 60;
|
|
RunResult r = RunLive::Run(Command::Shell("seq 1 " + std::to_string(20000 + i)), o);
|
|
if (r.code != 0 || r.output.size() < 100000) ++bad;
|
|
o.ask = [](const std::string&, bool) -> std::optional<std::string> { return std::string("y"); };
|
|
RunResult p = RunLive::Run(Command::Shell("seq 1 " + std::to_string(20000 + i)), o);
|
|
if (p.code != 0 || p.output.size() < 100000) ++bad;
|
|
});
|
|
}
|
|
for (auto& t : threads) t.join();
|
|
CHECK_EQ(bad.load(), 0);
|
|
}
|
|
|
|
static void TestStreams() {
|
|
StringSink sink;
|
|
LiveStream ls(2, 20, &sink, true);
|
|
ls.Feed("one\n"); ls.Feed("two\n"); ls.Feed("three\n");
|
|
ls.Close("done");
|
|
CHECK_EQ(sink.str(), std::string("\xe2\x94\x82 one\n\xe2\x94\x82 two\n\033[2A\r\033[K\xe2\x94\x82 two\n\r\033[K\xe2\x94\x82 three\n\xe2\x94\x82 done\n"));
|
|
StringSink empty;
|
|
LiveStream e(4, 20, &empty, true);
|
|
e.Close("exit 0");
|
|
CHECK_EQ(empty.str(), std::string("\xe2\x94\x82 (no output)\n\xe2\x94\x82 exit 0\n"));
|
|
CHECK_EQ(StreamStatus(3, "a\nb", 1.25, false), std::string("exit 3 \xc2\xb7 1.2s \xc2\xb7 2 lines \xc2\xb7 3 B"));
|
|
CHECK_EQ(StreamStatus(0, "", 0.0, true), std::string("timed out after 120s (killed) \xc2\xb7 0 lines \xc2\xb7 0 B"));
|
|
}
|
|
|
|
static void TestGlow() {
|
|
CHECK_EQ(Glow::Ellipsis(std::string(100, 'x'), 60).size(), (size_t)59 + 3);
|
|
CHECK_EQ(Glow::IconFor("shell"), std::string("\xe2\x96\xb6"));
|
|
auto d = Glow::DescribeCall("shell", "{\"command\": \"ls -la\"}");
|
|
CHECK(d.verb == "Run" && d.target == "ls -la" && d.extra == "shell" && d.preview == "ls -la");
|
|
d = Glow::DescribeCall("store_secret", "{\"name\": \"k\", \"value\": \"s3\"}");
|
|
CHECK(d.preview.find("s3") == std::string::npos);
|
|
d = Glow::DescribeCall("nope", "x y");
|
|
CHECK(d.verb == "nope" && d.preview == "x y");
|
|
std::vector<AgentRow> rows(2);
|
|
rows[0].id = 1; rows[0].status = "running"; rows[0].task = "t";
|
|
rows[1].id = 2; rows[1].status = "done"; rows[1].task = "u"; rows[1].tools = 0;
|
|
auto lanes = Glow::Lanes(rows, 6, 0, "");
|
|
CHECK(lanes.size() == 3 && lanes[0].find("lanes") != std::string::npos);
|
|
// LaneBoard paints twice (start, stop) and degrades to static lines when the row count changes
|
|
std::vector<std::vector<AgentRow>> seq = {{rows[0]}, rows};
|
|
size_t call = 0;
|
|
StringSink out;
|
|
LaneBoard board("p", [&](const std::string&) { return seq[std::min(call++, seq.size() - 1)]; }, 60, 6, &out);
|
|
board.Start(); board.Stop();
|
|
CHECK_EQ(board.painted(), 3);
|
|
CHECK(out.str().find("\033[") != std::string::npos);
|
|
}
|
|
|
|
static void TestSession() {
|
|
fs::path root = fs::temp_directory_path() / "detz_shell_test_proc";
|
|
fs::remove_all(root);
|
|
fs::create_directories(root / "sys/kernel/random");
|
|
std::ofstream(root / "stat") << "btime 1760000000\n";
|
|
std::ofstream(root / "sys/kernel/random/boot_id") << "ABCDEF12-3456\n";
|
|
fs::create_directories(root / "200"); fs::create_directories(root / "100");
|
|
std::ofstream(root / "200/stat") << "200 (bash) S 100 " << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5000 0 0\n";
|
|
std::ofstream(root / "200/cmdline") << std::string("bash\0--norc\0", 12);
|
|
std::ofstream(root / "100/stat") << "100 (init) S 0 " << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 0 0\n";
|
|
std::ofstream(root / "100/cmdline") << std::string("/sbin/init\0", 11);
|
|
EnvMap env;
|
|
ShellSession s = ShellSession::Current(&env, 200, root.string(), 1760001000, 100);
|
|
CHECK(s.shell == "bash" && s.pid == 200 && !s.parent && s.sid.find("-p200-abcdef12") != std::string::npos);
|
|
EnvMap pinned; pinned.Set("TAI_SESSION", "shared word");
|
|
ShellSession w = ShellSession::Current(&pinned, 200, root.string(), 1760001000, 100);
|
|
CHECK(w.sid == "sharedword" && w.pid == 200);
|
|
EnvMap nested; nested.Set("TAI_SESSION", "20250101-000000-p999-abcdef12");
|
|
ShellSession n = ShellSession::Current(&nested, 200, root.string(), 1760001000, 100);
|
|
CHECK(n.sid == s.sid && n.parent && *n.parent == "20250101-000000-p999-abcdef12");
|
|
CHECK_EQ(ShellSession::Md5Hex("abc"), std::string("900150983cd24fb0d6963f7d28e17f72"));
|
|
CHECK(ShellSession::ParseStat("1 (a b) S 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 77 0").ticks == 77);
|
|
CHECK(!ShellSession::ParseStat("garbage").ok);
|
|
fs::remove_all(root);
|
|
}
|
|
|
|
static void TestSandbox() {
|
|
CHECK_EQ(CurlHint("curl https://x"), std::string("\n") + kCurlHint);
|
|
CHECK_EQ(CurlHint("mycurl http"), std::string(""));
|
|
CHECK_EQ(Sandbox::RunArgs().size(), (size_t)6);
|
|
CHECK_EQ(Sandbox::UpdateArgs().size(), (size_t)4);
|
|
std::string e = Sandbox::ContainerEngine();
|
|
CHECK(e.empty() || e == "podman" || e == "docker");
|
|
CHECK_EQ(Sandbox::BoxState("/nonexistent/engine"), std::string("missing"));
|
|
CHECK(std::string(BoxData::BoxContainerfile()).find("FROM python:3.12-slim") == 0);
|
|
}
|
|
|
|
int main() {
|
|
TestUtf8(); TestText(); TestAnsi(); TestJson(); TestMarkdown(); TestDiff(); TestShlexGlobTargets();
|
|
TestProcAndRun(); TestBurst(); TestStreams(); TestGlow(); TestSession(); TestSandbox();
|
|
std::printf("%d checks, %d failures\n", gChecks, gFailures);
|
|
return gFailures ? 1 : 0;
|
|
}
|