Skip to content

Commit aa5644b

Browse files
committed
feature: add a new ConfigDictValidationTest.cpp to be executed in bazel
1 parent 3047958 commit aa5644b

4 files changed

Lines changed: 222 additions & 0 deletions

File tree

data/config/BUILD.bazel

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,19 @@ filegroup(
44
name = "config",
55
srcs = glob(["*.json"]),
66
)
7+
8+
cc_test(
9+
name = "config_dict_validation_test",
10+
srcs = ["ConfigDictValidationTest.cpp"],
11+
data = [
12+
":config",
13+
"//data/dictionary:binary_dictionaries",
14+
"//test/testcases:testcases_json",
15+
],
16+
deps = [
17+
"//src:simple_converter",
18+
"@bazel_tools//tools/cpp/runfiles",
19+
"@googletest//:gtest_main",
20+
"@rapidjson",
21+
],
22+
)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Open Chinese Convert
3+
*
4+
* End-to-end validation of all configs against consolidated testcases.json.
5+
*/
6+
7+
#include <fstream>
8+
#include <memory>
9+
#include <sstream>
10+
#include <string>
11+
#include <unordered_map>
12+
13+
#include "gtest/gtest.h"
14+
#include "rapidjson/document.h"
15+
#include "src/SimpleConverter.hpp"
16+
17+
#ifdef BAZEL
18+
#include "tools/cpp/runfiles/runfiles.h"
19+
using bazel::tools::cpp::runfiles::Runfiles;
20+
#endif
21+
22+
namespace opencc {
23+
namespace {
24+
25+
class ConfigDictValidationTest : public ::testing::Test {
26+
protected:
27+
void SetUp() override {
28+
#ifdef BAZEL
29+
runfiles_.reset(Runfiles::CreateForTest());
30+
ASSERT_NE(nullptr, runfiles_);
31+
testcasesPath_ = runfiles_->Rlocation("_main/test/testcases/testcases.json");
32+
configDir_ = runfiles_->Rlocation("_main/data/config");
33+
dictDir_ = runfiles_->Rlocation("_main/data/dictionary");
34+
#else
35+
FAIL() << "This test expects Bazel runfiles.";
36+
#endif
37+
}
38+
39+
std::string ReadFile(const std::string& path) {
40+
std::ifstream ifs(path);
41+
EXPECT_TRUE(ifs.is_open()) << path;
42+
std::stringstream buffer;
43+
buffer << ifs.rdbuf();
44+
return buffer.str();
45+
}
46+
47+
SimpleConverter& GetConverter(const std::string& config) {
48+
auto it = converters_.find(config);
49+
if (it != converters_.end()) {
50+
return *it->second;
51+
}
52+
const std::string configPath = configDir_ + "/" + config + ".json";
53+
auto inserted = converters_.emplace(
54+
config,
55+
std::make_unique<SimpleConverter>(configPath,
56+
std::vector<std::string>{
57+
configDir_, dictDir_}));
58+
return *inserted.first->second;
59+
}
60+
61+
std::unique_ptr<Runfiles> runfiles_;
62+
std::string testcasesPath_;
63+
std::string configDir_;
64+
std::string dictDir_;
65+
std::unordered_map<std::string, std::unique_ptr<SimpleConverter>>
66+
converters_;
67+
};
68+
69+
TEST_F(ConfigDictValidationTest, ConvertExpectedOutputs) {
70+
const std::string json = ReadFile(testcasesPath_);
71+
rapidjson::Document doc;
72+
doc.Parse(json.c_str());
73+
ASSERT_FALSE(doc.HasParseError());
74+
ASSERT_TRUE(doc.IsObject());
75+
ASSERT_TRUE(doc.HasMember("cases"));
76+
const auto& cases = doc["cases"];
77+
ASSERT_TRUE(cases.IsArray());
78+
79+
for (auto& testcase : cases.GetArray()) {
80+
ASSERT_TRUE(testcase.IsObject());
81+
ASSERT_TRUE(testcase.HasMember("input"));
82+
ASSERT_TRUE(testcase["input"].IsString());
83+
const std::string input = testcase["input"].GetString();
84+
const std::string id =
85+
testcase.HasMember("id") && testcase["id"].IsString()
86+
? testcase["id"].GetString()
87+
: "(unknown id)";
88+
ASSERT_TRUE(testcase.HasMember("expected"));
89+
const auto& expectedObj = testcase["expected"];
90+
ASSERT_TRUE(expectedObj.IsObject());
91+
for (auto itr = expectedObj.MemberBegin(); itr != expectedObj.MemberEnd();
92+
++itr) {
93+
const std::string config = itr->name.GetString();
94+
ASSERT_TRUE(itr->value.IsString());
95+
const std::string expected = itr->value.GetString();
96+
SimpleConverter& converter = GetConverter(config);
97+
EXPECT_EQ(expected, converter.Convert(input))
98+
<< "config=" << config << " case=" << id;
99+
}
100+
}
101+
}
102+
103+
} // namespace
104+
} // namespace opencc

test/testcases/BUILD.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ filegroup(
44
name = "testcases",
55
srcs = glob(["*.in"]) + glob(["*.ans"]),
66
)
7+
8+
genrule(
9+
name = "testcases_json",
10+
srcs = glob(["*.in"]) + glob(["*.ans"]),
11+
tools = ["gen_testcases_json.py"],
12+
outs = ["testcases.json"],
13+
cmd = "python3 $(location gen_testcases_json.py) $(SRCS) > $@",
14+
)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Build a consolidated testcases.json from *.in/*.ans pairs.
4+
5+
Usage (Bazel genrule):
6+
python3 gen_testcases_json.py <file1> <file2> ... > testcases.json
7+
8+
Input files should include matching *.in and *.ans files. The script will
9+
combine lines by input text and emit:
10+
{
11+
"cases": [
12+
{
13+
"id": "case_001",
14+
"input": "...",
15+
"expected": {
16+
"s2t": "...",
17+
"t2s": "..."
18+
}
19+
},
20+
...
21+
]
22+
}
23+
"""
24+
25+
from __future__ import annotations
26+
27+
import json
28+
import os
29+
import sys
30+
from collections import OrderedDict, defaultdict
31+
from typing import Dict, List
32+
33+
34+
def collect_pairs(paths: List[str]) -> Dict[str, Dict[str, str]]:
35+
"""Collect matching .in/.ans files keyed by basename."""
36+
pairs: Dict[str, Dict[str, str]] = defaultdict(dict)
37+
for path in paths:
38+
base = os.path.basename(path)
39+
if base.endswith(".in"):
40+
pairs[base[:-3]]["in"] = path
41+
elif base.endswith(".ans"):
42+
pairs[base[:-4]]["ans"] = path
43+
return pairs
44+
45+
46+
def load_cases(pairs: Dict[str, Dict[str, str]]) -> OrderedDict:
47+
"""Load input/expected lines and merge by input string."""
48+
case_map: "OrderedDict[str, Dict[str, str]]" = OrderedDict()
49+
for cfg in sorted(pairs.keys()):
50+
entry = pairs[cfg]
51+
if "in" not in entry or "ans" not in entry:
52+
continue
53+
with open(entry["in"], "r", encoding="utf-8") as fin:
54+
inputs = fin.read().splitlines()
55+
with open(entry["ans"], "r", encoding="utf-8") as fans:
56+
answers = fans.read().splitlines()
57+
if len(inputs) != len(answers):
58+
raise ValueError(
59+
f"Line count mismatch for {entry['in']} ({len(inputs)}) vs "
60+
f"{entry['ans']} ({len(answers)})")
61+
for text, expected in zip(inputs, answers):
62+
if text not in case_map:
63+
case_map[text] = {}
64+
if cfg in case_map[text] and case_map[text][cfg] != expected:
65+
raise ValueError(
66+
f"Conflicting expectation for '{text}' in {cfg}: "
67+
f"'{case_map[text][cfg]}' vs '{expected}'")
68+
case_map[text][cfg] = expected
69+
return case_map
70+
71+
72+
def main(argv: List[str]) -> int:
73+
if len(argv) < 2:
74+
print("Usage: gen_testcases_json.py <files...>", file=sys.stderr)
75+
return 1
76+
77+
pairs = collect_pairs(argv[1:])
78+
case_map = load_cases(pairs)
79+
80+
cases = []
81+
for idx, (text, expectations) in enumerate(case_map.items(), start=1):
82+
cases.append({
83+
"id": f"case_{idx:03d}",
84+
"input": text,
85+
"expected": expectations,
86+
})
87+
88+
json.dump({"cases": cases}, sys.stdout, ensure_ascii=False, indent=2)
89+
sys.stdout.write("\n")
90+
return 0
91+
92+
93+
if __name__ == "__main__":
94+
sys.exit(main(sys.argv))

0 commit comments

Comments
 (0)