#include <fstream>
#include <iostream>
#include <cstdlib>
#include <filesystem>
#include <map>
#include <sstream>
#include <unordered_map>
#include <vector>
#include "test/PortableUtil.hpp"
#include "src/Common.hpp"
#include "rapidjson/document.h"
#include "gtest/gtest.h"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <sys/stat.h>
#endif
#ifdef BAZEL
#include "tools/cpp/runfiles/runfiles.h"
using bazel::tools::cpp::runfiles::Runfiles;
#endif
namespace opencc {
namespace fs = std::filesystem;
#ifdef _WIN32
std::wstring WideFromUtf8(const std::string& utf8) {
if (utf8.empty()) {
return L"";
}
const int required =
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0);
if (required <= 1) {
return L"";
}
std::wstring wide(static_cast<size_t>(required), L'\0');
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, wide.data(), required);
wide.resize(static_cast<size_t>(required - 1));
return wide;
}
#endif
class CommandLineConvertTest : public ::testing::Test {
protected:
CommandLineConvertTest() { GetCurrentWorkingDirectory(); }
virtual ~CommandLineConvertTest() { free(originalWorkingDirectory); }
virtual void SetUp() {
#ifdef BAZEL
runfiles_.reset(Runfiles::CreateForTest());
#else
ASSERT_NE("", PROJECT_BINARY_DIR);
ASSERT_NE("", PROJECT_SOURCE_DIR);
ASSERT_EQ(0, portable_chdir(PROJECT_BINARY_DIR "/data"));
#endif
}
virtual void TearDown() { ASSERT_EQ(0, portable_chdir(originalWorkingDirectory)); }
std::string GetFileContents(const std::string& fileName) const {
std::ifstream fs(fileName, std::ios::binary);
EXPECT_TRUE(fs.is_open()) << fileName;
const std::string content((std::istreambuf_iterator<char>(fs)),
(std::istreambuf_iterator<char>()));
fs.close();
return content;
}
std::string GetFileContents(const fs::path& fileName) const {
std::ifstream stream(fileName, std::ios::binary);
EXPECT_TRUE(stream.is_open()) << fileName.u8string();
const std::string content((std::istreambuf_iterator<char>(stream)),
(std::istreambuf_iterator<char>()));
stream.close();
return content;
}
void GetCurrentWorkingDirectory() {
originalWorkingDirectory = portable_getcwd();
}
std::string OpenccCommand() const {
#ifdef BAZEL
#ifdef _WIN32
std::string path = runfiles_->Rlocation("_main/src/tools/command_line.exe");
if (path.empty()) {
path = runfiles_->Rlocation("_main/src/tools/command_line");
}
return path;
#else
return runfiles_->Rlocation("_main/src/tools/command_line");
#endif
#else
#ifndef _MSC_VER
return PROJECT_BINARY_DIR "/src/tools/opencc";
#else
#ifdef NDEBUG
return PROJECT_BINARY_DIR "/src/tools/Release/opencc.exe";
#else
return PROJECT_BINARY_DIR "/src/tools/Debug/opencc.exe";
#endif
#endif
#endif
}
static std::string QuotePath(const std::string& path) {
return "\"" + path + "\"";
}
std::string OutputDirectory() const {
#ifdef BAZEL
return ::testing::TempDir() + "/";
#else
return PROJECT_BINARY_DIR "/test/";
#endif
}
#if defined(BAZEL) && defined(OPENCC_ENABLE_RESOURCE_ZIP_TEST)
std::string ResourceZipFile() const {
return runfiles_->Rlocation("_main/data/opencc-resources.zip");
}
std::string ResourceOcd2ZipFile() const {
return runfiles_->Rlocation("_main/data/opencc-resources-ocd2.zip");
}
#endif
std::string ConfigurationDirectory() const {
#ifdef BAZEL
return "";
#else
return PROJECT_SOURCE_DIR "/data/config/";
#endif
}
std::string InputFile(const char* config) const {
return OutputDirectory() + config + ".in";
}
std::string OutputFile(const char* config) const {
return OutputDirectory() + config + ".out";
}
std::string TestCommand(const std::string& config,
const std::string& inputFile,
const std::string& outputFile,
const std::string& measuredResultFile = "",
const std::string& extraFlags = "") const {
std::string cmd = QuotePath(OpenccCommand()) + " -i " +
QuotePath(inputFile) + " -o " +
QuotePath(outputFile) + " -c " +
QuotePath(ConfigurationDirectory() + config + ".json");
if (!extraFlags.empty()) {
cmd += " " + extraFlags;
}
if (!measuredResultFile.empty()) {
cmd += " --measured_result " + QuotePath(measuredResultFile);
}
#ifdef BAZEL
const std::string dictFile =
runfiles_->Rlocation("_main/data/dictionary/STCharacters.ocd2");
const std::string dictDir =
dictFile.substr(0, dictFile.find_last_of("/\\"));
const std::string configFile =
runfiles_->Rlocation("_main/data/config/s2t.json");
const std::string configDir =
configFile.substr(0, configFile.find_last_of("/\\"));
cmd += " --path " + QuotePath(dictDir + "/") +
" --path " + QuotePath(configDir + "/");
#endif
#ifdef _WIN32
return "\"" + cmd + "\"";
#else
return cmd;
#endif
}
static int RunCommand(const std::string& cmd) {
#ifdef _WIN32
return _wsystem(WideFromUtf8(cmd).c_str());
#else
return system(cmd.c_str());
#endif
}
#if defined(BAZEL) && defined(OPENCC_ENABLE_RESOURCE_ZIP_TEST)
std::string TestResourceZipCommand(const std::string& config,
const std::string& inputFile,
const std::string& outputFile) const {
std::string cmd = QuotePath(OpenccCommand()) + " -i " +
QuotePath(inputFile) + " -o " + QuotePath(outputFile) +
" -c " + QuotePath(config + ".json") +
" --resource-zip " + QuotePath(ResourceZipFile());
#ifdef _WIN32
return "\"" + cmd + "\"";
#else
return cmd;
#endif
}
#endif
std::string TestCommandWithFlags(const std::string& config,
const std::string& inputFile,
const std::string& outputFile,
const std::string& extraFlags,
const std::string& measuredResultFile = "")
const {
return TestCommand(config, inputFile, outputFile, measuredResultFile,
extraFlags);
}
std::string TestStdinCommand(const std::string& config,
const std::string& inputFile,
const std::string& outputFile) const {
std::string cmd = QuotePath(OpenccCommand()) + " -c " +
QuotePath(ConfigurationDirectory() + config + ".json");
#ifdef BAZEL
const std::string dictFile =
runfiles_->Rlocation("_main/data/dictionary/STCharacters.ocd2");
const std::string dictDir =
dictFile.substr(0, dictFile.find_last_of("/\\"));
const std::string configFile =
runfiles_->Rlocation("_main/data/config/s2t.json");
const std::string configDir =
configFile.substr(0, configFile.find_last_of("/\\"));
cmd += " --path " + QuotePath(dictDir + "/") +
" --path " + QuotePath(configDir + "/");
#endif
cmd += " < " + QuotePath(inputFile) + " > " + QuotePath(outputFile);
#ifdef _WIN32
return "\"" + cmd + "\"";
#else
return cmd;
#endif
}
#ifndef _WIN32
std::string TestPipeCommand(const std::string& config,
const std::string& outputFile) const {
std::string cmd = "(printf '后台'; sleep 0.1; printf '老板') | " +
QuotePath(OpenccCommand()) + " -c " +
QuotePath(ConfigurationDirectory() + config + ".json");
#ifdef BAZEL
const std::string dictFile =
runfiles_->Rlocation("_main/data/dictionary/STCharacters.ocd2");
const std::string dictDir =
dictFile.substr(0, dictFile.find_last_of("/\\"));
const std::string configFile =
runfiles_->Rlocation("_main/data/config/s2t.json");
const std::string configDir =
configFile.substr(0, configFile.find_last_of("/\\"));
cmd += " --path " + QuotePath(dictDir + "/") +
" --path " + QuotePath(configDir + "/");
#endif
cmd += " > " + QuotePath(outputFile);
return cmd;
}
#endif
char* originalWorkingDirectory;
#ifdef BAZEL
std::unique_ptr<Runfiles> runfiles_;
#endif
};
struct CaseInput {
std::string input;
std::string expected;
};
using CasesByConfig = std::unordered_map<std::string, std::vector<CaseInput>>;
CasesByConfig LoadCases(const std::string& jsonPath) {
CasesByConfig cases;
std::string content;
{
std::ifstream ifs(jsonPath);
if (!ifs.is_open()) {
throw std::runtime_error("Cannot open " + jsonPath);
}
std::stringstream buffer;
buffer << ifs.rdbuf();
content = buffer.str();
}
rapidjson::Document doc;
doc.Parse<rapidjson::kParseCommentsFlag |
rapidjson::kParseTrailingCommasFlag>(content.c_str());
if (doc.HasParseError() || !doc.IsObject() || !doc.HasMember("cases") ||
!doc["cases"].IsArray()) {
throw std::runtime_error("Invalid testcases.json format");
}
for (auto& entry : doc["cases"].GetArray()) {
if (!entry.IsObject() || !entry.HasMember("input") ||
!entry["input"].IsString() || !entry.HasMember("expected") ||
!entry["expected"].IsObject()) {
continue;
}
const std::string input = entry["input"].GetString();
for (auto itr = entry["expected"].MemberBegin();
itr != entry["expected"].MemberEnd(); ++itr) {
if (!itr->value.IsString()) {
continue;
}
const std::string config = itr->name.GetString();
cases[config].push_back({input, itr->value.GetString()});
}
}
return cases;
}
TEST_F(CommandLineConvertTest, ConvertFromJson) {
#ifdef BAZEL
const std::string casesPath =
runfiles_->Rlocation("_main/test/testcases/testcases.json");
#else
const std::string casesPath = PROJECT_SOURCE_DIR "/test/testcases/testcases.json";
#endif
const CasesByConfig cases = LoadCases(casesPath);
for (const auto& entry : cases) {
const std::string& config = entry.first;
const std::string inputFile = InputFile(config.c_str());
const std::string outputFile = OutputFile(config.c_str());
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open()) << "Failed to open input file for writing: "
<< inputFile;
for (const auto& item : entry.second) {
ofs << item.input << "\n";
}
}
ASSERT_EQ(0,
system(TestCommandWithFlags(
config, inputFile, outputFile,
"--include-tofu-risk-dictionaries")
.c_str()));
std::ifstream ifs(outputFile, std::ios::binary);
ASSERT_TRUE(ifs.is_open());
std::string line;
size_t idx = 0;
while (std::getline(ifs, line)) {
if (!line.empty() && line.back() == '\r') {
line.pop_back(); }
ASSERT_LT(idx, entry.second.size());
EXPECT_EQ(entry.second[idx].expected, line)
<< "config=" << config << " index=" << idx;
idx++;
}
EXPECT_EQ(idx, entry.second.size()) << "config=" << config;
}
}
TEST_F(CommandLineConvertTest, SkipsTofuRiskDictionariesByDefault) {
const std::string inputFile = InputFile("tofu_risk_default");
const std::string outputFile = OutputFile("tofu_risk_default");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "㑮";
}
ASSERT_EQ(0, system(TestCommand("t2s", inputFile, outputFile).c_str()));
EXPECT_EQ("㑮", GetFileContents(outputFile));
}
TEST_F(CommandLineConvertTest, IncludeTofuRiskDictionariesFlagRestoresLegacy) {
const std::string inputFile = InputFile("tofu_risk_include");
const std::string outputFile = OutputFile("tofu_risk_include");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "㑮";
}
ASSERT_EQ(0, system(TestCommandWithFlags(
"t2s", inputFile, outputFile,
"--include-tofu-risk-dictionaries")
.c_str()));
EXPECT_EQ("𫝈", GetFileContents(outputFile));
}
#if defined(BAZEL) && defined(OPENCC_ENABLE_RESOURCE_ZIP_TEST)
TEST_F(CommandLineConvertTest, ResourceZipConvertsWithoutResourcePaths) {
const std::string inputFile = InputFile("resource_zip");
const std::string outputFile = OutputFile("resource_zip");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "打印机和鼠标";
}
ASSERT_EQ(0,
RunCommand(TestResourceZipCommand("s2twp", inputFile, outputFile)));
EXPECT_EQ("印表機和滑鼠", GetFileContents(outputFile));
}
TEST_F(CommandLineConvertTest, ResourceOcd2ZipConvertsWithoutResourcePaths) {
const std::string inputFile = InputFile("resource_ocd2_zip");
const std::string outputFile = OutputFile("resource_ocd2_zip");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "打印机和鼠标";
}
std::string cmd = QuotePath(OpenccCommand()) + " -i " +
QuotePath(inputFile) + " -o " + QuotePath(outputFile) +
" -c s2twp.json" +
" --resource-zip " + QuotePath(ResourceOcd2ZipFile());
#ifdef _WIN32
cmd = "\"" + cmd + "\"";
#endif
ASSERT_EQ(0, RunCommand(cmd));
EXPECT_EQ("印表機和滑鼠", GetFileContents(outputFile));
}
#endif
TEST_F(CommandLineConvertTest, StdinPreservesLineEndingsAndUnknownCharacters) {
const std::string config = "s2t";
const std::string inputFile = InputFile("stdin_line_endings");
const std::string outputFile = OutputFile("stdin_line_endings");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "鼠标=mouse\r\n123\n未登录";
}
ASSERT_EQ(0, system(TestStdinCommand(config, inputFile, outputFile).c_str()));
EXPECT_EQ("鼠標=mouse\r\n123\n未登錄", GetFileContents(outputFile));
}
#ifndef _WIN32
TEST_F(CommandLineConvertTest, WarnsWhenInputContainsVariationSelector) {
const std::string inputFile = InputFile("ivs_warning");
const std::string outputFile = OutputFile("ivs_warning");
const std::string stderrFile = OutputFile("ivs_warning.stderr");
const std::string variationSelector = "\xF3\xA0\x84\x80";
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "汉禰" << variationSelector;
}
const std::string command =
TestCommand("s2t", inputFile, outputFile) + " 2> " +
QuotePath(stderrFile);
ASSERT_EQ(0, system(command.c_str()));
EXPECT_EQ("漢禰" + variationSelector, GetFileContents(outputFile));
EXPECT_NE(std::string::npos,
GetFileContents(stderrFile).find(
"warning: input contains Unicode variation selectors"));
}
TEST_F(CommandLineConvertTest,
WarnsWhenSupplementaryVariationSelectorCrossesChunkBoundary) {
const std::string inputFile = InputFile("ivs_warning_chunk_boundary");
const std::string outputFile = OutputFile("ivs_warning_chunk_boundary");
const std::string stderrFile =
OutputFile("ivs_warning_chunk_boundary.stderr");
std::string input(1048576 - 1, 'a');
input.push_back(static_cast<char>(0xF3));
input.push_back(static_cast<char>(0xA0));
input.push_back(static_cast<char>(0x84));
input.push_back(static_cast<char>(0x80));
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << input;
}
const std::string command =
TestCommand("s2t", inputFile, outputFile) + " 2> " +
QuotePath(stderrFile);
ASSERT_EQ(0, system(command.c_str()));
EXPECT_EQ(input, GetFileContents(outputFile));
EXPECT_NE(std::string::npos,
GetFileContents(stderrFile).find(
"warning: input contains Unicode variation selectors"));
}
#endif
#ifndef _WIN32
TEST_F(CommandLineConvertTest, PipeShortReadContinuesUntilEof) {
const std::string outputFile = OutputFile("pipe_short_read");
ASSERT_EQ(0, system(TestPipeCommand("s2t", outputFile).c_str()));
ASSERT_EQ("後臺老闆", GetFileContents(outputFile));
}
#endif
TEST_F(CommandLineConvertTest, ConvertsFilesWithUnicodePaths) {
const fs::path unicodeDir =
fs::u8path(OutputDirectory()) / fs::u8path("opencc-中文路径-cli-😀-𠮷");
fs::create_directories(unicodeDir);
const fs::path inputFile = unicodeDir / fs::u8path("输入 文件.txt");
const fs::path outputFile = unicodeDir / fs::u8path("輸出 文件.txt");
const fs::path measuredResultFile =
unicodeDir / fs::u8path("測量 結果.json");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open()) << inputFile.u8string();
ofs << "开放中文转换";
}
ASSERT_EQ(0, RunCommand(TestCommand("s2t", inputFile.u8string(),
outputFile.u8string(),
measuredResultFile.u8string())));
EXPECT_EQ("開放中文轉換", GetFileContents(outputFile));
const std::string measured = GetFileContents(measuredResultFile);
rapidjson::Document doc;
doc.Parse(measured.c_str());
ASSERT_FALSE(doc.HasParseError()) << measured;
ASSERT_TRUE(doc.IsObject());
ASSERT_TRUE(doc.HasMember("input"));
EXPECT_EQ(inputFile.u8string(), std::string(doc["input"].GetString()));
}
TEST_F(CommandLineConvertTest, ConvertsInPlaceWithUnicodePath) {
const fs::path unicodeDir =
fs::u8path(OutputDirectory()) / fs::u8path("opencc-就地转换-cli-😀");
fs::create_directories(unicodeDir);
const fs::path file = unicodeDir / fs::u8path("输入输出同一文件.txt");
{
std::ofstream ofs(file, std::ios::binary);
ASSERT_TRUE(ofs.is_open()) << file.u8string();
ofs << "开放中文转换";
}
ASSERT_EQ(0, RunCommand(TestCommand("s2t", file.u8string(), file.u8string(),
"", "--in-place")));
EXPECT_EQ("開放中文轉換", GetFileContents(file));
}
TEST_F(CommandLineConvertTest, RejectsInPlaceConversionWithoutFlag) {
const fs::path unicodeDir =
fs::u8path(OutputDirectory()) / fs::u8path("opencc-拒绝就地转换-cli");
fs::create_directories(unicodeDir);
const fs::path file = unicodeDir / fs::u8path("输入输出同一文件.txt");
{
std::ofstream ofs(file, std::ios::binary);
ASSERT_TRUE(ofs.is_open()) << file.u8string();
ofs << "开放中文转换";
}
EXPECT_NE(0, RunCommand(TestCommand("s2t", file.u8string(), file.u8string())));
EXPECT_EQ("开放中文转换", GetFileContents(file));
}
TEST_F(CommandLineConvertTest, MissingInputDoesNotTruncateOutput) {
const fs::path unicodeDir =
fs::u8path(OutputDirectory()) / fs::u8path("opencc-缺失输入-cli");
fs::create_directories(unicodeDir);
const fs::path inputFile = unicodeDir / fs::u8path("不存在.txt");
const fs::path outputFile = unicodeDir / fs::u8path("已有输出.txt");
{
std::ofstream ofs(outputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open()) << outputFile.u8string();
ofs << "existing output";
}
EXPECT_NE(0, RunCommand(TestCommand("s2t", inputFile.u8string(),
outputFile.u8string())));
EXPECT_EQ("existing output", GetFileContents(outputFile));
}
TEST_F(CommandLineConvertTest, ConvertsInPlaceWithDifferentPathSpellings) {
const fs::path unicodeDir =
fs::u8path(OutputDirectory()) / fs::u8path("opencc-同文件不同路径-cli");
fs::create_directories(unicodeDir);
const fs::path file = unicodeDir / fs::u8path("输入输出同一文件.txt");
{
std::ofstream ofs(file, std::ios::binary);
ASSERT_TRUE(ofs.is_open()) << file.u8string();
ofs << "开放中文转换";
}
const fs::path spelledDifferently = unicodeDir / fs::u8path(".") /
fs::u8path("输入输出同一文件.txt");
ASSERT_EQ(0, RunCommand(TestCommand("s2t", file.u8string(),
spelledDifferently.u8string(), "",
"--in-place")));
EXPECT_EQ("開放中文轉換", GetFileContents(file));
}
#ifndef _WIN32
TEST_F(CommandLineConvertTest, InPlaceConversionPreservesFileMode) {
const fs::path unicodeDir =
fs::u8path(OutputDirectory()) / fs::u8path("opencc-保留权限-cli");
fs::create_directories(unicodeDir);
const fs::path file = unicodeDir / fs::u8path("输入输出同一文件.txt");
{
std::ofstream ofs(file, std::ios::binary);
ASSERT_TRUE(ofs.is_open()) << file.u8string();
ofs << "开放中文转换";
}
ASSERT_EQ(0, chmod(file.c_str(), 0644));
ASSERT_EQ(0, RunCommand(TestCommand("s2t", file.u8string(), file.u8string(),
"", "--in-place")));
EXPECT_EQ("開放中文轉換", GetFileContents(file));
struct stat info;
ASSERT_EQ(0, stat(file.c_str(), &info));
EXPECT_EQ(0644, info.st_mode & 07777);
}
TEST_F(CommandLineConvertTest, InPlaceConversionRejectsHardLinks) {
const fs::path unicodeDir =
fs::u8path(OutputDirectory()) / fs::u8path("opencc-拒绝硬链接-cli");
fs::create_directories(unicodeDir);
const fs::path file = unicodeDir / fs::u8path("真实输入输出.txt");
const fs::path hardLink = unicodeDir / fs::u8path("硬链接输出.txt");
{
std::ofstream ofs(file, std::ios::binary);
ASSERT_TRUE(ofs.is_open()) << file.u8string();
ofs << "开放中文转换";
}
std::error_code error;
fs::remove(hardLink, error);
fs::create_hard_link(file, hardLink, error);
if (error) {
GTEST_SKIP() << "Hard-link creation failed: " << error.message();
}
EXPECT_NE(0, RunCommand(TestCommand("s2t", file.u8string(),
hardLink.u8string(), "",
"--in-place")));
EXPECT_EQ("开放中文转换", GetFileContents(file));
EXPECT_EQ("开放中文转换", GetFileContents(hardLink));
}
#endif
TEST_F(CommandLineConvertTest, ConvertsInPlaceThroughSymlink) {
const fs::path unicodeDir =
fs::u8path(OutputDirectory()) / fs::u8path("opencc-符号链接-cli");
fs::create_directories(unicodeDir);
const fs::path file = unicodeDir / fs::u8path("真实输入输出.txt");
const fs::path symlink = unicodeDir / fs::u8path("符号链接输出.txt");
{
std::ofstream ofs(file, std::ios::binary);
ASSERT_TRUE(ofs.is_open()) << file.u8string();
ofs << "开放中文转换";
}
std::error_code error;
fs::remove(symlink, error);
fs::create_symlink(file.filename(), symlink, error);
if (error) {
GTEST_SKIP() << "Symlink creation failed: " << error.message();
}
ASSERT_EQ(0, RunCommand(TestCommand("s2t", file.u8string(),
symlink.u8string(), "", "--in-place")));
EXPECT_EQ("開放中文轉換", GetFileContents(file));
}
TEST_F(CommandLineConvertTest, WritesMeasuredResultJson) {
const std::string config = "s2t";
const std::string inputFile = InputFile(config.c_str()) + ".measured";
const std::string outputFile = OutputFile(config.c_str()) + ".measured";
const std::string measuredResultFile =
OutputDirectory() + config + ".measured_result.json";
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open()) << "Failed to open input file for writing: "
<< inputFile;
ofs << "开放中文转换" << std::endl;
}
ASSERT_EQ(0, system(TestCommand(config, inputFile, outputFile,
measuredResultFile).c_str()));
const std::string content = GetFileContents(measuredResultFile);
rapidjson::Document doc;
doc.Parse(content.c_str());
ASSERT_FALSE(doc.HasParseError());
ASSERT_TRUE(doc.IsObject());
ASSERT_TRUE(doc.HasMember("config"));
ASSERT_TRUE(doc["config"].IsString());
EXPECT_EQ(ConfigurationDirectory() + config + ".json",
std::string(doc["config"].GetString()));
ASSERT_TRUE(doc.HasMember("mode"));
EXPECT_STREQ("file", doc["mode"].GetString());
ASSERT_TRUE(doc.HasMember("load_ms"));
EXPECT_TRUE(doc["load_ms"].IsNumber());
ASSERT_TRUE(doc.HasMember("convert_ms"));
EXPECT_TRUE(doc["convert_ms"].IsNumber());
ASSERT_TRUE(doc.HasMember("write_ms"));
EXPECT_TRUE(doc["write_ms"].IsNumber());
ASSERT_TRUE(doc.HasMember("total_ms"));
EXPECT_TRUE(doc["total_ms"].IsNumber());
ASSERT_TRUE(doc.HasMember("input_bytes"));
EXPECT_TRUE(doc["input_bytes"].IsUint64());
ASSERT_TRUE(doc.HasMember("output_bytes"));
EXPECT_TRUE(doc["output_bytes"].IsUint64());
}
TEST_F(CommandLineConvertTest, SegmentationOutputIsJson) {
const std::string config = "s2twp";
const std::string inputFile = InputFile("segmentation_test");
const std::string outputFile = OutputFile("segmentation_test");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "\xe5\xbc\x80\xe6\x94\xbe\xe4\xb8\xad\xe6\x96\x87\xe8\xbd\xac\xe6"
"\x8d\xa2" << "\n";
}
ASSERT_EQ(0, system(TestCommandWithFlags(config, inputFile, outputFile,
"--segmentation").c_str()));
const std::string content = GetFileContents(outputFile);
rapidjson::Document doc;
doc.Parse(content.c_str());
ASSERT_FALSE(doc.HasParseError()) << "Output is not valid JSON: " << content;
ASSERT_TRUE(doc.IsObject());
ASSERT_TRUE(doc.HasMember("input"));
ASSERT_TRUE(doc["input"].IsString());
ASSERT_TRUE(doc.HasMember("segments"));
ASSERT_TRUE(doc["segments"].IsArray());
EXPECT_FALSE(doc["segments"].GetArray().Empty());
EXPECT_FALSE(doc.HasMember("stages"));
EXPECT_FALSE(doc.HasMember("output"));
}
TEST_F(CommandLineConvertTest, InspectOutputIsJson) {
const std::string config = "s2t";
const std::string inputFile = InputFile("inspect_test");
const std::string outputFile = OutputFile("inspect_test");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "\xe5\xbc\x80\xe6\x94\xbe\xe4\xb8\xad\xe6\x96\x87\xe8\xbd\xac\xe6"
"\x8d\xa2" << "\n";
}
ASSERT_EQ(0, system(TestCommandWithFlags(config, inputFile, outputFile,
"--inspect").c_str()));
const std::string content = GetFileContents(outputFile);
rapidjson::Document doc;
doc.Parse(content.c_str());
ASSERT_FALSE(doc.HasParseError()) << "Output is not valid JSON: " << content;
ASSERT_TRUE(doc.IsObject());
ASSERT_TRUE(doc.HasMember("input"));
ASSERT_TRUE(doc["input"].IsString());
ASSERT_TRUE(doc.HasMember("segments"));
ASSERT_TRUE(doc["segments"].IsArray());
ASSERT_TRUE(doc.HasMember("stages"));
ASSERT_TRUE(doc["stages"].IsArray());
ASSERT_TRUE(doc.HasMember("output"));
ASSERT_TRUE(doc["output"].IsString());
for (auto& stage : doc["stages"].GetArray()) {
ASSERT_TRUE(stage.IsObject());
ASSERT_TRUE(stage.HasMember("index"));
ASSERT_TRUE(stage["index"].IsUint64());
ASSERT_TRUE(stage.HasMember("segments"));
ASSERT_TRUE(stage["segments"].IsArray());
}
}
TEST_F(CommandLineConvertTest, SegmentationAndInspectAreMutuallyExclusive) {
const std::string config = "s2t";
const std::string inputFile = InputFile("mutex_test");
const std::string outputFile = OutputFile("mutex_test");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "test\n";
}
const int exitCode =
system(TestCommandWithFlags(config, inputFile, outputFile,
"--segmentation --inspect")
.c_str());
EXPECT_NE(0, exitCode);
}
TEST_F(CommandLineConvertTest, SegmentationFailsWhenConfigHasNoSegmentation) {
const std::string config = "s2t";
const std::string inputFile = InputFile("seg_no_seg_test");
const std::string outputFile = OutputFile("seg_no_seg_test");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "开放中文转换\n";
}
EXPECT_NE(0, system(TestCommandWithFlags(config, inputFile, outputFile,
"--segmentation").c_str()));
}
TEST_F(CommandLineConvertTest, MeasuredResultIncludesOutputMode) {
const std::string config = "s2t";
const std::string inputFile = InputFile("inspect_measured_test");
const std::string outputFile = OutputFile("inspect_measured_test");
const std::string measuredResultFile =
OutputDirectory() + "inspect_measured_result.json";
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "\xe5\xbc\x80\xe6\x94\xbe\xe4\xb8\xad\xe6\x96\x87\xe8\xbd\xac\xe6"
"\x8d\xa2" << "\n";
}
ASSERT_EQ(0,
system(TestCommandWithFlags(config, inputFile, outputFile,
"--inspect", measuredResultFile)
.c_str()));
const std::string content = GetFileContents(measuredResultFile);
rapidjson::Document doc;
doc.Parse(content.c_str());
ASSERT_FALSE(doc.HasParseError());
ASSERT_TRUE(doc.IsObject());
ASSERT_TRUE(doc.HasMember("output_mode"));
ASSERT_TRUE(doc["output_mode"].IsString());
EXPECT_STREQ("inspect", doc["output_mode"].GetString());
}
TEST_F(CommandLineConvertTest, SegmentationDoesNotRunConversionChain) {
const std::string config = "s2twp";
const std::string inputFile = InputFile("seg_no_convert_test");
const std::string outputFile = OutputFile("seg_no_convert_test");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "\xe5\xbc\x80\xe6\x94\xbe\xe4\xb8\xad\xe6\x96\x87\xe8\xbd\xac\xe6"
"\x8d\xa2" << "\n";
}
ASSERT_EQ(0, system(TestCommandWithFlags(config, inputFile, outputFile,
"--segmentation").c_str()));
const std::string content = GetFileContents(outputFile);
rapidjson::Document doc;
doc.Parse(content.c_str());
ASSERT_FALSE(doc.HasParseError());
ASSERT_TRUE(doc.IsObject());
ASSERT_TRUE(doc.HasMember("segments"));
ASSERT_TRUE(doc["segments"].IsArray());
std::string joined;
for (auto& seg : doc["segments"].GetArray()) {
ASSERT_TRUE(seg.IsString());
joined += seg.GetString();
}
const std::string simplified =
"\xe5\xbc\x80\xe6\x94\xbe\xe4\xb8\xad\xe6\x96\x87\xe8\xbd\xac\xe6"
"\x8d\xa2"; EXPECT_EQ(simplified, joined)
<< "Segments were converted — conversion chain must not run in "
"--segmentation mode";
}
TEST_F(CommandLineConvertTest, SegmentationNoSpuriousEofRecord) {
const std::string config = "s2twp";
const std::string inputFile = InputFile("seg_eof_test");
const std::string outputFile = OutputFile("seg_eof_test");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "\xe5\xbc\x80\xe6\x94\xbe\xe4\xb8\xad\xe6\x96\x87\xe8\xbd\xac\xe6"
"\x8d\xa2\n" << "\xe6\xb1\x89\xe5\xad\x97\n"; }
ASSERT_EQ(0, system(TestCommandWithFlags(config, inputFile, outputFile,
"--segmentation").c_str()));
const std::string content = GetFileContents(outputFile);
size_t newlines = 0;
for (char c : content) {
if (c == '\n') {
++newlines;
}
}
const size_t separators = (newlines > 0 && content.back() == '\n')
? newlines - 1
: newlines;
EXPECT_EQ(1u, separators) << "Expected exactly 2 JSON records (1 separator),"
" got extra records. Content: "
<< content;
size_t sep = content.find('\n');
ASSERT_NE(std::string::npos, sep) << "No separator found";
std::string first = content.substr(0, sep);
std::string rest = content.substr(sep + 1);
if (!rest.empty() && rest.back() == '\n') {
rest.pop_back();
}
rapidjson::Document d1, d2;
d1.Parse(first.c_str());
EXPECT_FALSE(d1.HasParseError()) << "First record invalid JSON: " << first;
EXPECT_TRUE(d1.IsObject());
d2.Parse(rest.c_str());
EXPECT_FALSE(d2.HasParseError()) << "Second record invalid JSON: " << rest;
EXPECT_TRUE(d2.IsObject());
}
TEST_F(CommandLineConvertTest, PreservesLineEnding_LF) {
const std::string config = "s2t";
const std::string inputFile = InputFile("lf_input");
const std::string outputFile = OutputFile("lf_output");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "第一行\n第二行\n第三行\n";
}
ASSERT_EQ(0, system(TestCommand(config, inputFile, outputFile).c_str()));
std::ifstream ifs(outputFile, std::ios::binary);
ASSERT_TRUE(ifs.is_open());
std::string content((std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>()));
ifs.close();
ASSERT_NE(std::string::npos, content.find('\n'));
ASSERT_EQ(std::string::npos, content.find('\r'));
}
TEST_F(CommandLineConvertTest, PreservesLineEnding_CRLF) {
const std::string config = "s2t";
const std::string inputFile = InputFile("crlf_input");
const std::string outputFile = OutputFile("crlf_output");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "第一行\r\n第二行\r\n第三行\r\n";
}
ASSERT_EQ(0, system(TestCommand(config, inputFile, outputFile).c_str()));
std::ifstream ifs(outputFile, std::ios::binary);
ASSERT_TRUE(ifs.is_open());
std::string content((std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>()));
ifs.close();
ASSERT_NE(std::string::npos, content.find('\r'));
ASSERT_NE(std::string::npos, content.find('\n'));
}
TEST_F(CommandLineConvertTest, PreservesLineEnding_CRLF_ASCII) {
const std::string config = "s2t";
const std::string inputFile = InputFile("crlf_ascii_input");
const std::string outputFile = OutputFile("crlf_ascii_output");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "hello\r\nworld\r\n";
}
ASSERT_EQ(0, system(TestCommand(config, inputFile, outputFile).c_str()));
std::ifstream ifs(outputFile, std::ios::binary);
ASSERT_TRUE(ifs.is_open());
std::string content((std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>()));
ifs.close();
ASSERT_NE(std::string::npos, content.find('\r'));
ASSERT_NE(std::string::npos, content.find('\n'));
}
TEST_F(CommandLineConvertTest, PreservesLineEnding_NoTrailingNewline_LF) {
const std::string config = "s2t";
const std::string inputFile = InputFile("no_trailing_lf_input");
const std::string outputFile = OutputFile("no_trailing_lf_output");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "第一行\n第二行"; }
ASSERT_EQ(0, system(TestCommand(config, inputFile, outputFile).c_str()));
std::ifstream ifs(outputFile, std::ios::binary);
ASSERT_TRUE(ifs.is_open());
std::string content((std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>()));
ifs.close();
ASSERT_NE(std::string::npos, content.find('\n'));
ASSERT_EQ(std::string::npos, content.find('\r'));
ASSERT_NE('\n', content.back());
}
TEST_F(CommandLineConvertTest, PreservesLineEnding_NoTrailingNewline_CRLF) {
const std::string config = "s2t";
const std::string inputFile = InputFile("no_trailing_crlf_input");
const std::string outputFile = OutputFile("no_trailing_crlf_output");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs << "第一行\r\n第二行"; }
ASSERT_EQ(0, system(TestCommand(config, inputFile, outputFile).c_str()));
std::ifstream ifs(outputFile, std::ios::binary);
ASSERT_TRUE(ifs.is_open());
std::string content((std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>()));
ifs.close();
ASSERT_NE(std::string::npos, content.find('\r'));
ASSERT_NE(std::string::npos, content.find('\n'));
ASSERT_NE('\r', content.back());
ASSERT_NE('\n', content.back());
}
TEST_F(CommandLineConvertTest, ChunkingBoundaryTruncationFix) {
const std::string config = "s2t";
const std::string inputFile = InputFile("chunk_boundary_test");
const std::string outputFile = OutputFile("chunk_boundary_test");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
std::string padding(1048576 - 3, 'a');
ofs << padding << "头发尾巴";
}
ASSERT_EQ(0, system(TestCommand(config, inputFile, outputFile).c_str()));
std::ifstream ifs(outputFile, std::ios::binary);
ASSERT_TRUE(ifs.is_open());
std::string content((std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>()));
ifs.close();
const std::string expectedEnd = "頭髮尾巴";
ASSERT_GE(content.size(), expectedEnd.size());
EXPECT_EQ(expectedEnd, content.substr(content.size() - expectedEnd.size()));
}
TEST_F(CommandLineConvertTest, ExactChunkSizeDoesNotTruncate) {
const std::string config = "s2t";
const std::string inputFile = InputFile("exact_chunk_test");
const std::string outputFile = OutputFile("exact_chunk_test");
{
std::ofstream ofs(inputFile, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
std::string padding(1048576, 'a');
ofs << padding;
}
ASSERT_EQ(0, system(TestCommand(config, inputFile, outputFile).c_str()));
std::ifstream ifs(outputFile, std::ios::binary);
ASSERT_TRUE(ifs.is_open());
std::string content((std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>()));
ifs.close();
EXPECT_EQ(1048576, content.size());
}
}