aboutsummaryrefslogtreecommitdiff
path: root/test/base
diff options
context:
space:
mode:
Diffstat (limited to 'test/base')
-rw-r--r--test/base/CMakeLists.txt40
-rw-r--r--test/base/Event2Test.cpp54
-rw-r--r--test/base/HandlerRegistryTest.cpp37
-rw-r--r--test/base/PropertyTreeTest.cpp26
-rw-r--r--test/base/SelfResolvableTest.cpp110
-rw-r--r--test/base/StringTest.cpp112
-rw-r--r--test/base/StringToNumberConverterTest.cpp166
-rw-r--r--test/base/StringUtilTest.cpp118
-rw-r--r--test/base/SubProcessHelper/CruEcho.cpp9
-rw-r--r--test/base/SubProcessHelper/CruTee.cpp10
-rw-r--r--test/base/SubProcessTest.cpp30
-rw-r--r--test/base/platform/unix/UnixFileStreamTest.cpp30
-rw-r--r--test/base/platform/unix/UnixFileTest.cpp38
-rw-r--r--test/base/platform/win/StreamConvertTest.cpp39
-rw-r--r--test/base/platform/win/Win32FileStreamTest.cpp37
15 files changed, 856 insertions, 0 deletions
diff --git a/test/base/CMakeLists.txt b/test/base/CMakeLists.txt
new file mode 100644
index 00000000..683c8295
--- /dev/null
+++ b/test/base/CMakeLists.txt
@@ -0,0 +1,40 @@
+add_executable(CruBaseTest
+ Event2Test.cpp
+ HandlerRegistryTest.cpp
+ PropertyTreeTest.cpp
+ SelfResolvableTest.cpp
+ StringTest.cpp
+ StringToNumberConverterTest.cpp
+ StringUtilTest.cpp
+ SubProcessTest.cpp
+)
+target_link_libraries(CruBaseTest PRIVATE CruBase CruTestBase)
+
+add_executable(CruTestHelperEcho
+ SubProcessHelper/CruEcho.cpp
+)
+
+add_executable(CruTestHelperTee
+ SubProcessHelper/CruTee.cpp
+)
+
+target_compile_definitions(CruBaseTest PRIVATE
+ CRU_TEST_HELPER_ECHO_LOCATION="$<TARGET_FILE:CruTestHelperEcho>"
+ CRU_TEST_HELPER_TEE_LOCATION="$<TARGET_FILE:CruTestHelperTee>"
+)
+
+if (UNIX AND NOT EMSCRIPTEN)
+ target_sources(CruBaseTest PRIVATE
+ platform/unix/UnixFileTest.cpp
+ platform/unix/UnixFileStreamTest.cpp
+ )
+endif()
+
+if (WIN32)
+ target_sources(CruBaseTest PRIVATE
+ platform/win/StreamConvertTest.cpp
+ platform/win/Win32FileStreamTest.cpp
+ )
+endif()
+
+cru_catch_discover_tests(CruBaseTest)
diff --git a/test/base/Event2Test.cpp b/test/base/Event2Test.cpp
new file mode 100644
index 00000000..0c67f28e
--- /dev/null
+++ b/test/base/Event2Test.cpp
@@ -0,0 +1,54 @@
+#include "cru/base/Event2.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+using cru::Event2;
+
+TEST_CASE("Event2", "[event2]") {
+ Event2 event;
+
+ int counter = 0;
+
+ auto handler = [&counter] { counter++; };
+ auto token = event.AddHandler(handler);
+
+ auto handler2 = [&counter](decltype(event)::Context* context) { counter++; };
+
+ SECTION("two handlers should work.") {
+ event.Raise();
+ REQUIRE(counter == 1);
+ event.Raise();
+ REQUIRE(counter == 2);
+
+ event.AddHandler(handler2);
+ event.Raise();
+ REQUIRE(counter == 4);
+ }
+
+ SECTION("handler revoker should work.") {
+ token.RevokeHandler();
+ event.Raise();
+ REQUIRE(counter == 0);
+
+ token = event.AddHandler(handler);
+ event.AddHandler(handler2);
+ event.Raise();
+ REQUIRE(counter == 2);
+
+ token.RevokeHandler();
+ event.Raise();
+ REQUIRE(counter == 3);
+ }
+
+ SECTION("stop handling should work.") {
+ auto short_circuit_handler = [&counter](decltype(event)::Context* context) {
+ context->SetStopHandling();
+ };
+
+ event.AddHandler(short_circuit_handler);
+ event.AddHandler(handler2);
+
+ event.Raise();
+ REQUIRE(counter == 1);
+ }
+}
diff --git a/test/base/HandlerRegistryTest.cpp b/test/base/HandlerRegistryTest.cpp
new file mode 100644
index 00000000..aacef70f
--- /dev/null
+++ b/test/base/HandlerRegistryTest.cpp
@@ -0,0 +1,37 @@
+#include "cru/base/HandlerRegistry.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <algorithm>
+
+TEST_CASE("HandlerRegistry", "[handler_registry]") {
+ using namespace cru;
+ HandlerRegistry<void()> registry;
+
+ int counter = 1;
+
+ auto tag1 = registry.AddHandler([&counter] { counter++; });
+ auto tag2 = registry.AddHandler([&counter] { counter++; });
+
+ for (const auto& handler : registry) {
+ handler();
+ }
+
+ REQUIRE(counter == 3);
+
+ registry.RemoveHandler(tag1);
+
+ for (const auto& handler : registry) {
+ handler();
+ }
+
+ REQUIRE(counter == 4);
+
+ registry.RemoveHandler(tag2);
+
+ for (const auto& handler : registry) {
+ handler();
+ }
+
+ REQUIRE(counter == 4);
+}
diff --git a/test/base/PropertyTreeTest.cpp b/test/base/PropertyTreeTest.cpp
new file mode 100644
index 00000000..24d7ca9e
--- /dev/null
+++ b/test/base/PropertyTreeTest.cpp
@@ -0,0 +1,26 @@
+#include "cru/base/PropertyTree.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+TEST_CASE("PropertyTree", "[property_tree]") {
+ using cru::PropertySubTreeRef;
+ using cru::PropertyTree;
+
+ PropertyTree tree({
+ {u"k1", u"v1"},
+ {u"k2", u"v2"},
+ {u"k3.sub", u"v3"},
+ });
+
+ REQUIRE(tree.GetValue(u"k1") == u"v1");
+ REQUIRE(tree.GetValue(u"k2") == u"v2");
+ REQUIRE(tree.GetValue(u"k3.sub") == u"v3");
+
+ PropertySubTreeRef sub_tree = tree.GetSubTreeRef(u"k3");
+ REQUIRE(sub_tree.GetValue(u"sub") == u"v3");
+
+ PropertySubTreeRef root_tree = sub_tree.GetParent();
+ REQUIRE(root_tree.GetValue(u"k1") == u"v1");
+ REQUIRE(root_tree.GetValue(u"k2") == u"v2");
+ REQUIRE(root_tree.GetValue(u"k3.sub") == u"v3");
+}
diff --git a/test/base/SelfResolvableTest.cpp b/test/base/SelfResolvableTest.cpp
new file mode 100644
index 00000000..c214bd77
--- /dev/null
+++ b/test/base/SelfResolvableTest.cpp
@@ -0,0 +1,110 @@
+#include "cru/base/Base.h"
+#include "cru/base/SelfResolvable.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <memory>
+
+namespace {
+class SelfResolvableTestClassBase {
+ public:
+ SelfResolvableTestClassBase() = default;
+ CRU_DELETE_COPY(SelfResolvableTestClassBase)
+ CRU_DEFAULT_MOVE(SelfResolvableTestClassBase)
+ virtual ~SelfResolvableTestClassBase() = default;
+};
+
+class SelfResolvableTestClass
+ : public SelfResolvableTestClassBase,
+ public cru::SelfResolvable<SelfResolvableTestClass> {
+ public:
+ SelfResolvableTestClass() : ptr_(new int(123)) {}
+ CRU_DELETE_COPY(SelfResolvableTestClass)
+ CRU_DEFAULT_MOVE(SelfResolvableTestClass)
+ ~SelfResolvableTestClass() override = default;
+
+ private:
+ std::shared_ptr<int> ptr_;
+};
+} // namespace
+
+TEST_CASE("SelfResolvable resolver should work.", "[self-resolvable]") {
+ SelfResolvableTestClass test_object;
+
+ auto resolver = test_object.CreateResolver();
+ REQUIRE(resolver.Resolve() == &test_object);
+
+ auto resolver_copy = resolver;
+ REQUIRE(resolver.Resolve() == &test_object);
+ REQUIRE(resolver.Resolve() == &test_object);
+
+ auto resolver_move = std::move(resolver_copy);
+ REQUIRE(resolver.Resolve() == &test_object);
+ REQUIRE(resolver_copy.IsValid() == false);
+ REQUIRE(resolver_move.Resolve() == &test_object);
+}
+
+TEST_CASE("SelfResolvable object destructed should work.",
+ "[self-resolvable]") {
+ SelfResolvableTestClass* test_object = new SelfResolvableTestClass();
+
+ auto resolver = test_object->CreateResolver();
+ auto resolver_copy = resolver;
+
+ delete test_object;
+
+ REQUIRE(resolver.Resolve() == nullptr);
+ REQUIRE(resolver_copy.Resolve() == nullptr);
+
+ auto resolver_copy2 = resolver_copy;
+ REQUIRE(resolver_copy2.Resolve() == nullptr);
+
+ auto resolver_move = std::move(resolver_copy);
+ REQUIRE(resolver_copy.IsValid() == false);
+ REQUIRE(resolver_move.Resolve() == nullptr);
+}
+
+TEST_CASE("SelfResolvable object moved should work.", "[self-resolvable]") {
+ SelfResolvableTestClass test_object;
+
+ auto resolver = test_object.CreateResolver();
+ auto resolver_copy = resolver;
+
+ SelfResolvableTestClass moved_object = std::move(test_object);
+
+ REQUIRE(resolver.Resolve() == &moved_object);
+ REQUIRE(resolver_copy.Resolve() == &moved_object);
+
+ auto resolver_copy2 = resolver_copy;
+ REQUIRE(resolver_copy2.Resolve() == &moved_object);
+
+ auto resolver_move = std::move(resolver_copy);
+ REQUIRE(resolver_copy.IsValid() == false);
+ REQUIRE(resolver_move.Resolve() == &moved_object);
+}
+
+TEST_CASE("SelfResolvable should work for casted type.", "[self-resolvable]") {
+ auto test_object = new SelfResolvableTestClass();
+
+ cru::ObjectResolver<SelfResolvableTestClassBase> base_resolver =
+ test_object->CreateResolver();
+
+ REQUIRE(base_resolver.Resolve() == test_object);
+
+ auto base_resolver2 = base_resolver;
+ REQUIRE(base_resolver2.Resolve() == test_object);
+
+ auto base_resolver3 = std::move(base_resolver2);
+ REQUIRE(base_resolver3.Resolve() == test_object);
+
+ auto moved_object = new SelfResolvableTestClass(std::move(*test_object));
+ delete test_object;
+
+ REQUIRE(base_resolver.Resolve() == moved_object);
+ REQUIRE(base_resolver3.Resolve() == moved_object);
+
+ delete moved_object;
+
+ REQUIRE(base_resolver.Resolve() == nullptr);
+ REQUIRE(base_resolver3.Resolve() == nullptr);
+}
diff --git a/test/base/StringTest.cpp b/test/base/StringTest.cpp
new file mode 100644
index 00000000..9ed351e5
--- /dev/null
+++ b/test/base/StringTest.cpp
@@ -0,0 +1,112 @@
+#include "cru/base/Format.h"
+#include "cru/base/String.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+TEST_CASE("String Append", "[string]") {
+ using cru::String;
+
+ String s;
+ s.append(u"ha");
+ s.append(s);
+ REQUIRE(s == String(u"haha"));
+}
+
+TEST_CASE("String IndexConvert", "[string]") {
+ using cru::String;
+
+ String s(u"123");
+ REQUIRE(s.IndexFromCodePointToCodeUnit(1) == 1);
+ REQUIRE(s.IndexFromCodeUnitToCodePoint(1) == 1);
+ REQUIRE(s.IndexFromCodeUnitToCodePoint(3) == 3);
+ REQUIRE(s.IndexFromCodeUnitToCodePoint(3) == 3);
+}
+
+TEST_CASE("String Format", "[string]") {
+ using cru::Format;
+ using cru::String;
+
+ REQUIRE(Format(u"{} + {} = {}", 123, 321, 444) == String(u"123 + 321 = 444"));
+}
+
+TEST_CASE("String Trim", "[string]") {
+ using cru::String;
+ REQUIRE(String(u" abc ").Trim() == u"abc");
+}
+
+TEST_CASE("String SplitToLines", "[string]") {
+ using cru::String;
+
+ String s(u"abc\ndef\nghi");
+ auto lines = s.SplitToLines();
+ REQUIRE(lines.size() == 3);
+ REQUIRE(lines[0] == String(u"abc"));
+ REQUIRE(lines[1] == String(u"def"));
+ REQUIRE(lines[2] == String(u"ghi"));
+}
+
+TEST_CASE("String SplitToLinesWithEmptyLine", "[string]") {
+ using cru::String;
+
+ String s(u"abc\n \ndef\n\nghi\n");
+ auto lines = s.SplitToLines();
+ REQUIRE(lines.size() == 6);
+ REQUIRE(lines[0] == String(u"abc"));
+ REQUIRE(lines[1] == String(u" "));
+ REQUIRE(lines[2] == String(u"def"));
+ REQUIRE(lines[3] == String(u""));
+ REQUIRE(lines[4] == String(u"ghi"));
+ REQUIRE(lines[5] == String(u""));
+}
+
+TEST_CASE("String SplitToLinesRemoveSpaceLine", "[string]") {
+ using cru::String;
+
+ String s(u"abc\n \ndef\n\nghi\n");
+ auto lines = s.SplitToLines(true);
+ REQUIRE(lines.size() == 3);
+ REQUIRE(lines[0] == String(u"abc"));
+ REQUIRE(lines[1] == String(u"def"));
+ REQUIRE(lines[2] == String(u"ghi"));
+}
+
+TEST_CASE("StringView ToUtf8", "[string]") {
+ using cru::StringView;
+ StringView utf16_text = u"aπ你🤣!";
+ std::string_view utf8_text = "aπ你🤣!";
+
+ REQUIRE(utf16_text.ToUtf8() == utf8_text);
+}
+
+TEST_CASE("String FromUtf8", "[string]") {
+ std::u16string_view utf16_text = u"aπ你🤣!";
+ std::string_view utf8_text = "aπ你🤣!";
+
+ REQUIRE(cru::String::FromUtf8(utf8_text) == utf16_text);
+}
+
+TEST_CASE("StringView ParseToDouble", "[string]") {
+ using cru::StringToNumberFlags;
+ using cru::StringView;
+ REQUIRE(StringView(u"3.14159").ParseToDouble() == 3.14159);
+ REQUIRE(
+ StringView(u" 3.14159")
+ .ParseToDouble(nullptr, StringToNumberFlags::kAllowLeadingSpaces) ==
+ 3.14159);
+ REQUIRE(StringView(u" 3.14159 ")
+ .ParseToDouble(nullptr,
+ StringToNumberFlags::kAllowLeadingSpaces |
+ StringToNumberFlags::kAllowTrailingSpaces) ==
+ 3.14159);
+}
+
+TEST_CASE("String ParseToDoubleList", "[string]") {
+ using cru::StringView;
+
+ auto list = StringView(u" 1.23 2.34 3.45 ").ParseToDoubleList();
+
+ REQUIRE(list.size() == 3);
+ REQUIRE(list[0] == 1.23);
+ REQUIRE(list[1] == 2.34);
+ REQUIRE(list[2] == 3.45);
+}
diff --git a/test/base/StringToNumberConverterTest.cpp b/test/base/StringToNumberConverterTest.cpp
new file mode 100644
index 00000000..82062bdb
--- /dev/null
+++ b/test/base/StringToNumberConverterTest.cpp
@@ -0,0 +1,166 @@
+#include "cru/base/Exception.h"
+#include "cru/base/StringToNumberConverter.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+TEST_CASE("StringToIntegerConverterImpl Base0", "[string]") {
+ using namespace cru;
+ StringToIntegerConverter converter({}, 0);
+ Index processed_characters_count;
+
+ REQUIRE(converter.Parse("12345678", &processed_characters_count) ==
+ StringToIntegerResult(false, 12345678));
+ REQUIRE(processed_characters_count == 8);
+
+ REQUIRE(converter.Parse("0", &processed_characters_count) ==
+ StringToIntegerResult(false, 0));
+ REQUIRE(processed_characters_count == 1);
+
+ REQUIRE(converter.Parse("012", &processed_characters_count) ==
+ StringToIntegerResult(false, 012));
+ REQUIRE(processed_characters_count == 3);
+
+ REQUIRE(converter.Parse("0x12", &processed_characters_count) ==
+ StringToIntegerResult(false, 0x12));
+ REQUIRE(processed_characters_count == 4);
+
+ REQUIRE(converter.Parse("0X12", &processed_characters_count) ==
+ StringToIntegerResult(false, 0x12));
+ REQUIRE(processed_characters_count == 4);
+
+ REQUIRE(converter.Parse("0b101", &processed_characters_count) ==
+ StringToIntegerResult(false, 0b101));
+ REQUIRE(processed_characters_count == 5);
+
+ REQUIRE(converter.Parse("0B101", &processed_characters_count) ==
+ StringToIntegerResult(false, 0b101));
+ REQUIRE(processed_characters_count == 5);
+
+ REQUIRE(converter.Parse("-123", &processed_characters_count) ==
+ StringToIntegerResult(true, 123));
+ REQUIRE(processed_characters_count == 4);
+
+ REQUIRE(converter.Parse("-0x10", &processed_characters_count) ==
+ StringToIntegerResult(true, 0x10));
+ REQUIRE(processed_characters_count == 5);
+}
+
+TEST_CASE("StringToIntegerConverterImpl Base0ForError", "[string]") {
+ using namespace cru;
+ StringToIntegerConverter converter({}, 0);
+ Index processed_characters_count;
+
+ REQUIRE(converter.Parse("a", &processed_characters_count) ==
+ StringToIntegerResult(false, 0));
+ REQUIRE(processed_characters_count == 0);
+
+ REQUIRE(converter.Parse("0a", &processed_characters_count) ==
+ StringToIntegerResult(false, 0));
+ REQUIRE(processed_characters_count == 0);
+
+ REQUIRE(converter.Parse("0x", &processed_characters_count) ==
+ StringToIntegerResult(false, 0));
+ REQUIRE(processed_characters_count == 0);
+
+ REQUIRE(converter.Parse("0xx", &processed_characters_count) ==
+ StringToIntegerResult(false, 0));
+ REQUIRE(processed_characters_count == 0);
+
+ REQUIRE(converter.Parse(" 0", &processed_characters_count) ==
+ StringToIntegerResult(false, 0));
+ REQUIRE(processed_characters_count == 0);
+
+ REQUIRE(converter.Parse("0 ", &processed_characters_count) ==
+ StringToIntegerResult(false, 0));
+ REQUIRE(processed_characters_count == 0);
+}
+
+TEST_CASE("StringToIntegerConverterImpl ThrowOnErrorFlag", "[string]") {
+ using namespace cru;
+ StringToIntegerConverter converter(StringToNumberFlags::kThrowOnError, 0);
+ Index processed_characters_count;
+ REQUIRE_THROWS_AS(converter.Parse("?", &processed_characters_count),
+ Exception);
+}
+
+TEST_CASE("StringToIntegerConverterImpl AllowLeadingZeroFlag", "[string]") {
+ using namespace cru;
+ StringToIntegerConverter converter(
+ StringToNumberFlags::kAllowLeadingSpaces, 0);
+ Index processed_characters_count;
+ REQUIRE(converter.Parse(" 123", &processed_characters_count) ==
+ StringToIntegerResult(false, 123));
+ REQUIRE(processed_characters_count == 6);
+}
+
+TEST_CASE("StringToIntegerConverterImpl AllowTrailingZeroFlag", "[string]") {
+ using namespace cru;
+ StringToIntegerConverter converter(
+ StringToNumberFlags::kAllowTrailingSpaces, 0);
+ Index processed_characters_count;
+ REQUIRE(converter.Parse("123 ", &processed_characters_count) ==
+ StringToIntegerResult(false, 123));
+ REQUIRE(processed_characters_count == 6);
+}
+
+TEST_CASE("StringToIntegerConverterImpl AllowTrailingJunk", "[string]") {
+ using namespace cru;
+ StringToIntegerConverter converter(
+ StringToNumberFlags::kAllowTrailingJunk, 0);
+ Index processed_characters_count;
+ REQUIRE(converter.Parse("123 12", &processed_characters_count) ==
+ StringToIntegerResult(false, 123));
+ REQUIRE(processed_characters_count == 3);
+}
+
+TEST_CASE("StringToIntegerConverterImpl AllowLeadingZeroForInteger",
+ "[string]") {
+ using namespace cru;
+ StringToIntegerConverter converter(
+ StringToNumberFlags::kAllowLeadingZeroForInteger, 0);
+ Index processed_characters_count;
+ REQUIRE(converter.Parse("0x0012", &processed_characters_count) ==
+ StringToIntegerResult(false, 0x12));
+ REQUIRE(processed_characters_count == 6);
+
+ REQUIRE(converter.Parse("000011", &processed_characters_count) ==
+ StringToIntegerResult(false, 000011));
+ REQUIRE(processed_characters_count == 6);
+
+ REQUIRE(converter.Parse("0b0011", &processed_characters_count) ==
+ StringToIntegerResult(false, 0b0011));
+ REQUIRE(processed_characters_count == 6);
+}
+
+TEST_CASE("StringToIntegerConverterImpl CompositeFlags", "[string]") {
+ using namespace cru;
+ StringToIntegerConverter converter(
+ StringToNumberFlags::kAllowLeadingSpaces |
+ StringToNumberFlags::kAllowTrailingJunk |
+ StringToNumberFlags::kAllowLeadingZeroForInteger,
+ 0);
+ Index processed_characters_count;
+
+ REQUIRE(converter.Parse(" 0x00123!!!", &processed_characters_count) ==
+ StringToIntegerResult(false, 0x00123));
+ REQUIRE(processed_characters_count == 10);
+}
+
+TEST_CASE("StringToIntegerConverterImpl OtherBase", "[string]") {
+ using namespace cru;
+ StringToIntegerConverter converter({}, 7);
+ Index processed_characters_count;
+
+ REQUIRE(converter.Parse("12", &processed_characters_count) ==
+ StringToIntegerResult(false, 9));
+ REQUIRE(processed_characters_count == 2);
+
+ REQUIRE(converter.Parse("-12", &processed_characters_count) ==
+ StringToIntegerResult(true, 9));
+ REQUIRE(processed_characters_count == 3);
+
+ converter.base = 11;
+ REQUIRE(converter.Parse("1a", &processed_characters_count) ==
+ StringToIntegerResult(false, 21));
+ REQUIRE(processed_characters_count == 2);
+}
diff --git a/test/base/StringUtilTest.cpp b/test/base/StringUtilTest.cpp
new file mode 100644
index 00000000..1da6e963
--- /dev/null
+++ b/test/base/StringUtilTest.cpp
@@ -0,0 +1,118 @@
+#include "cru/base/String.h"
+#include "cru/base/StringUtil.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+using cru::Index;
+using cru::k_invalid_code_point;
+
+TEST_CASE("StringUtil Utf8NextCodePoint", "[string]") {
+ using cru::Utf8NextCodePoint;
+ std::string_view text = "aπ你🤣!";
+ Index current = 0;
+ REQUIRE(Utf8NextCodePoint(text.data(), text.size(), current, &current) ==
+ 0x0061);
+ REQUIRE(Utf8NextCodePoint(text.data(), text.size(), current, &current) ==
+ 0x03C0);
+ REQUIRE(Utf8NextCodePoint(text.data(), text.size(), current, &current) ==
+ 0x4F60);
+ REQUIRE(Utf8NextCodePoint(text.data(), text.size(), current, &current) ==
+ 0x1F923);
+ REQUIRE(Utf8NextCodePoint(text.data(), text.size(), current, &current) ==
+ 0x0021);
+ REQUIRE(Utf8NextCodePoint(text.data(), text.size(), current, &current) ==
+ k_invalid_code_point);
+ REQUIRE(current == static_cast<Index>(text.size()));
+}
+
+TEST_CASE("StringUtil Utf16NextCodePoint", "[string]") {
+ using cru::Utf16NextCodePoint;
+ std::u16string_view text = u"aπ你🤣!";
+ Index current = 0;
+ REQUIRE(Utf16NextCodePoint(text.data(), text.size(), current, &current) ==
+ 0x0061);
+ REQUIRE(Utf16NextCodePoint(text.data(), text.size(), current, &current) ==
+ 0x03C0);
+ REQUIRE(Utf16NextCodePoint(text.data(), text.size(), current, &current) ==
+ 0x4F60);
+ REQUIRE(Utf16NextCodePoint(text.data(), text.size(), current, &current) ==
+ 0x1F923);
+ REQUIRE(Utf16NextCodePoint(text.data(), text.size(), current, &current) ==
+ 0x0021);
+ REQUIRE(Utf16NextCodePoint(text.data(), text.size(), current, &current) ==
+ k_invalid_code_point);
+ REQUIRE(current == static_cast<Index>(text.size()));
+}
+
+TEST_CASE("StringUtil Utf16PreviousCodePoint", "[string]") {
+ using cru::Utf16PreviousCodePoint;
+ std::u16string_view text = u"aπ你🤣!";
+ Index current = text.size();
+ REQUIRE(Utf16PreviousCodePoint(text.data(), text.size(), current, &current) ==
+ 0x0021);
+ REQUIRE(Utf16PreviousCodePoint(text.data(), text.size(), current, &current) ==
+ 0x1F923);
+ REQUIRE(Utf16PreviousCodePoint(text.data(), text.size(), current, &current) ==
+ 0x4F60);
+ REQUIRE(Utf16PreviousCodePoint(text.data(), text.size(), current, &current) ==
+ 0x03C0);
+ REQUIRE(Utf16PreviousCodePoint(text.data(), text.size(), current, &current) ==
+ 0x0061);
+ REQUIRE(Utf16PreviousCodePoint(text.data(), text.size(), current, &current) ==
+ k_invalid_code_point);
+ REQUIRE(current == 0);
+}
+
+TEST_CASE("StringUtil Utf8CodePointIterator", "[string]") {
+ using cru::Utf8CodePointIterator;
+ std::string_view text = "aπ你🤣!";
+ std::vector<cru::CodePoint> code_points;
+
+ for (auto cp : Utf8CodePointIterator(text.data(), text.size())) {
+ code_points.push_back(cp);
+ }
+
+ std::vector<cru::CodePoint> expected_code_points{0x0061, 0x03C0, 0x4F60,
+ 0x1F923, 0x0021};
+
+ REQUIRE(code_points == expected_code_points);
+}
+
+TEST_CASE("StringUtil Utf16CodePointIterator", "[string]") {
+ using cru::Utf16CodePointIterator;
+ std::u16string_view text = u"aπ你🤣!";
+ std::vector<cru::CodePoint> code_points;
+
+ for (auto cp : Utf16CodePointIterator(text.data(), text.size())) {
+ code_points.push_back(cp);
+ }
+
+ std::vector<cru::CodePoint> expected_code_points{0x0061, 0x03C0, 0x4F60,
+ 0x1F923, 0x0021};
+
+ REQUIRE(code_points == expected_code_points);
+}
+
+// TEST(WinString, IndexUtf8ToUtf16) {
+// using cru::platform::win::IndexUtf8ToUtf16;
+// std::string_view utf8_string = "aπ你🤣!";
+// std::wstring_view utf16_string = L"aπ你🤣!";
+// REQUIRE(IndexUtf8ToUtf16(utf8_string, 0, utf16_string), 0);
+// REQUIRE(IndexUtf8ToUtf16(utf8_string, 1, utf16_string), 1);
+// REQUIRE(IndexUtf8ToUtf16(utf8_string, 3, utf16_string), 2);
+// REQUIRE(IndexUtf8ToUtf16(utf8_string, 6, utf16_string), 3);
+// REQUIRE(IndexUtf8ToUtf16(utf8_string, 10, utf16_string), 5);
+// REQUIRE(IndexUtf8ToUtf16(utf8_string, 11, utf16_string), 6);
+// }
+
+// TEST(WinString, IndexUtf16ToUtf8) {
+// using cru::platform::win::IndexUtf16ToUtf8;
+// std::string_view utf8_string = "aπ你🤣!";
+// std::wstring_view utf16_string = L"aπ你🤣!";
+// REQUIRE(IndexUtf16ToUtf8(utf16_string, 0, utf8_string), 0);
+// REQUIRE(IndexUtf16ToUtf8(utf16_string, 1, utf8_string), 1);
+// REQUIRE(IndexUtf16ToUtf8(utf16_string, 2, utf8_string), 3);
+// REQUIRE(IndexUtf16ToUtf8(utf16_string, 3, utf8_string), 6);
+// REQUIRE(IndexUtf16ToUtf8(utf16_string, 5, utf8_string), 10);
+// REQUIRE(IndexUtf16ToUtf8(utf16_string, 6, utf8_string), 11);
+// }
diff --git a/test/base/SubProcessHelper/CruEcho.cpp b/test/base/SubProcessHelper/CruEcho.cpp
new file mode 100644
index 00000000..5f23c027
--- /dev/null
+++ b/test/base/SubProcessHelper/CruEcho.cpp
@@ -0,0 +1,9 @@
+#include <iostream>
+
+int main(int argc, char* argv[]) {
+ for (int i = 1; i < argc - 1; ++i) {
+ std::cout << argv[i] << " ";
+ }
+ std::cout << argv[argc - 1];
+ return 0;
+}
diff --git a/test/base/SubProcessHelper/CruTee.cpp b/test/base/SubProcessHelper/CruTee.cpp
new file mode 100644
index 00000000..4470d2a8
--- /dev/null
+++ b/test/base/SubProcessHelper/CruTee.cpp
@@ -0,0 +1,10 @@
+#include <iostream>
+#include <string>
+
+int main() {
+ std::string s;
+ while (std::cin >> s) {
+ std::cout << s;
+ }
+ return 0;
+}
diff --git a/test/base/SubProcessTest.cpp b/test/base/SubProcessTest.cpp
new file mode 100644
index 00000000..d353dec0
--- /dev/null
+++ b/test/base/SubProcessTest.cpp
@@ -0,0 +1,30 @@
+#include "cru/base/String.h"
+#include "cru/base/SubProcess.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+using cru::String;
+using cru::SubProcess;
+
+TEST_CASE("SubProcess", "[subprocess]") {
+ SECTION("echo should work.") {
+ SubProcess process = SubProcess::Create(
+ String::FromUtf8(CRU_TEST_HELPER_ECHO_LOCATION), {u"abc"});
+ process.Wait();
+ REQUIRE(process.GetExitResult().IsSuccess());
+ auto output = process.GetStdoutStream()->ReadToEndAsUtf8String();
+ REQUIRE(output == u"abc");
+ }
+
+ SECTION("tee should work.") {
+ constexpr auto str = "abc";
+ SubProcess process =
+ SubProcess::Create(String::FromUtf8(CRU_TEST_HELPER_TEE_LOCATION));
+ process.GetStdinStream()->Write(str, 3);
+ process.GetStdinStream()->Close();
+ process.Wait();
+ REQUIRE(process.GetExitResult().IsSuccess());
+ auto output = process.GetStdoutStream()->ReadToEndAsUtf8String();
+ REQUIRE(output == u"abc");
+ }
+}
diff --git a/test/base/platform/unix/UnixFileStreamTest.cpp b/test/base/platform/unix/UnixFileStreamTest.cpp
new file mode 100644
index 00000000..417fccc2
--- /dev/null
+++ b/test/base/platform/unix/UnixFileStreamTest.cpp
@@ -0,0 +1,30 @@
+#include "cru/base/platform/unix/UnixFileStream.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <fcntl.h>
+#include <filesystem>
+
+TEST_CASE("UnixFileStream Work", "[stream]") {
+ using namespace cru;
+ using namespace cru::io;
+ using namespace cru::platform::unix;
+
+ auto temp_file_path =
+ (std::filesystem::temp_directory_path() / "cru_test_temp.XXXXXX")
+ .generic_string();
+ mkstemp(temp_file_path.data());
+
+ UnixFileStream file(temp_file_path.c_str(), O_WRONLY | O_CREAT);
+ file.Write("abc", 3);
+ file.Close();
+
+ UnixFileStream file2(temp_file_path.c_str(), O_RDONLY);
+ auto buffer = std::make_unique<std::byte[]>(3);
+ file2.Read(buffer.get(), 3);
+ REQUIRE(std::string_view(reinterpret_cast<const char*>(buffer.get()), 3) ==
+ "abc");
+ file2.Close();
+
+ std::filesystem::remove(temp_file_path);
+}
diff --git a/test/base/platform/unix/UnixFileTest.cpp b/test/base/platform/unix/UnixFileTest.cpp
new file mode 100644
index 00000000..d5bba0db
--- /dev/null
+++ b/test/base/platform/unix/UnixFileTest.cpp
@@ -0,0 +1,38 @@
+
+#include "cru/base/platform/unix/UnixFileStream.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <fcntl.h>
+#include <filesystem>
+
+TEST_CASE("UnixFile Work", "[unix]") {
+ using namespace cru;
+ using namespace cru::platform::unix;
+
+ auto calledTimes = 0;
+ auto mockClose = [&calledTimes](int _) {
+ calledTimes += 1;
+ return 0;
+ };
+
+ auto temp_file_path =
+ (std::filesystem::temp_directory_path() / "cru_test_temp.XXXXXX")
+ .generic_string();
+ mkstemp(temp_file_path.data());
+
+ auto fdNumber = ::open(temp_file_path.c_str(), O_WRONLY | O_CREAT);
+
+ {
+ UnixFileDescriptor fd(fdNumber, true, std::move(mockClose));
+ REQUIRE(calledTimes == 0);
+ UnixFileDescriptor fd2(std::move(fd));
+ REQUIRE(calledTimes == 0);
+ UnixFileDescriptor fd3;
+ fd3 = std::move(fd2);
+ REQUIRE(calledTimes == 0);
+ }
+ REQUIRE(calledTimes == 1);
+
+ std::filesystem::remove(temp_file_path);
+}
diff --git a/test/base/platform/win/StreamConvertTest.cpp b/test/base/platform/win/StreamConvertTest.cpp
new file mode 100644
index 00000000..90ebb568
--- /dev/null
+++ b/test/base/platform/win/StreamConvertTest.cpp
@@ -0,0 +1,39 @@
+#include "cru/base/io/OpenFileFlag.h"
+#include "cru/base/platform/win/Exception.h"
+#include "cru/base/platform/win/StreamConvert.h"
+#include "cru/base/platform/win/Win32FileStream.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <cstdio>
+#include <filesystem>
+
+TEST_CASE("StreamConvert FileStreamWork", "[stream]") {
+ using namespace cru;
+ using namespace cru::io;
+ using namespace cru::platform::win;
+
+ auto temp_file_path =
+ (std::filesystem::temp_directory_path() / "cru_test_temp.XXXXXX")
+ .native();
+ _wmktemp(temp_file_path.data());
+
+ String path = temp_file_path;
+
+ Win32FileStream file(path, OpenFileFlags::Write | OpenFileFlags::Create);
+ file.Write("abc", 3);
+ file.Close();
+
+ Win32FileStream file2(path, OpenFileFlags::Read);
+ IStream* com_stream = ConvertStreamToComStream(&file2);
+ LARGE_INTEGER position;
+ position.QuadPart = 0;
+ ThrowIfFailed(com_stream->Seek(position, SEEK_SET, nullptr));
+ auto buffer = std::make_unique<char[]>(3);
+ ThrowIfFailed(com_stream->Read(buffer.get(), 3, nullptr));
+ REQUIRE(std::string_view(buffer.get(), 3) == "abc");
+ com_stream->Release();
+ file2.Close();
+
+ std::filesystem::remove(temp_file_path);
+}
diff --git a/test/base/platform/win/Win32FileStreamTest.cpp b/test/base/platform/win/Win32FileStreamTest.cpp
new file mode 100644
index 00000000..dc2f49d4
--- /dev/null
+++ b/test/base/platform/win/Win32FileStreamTest.cpp
@@ -0,0 +1,37 @@
+#include "cru/base/io/OpenFileFlag.h"
+#include "cru/base/platform/win/Win32FileStream.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <cstdio>
+#include <filesystem>
+
+TEST_CASE("Win32FileStream Work", "[stream]") {
+ using namespace cru;
+ using namespace cru::io;
+ using namespace cru::platform::win;
+
+ auto temp_file_path =
+ (std::filesystem::temp_directory_path() / "cru_test_temp.XXXXXX")
+ .native();
+ _wmktemp(temp_file_path.data());
+
+ String path = temp_file_path;
+
+ Win32FileStream file(path, OpenFileFlags::Write | OpenFileFlags::Create);
+ auto write_count = file.Write("abc", 3);
+ REQUIRE(write_count == 3);
+ file.Close();
+
+ REQUIRE(std::filesystem::file_size(path.ToUtf8()) == 3);
+
+ Win32FileStream file2(path, OpenFileFlags::Read);
+ auto buffer = std::make_unique<std::byte[]>(3);
+ auto read_count = file2.Read(buffer.get(), 3);
+ REQUIRE(read_count == 3);
+ REQUIRE(std::string_view(reinterpret_cast<const char*>(buffer.get()), 3) ==
+ "abc");
+ file2.Close();
+
+ std::filesystem::remove(temp_file_path);
+}