From 61d9486a9560b1aa9d7e9a0474f8a3e6f38c7dbb Mon Sep 17 00:00:00 2001 From: Frank Lin Date: Fri, 2 Jan 2026 14:15:05 -0800 Subject: [PATCH 1/4] feature: add a new ConfigDictValidationTest.cpp to be executed in bazel --- data/config/BUILD.bazel | 16 ++++ data/config/ConfigDictValidationTest.cpp | 104 +++++++++++++++++++++++ data/dictionary/BUILD.bazel | 10 ++- test/testcases/BUILD.bazel | 8 ++ test/testcases/gen_testcases_json.py | 94 ++++++++++++++++++++ 5 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 data/config/ConfigDictValidationTest.cpp create mode 100644 test/testcases/gen_testcases_json.py diff --git a/data/config/BUILD.bazel b/data/config/BUILD.bazel index ea3ee617d..33cfe0e24 100644 --- a/data/config/BUILD.bazel +++ b/data/config/BUILD.bazel @@ -4,3 +4,19 @@ filegroup( name = "config", srcs = glob(["*.json"]), ) + +cc_test( + name = "config_dict_validation_test", + srcs = ["ConfigDictValidationTest.cpp"], + data = [ + ":config", + "//data/dictionary:binary_dictionaries", + "//test/testcases:testcases_json", + ], + deps = [ + "//src:simple_converter", + "@bazel_tools//tools/cpp/runfiles", + "@googletest//:gtest_main", + "@rapidjson", + ], +) diff --git a/data/config/ConfigDictValidationTest.cpp b/data/config/ConfigDictValidationTest.cpp new file mode 100644 index 000000000..b96b86c17 --- /dev/null +++ b/data/config/ConfigDictValidationTest.cpp @@ -0,0 +1,104 @@ +/* + * Open Chinese Convert + * + * End-to-end validation of all configs against consolidated testcases.json. + */ + +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "rapidjson/document.h" +#include "src/SimpleConverter.hpp" + +#ifdef BAZEL +#include "tools/cpp/runfiles/runfiles.h" +using bazel::tools::cpp::runfiles::Runfiles; +#endif + +namespace opencc { +namespace { + +class ConfigDictValidationTest : public ::testing::Test { +protected: + void SetUp() override { +#ifdef BAZEL + runfiles_.reset(Runfiles::CreateForTest()); + ASSERT_NE(nullptr, runfiles_); + testcasesPath_ = runfiles_->Rlocation("_main/test/testcases/testcases.json"); + configDir_ = runfiles_->Rlocation("_main/data/config"); + dictDir_ = runfiles_->Rlocation("_main/data/dictionary"); +#else + FAIL() << "This test expects Bazel runfiles."; +#endif + } + + std::string ReadFile(const std::string& path) { + std::ifstream ifs(path); + EXPECT_TRUE(ifs.is_open()) << path; + std::stringstream buffer; + buffer << ifs.rdbuf(); + return buffer.str(); + } + + SimpleConverter& GetConverter(const std::string& config) { + auto it = converters_.find(config); + if (it != converters_.end()) { + return *it->second; + } + const std::string configPath = configDir_ + "/" + config + ".json"; + auto inserted = converters_.emplace( + config, + std::make_unique(configPath, + std::vector{ + configDir_, dictDir_})); + return *inserted.first->second; + } + + std::unique_ptr runfiles_; + std::string testcasesPath_; + std::string configDir_; + std::string dictDir_; + std::unordered_map> + converters_; +}; + +TEST_F(ConfigDictValidationTest, ConvertExpectedOutputs) { + const std::string json = ReadFile(testcasesPath_); + rapidjson::Document doc; + doc.Parse(json.c_str()); + ASSERT_FALSE(doc.HasParseError()); + ASSERT_TRUE(doc.IsObject()); + ASSERT_TRUE(doc.HasMember("cases")); + const auto& cases = doc["cases"]; + ASSERT_TRUE(cases.IsArray()); + + for (auto& testcase : cases.GetArray()) { + ASSERT_TRUE(testcase.IsObject()); + ASSERT_TRUE(testcase.HasMember("input")); + ASSERT_TRUE(testcase["input"].IsString()); + const std::string input = testcase["input"].GetString(); + const std::string id = + testcase.HasMember("id") && testcase["id"].IsString() + ? testcase["id"].GetString() + : "(unknown id)"; + ASSERT_TRUE(testcase.HasMember("expected")); + const auto& expectedObj = testcase["expected"]; + ASSERT_TRUE(expectedObj.IsObject()); + for (auto itr = expectedObj.MemberBegin(); itr != expectedObj.MemberEnd(); + ++itr) { + const std::string config = itr->name.GetString(); + ASSERT_TRUE(itr->value.IsString()); + const std::string expected = itr->value.GetString(); + SimpleConverter& converter = GetConverter(config); + EXPECT_EQ(expected, converter.Convert(input)) + << "config=" << config << " case=" << id; + } + } +} + +} // namespace +} // namespace opencc diff --git a/data/dictionary/BUILD.bazel b/data/dictionary/BUILD.bazel index 3a1f1aa0a..253f656f3 100644 --- a/data/dictionary/BUILD.bazel +++ b/data/dictionary/BUILD.bazel @@ -13,6 +13,14 @@ genrule( tools = ["//data/scripts:merge"], ) +# These component files are only used for merge_TWPhrases and should not +# produce standalone .ocd2 outputs. +PHRASE_PARTS = [ + "TWPhrasesIT.txt", + "TWPhrasesName.txt", + "TWPhrasesOther.txt", +] + [ genrule( name = "reverse_" + txt, @@ -30,7 +38,7 @@ genrule( ] ] -TEXT_DICTS = glob(["*.txt"]) + [ +TEXT_DICTS = glob(["*.txt"], exclude = PHRASE_PARTS) + [ "TWPhrases.txt", "TWVariantsRev.txt", "TWPhrasesRev.txt", diff --git a/test/testcases/BUILD.bazel b/test/testcases/BUILD.bazel index 4c9a63ab5..7b95a0b25 100644 --- a/test/testcases/BUILD.bazel +++ b/test/testcases/BUILD.bazel @@ -4,3 +4,11 @@ filegroup( name = "testcases", srcs = glob(["*.in"]) + glob(["*.ans"]), ) + +genrule( + name = "testcases_json", + srcs = glob(["*.in"]) + glob(["*.ans"]), + tools = ["gen_testcases_json.py"], + outs = ["testcases.json"], + cmd = "python3 $(location gen_testcases_json.py) $(SRCS) > $@", +) diff --git a/test/testcases/gen_testcases_json.py b/test/testcases/gen_testcases_json.py new file mode 100644 index 000000000..5cce75e19 --- /dev/null +++ b/test/testcases/gen_testcases_json.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +""" +Build a consolidated testcases.json from *.in/*.ans pairs. + +Usage (Bazel genrule): + python3 gen_testcases_json.py ... > testcases.json + +Input files should include matching *.in and *.ans files. The script will +combine lines by input text and emit: +{ + "cases": [ + { + "id": "case_001", + "input": "...", + "expected": { + "s2t": "...", + "t2s": "..." + } + }, + ... + ] +} +""" + +from __future__ import annotations + +import json +import os +import sys +from collections import OrderedDict, defaultdict +from typing import Dict, List + + +def collect_pairs(paths: List[str]) -> Dict[str, Dict[str, str]]: + """Collect matching .in/.ans files keyed by basename.""" + pairs: Dict[str, Dict[str, str]] = defaultdict(dict) + for path in paths: + base = os.path.basename(path) + if base.endswith(".in"): + pairs[base[:-3]]["in"] = path + elif base.endswith(".ans"): + pairs[base[:-4]]["ans"] = path + return pairs + + +def load_cases(pairs: Dict[str, Dict[str, str]]) -> OrderedDict: + """Load input/expected lines and merge by input string.""" + case_map: "OrderedDict[str, Dict[str, str]]" = OrderedDict() + for cfg in sorted(pairs.keys()): + entry = pairs[cfg] + if "in" not in entry or "ans" not in entry: + continue + with open(entry["in"], "r", encoding="utf-8") as fin: + inputs = fin.read().splitlines() + with open(entry["ans"], "r", encoding="utf-8") as fans: + answers = fans.read().splitlines() + if len(inputs) != len(answers): + raise ValueError( + f"Line count mismatch for {entry['in']} ({len(inputs)}) vs " + f"{entry['ans']} ({len(answers)})") + for text, expected in zip(inputs, answers): + if text not in case_map: + case_map[text] = {} + if cfg in case_map[text] and case_map[text][cfg] != expected: + raise ValueError( + f"Conflicting expectation for '{text}' in {cfg}: " + f"'{case_map[text][cfg]}' vs '{expected}'") + case_map[text][cfg] = expected + return case_map + + +def main(argv: List[str]) -> int: + if len(argv) < 2: + print("Usage: gen_testcases_json.py ", file=sys.stderr) + return 1 + + pairs = collect_pairs(argv[1:]) + case_map = load_cases(pairs) + + cases = [] + for idx, (text, expectations) in enumerate(case_map.items(), start=1): + cases.append({ + "id": f"case_{idx:03d}", + "input": text, + "expected": expectations, + }) + + json.dump({"cases": cases}, sys.stdout, ensure_ascii=False, indent=2) + sys.stdout.write("\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) From 7b9c89bc68ee9733dfb4acc2925b2f4cbbc1c894 Mon Sep 17 00:00:00 2001 From: Frank Lin Date: Tue, 13 Jan 2026 13:10:42 -0800 Subject: [PATCH 2/4] Changeover to JSON-based testcases and clean dictionary outputs - Switch all tests (C++ CLI, Python, Node) to consume `testcases.json` and drop `.in`/`.ans` dependencies; keep filegroup for the JSON. - Prune TWPhrases sub-dictionary artifacts and align DictionaryTest to current generated dict set. - Add rapidjson dep/path for CLI test, refresh_assets script fixes, and keep Bazel Python toolchain note. --- data/config/BUILD.bazel | 2 +- data/dictionary/DictionaryTest.cpp | 13 +- node/test.js | 89 +++---- python/tests/test_opencc.py | 33 +-- test/BUILD.bazel | 1 + test/CMakeLists.txt | 1 + test/CommandLineConvertTest.cpp | 145 +++++++---- test/testcases/BUILD.bazel | 10 +- test/testcases/gen_testcases_json.py | 94 ------- test/testcases/hk2s.ans | 3 - test/testcases/hk2s.in | 3 - test/testcases/hk2t.ans | 2 - test/testcases/hk2t.in | 2 - test/testcases/jp2t.ans | 4 - test/testcases/jp2t.in | 4 - test/testcases/s2hk.ans | 5 - test/testcases/s2hk.in | 5 - test/testcases/s2t.ans | 13 - test/testcases/s2t.in | 13 - test/testcases/s2tw.ans | 3 - test/testcases/s2tw.in | 3 - test/testcases/s2twp.ans | 7 - test/testcases/s2twp.in | 7 - test/testcases/t2hk.ans | 3 - test/testcases/t2hk.in | 3 - test/testcases/t2jp.ans | 4 - test/testcases/t2jp.in | 4 - test/testcases/t2s.ans | 2 - test/testcases/t2s.in | 2 - test/testcases/testcases.json | 372 +++++++++++++++++++++++++++ test/testcases/tw2s.ans | 1 - test/testcases/tw2s.in | 1 - test/testcases/tw2sp.ans | 6 - test/testcases/tw2sp.in | 6 - test/testcases/tw2t.ans | 3 - test/testcases/tw2t.in | 3 - 36 files changed, 515 insertions(+), 357 deletions(-) delete mode 100644 test/testcases/gen_testcases_json.py delete mode 100644 test/testcases/hk2s.ans delete mode 100644 test/testcases/hk2s.in delete mode 100644 test/testcases/hk2t.ans delete mode 100644 test/testcases/hk2t.in delete mode 100644 test/testcases/jp2t.ans delete mode 100644 test/testcases/jp2t.in delete mode 100644 test/testcases/s2hk.ans delete mode 100644 test/testcases/s2hk.in delete mode 100644 test/testcases/s2t.ans delete mode 100644 test/testcases/s2t.in delete mode 100644 test/testcases/s2tw.ans delete mode 100644 test/testcases/s2tw.in delete mode 100644 test/testcases/s2twp.ans delete mode 100644 test/testcases/s2twp.in delete mode 100644 test/testcases/t2hk.ans delete mode 100644 test/testcases/t2hk.in delete mode 100644 test/testcases/t2jp.ans delete mode 100644 test/testcases/t2jp.in delete mode 100644 test/testcases/t2s.ans delete mode 100644 test/testcases/t2s.in create mode 100755 test/testcases/testcases.json delete mode 100644 test/testcases/tw2s.ans delete mode 100644 test/testcases/tw2s.in delete mode 100644 test/testcases/tw2sp.ans delete mode 100644 test/testcases/tw2sp.in delete mode 100644 test/testcases/tw2t.ans delete mode 100644 test/testcases/tw2t.in diff --git a/data/config/BUILD.bazel b/data/config/BUILD.bazel index 33cfe0e24..e369163f4 100644 --- a/data/config/BUILD.bazel +++ b/data/config/BUILD.bazel @@ -11,7 +11,7 @@ cc_test( data = [ ":config", "//data/dictionary:binary_dictionaries", - "//test/testcases:testcases_json", + "//test/testcases", ], deps = [ "//src:simple_converter", diff --git a/data/dictionary/DictionaryTest.cpp b/data/dictionary/DictionaryTest.cpp index 7b931c722..24776214a 100644 --- a/data/dictionary/DictionaryTest.cpp +++ b/data/dictionary/DictionaryTest.cpp @@ -46,13 +46,12 @@ std::string DictionaryTest::runfile_dir_; INSTANTIATE_TEST_SUITE_P( , DictionaryTest, - ::testing::Values("HKVariants", "HKVariantsRevPhrases", - "JPShinjitaiCharacters", "JPShinjitaiPhrases", - "JPVariants", "STCharacters", "STPhrases", "TSCharacters", - "TSPhrases", "TWPhrasesIT", "TWPhrasesName", - "TWPhrasesOther", "TWVariants", "TWVariantsRevPhrases", - "TWPhrases", "TWVariantsRev", "TWPhrasesRev", - "HKVariantsRev", "JPVariantsRev"), + ::testing::Values( + "HKVariants", "HKVariantsRev", "HKVariantsRevPhrases", + "JPShinjitaiCharacters", "JPShinjitaiPhrases", "JPVariants", + "JPVariantsRev", "STCharacters", "STPhrases", "TSCharacters", + "TSPhrases", "TWPhrases", "TWPhrasesRev", "TWVariants", + "TWVariantsRev", "TWVariantsRevPhrases"), [](const testing::TestParamInfo& info) { return info.param; }); diff --git a/node/test.js b/node/test.js index 70d644412..e80eb528a 100644 --- a/node/test.js +++ b/node/test.js @@ -4,85 +4,56 @@ const util = require('util'); const OpenCC = require('./opencc'); -const configs = [ - 'hk2s', - 'hk2t', - 'jp2t', - 's2hk', - 's2t', - 's2tw', - 's2twp', - 't2hk', - 't2jp', - 't2s', - 'tw2s', - 'tw2sp', - 'tw2t', -]; +const cases = JSON.parse(fs.readFileSync('test/testcases/testcases.json', 'utf-8')).cases || []; -const testSync = function (config, done) { - const inputName = 'test/testcases/' + config + '.in'; - const outputName = 'test/testcases/' + config + '.ans'; - const configName = config + '.json'; - const opencc = new OpenCC(configName); - const text = fs.readFileSync(inputName, 'utf-8'); - const converted = opencc.convertSync(text); - const answer = fs.readFileSync(outputName, 'utf-8'); - assert.equal(converted, answer); +const testSync = function (tc, cfg, expected, done) { + const opencc = new OpenCC(cfg + '.json'); + const converted = opencc.convertSync(tc.input); + assert.equal(converted, expected); done(); }; -const testAsync = function (config, done) { - const inputName = 'test/testcases/' + config + '.in'; - const outputName = 'test/testcases/' + config + '.ans'; - const configName = config + '.json'; - const opencc = new OpenCC(configName); - fs.readFile(inputName, 'utf-8', function (err, text) { +const testAsync = function (tc, cfg, expected, done) { + const opencc = new OpenCC(cfg + '.json'); + opencc.convert(tc.input, function (err, converted) { if (err) return done(err); - opencc.convert(text, function (err, converted) { - if (err) return done(err); - fs.readFile(outputName, 'utf-8', function (err, answer) { - if (err) return done(err); - assert.equal(converted, answer); - done(); - }); - }); + assert.equal(converted, expected); + done(); }); }; -async function testAsyncPromise(config) { - const inputName = 'test/testcases/' + config + '.in'; - const outputName = 'test/testcases/' + config + '.ans'; - const configName = config + '.json'; - const opencc = new OpenCC(configName); - - const text = await util.promisify(fs.readFile)(inputName, 'utf-8'); - const converted = await opencc.convertPromise(text); - const answer = await util.promisify(fs.readFile)(outputName, 'utf-8'); - - assert.equal(converted, answer); -}; +async function testAsyncPromise(tc, cfg, expected) { + const opencc = new OpenCC(cfg + '.json'); + const converted = await opencc.convertPromise(tc.input); + assert.equal(converted, expected); +} describe('Sync API', function () { - configs.forEach(function (config) { - it(config, function (done) { - testSync(config, done); + cases.forEach(function (tc, idx) { + Object.entries(tc.expected || {}).forEach(function ([cfg, expected]) { + it('[' + cfg + '] case #' + (idx + 1), function (done) { + testSync(tc, cfg, expected, done); + }); }); }); }); describe('Async API', function () { - configs.forEach(function (config) { - it(config, function (done) { - testAsync(config, done); + cases.forEach(function (tc, idx) { + Object.entries(tc.expected || {}).forEach(function ([cfg, expected]) { + it('[' + cfg + '] case #' + (idx + 1), function (done) { + testAsync(tc, cfg, expected, done); + }); }); }); }); describe('Async Promise API', function () { - configs.forEach(function (config) { - it(config, function (done) { - testAsyncPromise(config).then(done); + cases.forEach(function (tc, idx) { + Object.entries(tc.expected || {}).forEach(function ([cfg, expected]) { + it('[' + cfg + '] case #' + (idx + 1), function (done) { + testAsyncPromise(tc, cfg, expected).then(done, done); + }); }); }); }); diff --git a/python/tests/test_opencc.py b/python/tests/test_opencc.py index 31b2b0453..ff34d0556 100644 --- a/python/tests/test_opencc.py +++ b/python/tests/test_opencc.py @@ -1,14 +1,13 @@ from __future__ import unicode_literals +import json import os import pytest import sys -from glob import glob - _this_dir = os.path.dirname(os.path.abspath(__file__)) _opencc_rootdir = os.path.abspath(os.path.join(_this_dir, '..', '..')) -_test_assets_dir = os.path.join(_opencc_rootdir, 'test', 'testcases') +_testcases_path = os.path.join(_opencc_rootdir, 'test', 'testcases', 'testcases.json') def test_import(): @@ -26,22 +25,18 @@ def test_init_delete_converter(): def test_conversion(): import opencc - for inpath in glob(os.path.join(_test_assets_dir, '*.in')): - pref = os.path.splitext(inpath)[0] - config = os.path.basename(pref) - converter = opencc.OpenCC(config) - anspath = '{}.{}'.format(pref, 'ans') - assert os.path.isfile(anspath) - - with open(inpath, 'rb') as f: - intexts = [l.strip().decode('utf-8') for l in f] - with open(anspath, 'rb') as f: - anstexts = [l.strip().decode('utf-8') for l in f] - assert len(intexts) == len(anstexts) - - for text, ans in zip(intexts, anstexts): - assert converter.convert(text) == ans, \ - 'Failed to convert {} for {} -> {}'.format(pref, text, ans) + with open(_testcases_path, 'r', encoding='utf-8') as f: + parsed = json.load(f) + + for case in parsed.get('cases', []): + input_text = case.get('input') + expected = case.get('expected', {}) + if not input_text or not isinstance(expected, dict): + continue + for cfg, ans in expected.items(): + converter = opencc.OpenCC(f'{cfg}.json') + assert converter.convert(input_text) == ans, \ + 'Failed to convert {} for {} -> {}'.format(cfg, input_text, ans) if __name__ == "__main__": diff --git a/test/BUILD.bazel b/test/BUILD.bazel index 71a118a7a..c32e46498 100644 --- a/test/BUILD.bazel +++ b/test/BUILD.bazel @@ -22,5 +22,6 @@ cc_test( "//src:common", "@bazel_tools//tools/cpp/runfiles", "@googletest//:gtest_main", + "@rapidjson", ], ) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 61ce03346..58acf13b8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,6 +1,7 @@ include_directories("${PROJECT_SOURCE_DIR}") include_directories("${PROJECT_BINARY_DIR}/src") include_directories("${PROJECT_SOURCE_DIR}/src") +include_directories("${PROJECT_SOURCE_DIR}/deps/rapidjson-1.1.0") set(CONFIG_TEST config_test/config_test.json diff --git a/test/CommandLineConvertTest.cpp b/test/CommandLineConvertTest.cpp index 7be8a398a..0e8cf983a 100644 --- a/test/CommandLineConvertTest.cpp +++ b/test/CommandLineConvertTest.cpp @@ -18,8 +18,13 @@ #include #include +#include +#include +#include +#include #include "src/Common.hpp" +#include "rapidjson/document.h" #include "gtest/gtest.h" #ifdef BAZEL @@ -76,14 +81,6 @@ class CommandLineConvertTest : public ::testing::Test { #endif } - std::string InputDirectory() const { -#ifdef BAZEL - return runfiles_->Rlocation("_main/test/testcases") + "/"; -#else - return CMAKE_SOURCE_DIR "/test/testcases/"; -#endif - } - std::string OutputDirectory() const { #ifdef BAZEL return ::testing::TempDir() + "/"; @@ -92,14 +89,6 @@ class CommandLineConvertTest : public ::testing::Test { #endif } - std::string AnswerDirectory() const { -#ifdef BAZEL - return runfiles_->Rlocation("_main/test/testcases") + "/"; -#else - return CMAKE_SOURCE_DIR "/test/testcases/"; -#endif - } - std::string ConfigurationDirectory() const { #ifdef BAZEL return ""; @@ -109,18 +98,15 @@ class CommandLineConvertTest : public ::testing::Test { } std::string InputFile(const char* config) const { - return InputDirectory() + config + ".in"; + return OutputDirectory() + config + ".in"; } std::string OutputFile(const char* config) const { return OutputDirectory() + config + ".out"; } - std::string AnswerFile(const char* config) const { - return AnswerDirectory() + config + ".ans"; - } - - std::string TestCommand(const char* config, const std::string& inputFile, + std::string TestCommand(const std::string& config, + const std::string& inputFile, const std::string& outputFile) const { std::string cmd = OpenccCommand() + " -i " + inputFile + " -o " + outputFile + " -c " + ConfigurationDirectory() + config + @@ -139,42 +125,89 @@ class CommandLineConvertTest : public ::testing::Test { #endif }; -class ConfigurationTest : public CommandLineConvertTest, - public ::testing::WithParamInterface {}; - -TEST_P(ConfigurationTest, Convert) { - const char* config = GetParam(); - const std::string inputFile = InputFile(config); - const std::string outputFile = OutputFile(config); - ASSERT_EQ(0, system(TestCommand(config, inputFile, outputFile).c_str())); - const std::string output = GetFileContents(OutputFile(config)); - const std::string answer = GetFileContents(AnswerFile(config)); - ASSERT_EQ(answer, output); -} +struct CaseInput { + std::string input; + std::string expected; +}; + +using CasesByConfig = std::unordered_map>; + +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(content.c_str()); + if (doc.HasParseError() || !doc.IsObject() || !doc.HasMember("cases") || + !doc["cases"].IsArray()) { + throw std::runtime_error("Invalid testcases.json format"); + } -TEST_P(ConfigurationTest, InPlaceConvert) { - const char* config = GetParam(); - // Copy input to output - const std::string inputFile = InputFile(config); - const std::string outputFile = OutputFile(config); - std::ifstream source(inputFile, std::ios::binary); - std::ofstream dest(outputFile, std::ios::binary); - dest << source.rdbuf(); - source.close(); - dest.close(); - // Test in-place convert (same file) - ASSERT_EQ(0, system(TestCommand(config, outputFile, outputFile).c_str())); - const std::string output = GetFileContents(OutputFile(config)); - const std::string answer = GetFileContents(AnswerFile(config)); - ASSERT_EQ(answer, output); + 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; } -INSTANTIATE_TEST_SUITE_P( - CommandLine, ConfigurationTest, - ::testing::Values("hk2s", "hk2t", "jp2t", "s2hk", "s2t", "s2tw", "s2twp", - "t2hk", "t2jp", "t2s", "tw2s", "tw2sp", "tw2t"), - [](const testing::TestParamInfo& info) { - return info.param; - }); +TEST_F(CommandLineConvertTest, ConvertFromJson) { +#ifdef BAZEL + const std::string casesPath = + runfiles_->Rlocation("_main/test/testcases/testcases.json"); +#else + const std::string casesPath = CMAKE_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()); + + // Write inputs into a temp file (one per line). + { + std::ofstream ofs(inputFile, std::ios::binary); + for (const auto& item : entry.second) { + ofs << item.input << "\n"; + } + } + + ASSERT_EQ(0, system(TestCommand(config, inputFile, outputFile).c_str())); + + // Read outputs and compare line by line. + std::ifstream ofs(outputFile, std::ios::binary); + ASSERT_TRUE(ofs.is_open()); + std::string line; + size_t idx = 0; + while (std::getline(ofs, line)) { + 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; + } +} } // namespace opencc diff --git a/test/testcases/BUILD.bazel b/test/testcases/BUILD.bazel index 7b95a0b25..0a1c3e136 100644 --- a/test/testcases/BUILD.bazel +++ b/test/testcases/BUILD.bazel @@ -2,13 +2,5 @@ package(default_visibility = ["//visibility:public"]) filegroup( name = "testcases", - srcs = glob(["*.in"]) + glob(["*.ans"]), -) - -genrule( - name = "testcases_json", - srcs = glob(["*.in"]) + glob(["*.ans"]), - tools = ["gen_testcases_json.py"], - outs = ["testcases.json"], - cmd = "python3 $(location gen_testcases_json.py) $(SRCS) > $@", + srcs = ["testcases.json"], ) diff --git a/test/testcases/gen_testcases_json.py b/test/testcases/gen_testcases_json.py deleted file mode 100644 index 5cce75e19..000000000 --- a/test/testcases/gen_testcases_json.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python3 -""" -Build a consolidated testcases.json from *.in/*.ans pairs. - -Usage (Bazel genrule): - python3 gen_testcases_json.py ... > testcases.json - -Input files should include matching *.in and *.ans files. The script will -combine lines by input text and emit: -{ - "cases": [ - { - "id": "case_001", - "input": "...", - "expected": { - "s2t": "...", - "t2s": "..." - } - }, - ... - ] -} -""" - -from __future__ import annotations - -import json -import os -import sys -from collections import OrderedDict, defaultdict -from typing import Dict, List - - -def collect_pairs(paths: List[str]) -> Dict[str, Dict[str, str]]: - """Collect matching .in/.ans files keyed by basename.""" - pairs: Dict[str, Dict[str, str]] = defaultdict(dict) - for path in paths: - base = os.path.basename(path) - if base.endswith(".in"): - pairs[base[:-3]]["in"] = path - elif base.endswith(".ans"): - pairs[base[:-4]]["ans"] = path - return pairs - - -def load_cases(pairs: Dict[str, Dict[str, str]]) -> OrderedDict: - """Load input/expected lines and merge by input string.""" - case_map: "OrderedDict[str, Dict[str, str]]" = OrderedDict() - for cfg in sorted(pairs.keys()): - entry = pairs[cfg] - if "in" not in entry or "ans" not in entry: - continue - with open(entry["in"], "r", encoding="utf-8") as fin: - inputs = fin.read().splitlines() - with open(entry["ans"], "r", encoding="utf-8") as fans: - answers = fans.read().splitlines() - if len(inputs) != len(answers): - raise ValueError( - f"Line count mismatch for {entry['in']} ({len(inputs)}) vs " - f"{entry['ans']} ({len(answers)})") - for text, expected in zip(inputs, answers): - if text not in case_map: - case_map[text] = {} - if cfg in case_map[text] and case_map[text][cfg] != expected: - raise ValueError( - f"Conflicting expectation for '{text}' in {cfg}: " - f"'{case_map[text][cfg]}' vs '{expected}'") - case_map[text][cfg] = expected - return case_map - - -def main(argv: List[str]) -> int: - if len(argv) < 2: - print("Usage: gen_testcases_json.py ", file=sys.stderr) - return 1 - - pairs = collect_pairs(argv[1:]) - case_map = load_cases(pairs) - - cases = [] - for idx, (text, expectations) in enumerate(case_map.items(), start=1): - cases.append({ - "id": f"case_{idx:03d}", - "input": text, - "expected": expectations, - }) - - json.dump({"cases": cases}, sys.stdout, ensure_ascii=False, indent=2) - sys.stdout.write("\n") - return 0 - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/test/testcases/hk2s.ans b/test/testcases/hk2s.ans deleted file mode 100644 index 8644ddc48..000000000 --- a/test/testcases/hk2s.ans +++ /dev/null @@ -1,3 +0,0 @@ -虚伪叹息 -潮湿灶台 -赞叹沙河涌汹涌的波浪 \ No newline at end of file diff --git a/test/testcases/hk2s.in b/test/testcases/hk2s.in deleted file mode 100644 index 3c4549754..000000000 --- a/test/testcases/hk2s.in +++ /dev/null @@ -1,3 +0,0 @@ -虛偽歎息 -潮濕灶台 -讚歎沙河涌洶湧的波浪 \ No newline at end of file diff --git a/test/testcases/hk2t.ans b/test/testcases/hk2t.ans deleted file mode 100644 index ae75d251c..000000000 --- a/test/testcases/hk2t.ans +++ /dev/null @@ -1,2 +0,0 @@ -爲賦新詞強說愁 -想到自己一緊張就口吃,我就沒胃口喫飯 \ No newline at end of file diff --git a/test/testcases/hk2t.in b/test/testcases/hk2t.in deleted file mode 100644 index 1bbc409c5..000000000 --- a/test/testcases/hk2t.in +++ /dev/null @@ -1,2 +0,0 @@ -為賦新詞強説愁 -想到自己一緊張就口吃,我就沒胃口吃飯 \ No newline at end of file diff --git a/test/testcases/jp2t.ans b/test/testcases/jp2t.ans deleted file mode 100644 index 9cbcdcfc1..000000000 --- a/test/testcases/jp2t.ans +++ /dev/null @@ -1,4 +0,0 @@ -舊字體歷史假名遣 新字體現代假名遣 -橫濱 絲魚川 伊豫國 -驛辨當 辨別 辯護士 瓣膜 -藝術 缺航 欠缺 飲料罐 \ No newline at end of file diff --git a/test/testcases/jp2t.in b/test/testcases/jp2t.in deleted file mode 100644 index 9178c8a99..000000000 --- a/test/testcases/jp2t.in +++ /dev/null @@ -1,4 +0,0 @@ -旧字体歴史仮名遣 新字体現代仮名遣 -横浜 糸魚川 伊予国 -駅弁当 弁別 弁護士 弁膜 -芸術 欠航 欠缺 飲料缶 \ No newline at end of file diff --git a/test/testcases/s2hk.ans b/test/testcases/s2hk.ans deleted file mode 100644 index 629c03236..000000000 --- a/test/testcases/s2hk.ans +++ /dev/null @@ -1,5 +0,0 @@ -虛偽嘆息 -潮濕灶台 -讚歎沙河涌洶湧的波浪 -為了核實這説法 -高劑量的苦瓜素還會抑制胚胎發育 \ No newline at end of file diff --git a/test/testcases/s2hk.in b/test/testcases/s2hk.in deleted file mode 100644 index 24a417d1f..000000000 --- a/test/testcases/s2hk.in +++ /dev/null @@ -1,5 +0,0 @@ -虚伪叹息 -潮湿灶台 -赞叹沙河涌汹涌的波浪 -为了核实这说法 -高剂量的苦瓜素还会抑制胚胎发育 \ No newline at end of file diff --git a/test/testcases/s2t.ans b/test/testcases/s2t.ans deleted file mode 100644 index 85a2ace2f..000000000 --- a/test/testcases/s2t.ans +++ /dev/null @@ -1,13 +0,0 @@ -誇誇其談 夸父逐日 -我幹什麼不干你事。 -太后的頭髮很乾燥。 -燕燕于飛,差池其羽。之子于歸,遠送於野。 -請成相,世之殃,愚闇愚闇墮賢良。人主無賢,如瞽無相何倀倀!請布基,慎聖人,愚而自專事不治。主忌苟勝,羣臣莫諫必逢災。 -曾經有一份真誠的愛情放在我面前,我沒有珍惜,等我失去的時候我才後悔莫及。人事間最痛苦的事莫過於此。如果上天能夠給我一個再來一次得機會,我會對那個女孩子說三個字,我愛你。如果非要在這份愛上加個期限,我希望是,一萬年。 -新的理論被發現了。 -金胄不是金色的甲冑。 -經理發現後勸諭兩人 -想到自己一緊張就口吃,我就沒胃口喫飯 -恒指最新消息,恒生指數跌破 2 萬點 -恒生銀行和恒大集團發佈財報 -高劑量的苦瓜素還會抑制胚胎發育 \ No newline at end of file diff --git a/test/testcases/s2t.in b/test/testcases/s2t.in deleted file mode 100644 index dbf6ba030..000000000 --- a/test/testcases/s2t.in +++ /dev/null @@ -1,13 +0,0 @@ -夸夸其谈 夸父逐日 -我干什么不干你事。 -太后的头发很干燥。 -燕燕于飞,差池其羽。之子于归,远送于野。 -请成相,世之殃,愚暗愚暗堕贤良。人主无贤,如瞽无相何伥伥!请布基,慎圣人,愚而自专事不治。主忌苟胜,群臣莫谏必逢灾。 -曾经有一份真诚的爱情放在我面前,我没有珍惜,等我失去的时候我才后悔莫及。人事间最痛苦的事莫过于此。如果上天能够给我一个再来一次得机会,我会对那个女孩子说三个字,我爱你。如果非要在这份爱上加个期限,我希望是,一万年。 -新的理论被发现了。 -金胄不是金色的甲胄。 -经理发现后劝谕两人 -想到自己一紧张就口吃,我就没胃口吃饭 -恒指最新消息,恒生指数跌破 2 万点 -恒生银行和恒大集团发布财报 -高剂量的苦瓜素还会抑制胚胎发育 \ No newline at end of file diff --git a/test/testcases/s2tw.ans b/test/testcases/s2tw.ans deleted file mode 100644 index 379247f32..000000000 --- a/test/testcases/s2tw.ans +++ /dev/null @@ -1,3 +0,0 @@ -著裝汙染虛偽發洩稜柱群眾裡面 -鯰魚和鯰魚是一種生物。 -高劑量的苦瓜素還會抑制胚胎發育 diff --git a/test/testcases/s2tw.in b/test/testcases/s2tw.in deleted file mode 100644 index 5e79e81c5..000000000 --- a/test/testcases/s2tw.in +++ /dev/null @@ -1,3 +0,0 @@ -着装污染虚伪发泄棱柱群众里面 -鲶鱼和鲇鱼是一种生物。 -高剂量的苦瓜素还会抑制胚胎发育 diff --git a/test/testcases/s2twp.ans b/test/testcases/s2twp.ans deleted file mode 100644 index 042bb1516..000000000 --- a/test/testcases/s2twp.ans +++ /dev/null @@ -1,7 +0,0 @@ -滑鼠裡面的矽二極體壞了,導致游標解析度降低。 -我們在寮國的伺服器的硬碟需要使用網際網路演算法軟體解決非同步的問題。 -為什麼你在床裡面睡著? -海內存知己 -摩爾線程 -字節跳動 -高劑量的苦瓜素還會抑制胚胎發育 \ No newline at end of file diff --git a/test/testcases/s2twp.in b/test/testcases/s2twp.in deleted file mode 100644 index c2c0c1036..000000000 --- a/test/testcases/s2twp.in +++ /dev/null @@ -1,7 +0,0 @@ -鼠标里面的硅二极管坏了,导致光标分辨率降低。 -我们在老挝的服务器的硬盘需要使用互联网算法软件解决异步的问题。 -为什么你在床里面睡着? -海内存知己 -摩尔线程 -字节跳动 -高剂量的苦瓜素还会抑制胚胎发育 \ No newline at end of file diff --git a/test/testcases/t2hk.ans b/test/testcases/t2hk.ans deleted file mode 100644 index 704b68785..000000000 --- a/test/testcases/t2hk.ans +++ /dev/null @@ -1,3 +0,0 @@ -潮濕的露台 -為了核實這説法 -包粽子活動告一段落 \ No newline at end of file diff --git a/test/testcases/t2hk.in b/test/testcases/t2hk.in deleted file mode 100644 index 26ac2e71f..000000000 --- a/test/testcases/t2hk.in +++ /dev/null @@ -1,3 +0,0 @@ -潮溼的露臺 -爲了覈實這說法 -包糉子活動告一段落 \ No newline at end of file diff --git a/test/testcases/t2jp.ans b/test/testcases/t2jp.ans deleted file mode 100644 index c5e8a381d..000000000 --- a/test/testcases/t2jp.ans +++ /dev/null @@ -1,4 +0,0 @@ -旧字体歴史仮名遣 新字体現代仮名遣 -横浜 糸魚川 伊予国 -駅弁当 弁別 弁護士 弁膜 -芸術 欠航 飲料缶 \ No newline at end of file diff --git a/test/testcases/t2jp.in b/test/testcases/t2jp.in deleted file mode 100644 index c2626d233..000000000 --- a/test/testcases/t2jp.in +++ /dev/null @@ -1,4 +0,0 @@ -舊字體歷史假名遣 新字體現代假名遣 -橫濱 絲魚川 伊豫國 -驛辨當 辨別 辯護士 瓣膜 -藝術 缺航 飲料罐 \ No newline at end of file diff --git a/test/testcases/t2s.ans b/test/testcases/t2s.ans deleted file mode 100644 index 299716adf..000000000 --- a/test/testcases/t2s.ans +++ /dev/null @@ -1,2 +0,0 @@ -曾经有一份真诚的爱情放在我面前,我没有珍惜,等我失去的时候我才后悔莫及。人事间最痛苦的事莫过于此。如果上天能够给我一个再来一次得机会,我会对那个女孩子说三个字,我爱你。如果非要在这份爱上加个期限,我希望是,一万年。 -二𫫇英 \ No newline at end of file diff --git a/test/testcases/t2s.in b/test/testcases/t2s.in deleted file mode 100644 index eef2f346e..000000000 --- a/test/testcases/t2s.in +++ /dev/null @@ -1,2 +0,0 @@ -曾經有一份真誠的愛情放在我面前,我沒有珍惜,等我失去的時候我才後悔莫及。人事間最痛苦的事莫過於此。如果上天能夠給我一個再來一次得機會,我會對那個女孩子說三個字,我愛你。如果非要在這份愛上加個期限,我希望是,一萬年。 -二噁英 \ No newline at end of file diff --git a/test/testcases/testcases.json b/test/testcases/testcases.json new file mode 100755 index 000000000..98c0e3515 --- /dev/null +++ b/test/testcases/testcases.json @@ -0,0 +1,372 @@ +{ + "cases": [ + { + "id": "case_001", + "input": "虛偽歎息", + "expected": { + "hk2s": "虚伪叹息" + } + }, + { + "id": "case_002", + "input": "潮濕灶台", + "expected": { + "hk2s": "潮湿灶台" + } + }, + { + "id": "case_003", + "input": "讚歎沙河涌洶湧的波浪", + "expected": { + "hk2s": "赞叹沙河涌汹涌的波浪" + } + }, + { + "id": "case_004", + "input": "為賦新詞強説愁", + "expected": { + "hk2t": "爲賦新詞強說愁" + } + }, + { + "id": "case_005", + "input": "想到自己一緊張就口吃,我就沒胃口吃飯", + "expected": { + "hk2t": "想到自己一緊張就口吃,我就沒胃口喫飯", + "tw2t": "想到自己一緊張就口吃,我就沒胃口喫飯" + } + }, + { + "id": "case_006", + "input": "旧字体歴史仮名遣 新字体現代仮名遣", + "expected": { + "jp2t": "舊字體歷史假名遣 新字體現代假名遣" + } + }, + { + "id": "case_007", + "input": "横浜 糸魚川 伊予国", + "expected": { + "jp2t": "橫濱 絲魚川 伊豫國" + } + }, + { + "id": "case_008", + "input": "駅弁当 弁別 弁護士 弁膜", + "expected": { + "jp2t": "驛辨當 辨別 辯護士 瓣膜" + } + }, + { + "id": "case_009", + "input": "芸術 欠航 欠缺 飲料缶", + "expected": { + "jp2t": "藝術 缺航 欠缺 飲料罐" + } + }, + { + "id": "case_010", + "input": "虚伪叹息", + "expected": { + "s2hk": "虛偽嘆息" + } + }, + { + "id": "case_011", + "input": "潮湿灶台", + "expected": { + "s2hk": "潮濕灶台" + } + }, + { + "id": "case_012", + "input": "赞叹沙河涌汹涌的波浪", + "expected": { + "s2hk": "讚歎沙河涌洶湧的波浪" + } + }, + { + "id": "case_013", + "input": "为了核实这说法", + "expected": { + "s2hk": "為了核實這説法" + } + }, + { + "id": "case_014", + "input": "高剂量的苦瓜素还会抑制胚胎发育", + "expected": { + "s2hk": "高劑量的苦瓜素還會抑制胚胎發育", + "s2t": "高劑量的苦瓜素還會抑制胚胎發育", + "s2tw": "高劑量的苦瓜素還會抑制胚胎發育", + "s2twp": "高劑量的苦瓜素還會抑制胚胎發育" + } + }, + { + "id": "case_015", + "input": "夸夸其谈 夸父逐日", + "expected": { + "s2t": "誇誇其談 夸父逐日" + } + }, + { + "id": "case_016", + "input": "我干什么不干你事。", + "expected": { + "s2t": "我幹什麼不干你事。" + } + }, + { + "id": "case_017", + "input": "太后的头发很干燥。", + "expected": { + "s2t": "太后的頭髮很乾燥。" + } + }, + { + "id": "case_018", + "input": "燕燕于飞,差池其羽。之子于归,远送于野。", + "expected": { + "s2t": "燕燕于飛,差池其羽。之子于歸,遠送於野。" + } + }, + { + "id": "case_019", + "input": "请成相,世之殃,愚暗愚暗堕贤良。人主无贤,如瞽无相何伥伥!请布基,慎圣人,愚而自专事不治。主忌苟胜,群臣莫谏必逢灾。", + "expected": { + "s2t": "請成相,世之殃,愚闇愚闇墮賢良。人主無賢,如瞽無相何倀倀!請布基,慎聖人,愚而自專事不治。主忌苟勝,羣臣莫諫必逢災。" + } + }, + { + "id": "case_020", + "input": "曾经有一份真诚的爱情放在我面前,我没有珍惜,等我失去的时候我才后悔莫及。人事间最痛苦的事莫过于此。如果上天能够给我一个再来一次得机会,我会对那个女孩子说三个字,我爱你。如果非要在这份爱上加个期限,我希望是,一万年。", + "expected": { + "s2t": "曾經有一份真誠的愛情放在我面前,我沒有珍惜,等我失去的時候我才後悔莫及。人事間最痛苦的事莫過於此。如果上天能夠給我一個再來一次得機會,我會對那個女孩子說三個字,我愛你。如果非要在這份愛上加個期限,我希望是,一萬年。" + } + }, + { + "id": "case_021", + "input": "新的理论被发现了。", + "expected": { + "s2t": "新的理論被發現了。" + } + }, + { + "id": "case_022", + "input": "金胄不是金色的甲胄。", + "expected": { + "s2t": "金胄不是金色的甲冑。" + } + }, + { + "id": "case_023", + "input": "经理发现后劝谕两人", + "expected": { + "s2t": "經理發現後勸諭兩人" + } + }, + { + "id": "case_024", + "input": "想到自己一紧张就口吃,我就没胃口吃饭", + "expected": { + "s2t": "想到自己一緊張就口吃,我就沒胃口喫飯" + } + }, + { + "id": "case_025", + "input": "恒指最新消息,恒生指数跌破 2 万点", + "expected": { + "s2t": "恒指最新消息,恒生指數跌破 2 萬點" + } + }, + { + "id": "case_026", + "input": "恒生银行和恒大集团发布财报", + "expected": { + "s2t": "恒生銀行和恒大集團發佈財報" + } + }, + { + "id": "case_027", + "input": "着装污染虚伪发泄棱柱群众里面", + "expected": { + "s2tw": "著裝汙染虛偽發洩稜柱群眾裡面" + } + }, + { + "id": "case_028", + "input": "鲶鱼和鲇鱼是一种生物。", + "expected": { + "s2tw": "鯰魚和鯰魚是一種生物。" + } + }, + { + "id": "case_029", + "input": "鼠标里面的硅二极管坏了,导致光标分辨率降低。", + "expected": { + "s2twp": "滑鼠裡面的矽二極體壞了,導致游標解析度降低。" + } + }, + { + "id": "case_030", + "input": "我们在老挝的服务器的硬盘需要使用互联网算法软件解决异步的问题。", + "expected": { + "s2twp": "我們在寮國的伺服器的硬碟需要使用網際網路演算法軟體解決非同步的問題。" + } + }, + { + "id": "case_031", + "input": "为什么你在床里面睡着?", + "expected": { + "s2twp": "為什麼你在床裡面睡著?" + } + }, + { + "id": "case_032", + "input": "海内存知己", + "expected": { + "s2twp": "海內存知己" + } + }, + { + "id": "case_033", + "input": "摩尔线程", + "expected": { + "s2twp": "摩爾線程" + } + }, + { + "id": "case_034", + "input": "字节跳动", + "expected": { + "s2twp": "字節跳動" + } + }, + { + "id": "case_035", + "input": "潮溼的露臺", + "expected": { + "t2hk": "潮濕的露台" + } + }, + { + "id": "case_036", + "input": "爲了覈實這說法", + "expected": { + "t2hk": "為了核實這説法" + } + }, + { + "id": "case_037", + "input": "包糉子活動告一段落", + "expected": { + "t2hk": "包粽子活動告一段落" + } + }, + { + "id": "case_038", + "input": "舊字體歷史假名遣 新字體現代假名遣", + "expected": { + "t2jp": "旧字体歴史仮名遣 新字体現代仮名遣" + } + }, + { + "id": "case_039", + "input": "橫濱 絲魚川 伊豫國", + "expected": { + "t2jp": "横浜 糸魚川 伊予国" + } + }, + { + "id": "case_040", + "input": "驛辨當 辨別 辯護士 瓣膜", + "expected": { + "t2jp": "駅弁当 弁別 弁護士 弁膜" + } + }, + { + "id": "case_041", + "input": "藝術 缺航 飲料罐", + "expected": { + "t2jp": "芸術 欠航 飲料缶" + } + }, + { + "id": "case_042", + "input": "曾經有一份真誠的愛情放在我面前,我沒有珍惜,等我失去的時候我才後悔莫及。人事間最痛苦的事莫過於此。如果上天能夠給我一個再來一次得機會,我會對那個女孩子說三個字,我愛你。如果非要在這份愛上加個期限,我希望是,一萬年。", + "expected": { + "t2s": "曾经有一份真诚的爱情放在我面前,我没有珍惜,等我失去的时候我才后悔莫及。人事间最痛苦的事莫过于此。如果上天能够给我一个再来一次得机会,我会对那个女孩子说三个字,我爱你。如果非要在这份爱上加个期限,我希望是,一万年。" + } + }, + { + "id": "case_043", + "input": "二噁英", + "expected": { + "t2s": "二𫫇英" + } + }, + { + "id": "case_044", + "input": "著裝著作汙染虛偽發洩稜柱群眾裡面", + "expected": { + "tw2s": "着装著作污染虚伪发泄棱柱群众里面" + } + }, + { + "id": "case_045", + "input": "滑鼠裡面的矽二極體壞了,導致游標解析度降低。", + "expected": { + "tw2sp": "鼠标里面的硅二极管坏了,导致光标分辨率降低。" + } + }, + { + "id": "case_046", + "input": "我們在寮國的伺服器的硬碟需要使用網際網路演算法軟體解決非同步的問題。", + "expected": { + "tw2sp": "我们在老挝的服务器的硬盘需要使用互联网算法软件解决异步的问题。" + } + }, + { + "id": "case_047", + "input": "為什麼你在床裡面睡著?", + "expected": { + "tw2sp": "为什么你在床里面睡着?" + } + }, + { + "id": "case_048", + "input": "用滑鼠點選正規表示式", + "expected": { + "tw2sp": "用鼠标点击正则表达式" + } + }, + { + "id": "case_049", + "input": "KB大橋也被視為帛琉人的後花園", + "expected": { + "tw2sp": "KB大桥也被视为帕劳人的后花园" + } + }, + { + "id": "case_050", + "input": "這個軟體裡有一套軟體動物的資料庫", + "expected": { + "tw2sp": "这个软件里有一套软体动物的数据库" + } + }, + { + "id": "case_051", + "input": "為了眾人化妝床頭裡面衛生,醞釀群峰鐵鉤嘆氣事件", + "expected": { + "tw2t": "爲了衆人化妝牀頭裏面衛生,醞釀羣峯鐵鉤嘆氣事件" + } + }, + { + "id": "case_052", + "input": "在廚房裡做手擀麵", + "expected": { + "tw2t": "在廚房裏做手擀麪" + } + } + ] +} diff --git a/test/testcases/tw2s.ans b/test/testcases/tw2s.ans deleted file mode 100644 index b010f7b8b..000000000 --- a/test/testcases/tw2s.ans +++ /dev/null @@ -1 +0,0 @@ -着装著作污染虚伪发泄棱柱群众里面 \ No newline at end of file diff --git a/test/testcases/tw2s.in b/test/testcases/tw2s.in deleted file mode 100644 index 5595eb5f4..000000000 --- a/test/testcases/tw2s.in +++ /dev/null @@ -1 +0,0 @@ -著裝著作汙染虛偽發洩稜柱群眾裡面 \ No newline at end of file diff --git a/test/testcases/tw2sp.ans b/test/testcases/tw2sp.ans deleted file mode 100644 index 1e09c324e..000000000 --- a/test/testcases/tw2sp.ans +++ /dev/null @@ -1,6 +0,0 @@ -鼠标里面的硅二极管坏了,导致光标分辨率降低。 -我们在老挝的服务器的硬盘需要使用互联网算法软件解决异步的问题。 -为什么你在床里面睡着? -用鼠标点击正则表达式 -KB大桥也被视为帕劳人的后花园 -这个软件里有一套软体动物的数据库 diff --git a/test/testcases/tw2sp.in b/test/testcases/tw2sp.in deleted file mode 100644 index af20d97c6..000000000 --- a/test/testcases/tw2sp.in +++ /dev/null @@ -1,6 +0,0 @@ -滑鼠裡面的矽二極體壞了,導致游標解析度降低。 -我們在寮國的伺服器的硬碟需要使用網際網路演算法軟體解決非同步的問題。 -為什麼你在床裡面睡著? -用滑鼠點選正規表示式 -KB大橋也被視為帛琉人的後花園 -這個軟體裡有一套軟體動物的資料庫 diff --git a/test/testcases/tw2t.ans b/test/testcases/tw2t.ans deleted file mode 100644 index aa48668bd..000000000 --- a/test/testcases/tw2t.ans +++ /dev/null @@ -1,3 +0,0 @@ -爲了衆人化妝牀頭裏面衛生,醞釀羣峯鐵鉤嘆氣事件 -在廚房裏做手擀麪 -想到自己一緊張就口吃,我就沒胃口喫飯 \ No newline at end of file diff --git a/test/testcases/tw2t.in b/test/testcases/tw2t.in deleted file mode 100644 index be88d1bf2..000000000 --- a/test/testcases/tw2t.in +++ /dev/null @@ -1,3 +0,0 @@ -為了眾人化妝床頭裡面衛生,醞釀群峰鐵鉤嘆氣事件 -在廚房裡做手擀麵 -想到自己一緊張就口吃,我就沒胃口吃飯 \ No newline at end of file From 520aad443d3987786c5d3bf3f535e679ce0b75f2 Mon Sep 17 00:00:00 2001 From: Frank Lin Date: Fri, 2 Jan 2026 17:43:44 -0800 Subject: [PATCH 3/4] Normalize CommandLineConvertTest for CRLF comparisons on Windows --- test/CommandLineConvertTest.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/CommandLineConvertTest.cpp b/test/CommandLineConvertTest.cpp index 0e8cf983a..58209a9fb 100644 --- a/test/CommandLineConvertTest.cpp +++ b/test/CommandLineConvertTest.cpp @@ -201,6 +201,9 @@ TEST_F(CommandLineConvertTest, ConvertFromJson) { std::string line; size_t idx = 0; while (std::getline(ofs, line)) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); // normalize Windows CRLF + } ASSERT_LT(idx, entry.second.size()); EXPECT_EQ(entry.second[idx].expected, line) << "config=" << config << " index=" << idx; From 9a6cc8b24eb18c91639dea42d8691663820dd78e Mon Sep 17 00:00:00 2001 From: Frank Lin Date: Fri, 2 Jan 2026 17:53:38 -0800 Subject: [PATCH 4/4] Address review feedback for tests and Bazel-only validation - Rename and guard streams in CommandLineConvertTest; ensure input file opens and normalize CRLF. - Fix node test promise handling to propagate errors correctly. - Mark ConfigDictValidationTest as Bazel-only to skip CMake builds. --- data/config/ConfigDictValidationTest.cpp | 9 +++++++-- node/test.js | 2 +- test/CommandLineConvertTest.cpp | 8 +++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/data/config/ConfigDictValidationTest.cpp b/data/config/ConfigDictValidationTest.cpp index b96b86c17..412baa93e 100644 --- a/data/config/ConfigDictValidationTest.cpp +++ b/data/config/ConfigDictValidationTest.cpp @@ -4,6 +4,11 @@ * End-to-end validation of all configs against consolidated testcases.json. */ +#ifndef BAZEL +// This test is Bazel-only; CMake builds should skip compiling it. +static_assert(false, "ConfigDictValidationTest is only supported under Bazel"); +#else + #include #include #include @@ -14,10 +19,8 @@ #include "rapidjson/document.h" #include "src/SimpleConverter.hpp" -#ifdef BAZEL #include "tools/cpp/runfiles/runfiles.h" using bazel::tools::cpp::runfiles::Runfiles; -#endif namespace opencc { namespace { @@ -102,3 +105,5 @@ TEST_F(ConfigDictValidationTest, ConvertExpectedOutputs) { } // namespace } // namespace opencc + +#endif // BAZEL diff --git a/node/test.js b/node/test.js index e80eb528a..ffc85860e 100644 --- a/node/test.js +++ b/node/test.js @@ -52,7 +52,7 @@ describe('Async Promise API', function () { cases.forEach(function (tc, idx) { Object.entries(tc.expected || {}).forEach(function ([cfg, expected]) { it('[' + cfg + '] case #' + (idx + 1), function (done) { - testAsyncPromise(tc, cfg, expected).then(done, done); + testAsyncPromise(tc, cfg, expected).then(() => done(), done); }); }); }); diff --git a/test/CommandLineConvertTest.cpp b/test/CommandLineConvertTest.cpp index 58209a9fb..f53650e33 100644 --- a/test/CommandLineConvertTest.cpp +++ b/test/CommandLineConvertTest.cpp @@ -188,6 +188,8 @@ TEST_F(CommandLineConvertTest, ConvertFromJson) { // Write inputs into a temp file (one per line). { 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"; } @@ -196,11 +198,11 @@ TEST_F(CommandLineConvertTest, ConvertFromJson) { ASSERT_EQ(0, system(TestCommand(config, inputFile, outputFile).c_str())); // Read outputs and compare line by line. - std::ifstream ofs(outputFile, std::ios::binary); - ASSERT_TRUE(ofs.is_open()); + std::ifstream ifs(outputFile, std::ios::binary); + ASSERT_TRUE(ifs.is_open()); std::string line; size_t idx = 0; - while (std::getline(ofs, line)) { + while (std::getline(ifs, line)) { if (!line.empty() && line.back() == '\r') { line.pop_back(); // normalize Windows CRLF }