Skip to content

Commit e8118b2

Browse files
yoshito-umaokayumaoka
authored andcommitted
Improved AMDJS merge implemetation
- Changed AMDJS merge implementation to use Rhino JS parser. Previous implementation used regex matcher for each line, and trying to replace value. This implementation was fragile and it does not work various different cases. - Previous merge operation tried to wrap long lines, but the new implementation put translated value in a single line. We may revisit this and re-implement line wrapping logic with the new code base. - Fixed #148 caused by the previous merge implementation. - Previous AMDJS filter write/merge implementation did not escape string literal properly - such as `\n` for line feed, `\"` for quoataion and so on (note: parser worked OK because Rhino parser does unescaping). Added proper string literal escape code. - Added new data drive test framework. With this framework, we can easily add parse/write/merge test cases without writing new JUnit test methods. - Deleted old AMDJS test case files not used.
1 parent a628eca commit e8118b2

37 files changed

Lines changed: 642 additions & 494 deletions

gp-res-filter/src/main/java/com/ibm/g11n/pipeline/resfilter/impl/AmdJsResource.java

Lines changed: 181 additions & 214 deletions
Large diffs are not rendered by default.
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/*
2+
* Copyright IBM Corp. 2019
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.ibm.g11n.pipeline.resfilter;
17+
18+
import java.io.BufferedReader;
19+
import java.io.ByteArrayOutputStream;
20+
import java.io.IOException;
21+
import java.io.InputStream;
22+
import java.io.InputStreamReader;
23+
import java.nio.charset.Charset;
24+
import java.nio.charset.StandardCharsets;
25+
import java.util.LinkedHashMap;
26+
import java.util.Map;
27+
import java.util.Set;
28+
import java.util.TreeSet;
29+
30+
import org.junit.Assert;
31+
import org.junit.Test;
32+
33+
import com.google.gson.Gson;
34+
35+
/**
36+
* Data driven resource filter test
37+
*
38+
* @author Yoshito Umaoka
39+
*/
40+
public class DataDrivenResourceFilterTest {
41+
//
42+
// Data drive test runs resource filter methods on a set of test data files. A test data set may contain
43+
// following files.
44+
//
45+
// 1) bundle.json (required)
46+
//
47+
// Serialized LanguageBundle object in JSON format. This content is used as expected result for
48+
// parse test, and input for write/merge test. If this file is not found, the test case will be
49+
// skipped.
50+
//
51+
// 2) input.<ext>
52+
//
53+
// Resource file used for parse test. <ext> is appropriate file extension for the resource type,
54+
// such as .properties for Java property resource bundle, .json for JSON resource bundle.
55+
// This file is used as input for ResoruceFilter#parse method. The result will be compared with
56+
// the content of bundle.json. If absent, parse test will be skipped.
57+
//
58+
// 3) parse_options.json
59+
//
60+
// JSON expression of FilterOptions object used for parse test.
61+
//
62+
// 4) expected_write.<ext>
63+
//
64+
// Expected output for write test. A write test load the contents from bundle.json
65+
// A write test load the content from bundle.json, then writes out the content using ResourceFilter#write
66+
// method. The output will be compared with this file.
67+
//
68+
// 5) write_options.json
69+
//
70+
// JSON expression of FilterOptions object used for write test.
71+
//
72+
// 6) merge_base.<ext>
73+
//
74+
// Resource file used as base resource in merge test. A merge test loads bundle content from
75+
// bundle.json, then replace corresponding resource value in this resource file and emits the
76+
// result. The output will be compared with expected_merge.<ext> below.
77+
//
78+
// 7) expected_merge.<ext> (optional)
79+
//
80+
// Expected output for merge test above.
81+
//
82+
// 8) merge_options.json
83+
//
84+
// JSON expression of FilterOptions object used for merge test.
85+
//
86+
// A test of test cases are placed in folders in sequential numbers under test base path. For example,
87+
// AMDJS test cases are placed under data-driven-test-cases/AMDJS in test resource class path.
88+
//
89+
// data-driven-test-cases/AMDJS/1
90+
// data-driven-test-cases/AMDJS/2
91+
// data-driven-test-cases/AMDJS/3
92+
// ...
93+
//
94+
// To run these test cases, use a method runTest as below.
95+
//
96+
// runTest("data-driven-test-cases/AMDJS", ResourceFilterFactory.getResourceFilter("AMDJS"), "js", StandardCharsets.UTF_8);
97+
//
98+
// The method walks through test case folder starting from 1, and stops when a next sequential number
99+
// is missing (therefore, test case number must be contiguous).
100+
101+
/**
102+
* Runs a set of data driven resource filter tests for a filter.
103+
*
104+
* @param path Test case base path
105+
* @param filter An instance of ResourceFilter used for testing
106+
* @param fileExtension File extension for the target format
107+
* @param charset Charset used for reading/writing test resource files
108+
*/
109+
private void runTest(String path, ResourceFilter filter, String fileExtension, Charset charset) throws IOException, ResourceFilterException {
110+
Gson gson = new Gson();
111+
for (int i = 1;; i++) {
112+
String basePath = path + "/" + i;
113+
ClassLoader loader = this.getClass().getClassLoader();
114+
115+
// Load the content of bundle.json
116+
String bundlePath = basePath + "/bundle.json";
117+
InputStream bundleIs = loader.getResourceAsStream(bundlePath);
118+
if (bundleIs == null) {
119+
// No more test data
120+
break;
121+
}
122+
LanguageBundle bundle = null;
123+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(bundleIs, StandardCharsets.UTF_8))) {
124+
bundle = gson.fromJson(reader, LanguageBundle.class);
125+
}
126+
127+
// parse test - requires input.<ext>
128+
String inputPath = basePath + "/input." + fileExtension;
129+
InputStream inputIs = loader.getResourceAsStream(inputPath);
130+
131+
if (inputIs != null) {
132+
FilterOptions parseOptions = loadFilterOptions(basePath + "/parse_options.json", loader, gson);
133+
LanguageBundle parsedBundle = filter.parse(inputIs, parseOptions);
134+
compareBundles(bundle, parsedBundle, basePath + ":parse");
135+
}
136+
137+
138+
// write test - requires expected_write.<ext>
139+
String expectedWriteResult = loadContent(basePath + "/expected_write." + fileExtension, loader, charset);
140+
if (expectedWriteResult != null) {
141+
FilterOptions writeOptions = loadFilterOptions(basePath + "/write_options.json", loader, gson);
142+
ByteArrayOutputStream writeOs = new ByteArrayOutputStream();
143+
filter.write(writeOs, bundle, writeOptions);
144+
String writeResult = new String(writeOs.toByteArray(), charset);
145+
compareContent(expectedWriteResult, writeResult, basePath + ":write");
146+
}
147+
148+
// merge test - requires merge_base.<ext> and expected_merge.<ext>
149+
String mergeBasePath = basePath + "/merge_base." + fileExtension;
150+
InputStream mergeBaseIs = loader.getResourceAsStream(mergeBasePath);
151+
if (mergeBaseIs != null) {
152+
String expectedMergeResult = loadContent(basePath + "/expected_merge." + fileExtension, loader, charset);
153+
if (expectedMergeResult != null) {
154+
FilterOptions mergeOptions = loadFilterOptions(basePath + "/merge_options.json", loader, gson);
155+
ByteArrayOutputStream mergeOs = new ByteArrayOutputStream();
156+
filter.merge(mergeBaseIs, mergeOs, bundle, mergeOptions);
157+
String mergeResult = new String(mergeOs.toByteArray(), charset);
158+
compareContent(expectedMergeResult, mergeResult, basePath + ":merge");
159+
}
160+
}
161+
}
162+
}
163+
164+
private static FilterOptions loadFilterOptions(String path, ClassLoader loader, Gson gson) throws IOException {
165+
InputStream is = loader.getResourceAsStream(path);
166+
FilterOptions filterOptions = null;
167+
if (is != null) {
168+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
169+
filterOptions = gson.fromJson(reader, FilterOptions.class);
170+
}
171+
}
172+
return filterOptions;
173+
}
174+
175+
private static String loadContent(String path, ClassLoader loader, Charset charset) throws IOException {
176+
InputStream is = loader.getResourceAsStream(path);
177+
if (is == null) {
178+
return null;
179+
}
180+
181+
StringBuilder result = new StringBuilder();
182+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, charset))) {
183+
char[] buf = new char[1024];
184+
int len;
185+
while ((len = reader.read(buf)) >= 0) {
186+
result.append(buf, 0, len);
187+
}
188+
}
189+
190+
return result.toString();
191+
}
192+
193+
private static void compareBundles(LanguageBundle expected, LanguageBundle actual, String testDesc) {
194+
String msgBase = "[" + testDesc + "] ";
195+
Assert.assertEquals(msgBase + "Notes", expected.getNotes(), actual.getNotes());
196+
Assert.assertEquals(msgBase + "Metadata", expected.getMetadata(), actual.getMetadata());
197+
Assert.assertEquals(msgBase + "Embedded laguage code", expected.getEmbeddedLanguageCode(), actual.getEmbeddedLanguageCode());;
198+
Assert.assertEquals(msgBase + "Embedded source language code", expected.getEmbeddedSourceLanguageCode(), actual.getEmbeddedSourceLanguageCode());
199+
200+
// Compare resource strings
201+
Map<String, ResourceString> expMap = new LinkedHashMap<>();
202+
for (ResourceString res : expected.getResourceStrings()) {
203+
expMap.put(res.getKey(), res);
204+
}
205+
206+
Set<String> actKeys = new TreeSet<>();
207+
for (ResourceString actResString: actual.getResourceStrings()) {
208+
String key = actResString.getKey();
209+
ResourceString expResString = expMap.get(key);
210+
Assert.assertNotNull(msgBase + "Extra key - " + key, expResString);
211+
actKeys.add(key);
212+
Assert.assertEquals(msgBase + "ResourceString - " + key, expResString, actResString);
213+
}
214+
215+
Set<String> expKeys = new TreeSet<>(expMap.keySet());
216+
expKeys.removeAll(actKeys);
217+
Assert.assertTrue("Missing keys - " + expKeys, expKeys.isEmpty());
218+
}
219+
220+
private static void compareContent(String expected, String actual, String testDesc) {
221+
String msgBase = "[" + testDesc + "] ";
222+
223+
// compare line by line
224+
String[] actualLines = actual.split("\\n");
225+
String[] expectedLines = expected.split("\\n");
226+
227+
for (int idx = 0; idx < actualLines.length && idx < expectedLines.length; idx++) {
228+
int lineNo = idx + 1;
229+
Assert.assertEquals(msgBase + "Text at line: " + lineNo, expectedLines[idx], actualLines[idx]);
230+
}
231+
232+
Assert.assertFalse(msgBase + "Extra lines", expectedLines.length < actualLines.length);
233+
Assert.assertFalse(msgBase + "Missing lines", expectedLines.length > actualLines.length);
234+
}
235+
236+
@Test
237+
public void testAmdJs() throws IOException, ResourceFilterException {
238+
runTest("data-driven-test-cases/AMDJS", ResourceFilterFactory.getResourceFilter("AMDJS"), "js", StandardCharsets.UTF_8);
239+
}
240+
}

0 commit comments

Comments
 (0)