blob: 4e79e7156a736a5cfa20a92afda7aa9e32baae16 [file] [log] [blame]
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
*******************************************************************************
* Copyright (C) 2015, International Business Machines Corporation and *
* others. All Rights Reserved. *
*******************************************************************************
*/
package com.ibm.icu.dev.test.format;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.dev.test.TestUtil;
import com.ibm.icu.impl.Utility;
/**
* A collection of methods to run the data driven number format test suite.
*/
public class DataDrivenNumberFormatTestUtility extends TestFmwk {
/**
* Base class for code under test.
*/
public static abstract class CodeUnderTest {
/**
* Returns the ID of the code under test. This ID is used to identify
* tests that are known to fail for this particular code under test.
* This implementation returns null which means that by default all
* tests should work with this code under test.
* @return 'J' means ICU4J, 'K' means JDK
*/
public Character Id() {
return null;
}
/**
* Runs a single formatting test. On success, returns null.
* On failure, returns the error. This implementation just returns null.
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
public String format(DataDrivenNumberFormatTestData tuple) {
if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
/**
* Runs a single toPattern test. On success, returns null.
* On failure, returns the error. This implementation just returns null.
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
public String toPattern(DataDrivenNumberFormatTestData tuple) {
if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
/**
* Runs a single parse test. On success, returns null.
* On failure, returns the error. This implementation just returns null.
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
public String parse(DataDrivenNumberFormatTestData tuple) {
if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
/**
* Runs a single parse currency test. On success, returns null.
* On failure, returns the error. This implementation just returns null.
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
public String parseCurrency(DataDrivenNumberFormatTestData tuple) {
if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
/**
* Runs a single select test. On success, returns null.
* On failure, returns the error. This implementation just returns null.
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
public String select(DataDrivenNumberFormatTestData tuple) {
if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
}
private static enum RunMode {
SKIP_KNOWN_FAILURES,
INCLUDE_KNOWN_FAILURES,
CHECK_FOR_UNKNOWN_IDS
}
private final CodeUnderTest codeUnderTest;
private final String allowedIDs;
private String fileLine = null;
private int fileLineNumber = 0;
private String fileTestName = "";
private DataDrivenNumberFormatTestData tuple = new DataDrivenNumberFormatTestData();
/**
* Runs all the tests in the data driven test suite against codeUnderTest.
* @param fileName The name of the test file. A relative file name under
* com/ibm/icu/dev/data such as "data.txt"
* @param codeUnderTest the code under test
*/
public static void runSuite(
String fileName, CodeUnderTest codeUnderTest) {
new DataDrivenNumberFormatTestUtility(codeUnderTest)
.run(fileName, RunMode.SKIP_KNOWN_FAILURES);
}
/**
* Runs every format test in data driven test suite including those
* that are known to fail. If a test is supposed to fail but actually
* passes, an error is printed.
*
* @param fileName The name of the test file. A relative file name under
* com/ibm/icu/dev/data such as "data.txt"
* @param codeUnderTest the code under test
*/
public static void runFormatSuiteIncludingKnownFailures(
String fileName, CodeUnderTest codeUnderTest) {
new DataDrivenNumberFormatTestUtility(codeUnderTest)
.run(fileName, RunMode.INCLUDE_KNOWN_FAILURES);
}
/**
* Checks the data file for no unknown IDs in "breaks" columns.
*/
public static void checkNoUnknownIDs(String fileName, String allowedIDs) {
new DataDrivenNumberFormatTestUtility(allowedIDs).run(fileName, RunMode.CHECK_FOR_UNKNOWN_IDS);
}
private DataDrivenNumberFormatTestUtility(CodeUnderTest codeUnderTest) {
this.codeUnderTest = codeUnderTest;
this.allowedIDs = null;
}
private DataDrivenNumberFormatTestUtility(String allowedIDs) {
this.codeUnderTest = null;
this.allowedIDs = allowedIDs;
}
private void run(String fileName, RunMode runMode) {
char codeUnderTestId = (codeUnderTest == null) ? 0 : codeUnderTest.Id();
BufferedReader in = null;
try {
in = TestUtil.getDataReader("numberformattestspecification.txt", "UTF-8");
// read first line and remove BOM if present
readLine(in);
if (fileLine != null && fileLine.charAt(0) == '\uFEFF') {
fileLine = fileLine.substring(1);
}
int state = 0;
List<String> columnValues;
List<String> columnNames = null;
while (true) {
if (fileLine == null || fileLine.length() == 0) {
if (!readLine(in)) {
break;
}
if (fileLine.length() == 0 && state == 2) {
state = 0;
}
continue;
}
if (fileLine.startsWith("//")) {
fileLine = null;
continue;
}
// Initial setup of test.
if (state == 0) {
if (fileLine.startsWith("test ")) {
fileTestName = fileLine;
tuple = new DataDrivenNumberFormatTestData();
} else if (fileLine.startsWith("set ")) {
if (!setTupleField()) {
return;
}
} else if(fileLine.startsWith("begin")) {
state = 1;
} else {
showError("Unrecognized verb.");
return;
}
// column specification
} else if (state == 1) {
columnNames = splitBy((char) 0x09);
state = 2;
// "breaks" column must be last!
if (columnNames.contains("breaks")
&& columnNames.indexOf("breaks") != columnNames.size() - 1) {
showError("'breaks' column must be last!");
}
// run the tests
} else {
int columnNamesSize = columnNames.size();
columnValues = splitBy(columnNamesSize, (char) 0x09);
int columnValuesSize = columnValues.size();
for (int i = 0; i < columnValuesSize; ++i) {
if (!setField(columnNames.get(i), columnValues.get(i))) {
return;
}
}
for (int i = columnValuesSize; i < columnNamesSize; ++i) {
if (!clearField(columnNames.get(i))) {
return;
}
}
if (runMode == RunMode.CHECK_FOR_UNKNOWN_IDS) {
String actualIDs = tuple.breaks;
if (actualIDs != null) {
// Make sure there are no IDs in actualIDs that are not in allowedIDs.
// As a bonus, check that actualIDs are in alphabetical order.
char prevID = 0;
for (int i=0; i<actualIDs.length(); i++) {
char currID = actualIDs.charAt(i);
if (allowedIDs.indexOf(currID) == -1) {
showError("Unknown ID: " + currID);
}
if (prevID > currID) {
showError("IDs not in alphabetical order: " + actualIDs);
}
prevID = currID;
}
}
} else if (runMode == RunMode.INCLUDE_KNOWN_FAILURES || !breaks(codeUnderTestId)) {
String errorMessage;
Exception err = null;
boolean shouldFail = (tuple.output != null && tuple.output.equals("fail"))
? !breaks(codeUnderTestId)
: breaks(codeUnderTestId);
try {
errorMessage = isPass(tuple);
} catch (Exception e) {
err = e;
errorMessage = "Exception: " + e + ": " + e.getCause();
}
if (shouldFail && errorMessage == null) {
showError("Expected failure, but passed");
} else if (!shouldFail && errorMessage != null) {
if (err != null) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(os);
err.printStackTrace(ps);
String stackTrace = os.toString();
showError(errorMessage + " Stack trace: " + stackTrace.substring(0, 500));
} else {
showError(errorMessage);
}
}
}
}
fileLine = null;
}
} catch (Exception e) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(os);
e.printStackTrace(ps);
String stackTrace = os.toString();
showError("MAJOR ERROR: " + e.toString() + " Stack trace: " + stackTrace.substring(0,500));
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private boolean breaks(char code) {
String breaks = tuple.breaks == null ? "" : tuple.breaks;
return (breaks.toUpperCase().indexOf(code) != -1);
}
private static boolean isSpace(char c) {
return (c == 0x09 || c == 0x20 || c == 0x3000);
}
private boolean setTupleField() {
List<String> parts = splitBy(3, (char) 0x20);
if (parts.size() < 3) {
showError("Set expects 2 parameters");
return false;
}
return setField(parts.get(1), parts.get(2));
}
private boolean setField(String name, String value) {
try {
tuple.setField(name, Utility.unescape(value));
return true;
} catch (Exception e) {
showError("No such field: " + name + ", or bad value: " + value + ": " + e);
return false;
}
}
private boolean clearField(String name) {
try {
tuple.clearField(name);
return true;
} catch (Exception e) {
showError("Field cannot be clared: " + name);
return false;
}
}
private void showError(String message) {
TestFmwk.errln(String.format("line %d: %s\n%s\n%s", fileLineNumber, Utility.escape(message), fileTestName,fileLine));
}
private List<String> splitBy(char delimiter) {
return splitBy(Integer.MAX_VALUE, delimiter);
}
private List<String> splitBy(int max, char delimiter) {
ArrayList<String> result = new ArrayList<String>();
int colIdx = 0;
int colStart = 0;
int len = fileLine.length();
for (int idx = 0; colIdx < max - 1 && idx < len; ++idx) {
char ch = fileLine.charAt(idx);
if (ch == delimiter) {
result.add(
fileLine.substring(colStart, idx));
++colIdx;
colStart = idx + 1;
}
}
result.add(fileLine.substring(colStart, len));
return result;
}
private boolean readLine(BufferedReader in) throws IOException {
String line = in.readLine();
if (line == null) {
fileLine = null;
return false;
}
++fileLineNumber;
// Strip trailing comments and spaces
int idx = line.length();
for (; idx > 0; idx--) {
if (!isSpace(line.charAt(idx -1))) {
break;
}
}
fileLine = idx == 0 ? "" : line;
return true;
}
private String isPass(DataDrivenNumberFormatTestData tuple) {
StringBuilder result = new StringBuilder();
if (tuple.format != null && tuple.output != null) {
String errorMessage = codeUnderTest.format(tuple);
if (errorMessage != null) {
result.append(errorMessage);
}
} else if (tuple.toPattern != null || tuple.toLocalizedPattern != null) {
String errorMessage = codeUnderTest.toPattern(tuple);
if (errorMessage != null) {
result.append(errorMessage);
}
} else if (tuple.parse != null && tuple.output != null && tuple.outputCurrency != null) {
String errorMessage = codeUnderTest.parseCurrency(tuple);
if (errorMessage != null) {
result.append(errorMessage);
}
} else if (tuple.parse != null && tuple.output != null) {
String errorMessage = codeUnderTest.parse(tuple);
if (errorMessage != null) {
result.append(errorMessage);
}
} else if (tuple.plural != null) {
String errorMessage = codeUnderTest.select(tuple);
if (errorMessage != null) {
result.append(errorMessage);
}
} else {
result.append("Unrecognized test type.");
}
if (result.length() > 0) {
result.append(": ");
result.append(tuple);
return result.toString();
}
return null;
}
}