Source
stdlib/tests/io_test.cpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
#include "io.hpp"5
#include <complex>6
#include <filesystem>7
#include <iostream>8
#include <sstream>9
#include <string>10
#include <unordered_map>11
#include <vector>13
#include <gtest/gtest.h>15
namespace io = cheatah::io;17
TEST(CheatahIo, StrRendersComplex) {18
using C = std::complex<double>;19
EXPECT_EQ(io::str(C(8, 3)), "8+3j"); // positive imaginary20
EXPECT_EQ(io::str(C(1, -2)), "1-2j"); // negative imaginary21
EXPECT_EQ(io::str(C(1, 0)), "1+0j"); // +0 (not "-0") after a conjugate22
EXPECT_EQ(io::str(std::conj(C(1, 0))), "1+0j");23
// repr matches str (numbers are not quoted), and a list of complex prints readably.24
EXPECT_EQ(io::repr(C(2, -5)), "2-5j");25
EXPECT_EQ(io::str(std::vector<C>{C(0, 1), C(2, 0)}), "[0+1j, 2+0j]");26
}28
TEST(CheatahIo, StrByteWidthIntsAreNumbers) {29
// i8/u8 (signed char / unsigned char) print as NUMBERS, not characters — mirrors30
// builtins::str, so print/repr of a narrow-width value shows digits. See io.hpp str(signed char).31
EXPECT_EQ(io::str(static_cast<signed char>(65)), "65");32
EXPECT_EQ(io::str(static_cast<signed char>(-5)), "-5");33
EXPECT_EQ(io::str(static_cast<unsigned char>(200)), "200");34
EXPECT_EQ(io::str(static_cast<unsigned char>(0)), "0");35
}37
// The io concept must accept exactly what the templates already take (streamable38
// types) and reject the rest, so a bad call gives a clear diagnostic.39
static_assert(io::Streamable<int>);40
static_assert(io::Streamable<std::string>);41
static_assert(io::Streamable<const char*>);42
static_assert(!io::Streamable<std::vector<int>>);44
// print() requires Printable, NOT Streamable: a value is printable if it streams45
// directly, has a `str()`, or is a list/dict of printable elements.46
namespace {47
struct WithStr { std::string str() const { return "W"; } };48
struct NotPrintable { int z; }; // not streamable, no str()49
} // namespace50
static_assert(io::Printable<int>);51
static_assert(io::Printable<std::vector<int>>);52
static_assert(io::Printable<std::vector<std::vector<std::string>>>);53
static_assert(io::Printable<std::unordered_map<std::string, int>>);54
static_assert(io::Printable<WithStr>);55
static_assert(!io::Printable<NotPrintable>); // no stream op, no str()56
static_assert(!io::Printable<std::vector<NotPrintable>>); // element not printable58
TEST(CheatahIo, PrintWritesSpaceSeparatedLine) {59
std::ostringstream cap;60
std::streambuf* old = std::cout.rdbuf(cap.rdbuf());61
io::print("meow", 42, "purr");62
std::cout.rdbuf(old);63
EXPECT_EQ(cap.str(), "meow 42 purr\n");64
}66
TEST(CheatahIo, PrintNoArgsIsJustNewline) {67
std::ostringstream cap;68
std::streambuf* old = std::cout.rdbuf(cap.rdbuf());69
io::print();70
std::cout.rdbuf(old);71
EXPECT_EQ(cap.str(), "\n");72
}74
namespace {75
// A stand-in for a cheatah struct: it both streams (operator<<, the compact form rprint/str76
// use) and exposes a cheatah_pretty_print member (the pretty form print uses).77
struct PrettyStub {78
friend std::ostream& operator<<(std::ostream& os, const PrettyStub&) { return os << "P(compact)"; }79
void cheatah_pretty_print(std::ostream& os, long long indent) const {80
os << std::string(static_cast<std::size_t>(indent), ' ') << "P(\n x = 1\n)";81
}82
};83
} // namespace85
TEST(CheatahIo, PrintPrettyPrintsStructs) {86
std::ostringstream cap;87
std::streambuf* old = std::cout.rdbuf(cap.rdbuf());88
io::print(PrettyStub{}); // has cheatah_pretty_print -> pretty branch89
std::cout.rdbuf(old);90
EXPECT_EQ(cap.str(), "P(\n x = 1\n)\n");91
}93
TEST(CheatahIo, RprintIsCompact) {94
std::ostringstream cap;95
std::streambuf* old = std::cout.rdbuf(cap.rdbuf());96
io::rprint(PrettyStub{}, 7); // compact (operator<<) + a scalar, space-separated97
std::cout.rdbuf(old);98
EXPECT_EQ(cap.str(), "P(compact) 7\n");99
}101
TEST(CheatahIo, StrRendersContainersAndObjects) {102
EXPECT_EQ(io::str(std::vector<long long>{1, 2, 3}), "[1, 2, 3]");103
EXPECT_EQ(io::str(std::vector<std::string>{"a", "b"}), "['a', 'b']"); // nested strings quoted104
EXPECT_EQ(io::str(std::vector<std::vector<long long>>{{1, 2}, {3, 4}}), "[[1, 2], [3, 4]]");105
std::unordered_map<std::string, long long> m{{"a", 1}};106
EXPECT_EQ(io::str(m), "{'a': 1}");107
EXPECT_EQ(io::str(WithStr{}), "W"); // a type with a str() method108
EXPECT_EQ(io::str(std::vector<WithStr>{{}, {}}), "[W, W]");109
}111
TEST(CheatahIo, StrFormatsPythonStyle) {112
EXPECT_EQ(io::str(42), "42");113
EXPECT_EQ(io::str(true), "True");114
EXPECT_EQ(io::str(false), "False");115
EXPECT_EQ(io::str(std::string("meow")), "meow");116
}118
TEST(CheatahIo, FixedFormatsAndRounds) {119
EXPECT_EQ(io::fixed(3.14159, 2), "3.14");120
EXPECT_EQ(io::fixed(2.675, 2), "2.67"); // binary 2.675 is 2.67499…: correct rounding121
EXPECT_EQ(io::fixed(12.0, 1), "12.0");122
EXPECT_EQ(io::fixed(0.5, 0), "0"); // half-to-even at the integer boundary123
EXPECT_EQ(io::fixed(1.5, 0), "2");124
EXPECT_EQ(io::fixed(-1.25, 1), "-1.2");125
EXPECT_EQ(io::fixed(7.0, -3), "7"); // negative places clamps to 0126
EXPECT_EQ(io::fixed(0.1, 17), "0.10000000000000001"); // places capped at 17127
}129
TEST(CheatahIo, ReprQuotesStrings) {130
EXPECT_EQ(io::repr(std::string("meow")), "'meow'");131
EXPECT_EQ(io::repr("purr"), "'purr'");132
EXPECT_EQ(io::repr(42), "42");133
}135
TEST(CheatahIo, FormatSubstitutesBraces) {136
EXPECT_EQ(io::format("{} ate {} fish", "cat", 3), "cat ate 3 fish");137
EXPECT_EQ(io::format("no slots here", 1, 2), "no slots here");138
}140
TEST(CheatahIo, FileWriteThenReadWhole) {141
const std::string path = "purr_io_whole_tmp.txt";142
{143
io::File f = io::open(path, "w");144
f.write("meow\npurr\n");145
}146
{147
io::File f = io::open(path, "r");148
EXPECT_EQ(f.read(), "meow\npurr\n");149
}150
std::filesystem::remove(path);151
}153
TEST(CheatahIo, FileReadlineThenReadlines) {154
const std::string path = "purr_io_lines_tmp.txt";155
{156
io::File f = io::open(path, "w");157
f.write("meow\npurr\nnap\n");158
}159
{160
io::File f = io::open(path, "r");161
EXPECT_EQ(f.readline(), "meow");162
const std::vector<std::string> rest = f.readlines();163
ASSERT_EQ(rest.size(), 2u);164
EXPECT_EQ(rest[0], "purr");165
EXPECT_EQ(rest[1], "nap");166
}167
std::filesystem::remove(path);168
}170
TEST(CheatahIo, FileAppendMode) {171
const std::string path = "purr_io_append_tmp.txt";172
{173
io::File f = io::open(path, "w");174
f.write("meow\n");175
}176
{177
io::File f = io::open(path, "a");178
f.write("purr\n");179
}180
{181
io::File f = io::open(path, "r");182
EXPECT_EQ(f.read(), "meow\npurr\n");183
}184
std::filesystem::remove(path);185
}187
TEST(CheatahIo, FileIsOpenAndClose) {188
const std::string path = "purr_io_isopen_tmp.txt";189
io::File f = io::open(path, "w");190
EXPECT_TRUE(f.is_open());191
f.write("x");192
f.close();193
EXPECT_FALSE(f.is_open());194
std::filesystem::remove(path);195
}197
TEST(CheatahIo, InputReadsALine) {198
std::istringstream fake("hello world\nsecond\n");199
std::streambuf* saved = std::cin.rdbuf(fake.rdbuf()); // feed stdin200
EXPECT_EQ(io::input("prompt> "), "hello world");201
EXPECT_EQ(io::input(), "second");202
std::cin.rdbuf(saved); // restore203
}205
TEST(CheatahIo, FormatMultiArgAndExtraArgs) {206
EXPECT_EQ(io::format("{}-{}", 1, 2), "1-2"); // recursion across placeholders207
EXPECT_EQ(io::format("{}", 1, 2, 3), "1"); // more args than placeholders → dropped208
EXPECT_EQ(io::format("none", 7), "none"); // no placeholders at all209
}211
TEST(CheatahIo, ReadFileWholeAndBinary) {212
const std::string path = "purr_io_readfile_tmp.bin";213
std::string payload = "meow\npurr\n";214
payload.push_back('\0'); // embedded NUL — read_file must preserve it215
payload += "after-nul";216
{217
io::File f = io::open(path, "wb");218
f.write(payload);219
}220
EXPECT_EQ(io::read_file(path), payload); // whole file, byte-exact221
EXPECT_EQ(io::read_file("no_such_file_qzx.bin"), ""); // missing → ""222
std::filesystem::remove(path);223
}