package hextostring.tests;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import hextostring.ConvertOptions;
import hextostring.convert.Converter;
import hextostring.convert.ConverterFactory;
import hextostring.debug.DebuggableStrings;
import hextostring.format.Formatter;
import hextostring.format.FormatterFactory;
import hextostring.replacement.Replacements;
import hextostring.utils.Charsets;
import hextostring.utils.IndentablePrintStream;
import main.MainOptions;
import main.options.domain.ValueOutOfDomainException;
/**
* Main test class.
*
* A test consists of an input and a corresponding expected,
* manually written output.
*
* Every input file is converted and compared to the expected output.
*
* Test files are put into the "tests" directory at the root of the project.
* Its architecture is as follows:
*
* tests
* |-sjis (contains tests for games using Shift JIS)
* | |-handmade (contains tests crafted without relying on a game)
* | |-[game1]
* | | |-cmd.txt (contains description of options optimized for this game)
* | | |-input
* | | | |-0001.txt
* | | | |-0002.txt
* | | | |-...
* | | |-expected_output
* | | |-0001.txt
* | | |-0002.txt
* | | |-...
* | |-[game2]
* | |-...
* |-utf16-be (contains tests for games using UTF-16 Big Endian)
* | |-[gameA]
* | | |-cmd.txt
* | | |-input
* | | | |-0001.txt
* | | | |-0002.txt
* | | | |-...
* | | |-expected_output
* | | |-0001.txt
* | | |-0002.txt
* | | |-...
* | |-[gameB]
* | |-...
* |-utf16-le (contains tests for games using UTF-16 Little Endian)
* | |-...
* |-utf8 (contains tests for games using UTF-8)
* |-...
*
* It is assumed that all .txt files are encoded in UTF-8, without the BOM.
* Their newline is always LF, never CR or CRLF
*
* @author Maxime PIA
*/
public class TestsLauncher {
private static Formatter formatter =
FormatterFactory.getFormatterInstance(true);
private static Converter currentConverter;
private static IndentablePrintStream out = new IndentablePrintStream();
private static final String INPUT_DIR = "input";
private static final String EXPECTED_OUTPUT_DIR = "expected_output";
private static final String CMD_FILE = "cmd.txt";
private static final String SJIS_DIR = "sjis";
private static final String UTF16BE_DIR = "utf16-be";
private static final String UTF16LE_DIR = "utf16-le";
private static final String UTF8_DIR = "utf8";
private static Logger logger = LogManager.getLogger(TestsLauncher.class);
private static File[] listSortedFiles(File f) {
File[] files = f.listFiles();
Arrays.sort(files);
return files;
}
private static ConvertOptions getConvertOptions(File cmdFile)
throws IOException, ValueOutOfDomainException {
String cmd = new String(
Files.readAllBytes(cmdFile.toPath()),
Charsets.UTF8
);
return new MainOptions(cmd.split(" ")).getConvertOptions();
}
private static boolean compare(File inputFile, File expectedOutputFile,
int indentLevel, ConvertOptions opts) {
try {
String input = new String(
Files.readAllBytes(inputFile.toPath()),
Charsets.UTF8
);
String expectedOutput = new String(
Files.readAllBytes(expectedOutputFile.toPath()),
Charsets.UTF8
);
DebuggableStrings dInput = currentConverter.convert(input);
formatter.format(dInput.getValidLineList());
String actualOutput = dInput.toString(
opts == null
? ConvertOptions.DEFAULT_DEBUGGING_FLAGS
: opts.getDebuggingFlags(),
opts == null
? ConvertOptions.DEFAULT_STRICTNESS
: opts.getStrictness()
);
out.print(inputFile.getName(), indentLevel);
// introduce tolerance to newline discrepancies
String cr = new String(Character.toChars(0xD));
String lf = new String(Character.toChars(0xA));
actualOutput = actualOutput.replace(cr + lf, lf).replace(cr, lf);
if (expectedOutput.equals(actualOutput)) {
out.println(" OK");
return true;
} else {
out.println(" FAILURE");
out.println("expected:", indentLevel + 1);
out.println(expectedOutput, indentLevel + 2);
out.println("instead of:", indentLevel + 1);
out.println(actualOutput, indentLevel + 2);
return false;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private static Map<Boolean, Integer> compareAll(File inputDirectory,
File expectedOutputDirectory, int indentLevel, ConvertOptions opts) {
File[] inputs = listSortedFiles(inputDirectory);
File[] expectedOutputs = listSortedFiles(expectedOutputDirectory);
Map<Boolean, Integer> testResult = new HashMap<>();
testResult.put(false, 0);
testResult.put(true, 0);
for (int i = 0; i < inputs.length && i < expectedOutputs.length; ++i) {
boolean eq =
compare(inputs[i], expectedOutputs[i], indentLevel, opts);
testResult.put(eq, testResult.get(eq) + 1);
}
return testResult;
}
private static boolean isEncoding(File f, String encodingName) {
return f == null
? false
: f.getName().equals(encodingName)
|| isEncoding(f.getParentFile(), encodingName);
}
private static void setEncoding(File f, boolean detectEncoding,
Replacements r) {
if (detectEncoding) {
currentConverter =
ConverterFactory.getConverterInstance(Charsets.DETECT, r);
} else if (isEncoding(f, SJIS_DIR)) {
currentConverter =
ConverterFactory.getConverterInstance(Charsets.SHIFT_JIS, r);
} else if (isEncoding(f, UTF16BE_DIR)) {
currentConverter =
ConverterFactory.getConverterInstance(Charsets.UTF16_BE, r);
} else if (isEncoding(f, UTF16LE_DIR)) {
currentConverter =
ConverterFactory.getConverterInstance(Charsets.UTF16_LE, r);
} else if (isEncoding(f, UTF8_DIR)) {
currentConverter =
ConverterFactory.getConverterInstance(Charsets.UTF8, r);
}
}
private static File getFileByName(File[] files, String name) {
for(File f : files) {
if (f.getName().equals(name)) {
return f;
}
}
return null;
}
private static Map<Boolean, Integer> goThrough(File f, int indentLevel,
boolean detectEncoding) throws IOException, ValueOutOfDomainException {
Map<Boolean, Integer> dirResult = new HashMap<>();
dirResult.put(false, 0);
dirResult.put(true, 0);
out.println(f.getName(), indentLevel);
if (f.isDirectory()) {
File[] files = listSortedFiles(f);
boolean goFurther = true;
List<Map<Boolean, Integer>> comparisonResults = new LinkedList<>();
if (indentLevel == 2) {
ConvertOptions convOpts = null;
File cmd = getFileByName(files, CMD_FILE);
if (cmd != null) {
convOpts = getConvertOptions(cmd);
}
setEncoding(f, detectEncoding, convOpts == null
? null
: convOpts.getReplacements());
comparisonResults.add(compareAll(
getFileByName(files, INPUT_DIR),
getFileByName(files, EXPECTED_OUTPUT_DIR),
indentLevel + 1,
convOpts
));
goFurther = false;
}
// recursively go through every directory
if (goFurther) {
for (File children : files) {
comparisonResults.add(
goThrough(children, indentLevel + 1, detectEncoding)
);
}
}
for (Map<Boolean, Integer> result : comparisonResults) {
dirResult.put(false, dirResult.get(false) + result.get(false));
dirResult.put(true, dirResult.get(true) + result.get(true));
}
}
displayTestResult(dirResult, f.getName(), indentLevel);
return dirResult;
}
private static void displayTestResult(Map<Boolean, Integer> result,
String testName, int indentLevel) {
double percentageOk =
result.get(true) * 100.0
/ (result.get(true) + result.get(false));
DecimalFormat df = new DecimalFormat("##0.00");
df.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US));
out.println("Total " + testName + ": "
+ df.format(percentageOk) + "% OK", indentLevel);
}
private static final String NODETECT_MSG = "Without encoding detection:\n";
private static final String DETECT_MSG = "With encoding detection:\n";
private static final String SEPARATOR = "\n----------\n\n";
private static void goThrough(String path) throws IOException,
ValueOutOfDomainException {
File f = new File(path);
out.println(NODETECT_MSG);
goThrough(f, 0, false);
out.println(SEPARATOR + DETECT_MSG);
goThrough(f, 0, true);
}
private static void compare(String inputPath, String expectedOutputPath,
ConvertOptions convOpts) {
File input = new File(inputPath);
if (!input.isFile()) {
throw new IllegalArgumentException(
input.getPath() + " doesn't exist."
);
}
File expectedOutput = new File(expectedOutputPath);
out.println(NODETECT_MSG);
setEncoding(input.getParentFile(), false, convOpts.getReplacements());
compare(input, expectedOutput, 0, convOpts);
out.println(SEPARATOR + DETECT_MSG);
setEncoding(input.getParentFile(), true, convOpts.getReplacements());
compare(input, expectedOutput, 0, convOpts);
}
/**
* Starts the test campaign.
*
* @param args
* args[0] = directory to test, without initial an final "/"
* args[1] = number identifying a test, without ".txt" (optional)
* @throws ValueOutOfDomainException
* @throws IOException
*/
public static void main(String[] args) throws IOException,
ValueOutOfDomainException {
logger.info("Starting conversion tests...");
String testsDirectory =
TestsLauncher.class.getResource("/tests").getPath();
if (args.length > 0) {
String directory = testsDirectory + args[0];
if (args.length > 1) {
String testFile = args[1] + ".txt";
File cmd = new File(directory + "/" + CMD_FILE);
ConvertOptions convOpts = cmd.exists()
? getConvertOptions(cmd)
: null;
compare(
directory + "/" + INPUT_DIR + "/" + testFile,
directory + "/" + EXPECTED_OUTPUT_DIR + "/" + testFile,
convOpts
);
} else {
goThrough(directory);
}
} else {
goThrough(testsDirectory);
}
logger.info("Conversion tests over.");
}
}