diff options
Diffstat (limited to 'src/cz/crcs/ectester/common')
29 files changed, 1089 insertions, 287 deletions
diff --git a/src/cz/crcs/ectester/common/cli/CLITools.java b/src/cz/crcs/ectester/common/cli/CLITools.java index 91f121f..a9d036e 100644 --- a/src/cz/crcs/ectester/common/cli/CLITools.java +++ b/src/cz/crcs/ectester/common/cli/CLITools.java @@ -22,7 +22,7 @@ public class CLITools { public static void help(String prog, String header, Options options, String footer, boolean usage) { HelpFormatter help = new HelpFormatter(); help.setOptionComparator(null); - help.printHelp(prog, header, options, footer, usage); + help.printHelp(Colors.bold(prog), header, options, footer, usage); } private static void help(HelpFormatter help, PrintWriter pw, CommandLineParser cli, Options opts, int depth) { @@ -37,7 +37,8 @@ public class CLITools { } tp.getParsers().forEach((key, value) -> { pw.println(); - help.printWrapped(pw, HelpFormatter.DEFAULT_WIDTH, String.format("%" + depth + "s" + key + ":", " ")); + String description = value.getDescription() == null ? "" : " | " + value.getDescription() + " |"; + help.printWrapped(pw, HelpFormatter.DEFAULT_WIDTH, String.format("%" + depth + "s" + key + ":" + description, " ")); CLITools.help(help, pw, value.getParser(), value.getOptions(), depth + 1); }); } @@ -96,7 +97,7 @@ public class CLITools { StringWriter uw = new StringWriter(); PrintWriter upw = new PrintWriter(uw); usage(help, upw, baseParser, baseOpts); - pw.print("usage: " + prog); + pw.print("usage: " + Colors.bold(prog)); help.printWrapped(pw, HelpFormatter.DEFAULT_WIDTH, uw.toString()); upw.close(); pw.println(); diff --git a/src/cz/crcs/ectester/common/cli/Colors.java b/src/cz/crcs/ectester/common/cli/Colors.java new file mode 100644 index 0000000..7601088 --- /dev/null +++ b/src/cz/crcs/ectester/common/cli/Colors.java @@ -0,0 +1,97 @@ +package cz.crcs.ectester.common.cli; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * @author Diogo Nunes + * @author Jan Jancar johny@neuromancer.sk + * Adapted from https://github.com/dialex/JCDP/ + */ +public class Colors { + public static boolean enabled = false; + + public interface ANSIParam { + } + + public enum Foreground implements ANSIParam { + BLACK("30"), RED("31"), GREEN("32"), YELLOW("33"), BLUE("34"), MAGENTA("35"), CYAN("36"), WHITE("37"), NONE(""); + private final String code; + + Foreground(String code) { + this.code = code; + } + + @Override + public String toString() { + return code; + } + } + + public enum Background implements ANSIParam { + BLACK("40"), RED("41"), GREEN("42"), YELLOW("43"), BLUE("44"), MAGENTA("45"), CYAN("46"), WHITE("47"), NONE(""); + private final String code; + + Background(String code) { + this.code = code; + } + + @Override + public String toString() { + return code; + } + } + + public enum Attribute implements ANSIParam { + CLEAR("0"), BOLD("1"), LIGHT("1"), DARK("2"), UNDERLINE("4"), REVERSE("7"), HIDDEN("8"), NONE(""); + private final String code; + + Attribute(String code) { + this.code = code; + } + + @Override + public String toString() { + return code; + } + } + + private static final String PREFIX = "\033["; + private static final String SEPARATOR = ";"; + private static final String POSTFIX = "m"; + + public static String colored(String text, ANSIParam... params) { + if (!enabled) { + return text; + } + Optional<Foreground> fg = Arrays.stream(params).filter(Foreground.class::isInstance).map(Foreground.class::cast).findFirst(); + Optional<Background> bg = Arrays.stream(params).filter(Background.class::isInstance).map(Background.class::cast).findFirst(); + List<Attribute> attr = Arrays.stream(params).filter(Attribute.class::isInstance).distinct().map(Attribute.class::cast).collect(Collectors.toList()); + + List<ANSIParam> apply = new LinkedList<>(); + apply.addAll(attr); + fg.ifPresent(apply::add); + bg.ifPresent(apply::add); + List<String> codes = apply.stream().map(Object::toString).collect(Collectors.toList()); + return PREFIX + String.join(SEPARATOR, codes) + POSTFIX + text + PREFIX + Attribute.CLEAR + POSTFIX; + } + + public static String error(String text) { + return colored(text, Foreground.RED, Attribute.BOLD); + } + + public static String ok(String text) { + return colored(text, Foreground.GREEN, Attribute.BOLD); + } + + public static String bold(String text) { + return colored(text, Attribute.BOLD); + } + + public static String underline(String text) { + return colored(text, Attribute.UNDERLINE); + } +}
\ No newline at end of file diff --git a/src/cz/crcs/ectester/common/cli/ParserOptions.java b/src/cz/crcs/ectester/common/cli/ParserOptions.java index ee2097e..7300cbb 100644 --- a/src/cz/crcs/ectester/common/cli/ParserOptions.java +++ b/src/cz/crcs/ectester/common/cli/ParserOptions.java @@ -3,25 +3,22 @@ package cz.crcs.ectester.common.cli; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.Options; -import java.util.Collections; -import java.util.List; - /** * @author Jan Jancar johny@neuromancer.sk */ public class ParserOptions { private CommandLineParser parser; private Options options; - private List<Argument> arguments; + private String description; public ParserOptions(CommandLineParser parser, Options options) { this.parser = parser; this.options = options; } - public ParserOptions(CommandLineParser parser, Options options, List<Argument> arguments) { + public ParserOptions(CommandLineParser parser, Options options, String description) { this(parser, options); - this.arguments = arguments; + this.description = description; } public CommandLineParser getParser() { @@ -32,7 +29,7 @@ public class ParserOptions { return options; } - public List<Argument> getArguments() { - return Collections.unmodifiableList(arguments); + public String getDescription() { + return description; } } diff --git a/src/cz/crcs/ectester/common/cli/TreeParser.java b/src/cz/crcs/ectester/common/cli/TreeParser.java index f1a1980..23f59b1 100644 --- a/src/cz/crcs/ectester/common/cli/TreeParser.java +++ b/src/cz/crcs/ectester/common/cli/TreeParser.java @@ -82,7 +82,7 @@ public class TreeParser implements CommandLineParser { } } else { if (required) { - throw new MissingOptionException(new ArrayList(parsers.keySet())); + throw new MissingOptionException(new ArrayList<>(parsers.keySet())); } } diff --git a/src/cz/crcs/ectester/common/ec/EC_Category.java b/src/cz/crcs/ectester/common/ec/EC_Category.java index 32a788d..9c65f3b 100644 --- a/src/cz/crcs/ectester/common/ec/EC_Category.java +++ b/src/cz/crcs/ectester/common/ec/EC_Category.java @@ -1,5 +1,7 @@ package cz.crcs.ectester.common.ec; +import cz.crcs.ectester.common.cli.Colors; + import java.util.Collections; import java.util.Map; import java.util.Objects; @@ -72,13 +74,13 @@ public class EC_Category { @Override public String toString() { StringBuilder out = new StringBuilder(); - out.append("\t- ").append(name).append((desc == null || desc.equals("")) ? "" : ": " + desc); + out.append("\t- ").append(Colors.bold(name)).append((desc == null || desc.equals("")) ? "" : ": " + desc); out.append(System.lineSeparator()); Map<String, EC_Curve> curves = getObjects(EC_Curve.class); int size = curves.size(); if (size > 0) { - out.append("\t\tCurves: "); + out.append(Colors.bold("\t\tCurves: ")); for (Map.Entry<String, EC_Curve> curve : curves.entrySet()) { out.append(curve.getKey()); size--; @@ -91,7 +93,7 @@ public class EC_Category { Map<String, EC_Key> keys = getObjects(EC_Key.class); size = keys.size(); if (size > 0) { - out.append("\t\tKeys: "); + out.append(Colors.bold("\t\tKeys: ")); for (Map.Entry<String, EC_Key> key : keys.entrySet()) { out.append(key.getKey()); size--; @@ -104,7 +106,7 @@ public class EC_Category { Map<String, EC_Keypair> keypairs = getObjects(EC_Keypair.class); size = keypairs.size(); if (size > 0) { - out.append("\t\tKeypairs: "); + out.append(Colors.bold("\t\tKeypairs: ")); for (Map.Entry<String, EC_Keypair> key : keypairs.entrySet()) { out.append(key.getKey()); size--; @@ -117,7 +119,7 @@ public class EC_Category { Map<String, EC_KAResult> results = getObjects(EC_KAResult.class); size = results.size(); if (size > 0) { - out.append("\t\tResults: "); + out.append(Colors.bold("\t\tResults: ")); for (Map.Entry<String, EC_KAResult> result : results.entrySet()) { out.append(result.getKey()); size--; diff --git a/src/cz/crcs/ectester/common/ec/EC_Curve.java b/src/cz/crcs/ectester/common/ec/EC_Curve.java index 173fd29..6c0d060 100644 --- a/src/cz/crcs/ectester/common/ec/EC_Curve.java +++ b/src/cz/crcs/ectester/common/ec/EC_Curve.java @@ -51,10 +51,10 @@ public class EC_Curve extends EC_Params { @Override public String toString() { - return "<" + getId() + "> " + (field == KeyPair.ALG_EC_FP ? "Prime" : "Binary") + " field Elliptic curve (" + String.valueOf(bits) + "b)" + (desc == null ? "" : ": " + desc); + return "<" + getId() + "> " + (field == KeyPair.ALG_EC_FP ? "Prime" : "Binary") + " field Elliptic curve (" + String.valueOf(bits) + "b)" + (desc == null ? "" : ": " + desc) + System.lineSeparator() + super.toString(); } - public ECParameterSpec toSpec() { + public EllipticCurve toCurve() { ECField field; if (this.field == KeyPair.ALG_EC_FP) { field = new ECFieldFp(new BigInteger(1, getData(0))); @@ -71,7 +71,11 @@ public class EC_Curve extends EC_Params { BigInteger a = new BigInteger(1, getParam(EC_Consts.PARAMETER_A)[0]); BigInteger b = new BigInteger(1, getParam(EC_Consts.PARAMETER_B)[0]); - EllipticCurve curve = new EllipticCurve(field, a, b); + return new EllipticCurve(field, a, b); + } + + public ECParameterSpec toSpec() { + EllipticCurve curve = toCurve(); byte[][] G = getParam(EC_Consts.PARAMETER_G); BigInteger gx = new BigInteger(1, G[0]); diff --git a/src/cz/crcs/ectester/common/ec/EC_Data.java b/src/cz/crcs/ectester/common/ec/EC_Data.java index c048ef7..abe6e93 100644 --- a/src/cz/crcs/ectester/common/ec/EC_Data.java +++ b/src/cz/crcs/ectester/common/ec/EC_Data.java @@ -14,7 +14,7 @@ import java.util.regex.Pattern; * * @author Jan Jancar johny@neuromancer.sk */ -public abstract class EC_Data { +public abstract class EC_Data implements Comparable<EC_Data> { String id; int count; byte[][] data; @@ -123,7 +123,7 @@ public abstract class EC_Data { public boolean readCSV(InputStream in) { Scanner s = new Scanner(in); - s.useDelimiter(",|;"); + s.useDelimiter("[,;]"); List<String> data = new LinkedList<>(); while (s.hasNext()) { String field = s.next(); @@ -214,4 +214,52 @@ public abstract class EC_Data { } return Arrays.deepHashCode(this.data); } + + @Override + public int compareTo(EC_Data o) { + if (o == this) return 0; + if (this.id != null && o.id != null) { + + int minLength = Math.min(this.id.length(), o.id.length()); + for (int i = 0; i < minLength; i++) { + if (this.id.charAt(i) != o.id.charAt(i)) { + String thisEnd = this.id.substring(i); + String oEnd = o.id.substring(i); + try { + int thisIndex = Integer.parseInt(thisEnd); + int oIndex = Integer.parseInt(oEnd); + return Integer.compare(thisIndex, oIndex); + } catch (NumberFormatException ignored) { + break; + } + } + } + return this.id.compareTo(o.id); + } else if (this.id == null && o.id == null) { + if (Arrays.equals(this.data, o.data)) { + return 0; + } else { + int minCount = (this.count < o.count) ? this.count : o.count; + for (int i = 0; i < minCount; ++i) { + byte[] thisData = this.data[i]; + byte[] oData = o.data[i]; + int innerMinCount = (thisData.length < oData.length) ? thisData.length : oData.length; + for (int j = 0; j < innerMinCount; ++j) { + if (thisData[j] < oData[j]) { + return -1; + } else if (thisData[j] > oData[j]) { + return 1; + } + } + } + } + } else { + if (this.id == null) { + return -1; + } else { + return 1; + } + } + return 0; + } } diff --git a/src/cz/crcs/ectester/common/ec/EC_KAResult.java b/src/cz/crcs/ectester/common/ec/EC_KAResult.java index 8a5fcb4..4e97950 100644 --- a/src/cz/crcs/ectester/common/ec/EC_KAResult.java +++ b/src/cz/crcs/ectester/common/ec/EC_KAResult.java @@ -59,7 +59,7 @@ public class EC_KAResult extends EC_Data { @Override public String toString() { - return "<" + getId() + "> " + ka + " result over " + curve + ", " + oneKey + " + " + otherKey + (desc == null ? "" : ": " + desc); + return "<" + getId() + "> " + ka + " result over " + curve + ", " + oneKey + " + " + otherKey + (desc == null ? "" : ": " + desc) + System.lineSeparator() + super.toString(); } } diff --git a/src/cz/crcs/ectester/common/ec/EC_Key.java b/src/cz/crcs/ectester/common/ec/EC_Key.java index a34b0e7..754775d 100644 --- a/src/cz/crcs/ectester/common/ec/EC_Key.java +++ b/src/cz/crcs/ectester/common/ec/EC_Key.java @@ -54,7 +54,7 @@ public class EC_Key extends EC_Params { @Override public String toString() { - return "<" + getId() + "> EC Public key, over " + getCurve() + (getDesc() == null ? "" : ": " + getDesc()); + return "<" + getId() + "> EC Public key, over " + getCurve() + (getDesc() == null ? "" : ": " + getDesc()) + System.lineSeparator() + super.toString(); } } @@ -77,7 +77,7 @@ public class EC_Key extends EC_Params { @Override public String toString() { - return "<" + getId() + "> EC Private key, over " + getCurve() + (getDesc() == null ? "" : ": " + getDesc()); + return "<" + getId() + "> EC Private key, over " + getCurve() + (getDesc() == null ? "" : ": " + getDesc()) + System.lineSeparator() + super.toString(); } } } diff --git a/src/cz/crcs/ectester/common/ec/EC_Keypair.java b/src/cz/crcs/ectester/common/ec/EC_Keypair.java index 53632cd..24ddba7 100644 --- a/src/cz/crcs/ectester/common/ec/EC_Keypair.java +++ b/src/cz/crcs/ectester/common/ec/EC_Keypair.java @@ -36,6 +36,6 @@ public class EC_Keypair extends EC_Params { @Override public String toString() { - return "<" + getId() + "> EC Keypair, over " + curve + (desc == null ? "" : ": " + desc); + return "<" + getId() + "> EC Keypair, over " + curve + (desc == null ? "" : ": " + desc) + System.lineSeparator() + super.toString(); } } diff --git a/src/cz/crcs/ectester/common/ec/EC_Params.java b/src/cz/crcs/ectester/common/ec/EC_Params.java index 1c066e7..b08fdfd 100644 --- a/src/cz/crcs/ectester/common/ec/EC_Params.java +++ b/src/cz/crcs/ectester/common/ec/EC_Params.java @@ -9,10 +9,11 @@ import java.util.List; /** * A list of EC parameters, can contain a subset of the Fp/F2M, A, B, G, R, K, W, S parameters. - * + * <p> * The set of parameters is uniquely identified by a short bit string. * The parameters can be exported to a byte array via <code>flatten()</code> or to a comma delimited * string via <code>expand()</code>. + * * @author Jan Jancar johny@neuromancer.sk */ public class EC_Params extends EC_Data { @@ -61,15 +62,15 @@ public class EC_Params extends EC_Data { if (masked == EC_Consts.PARAMETER_F2M) { result = new byte[4][]; result[0] = data[i].clone(); - result[1] = data[i+1].clone(); - result[2] = data[i+2].clone(); - result[3] = data[i+3].clone(); + result[1] = data[i + 1].clone(); + result[2] = data[i + 2].clone(); + result[3] = data[i + 3].clone(); break; } if (masked == EC_Consts.PARAMETER_G || masked == EC_Consts.PARAMETER_W) { result = new byte[2][]; result[0] = data[i].clone(); - result[1] = data[i+1].clone(); + result[1] = data[i + 1].clone(); break; } result = new byte[1][]; @@ -189,6 +190,6 @@ public class EC_Params extends EC_Data { } paramMask = (short) (paramMask << 1); } - return out.toArray(new String[out.size()]); + return out.toArray(new String[0]); } } diff --git a/src/cz/crcs/ectester/common/output/BaseTextTestWriter.java b/src/cz/crcs/ectester/common/output/BaseTextTestWriter.java index 29eb671..ee55069 100644 --- a/src/cz/crcs/ectester/common/output/BaseTextTestWriter.java +++ b/src/cz/crcs/ectester/common/output/BaseTextTestWriter.java @@ -1,16 +1,25 @@ package cz.crcs.ectester.common.output; +import cz.crcs.ectester.common.cli.Colors; import cz.crcs.ectester.common.test.*; import java.io.PrintStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; /** + * An absctract basis of a TextTestWriter, which outputs in a human readable format, into console. + * Requires the implementation of: + * <code>String testableString(Testable t)</code> + * <code>String deviceString(TestSuite t)</code> + * * @author Jan Jancar johny@neuromancer.sk */ public abstract class BaseTextTestWriter implements TestWriter { private PrintStream output; - public static int BASE_WIDTH = 90; + public static int BASE_WIDTH = 105; public BaseTextTestWriter(PrintStream output) { this.output = output; @@ -18,43 +27,76 @@ public abstract class BaseTextTestWriter implements TestWriter { @Override public void begin(TestSuite suite) { - output.println("═══ Running test suite: " + suite.getName() + " ═══"); - output.println("═══ " + suite.getDescription()); + output.println("═══ " + Colors.underline("Running test suite:") + " " + Colors.bold(suite.getName()) + " ═══"); + for (String d : suite.getDescription()) { + output.println("═══ " + d); + } + DateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss"); + Date date = new Date(); + output.println("═══ " + Colors.underline("Date:") + " " + dateFormat.format(date)); output.print(deviceString(suite)); } + /** + * @param t + * @return + */ protected abstract String testableString(Testable t); + /** + * @param suite + * @return + */ protected abstract String deviceString(TestSuite suite); - private String testString(Test t, String prefix) { - if (!t.hasRun()) { - return null; - } + private String testString(Test t, String prefix, int index) { boolean compound = t instanceof CompoundTest; + Result result = t.getResult(); + + String line = ""; + if (prefix.equals("")) { + char charLine[] = new char[BASE_WIDTH + 24]; + new String(new char[BASE_WIDTH + 24]).replace("\0", "━").getChars(0, charLine.length - 1, charLine, 0); + charLine[0] = '■'; + charLine[4] = '┳'; + charLine[BASE_WIDTH + 1] = '┳'; + charLine[BASE_WIDTH + 13] = '┳'; + charLine[BASE_WIDTH + 23] = '┓'; + line = new String(charLine) + System.lineSeparator(); + } + StringBuilder out = new StringBuilder(); - out.append(t.ok() ? " OK " : "NOK "); - out.append(compound ? "┳ " : "━ "); - int width = BASE_WIDTH - (prefix.length() + out.length()); + out.append(t.ok() ? Colors.ok(" OK ") : Colors.error("NOK ")); + out.append(compound ? (prefix.equals("") ? "╋ " : "┳ ") : "━ "); + int width = BASE_WIDTH - (prefix.length() + 6); String widthSpec = "%-" + String.valueOf(width) + "s"; - out.append(String.format(widthSpec, t.getDescription())); + String desc = ((prefix.equals("")) ? "(" + index + ") " : "") + t.getDescription(); + out.append(String.format(widthSpec, desc)); out.append(" ┃ "); - out.append(String.format("%-9s", t.getResultValue().name())); + Colors.Foreground valueColor; + if (result.getValue().ok()) { + valueColor = Colors.Foreground.GREEN; + } else if (result.getValue().equals(Result.Value.ERROR)) { + valueColor = Colors.Foreground.RED; + } else { + valueColor = Colors.Foreground.YELLOW; + } + out.append(Colors.colored(String.format("%-9s", result.getValue().name()), Colors.Attribute.BOLD, valueColor)); out.append(" ┃ "); if (compound) { CompoundTest test = (CompoundTest) t; - out.append(test.getResultCause()); + out.append(String.valueOf(result.getCause())); out.append(System.lineSeparator()); - Test[] tests = test.getTests(); + Test[] tests = test.getStartedTests(); for (int i = 0; i < tests.length; ++i) { if (i == tests.length - 1) { out.append(prefix).append(" ┗ "); - out.append(testString(tests[i], prefix + " ")); + out.append(testString(tests[i], prefix + " ", index)); } else { out.append(prefix).append(" ┣ "); - out.append(testString(tests[i], prefix + " ┃ ")); + out.append(testString(tests[i], prefix + " ┃ ", index)); } if (i != tests.length - 1) { @@ -62,17 +104,38 @@ public abstract class BaseTextTestWriter implements TestWriter { } } } else { - SimpleTest test = (SimpleTest) t; + SimpleTest<? extends BaseTestable> test = (SimpleTest<? extends BaseTestable>) t; out.append(testableString(test.getTestable())); } - return out.toString(); + return line + out.toString(); } @Override - public void outputTest(Test t) { + public void outputTest(Test t, int index) { if (!t.hasRun()) return; - output.println(testString(t, "")); + output.println(testString(t, "", index)); + output.flush(); + } + + private String errorString(Throwable error) { + StringBuilder sb = new StringBuilder(); + for (Throwable t = error; t != null; t = t.getCause()) { + sb.append("═══ ").append(t.toString()).append(" ═══"); + sb.append(System.lineSeparator()); + } + sb.append("═══ ═══").append(System.lineSeparator()); + for (StackTraceElement s : error.getStackTrace()) { + sb.append("═══ ").append(s.toString()).append(" ═══"); + sb.append(System.lineSeparator()); + } + return sb.toString(); + } + + @Override + public void outputError(Test t, Throwable cause, int index) { + output.println(testString(t, "", index)); + output.print(errorString(cause)); output.flush(); } diff --git a/src/cz/crcs/ectester/common/output/BaseXMLTestWriter.java b/src/cz/crcs/ectester/common/output/BaseXMLTestWriter.java index f3e9411..53970dd 100644 --- a/src/cz/crcs/ectester/common/output/BaseXMLTestWriter.java +++ b/src/cz/crcs/ectester/common/output/BaseXMLTestWriter.java @@ -15,6 +15,9 @@ import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.OutputStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; /** * @author Jan Jancar johny@neuromancer.sk @@ -24,6 +27,7 @@ public abstract class BaseXMLTestWriter implements TestWriter { private DocumentBuilder db; protected Document doc; private Node root; + private Node tests; public BaseXMLTestWriter(OutputStream output) throws ParserConfigurationException { this.output = output; @@ -35,28 +39,65 @@ public abstract class BaseXMLTestWriter implements TestWriter { doc = db.newDocument(); Element rootElem = doc.createElement("testSuite"); rootElem.setAttribute("name", suite.getName()); - rootElem.setAttribute("desc", suite.getDescription()); + rootElem.setAttribute("desc", suite.getTextDescription()); + DateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss"); + Date date = new Date(); + rootElem.setAttribute("date", dateFormat.format(date)); root = rootElem; doc.appendChild(root); root.appendChild(deviceElement(suite)); + tests = doc.createElement("tests"); + root.appendChild(tests); } protected abstract Element testableElement(Testable t); protected abstract Element deviceElement(TestSuite suite); - private Element testElement(Test t) { + private String causeString(Object cause) { + if (cause == null) { + return "null"; + } else if (cause instanceof Throwable) { + StringBuilder sb = new StringBuilder(); + for (Throwable t = (Throwable) cause; t != null; t = t.getCause()) { + sb.append(t.toString()); + sb.append(System.lineSeparator()); + } + return sb.toString(); + } else { + return cause.toString(); + } + } + + private Element resultElement(Result result) { + Element resultElem = doc.createElement("result"); + + Element ok = doc.createElement("ok"); + ok.setTextContent(String.valueOf(result.ok())); + Element value = doc.createElement("value"); + value.setTextContent(result.getValue().name()); + Element cause = doc.createElement("cause"); + cause.setTextContent(causeString(result.getCause())); + + resultElem.appendChild(ok); + resultElem.appendChild(value); + resultElem.appendChild(cause); + + return resultElem; + } + + private Element testElement(Test t, int index) { Element testElem; if (t instanceof CompoundTest) { CompoundTest test = (CompoundTest) t; testElem = doc.createElement("test"); testElem.setAttribute("type", "compound"); - for (Test innerTest : test.getTests()) { - testElem.appendChild(testElement(innerTest)); + for (Test innerTest : test.getStartedTests()) { + testElem.appendChild(testElement(innerTest, -1)); } } else { - SimpleTest test = (SimpleTest) t; + SimpleTest<? extends BaseTestable> test = (SimpleTest<? extends BaseTestable>) t; testElem = testableElement(test.getTestable()); } @@ -64,26 +105,26 @@ public abstract class BaseXMLTestWriter implements TestWriter { description.setTextContent(t.getDescription()); testElem.appendChild(description); - Element result = doc.createElement("result"); - Element ok = doc.createElement("ok"); - ok.setTextContent(String.valueOf(t.ok())); - Element value = doc.createElement("value"); - value.setTextContent(t.getResultValue().name()); - Element cause = doc.createElement("cause"); - cause.setTextContent(t.getResultCause()); - result.appendChild(ok); - result.appendChild(value); - result.appendChild(cause); + Element result = resultElement(t.getResult()); testElem.appendChild(result); + if (index != -1) { + testElem.setAttribute("index", String.valueOf(index)); + } + return testElem; } @Override - public void outputTest(Test t) { + public void outputTest(Test t, int index) { if (!t.hasRun()) return; - root.appendChild(testElement(t)); + tests.appendChild(testElement(t, index)); + } + + @Override + public void outputError(Test t, Throwable cause, int index) { + tests.appendChild(testElement(t, index)); } @Override diff --git a/src/cz/crcs/ectester/common/output/BaseYAMLTestWriter.java b/src/cz/crcs/ectester/common/output/BaseYAMLTestWriter.java index 0769e83..e054563 100644 --- a/src/cz/crcs/ectester/common/output/BaseYAMLTestWriter.java +++ b/src/cz/crcs/ectester/common/output/BaseYAMLTestWriter.java @@ -5,10 +5,9 @@ import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; import java.io.PrintStream; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; /** * @author Jan Jancar johny@neuromancer.sk @@ -26,12 +25,15 @@ public abstract class BaseYAMLTestWriter implements TestWriter { @Override public void begin(TestSuite suite) { output.println("---"); - testRun = new HashMap<>(); - testSuite = new HashMap<>(); + testRun = new LinkedHashMap<>(); + testSuite = new LinkedHashMap<>(); tests = new LinkedList<>(); testSuite.put("name", suite.getName()); - testSuite.put("desc", suite.getDescription()); + testSuite.put("desc", suite.getTextDescription()); + DateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss"); + Date date = new Date(); + testRun.put("date", dateFormat.format(date)); testRun.put("suite", testSuite); testRun.put("device", deviceObject(suite)); testRun.put("tests", tests); @@ -41,37 +43,64 @@ public abstract class BaseYAMLTestWriter implements TestWriter { abstract protected Map<String, Object> deviceObject(TestSuite suite); - private Map<String, Object> testObject(Test t) { + private Object causeObject(Object cause) { + if (cause == null) { + return null; + } else if (cause instanceof Throwable) { + StringBuilder sb = new StringBuilder(); + for (Throwable t = (Throwable) cause; t != null; t = t.getCause()) { + sb.append(t.toString()); + sb.append(System.lineSeparator()); + } + return sb.toString(); + } else { + return cause.toString(); + } + } + + private Map<String, Object> resultObject(Result result) { + Map<String, Object> resultObject = new LinkedHashMap<>(); + resultObject.put("ok", result.ok()); + resultObject.put("value", result.getValue().name()); + resultObject.put("cause", causeObject(result.getCause())); + return resultObject; + } + + private Map<String, Object> testObject(Test t, int index) { Map<String, Object> testObj; if (t instanceof CompoundTest) { CompoundTest test = (CompoundTest) t; testObj = new HashMap<>(); testObj.put("type", "compound"); List<Map<String, Object>> innerTests = new LinkedList<>(); - for (Test innerTest : test.getTests()) { - innerTests.add(testObject(innerTest)); + for (Test innerTest : test.getStartedTests()) { + innerTests.add(testObject(innerTest, -1)); } testObj.put("tests", innerTests); } else { - SimpleTest test = (SimpleTest) t; + SimpleTest<? extends BaseTestable> test = (SimpleTest<? extends BaseTestable>) t; testObj = testableObject(test.getTestable()); } testObj.put("desc", t.getDescription()); - Map<String, Object> result = new HashMap<>(); - result.put("ok", t.ok()); - result.put("value", t.getResultValue().name()); - result.put("cause", t.getResultCause()); - testObj.put("result", result); + testObj.put("result", resultObject(t.getResult())); + if (index != -1) { + testObj.put("index", index); + } return testObj; } @Override - public void outputTest(Test t) { + public void outputTest(Test t, int index) { if (!t.hasRun()) return; - tests.add(testObject(t)); + tests.add(testObject(t, index)); + } + + @Override + public void outputError(Test t, Throwable cause, int index) { + tests.add(testObject(t, index)); } @Override @@ -81,7 +110,7 @@ public abstract class BaseYAMLTestWriter implements TestWriter { options.setPrettyFlow(true); Yaml yaml = new Yaml(options); - Map<String, Object> result = new HashMap<>(); + Map<String, Object> result = new LinkedHashMap<>(); result.put("testRun", testRun); String out = yaml.dump(result); diff --git a/src/cz/crcs/ectester/common/output/TeeTestWriter.java b/src/cz/crcs/ectester/common/output/TeeTestWriter.java new file mode 100644 index 0000000..58a0a15 --- /dev/null +++ b/src/cz/crcs/ectester/common/output/TeeTestWriter.java @@ -0,0 +1,43 @@ +package cz.crcs.ectester.common.output; + +import cz.crcs.ectester.common.test.Test; +import cz.crcs.ectester.common.test.TestSuite; + +/** + * @author Jan Jancar johny@neuromancer.sk + */ +public class TeeTestWriter implements TestWriter { + protected TestWriter[] writers; + + public TeeTestWriter(TestWriter... writers) { + this.writers = writers; + } + + @Override + public void begin(TestSuite suite) { + for (TestWriter writer : writers) { + writer.begin(suite); + } + } + + @Override + public void outputTest(Test t, int index) { + for (TestWriter writer : writers) { + writer.outputTest(t, index); + } + } + + @Override + public void outputError(Test t, Throwable cause, int index) { + for (TestWriter writer : writers) { + writer.outputError(t, cause, index); + } + } + + @Override + public void end() { + for (TestWriter writer : writers) { + writer.end(); + } + } +} diff --git a/src/cz/crcs/ectester/common/output/TestWriter.java b/src/cz/crcs/ectester/common/output/TestWriter.java index 0ecfd5a..eb95804 100644 --- a/src/cz/crcs/ectester/common/output/TestWriter.java +++ b/src/cz/crcs/ectester/common/output/TestWriter.java @@ -7,9 +7,33 @@ import cz.crcs.ectester.common.test.TestSuite; * @author Jan Jancar johny@neuromancer.sk */ public interface TestWriter { + /** + * Begin writing the <code>TestSuite suite</code>. + * This should reset all the internal state of the writer + * and prepare it to output tests from <code>suite</code>. + * It may also write any header part of the output of the + * writer but doesn't have to. + * + * @param suite The <code>TestSuite</code> to start writing. + */ void begin(TestSuite suite); - void outputTest(Test t); + /** + * + * @param t + * @param index + */ + void outputTest(Test t, int index); + /** + * @param t + * @param cause + * @param index + */ + void outputError(Test t, Throwable cause, int index); + + /** + * + */ void end(); } diff --git a/src/cz/crcs/ectester/common/test/BaseTestable.java b/src/cz/crcs/ectester/common/test/BaseTestable.java index a4b9a00..3c304d9 100644 --- a/src/cz/crcs/ectester/common/test/BaseTestable.java +++ b/src/cz/crcs/ectester/common/test/BaseTestable.java @@ -3,10 +3,11 @@ package cz.crcs.ectester.common.test; /** * @author Jan Jancar johny@neuromancer.sk */ -public abstract class BaseTestable implements Testable { +public abstract class BaseTestable implements Testable, Cloneable { protected boolean hasRun; protected boolean ok; protected boolean error; + protected Object errorCause; @Override public boolean hasRun() { @@ -24,9 +25,20 @@ public abstract class BaseTestable implements Testable { } @Override + public Object errorCause() { + return errorCause; + } + + @Override public void reset() { hasRun = false; ok = false; error = false; + errorCause = null; + } + + @Override + protected BaseTestable clone() throws CloneNotSupportedException { + return (BaseTestable) super.clone(); } } diff --git a/src/cz/crcs/ectester/common/test/CompoundTest.java b/src/cz/crcs/ectester/common/test/CompoundTest.java index 10ecf9c..ba4ad4f 100644 --- a/src/cz/crcs/ectester/common/test/CompoundTest.java +++ b/src/cz/crcs/ectester/common/test/CompoundTest.java @@ -2,6 +2,7 @@ package cz.crcs.ectester.common.test; import java.util.Arrays; import java.util.Objects; +import java.util.function.Consumer; import java.util.function.Function; /** @@ -9,30 +10,64 @@ import java.util.function.Function; * * @author Jan Jancar johny@neuromancer.sk */ -public class CompoundTest extends Test { - private Function<Test[], Result> callback; +public class CompoundTest extends Test implements Cloneable { + private Function<Test[], Result> resultCallback; + private Consumer<Test[]> runCallback; private Test[] tests; - private String description; + private String description = ""; - private CompoundTest(Function<Test[], Result> callback, Test... tests) { - this.callback = callback; + private final static Consumer<Test[]> RUN_ALL = tests -> { + for (Test t : tests) { + t.run(); + } + }; + + private final static Consumer<Test[]> RUN_GREEDY_ALL = tests -> { + for (Test t : tests) { + t.run(); + if (!t.ok()) { + break; + } + } + }; + + private final static Consumer<Test[]> RUN_GREEDY_ANY = tests -> { + for (Test t : tests) { + t.run(); + if (t.ok()) { + break; + } + } + }; + + private CompoundTest(Function<Test[], Result> resultCallback, Consumer<Test[]> runCallback, Test... tests) { + this.resultCallback = resultCallback; + this.runCallback = runCallback; this.tests = Arrays.stream(tests).filter(Objects::nonNull).toArray(Test[]::new); } - private CompoundTest(Function<Test[], Result> callback, String descripiton, Test... tests) { - this(callback, tests); + private CompoundTest(Function<Test[], Result> callback, Consumer<Test[]> runCallback, String descripiton, Test... tests) { + this(callback, runCallback, tests); this.description = descripiton; } public static CompoundTest function(Function<Test[], Result> callback, Test... tests) { - return new CompoundTest(callback, tests); + return new CompoundTest(callback, RUN_ALL, tests); + } + + public static CompoundTest function(Function<Test[], Result> callback, Consumer<Test[]> runCallback, Test... tests) { + return new CompoundTest(callback, runCallback, tests); } public static CompoundTest function(Function<Test[], Result> callback, String description, Test... tests) { - return new CompoundTest(callback, description, tests); + return new CompoundTest(callback, RUN_ALL, description, tests); } - public static CompoundTest all(Result.ExpectedValue what, Test... all) { + public static CompoundTest function(Function<Test[], Result> callback, Consumer<Test[]> runCallback, String description, Test... tests) { + return new CompoundTest(callback, runCallback, description, tests); + } + + private static CompoundTest expectAll(Result.ExpectedValue what, Consumer<Test[]> runCallback, Test[] all) { return new CompoundTest((tests) -> { for (Test test : tests) { if (!Result.Value.fromExpected(what, test.ok()).ok()) { @@ -40,7 +75,11 @@ public class CompoundTest extends Test { } } return new Result(Result.Value.SUCCESS, "All sub-tests had the expected result."); - }, all); + }, runCallback, all); + } + + public static CompoundTest all(Result.ExpectedValue what, Test... all) { + return expectAll(what, RUN_ALL, all); } public static CompoundTest all(Result.ExpectedValue what, String description, Test... all) { @@ -49,7 +88,47 @@ public class CompoundTest extends Test { return result; } - public static CompoundTest any(Result.ExpectedValue what, Test... any) { + public static CompoundTest greedyAll(Result.ExpectedValue what, Test... all) { + return expectAll(what, RUN_GREEDY_ALL, all); + } + + public static CompoundTest greedyAll(Result.ExpectedValue what, String description, Test... all) { + CompoundTest result = CompoundTest.greedyAll(what, all); + result.setDescription(description); + return result; + } + + public static CompoundTest greedyAllTry(Result.ExpectedValue what, Test... all) { + return new CompoundTest((tests) -> { + int run = 0; + int ok = 0; + for (Test test : tests) { + if (test.hasRun()) { + run++; + if (Result.Value.fromExpected(what, test.ok()).ok()) { + ok++; + } + } + } + if (run == tests.length) { + if (ok == run) { + return new Result(Result.Value.SUCCESS, "All sub-tests had the expected result."); + } else { + return new Result(Result.Value.FAILURE, "Some sub-tests did not have the expected result."); + } + } else { + return new Result(Result.Value.SUCCESS, "All considered sub-tests had the expected result."); + } + }, RUN_GREEDY_ALL, all); + } + + public static CompoundTest greedyAllTry(Result.ExpectedValue what, String description, Test... all) { + CompoundTest result = CompoundTest.greedyAllTry(what, all); + result.setDescription(description); + return result; + } + + private static CompoundTest expectAny(Result.ExpectedValue what, Consumer<Test[]> runCallback, Test[] any) { return new CompoundTest((tests) -> { for (Test test : tests) { if (Result.Value.fromExpected(what, test.ok()).ok()) { @@ -57,7 +136,21 @@ public class CompoundTest extends Test { } } return new Result(Result.Value.FAILURE, "None of the sub-tests had the expected result."); - }, any); + }, runCallback, any); + } + + public static CompoundTest greedyAny(Result.ExpectedValue what, Test... any) { + return expectAny(what, RUN_GREEDY_ANY, any); + } + + public static CompoundTest greedyAny(Result.ExpectedValue what, String description, Test... any) { + CompoundTest result = CompoundTest.greedyAny(what, any); + result.setDescription(description); + return result; + } + + public static CompoundTest any(Result.ExpectedValue what, Test... any) { + return expectAny(what, RUN_ALL, any); } public static CompoundTest any(Result.ExpectedValue what, String description, Test... any) { @@ -74,7 +167,7 @@ public class CompoundTest extends Test { } } return new Result(Result.Value.SUCCESS, "All sub-tests matched the expected mask."); - }, masked); + }, RUN_ALL, masked); } public static CompoundTest mask(Result.ExpectedValue[] results, String description, Test... masked) { @@ -84,20 +177,25 @@ public class CompoundTest extends Test { } public Test[] getTests() { - return tests; + return tests.clone(); } - @Override - public void run() throws TestException { - if (hasRun) - return; + public Test[] getRunTests() { + return Arrays.stream(tests).filter(Test::hasRun).toArray(Test[]::new); + } - for (Test test : tests) { - test.run(); - } + public Test[] getStartedTests() { + return Arrays.stream(tests).filter(Test::hasStarted).toArray(Test[]::new); + } - result = callback.apply(tests); - this.hasRun = true; + public Test[] getSkippedTests() { + return Arrays.stream(tests).filter((test) -> !test.hasRun()).toArray(Test[]::new); + } + + @Override + protected void runSelf() { + runCallback.accept(tests); + result = resultCallback.apply(tests); } public void setDescription(String description) { @@ -108,4 +206,9 @@ public class CompoundTest extends Test { public String getDescription() { return description; } + + @Override + public CompoundTest clone() throws CloneNotSupportedException { + return (CompoundTest) super.clone(); + } } diff --git a/src/cz/crcs/ectester/common/test/Result.java b/src/cz/crcs/ectester/common/test/Result.java index 11fcb4d..f065f9c 100644 --- a/src/cz/crcs/ectester/common/test/Result.java +++ b/src/cz/crcs/ectester/common/test/Result.java @@ -8,13 +8,13 @@ package cz.crcs.ectester.common.test; public class Result { private Value value; - private String cause; + private Object cause; public Result(Value value) { this.value = value; } - public Result(Value value, String cause) { + public Result(Value value, Object cause) { this(value); this.cause = cause; } @@ -23,7 +23,7 @@ public class Result { return value; } - public String getCause() { + public Object getCause() { return cause; } @@ -49,18 +49,24 @@ public class Result { * A result value of a Test. */ public enum Value { - SUCCESS(true), - FAILURE(false), - UXSUCCESS(false), - XFAILURE(true), - ERROR(false); + SUCCESS(true, "Expected success."), + FAILURE(false, "Unexpected failure."), + UXSUCCESS(false, "Unexpected success."), + XFAILURE(true, "Expected failure."), + ERROR(false, "Error."); private boolean ok; + private String desc; Value(boolean ok) { this.ok = ok; } + Value(boolean ok, String desc) { + this(ok); + this.desc = desc; + } + public static Value fromExpected(ExpectedValue expected, boolean successful) { switch (expected) { case SUCCESS: @@ -68,7 +74,7 @@ public class Result { case FAILURE: return successful ? UXSUCCESS : XFAILURE; case ANY: - return SUCCESS; + return successful ? SUCCESS : XFAILURE; } return SUCCESS; } @@ -83,6 +89,10 @@ public class Result { public boolean ok() { return ok; } + + public String description() { + return desc; + } } /** diff --git a/src/cz/crcs/ectester/common/test/SimpleTest.java b/src/cz/crcs/ectester/common/test/SimpleTest.java index f68320a..d2b3e94 100644 --- a/src/cz/crcs/ectester/common/test/SimpleTest.java +++ b/src/cz/crcs/ectester/common/test/SimpleTest.java @@ -4,11 +4,17 @@ package cz.crcs.ectester.common.test; * @param <T> * @author Jan Jancar johny@neuromancer.sk */ -public abstract class SimpleTest<T extends BaseTestable> extends Test { +public abstract class SimpleTest<T extends BaseTestable> extends Test implements Testable { protected T testable; protected TestCallback<T> callback; public SimpleTest(T testable, TestCallback<T> callback) { + if (testable == null) { + throw new IllegalArgumentException("testable is null."); + } + if (callback == null) { + throw new IllegalArgumentException("callback is null."); + } this.testable = testable; this.callback = callback; } @@ -16,4 +22,17 @@ public abstract class SimpleTest<T extends BaseTestable> extends Test { public T getTestable() { return testable; } + + @Override + protected void runSelf() { + testable.run(); + result = callback.apply(testable); + } + + @Override + public SimpleTest clone() throws CloneNotSupportedException { + SimpleTest clone = (SimpleTest) super.clone(); + clone.testable = testable.clone(); + return clone; + } } diff --git a/src/cz/crcs/ectester/common/test/Test.java b/src/cz/crcs/ectester/common/test/Test.java index 3d0baf6..8bf9502 100644 --- a/src/cz/crcs/ectester/common/test/Test.java +++ b/src/cz/crcs/ectester/common/test/Test.java @@ -7,33 +7,17 @@ import static cz.crcs.ectester.common.test.Result.Value; * * @author Jan Jancar johny@neuromancer.sk */ -public abstract class Test implements Testable { +public abstract class Test implements Testable, Cloneable { protected boolean hasRun; + protected boolean hasStarted; protected Result result; public Result getResult() { - if (!hasRun) { - return null; - } return result; } - public Value getResultValue() { - if (!hasRun) { - return null; - } - return result.getValue(); - } - - public String getResultCause() { - if (!hasRun) { - return null; - } - return result.getCause(); - } - public boolean ok() { - if (!hasRun) { + if (result == null) { return true; } return result.ok(); @@ -41,26 +25,59 @@ public abstract class Test implements Testable { @Override public boolean error() { - if (!hasRun) { + if (result == null) { return false; } return result.compareTo(Value.ERROR); } @Override + public Object errorCause() { + if (result == null || !result.compareTo(Value.ERROR)) { + return null; + } + return result.getCause(); + } + + @Override public boolean hasRun() { return hasRun; } + public boolean hasStarted() { + return hasStarted; + } + @Override public void reset() { hasRun = false; + hasStarted = false; result = null; } public abstract String getDescription(); @Override - public abstract void run() throws TestException; + public Test clone() throws CloneNotSupportedException { + return (Test) super.clone(); + } + + @Override + public void run() { + if (hasRun) + return; + try { + hasStarted = true; + runSelf(); + hasRun = true; + } catch (TestException e) { + result = new Result(Value.ERROR, e); + throw e; + } catch (Exception e) { + result = new Result(Value.ERROR, e); + throw new TestException(e); + } + } + protected abstract void runSelf(); } diff --git a/src/cz/crcs/ectester/common/test/TestException.java b/src/cz/crcs/ectester/common/test/TestException.java index 008e9f6..291a073 100644 --- a/src/cz/crcs/ectester/common/test/TestException.java +++ b/src/cz/crcs/ectester/common/test/TestException.java @@ -2,11 +2,12 @@ package cz.crcs.ectester.common.test; /** * A TestException is an Exception that can be thrown during the running of a Testable, - * or a TestSuite. It means that the Testable/TestSuite encountered an unexpected error - * during it's run which points to an error in ECTester or it's runtime environment.cd + * or a Test. It means that the Testable/TestSuite encountered an unexpected error + * and has to terminate. + * * @author Jan Jancar johny@neuromancer.sk */ -public class TestException extends Exception { +public class TestException extends RuntimeException { public TestException(Throwable e) { super(e); } diff --git a/src/cz/crcs/ectester/common/test/TestSuite.java b/src/cz/crcs/ectester/common/test/TestSuite.java index f4f30ee..b12680a 100644 --- a/src/cz/crcs/ectester/common/test/TestSuite.java +++ b/src/cz/crcs/ectester/common/test/TestSuite.java @@ -1,56 +1,100 @@ package cz.crcs.ectester.common.test; import cz.crcs.ectester.common.output.TestWriter; -import cz.crcs.ectester.data.EC_Store; - -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.stream.Collectors; /** * @author Jan Jancar johny@neuromancer.sk */ public abstract class TestSuite { protected String name; - protected String description; - protected TestWriter writer; + protected String[] description; + private TestWriter writer; + private Test running; + private int ran = 0; + private int runFrom = 0; + private int runTo = -1; - public TestSuite(TestWriter writer, String name, String description) { + public TestSuite(TestWriter writer, String name, String... description) { this.writer = writer; this.name = name; this.description = description; } - public void run() throws TestException { + /** + * Run the <code>TestSuite</code>. + */ + public void run() { + run(0); + } + + public void run(int from) { + run(from, -1); + } + + public void run(int from, int to) { + this.runFrom = from; + this.runTo = to; writer.begin(this); try { runTests(); + } catch (TestException e) { + writer.outputError(running, e, ran); } catch (Exception e) { - throw new TestException(e); + writer.end(); + throw new TestSuiteException(e); } writer.end(); } - protected Test runTest(Test t) throws TestException { + /** + * Run the given test and return it back. + * + * @param t The test to run. + * @return The test that was run. + * @throws TestException + */ + protected <T extends Test> T runTest(T t) { + running = t; t.run(); + running = null; return t; } - protected Test doTest(Test t) throws TestException { - t.run(); - writer.outputTest(t); + /** + * Run the given test, output it and return it back. + * + * @param t The test to run. + * @return The test that was run. + * @throws TestException + */ + protected <T extends Test> T doTest(T t) { + if (ran >= runFrom && (runTo < 0 || ran <= runTo)) { + runTest(t); + writer.outputTest(t, ran); + } + ran++; return t; } + /** + * + */ protected abstract void runTests() throws Exception; public String getName() { return name; } - public String getDescription() { + public String[] getDescription() { return description; } + public String getTextDescription() { + return String.join(System.lineSeparator(), description); + } + + public String toString() { + return null; + } + } diff --git a/src/cz/crcs/ectester/common/test/TestSuiteException.java b/src/cz/crcs/ectester/common/test/TestSuiteException.java new file mode 100644 index 0000000..cc3cfda --- /dev/null +++ b/src/cz/crcs/ectester/common/test/TestSuiteException.java @@ -0,0 +1,13 @@ +package cz.crcs.ectester.common.test; + +/** + * An unexpected exception was thrown while running a TestSuite, outside Test + * or a Testable. + * + * @author Jan Jancar johny@neuromancer.sk + */ +public class TestSuiteException extends RuntimeException { + public TestSuiteException(Throwable e) { + super(e); + } +} diff --git a/src/cz/crcs/ectester/common/test/Testable.java b/src/cz/crcs/ectester/common/test/Testable.java index 33c9485..7b4545c 100644 --- a/src/cz/crcs/ectester/common/test/Testable.java +++ b/src/cz/crcs/ectester/common/test/Testable.java @@ -15,6 +15,11 @@ public interface Testable { boolean error(); /** + * @return The cause of an error, if it happened, otherwise null. + */ + Object errorCause(); + + /** * @return Whether this runnable was run. */ boolean hasRun(); @@ -29,5 +34,5 @@ public interface Testable { * * @throws TestException If an unexpected exception/error is encountered. */ - void run() throws TestException; + void run(); } diff --git a/src/cz/crcs/ectester/common/util/ByteUtil.java b/src/cz/crcs/ectester/common/util/ByteUtil.java index 90c6eaa..daacabb 100644 --- a/src/cz/crcs/ectester/common/util/ByteUtil.java +++ b/src/cz/crcs/ectester/common/util/ByteUtil.java @@ -7,15 +7,27 @@ package cz.crcs.ectester.common.util; * @author Jan Jancar johny@neuromancer.sk */ public class ByteUtil { + + /** + * Gen a short from a byte array at <code>offset</code>, big-endian. + * @return the short value + */ public static short getShort(byte[] array, int offset) { return (short) (((array[offset] & 0xFF) << 8) | (array[offset + 1] & 0xFF)); } + /** + * Set a short in a byte array at <code>offset</code>, big-endian. + */ public static void setShort(byte[] array, int offset, short value) { array[offset + 1] = (byte) (value & 0xFF); array[offset] = (byte) ((value >> 8) & 0xFF); } + /** + * Compare two byte arrays upto <code>length</code> and get first difference. + * @return the position of the first difference in the two byte arrays, or <code>length</code> if they are equal. + */ public static int diffBytes(byte[] one, int oneOffset, byte[] other, int otherOffset, int length) { for (int i = 0; i < length; ++i) { byte a = one[i + oneOffset]; @@ -27,10 +39,17 @@ public class ByteUtil { return length; } + /** + * Compare two byte arrays, upto <code>length</code>. + * @return whether the arrays are equal upto <code>length</code> + */ public static boolean compareBytes(byte[] one, int oneOffset, byte[] other, int otherOffset, int length) { return diffBytes(one, oneOffset, other, otherOffset, length) == length; } + /** + * Test if the byte array has all values equal to <code>value</code>. + */ public static boolean allValue(byte[] array, byte value) { for (byte a : array) { if (a != value) @@ -39,10 +58,38 @@ public class ByteUtil { return true; } + public static byte[] shortToBytes(short value) { + byte[] result = new byte[2]; + setShort(result, 0, value); + return result; + } + + public static byte[] shortToBytes(short[] shorts) { + if (shorts == null) { + return null; + } + byte[] result = new byte[shorts.length * 2]; + for (int i = 0; i < shorts.length; ++i) { + setShort(result, 2 * i, shorts[i]); + } + return result; + } + + /** + * Parse a hex string into a byte array, big-endian. + * @param hex The String to parse. + * @return the byte array from the hex string. + */ public static byte[] hexToBytes(String hex) { return hexToBytes(hex, true); } + /** + * Parse a hex string into a byte-array, specify endianity. + * @param hex The String to parse. + * @param bigEndian Whether to parse as big-endian. + * @return the byte array from the hex string. + */ public static byte[] hexToBytes(String hex, boolean bigEndian) { hex = hex.replace(" ", ""); int len = hex.length(); @@ -125,4 +172,8 @@ public class ByteUtil { } return out; } + + public static byte[] prependLength(byte[] data) { + return concatenate(ByteUtil.shortToBytes((short) data.length), data); + } } diff --git a/src/cz/crcs/ectester/common/util/CardUtil.java b/src/cz/crcs/ectester/common/util/CardUtil.java index 8285d8b..a628d5b 100644 --- a/src/cz/crcs/ectester/common/util/CardUtil.java +++ b/src/cz/crcs/ectester/common/util/CardUtil.java @@ -4,8 +4,10 @@ import cz.crcs.ectester.applet.ECTesterApplet; import cz.crcs.ectester.applet.EC_Consts; import javacard.framework.ISO7816; import javacard.security.CryptoException; +import javacard.security.KeyPair; -import static cz.crcs.ectester.applet.ECTesterApplet.*; +import java.util.LinkedList; +import java.util.List; /** * @author Petr Svenda petr@svenda.com @@ -15,13 +17,19 @@ public class CardUtil { public static byte getKA(String name) { switch (name) { case "DH": - case "ECDH": - return ECTesterApplet.KeyAgreement_ALG_EC_SVDP_DH; + return EC_Consts.KeyAgreement_ALG_EC_SVDP_DH; case "DHC": - case "ECDHC": - return ECTesterApplet.KeyAgreement_ALG_EC_SVDP_DHC; + return EC_Consts.KeyAgreement_ALG_EC_SVDP_DHC; + case "DH_PLAIN": + return EC_Consts.KeyAgreement_ALG_EC_SVDP_DH_PLAIN; + case "DHC_PLAIN": + return EC_Consts.KeyAgreement_ALG_EC_SVDP_DHC_PLAIN; + case "PACE_GM": + return EC_Consts.KeyAgreement_ALG_EC_PACE_GM; + case "DH_PLAIN_XY": + return EC_Consts.KeyAgreement_ALG_EC_SVDP_DH_PLAIN_XY; default: - return ECTesterApplet.KeyAgreement_ALG_EC_SVDP_DH; + return EC_Consts.KeyAgreement_ALG_EC_SVDP_DH; } } @@ -70,75 +78,102 @@ public class CardUtil { } public static String getSW(short sw) { - switch (sw) { - case ISO7816.SW_APPLET_SELECT_FAILED: - return "APPLET_SELECT_FAILED"; - case ISO7816.SW_BYTES_REMAINING_00: - return "BYTES_REMAINING"; - case ISO7816.SW_CLA_NOT_SUPPORTED: - return "CLA_NOT_SUPPORTED"; - case ISO7816.SW_COMMAND_NOT_ALLOWED: - return "COMMAND_NOT_ALLOWED"; - case ISO7816.SW_CONDITIONS_NOT_SATISFIED: - return "CONDITIONS_NOT_SATISFIED"; - case ISO7816.SW_CORRECT_LENGTH_00: - return "CORRECT_LENGTH"; - case ISO7816.SW_DATA_INVALID: - return "DATA_INVALID"; - case ISO7816.SW_FILE_FULL: - return "FILE_FULL"; - case ISO7816.SW_FILE_INVALID: - return "FILE_INVALID"; - case ISO7816.SW_FILE_NOT_FOUND: - return "FILE_NOT_FOUND"; - case ISO7816.SW_FUNC_NOT_SUPPORTED: - return "FUNC_NOT_SUPPORTED"; - case ISO7816.SW_INCORRECT_P1P2: - return "INCORRECT_P1P2"; - case ISO7816.SW_INS_NOT_SUPPORTED: - return "INS_NOT_SUPPORTED"; - case ISO7816.SW_LOGICAL_CHANNEL_NOT_SUPPORTED: - return "LOGICAL_CHANNEL_NOT_SUPPORTED"; - case ISO7816.SW_RECORD_NOT_FOUND: - return "RECORD_NOT_FOUND"; - case ISO7816.SW_SECURE_MESSAGING_NOT_SUPPORTED: - return "SECURE_MESSAGING_NOT_SUPPORTED"; - case ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED: - return "SECURITY_STATUS_NOT_SATISFIED"; - case ISO7816.SW_UNKNOWN: - return "UNKNOWN"; - case ISO7816.SW_WARNING_STATE_UNCHANGED: - return "WARNING_STATE_UNCHANGED"; - case ISO7816.SW_WRONG_DATA: - return "WRONG_DATA"; - case ISO7816.SW_WRONG_LENGTH: - return "WRONG_LENGTH"; - case ISO7816.SW_WRONG_P1P2: - return "WRONG_P1P2"; - case CryptoException.ILLEGAL_VALUE: - return "ILLEGAL_VALUE"; - case CryptoException.UNINITIALIZED_KEY: - return "UNINITIALIZED_KEY"; - case CryptoException.NO_SUCH_ALGORITHM: - return "NO_SUCH_ALG"; - case CryptoException.INVALID_INIT: - return "INVALID_INIT"; - case CryptoException.ILLEGAL_USE: - return "ILLEGAL_USE"; - case ECTesterApplet.SW_SIG_VERIFY_FAIL: - return "SIG_VERIFY_FAIL"; - case ECTesterApplet.SW_DH_DHC_MISMATCH: - return "DH_DHC_MISMATCH"; - case ECTesterApplet.SW_KEYPAIR_NULL: - return "KEYPAIR_NULL"; - case ECTesterApplet.SW_KA_NULL: - return "KA_NULL"; - case ECTesterApplet.SW_SIGNATURE_NULL: - return "SIGNATURE_NULL"; - case ECTesterApplet.SW_OBJECT_NULL: - return "OBJECT_NULL"; + int upper = (sw & 0xff00) >> 8; + int lower = (sw & 0xff); + switch (upper) { + case 0xf1: + return String.format("CryptoException(%d)", lower); + case 0xf2: + return String.format("SystemException(%d)", lower); + case 0xf3: + return String.format("PINException(%d)", lower); + case 0xf4: + return String.format("TransactionException(%d)", lower); + case 0xf5: + return String.format("CardRuntimeException(%d)", lower); default: - return "unknown"; + switch (sw) { + case ISO7816.SW_APPLET_SELECT_FAILED: + return "APPLET_SELECT_FAILED"; + case ISO7816.SW_BYTES_REMAINING_00: + return "BYTES_REMAINING"; + case ISO7816.SW_CLA_NOT_SUPPORTED: + return "CLA_NOT_SUPPORTED"; + case ISO7816.SW_COMMAND_NOT_ALLOWED: + return "COMMAND_NOT_ALLOWED"; + case ISO7816.SW_CONDITIONS_NOT_SATISFIED: + return "CONDITIONS_NOT_SATISFIED"; + case ISO7816.SW_CORRECT_LENGTH_00: + return "CORRECT_LENGTH"; + case ISO7816.SW_DATA_INVALID: + return "DATA_INVALID"; + case ISO7816.SW_FILE_FULL: + return "FILE_FULL"; + case ISO7816.SW_FILE_INVALID: + return "FILE_INVALID"; + case ISO7816.SW_FILE_NOT_FOUND: + return "FILE_NOT_FOUND"; + case ISO7816.SW_FUNC_NOT_SUPPORTED: + return "FUNC_NOT_SUPPORTED"; + case ISO7816.SW_INCORRECT_P1P2: + return "INCORRECT_P1P2"; + case ISO7816.SW_INS_NOT_SUPPORTED: + return "INS_NOT_SUPPORTED"; + case ISO7816.SW_LOGICAL_CHANNEL_NOT_SUPPORTED: + return "LOGICAL_CHANNEL_NOT_SUPPORTED"; + case ISO7816.SW_RECORD_NOT_FOUND: + return "RECORD_NOT_FOUND"; + case ISO7816.SW_SECURE_MESSAGING_NOT_SUPPORTED: + return "SECURE_MESSAGING_NOT_SUPPORTED"; + case ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED: + return "SECURITY_STATUS_NOT_SATISFIED"; + case ISO7816.SW_UNKNOWN: + return "UNKNOWN"; + case ISO7816.SW_WARNING_STATE_UNCHANGED: + return "WARNING_STATE_UNCHANGED"; + case ISO7816.SW_WRONG_DATA: + return "WRONG_DATA"; + case ISO7816.SW_WRONG_LENGTH: + return "WRONG_LENGTH"; + case ISO7816.SW_WRONG_P1P2: + return "WRONG_P1P2"; + case CryptoException.ILLEGAL_VALUE: + return "ILLEGAL_VALUE"; + case CryptoException.UNINITIALIZED_KEY: + return "UNINITIALIZED_KEY"; + case CryptoException.NO_SUCH_ALGORITHM: + return "NO_SUCH_ALG"; + case CryptoException.INVALID_INIT: + return "INVALID_INIT"; + case CryptoException.ILLEGAL_USE: + return "ILLEGAL_USE"; + case ECTesterApplet.SW_SIG_VERIFY_FAIL: + return "SIG_VERIFY_FAIL"; + case ECTesterApplet.SW_DH_DHC_MISMATCH: + return "DH_DHC_MISMATCH"; + case ECTesterApplet.SW_KEYPAIR_NULL: + return "KEYPAIR_NULL"; + case ECTesterApplet.SW_KA_NULL: + return "KA_NULL"; + case ECTesterApplet.SW_SIGNATURE_NULL: + return "SIGNATURE_NULL"; + case ECTesterApplet.SW_OBJECT_NULL: + return "OBJECT_NULL"; + case ECTesterApplet.SW_Exception: + return "Exception"; + case ECTesterApplet.SW_ArrayIndexOutOfBoundsException: + return "ArrayIndexOutOfBoundsException"; + case ECTesterApplet.SW_ArithmeticException: + return "ArithmeticException"; + case ECTesterApplet.SW_ArrayStoreException: + return "ArrayStoreException"; + case ECTesterApplet.SW_NullPointerException: + return "NullPointerException"; + case ECTesterApplet.SW_NegativeArraySizeException: + return "NegativeArraySizeException"; + default: + return "unknown"; + } } } @@ -151,46 +186,122 @@ public class CardUtil { } } - public static String getCorruption(short corruptionType) { - switch (corruptionType) { - case EC_Consts.CORRUPTION_NONE: - return "NONE"; - case EC_Consts.CORRUPTION_FIXED: - return "FIXED"; - case EC_Consts.CORRUPTION_ONE: - return "ONE"; - case EC_Consts.CORRUPTION_ZERO: - return "ZERO"; - case EC_Consts.CORRUPTION_ONEBYTERANDOM: - return "ONE_BYTE_RANDOM"; - case EC_Consts.CORRUPTION_FULLRANDOM: - return "FULL_RANDOM"; - case EC_Consts.CORRUPTION_INCREMENT: - return "INCREMENT"; - case EC_Consts.CORRUPTION_INFINITY: - return "INFINITY"; - case EC_Consts.CORRUPTION_COMPRESS: - return "COMPRESSED"; - case EC_Consts.CORRUPTION_MAX: - return "MAX"; - default: - return "unknown"; + public static String getParams(short params) { + if (params == 0) { + return ""; + } + List<String> ps = new LinkedList<>(); + short paramMask = EC_Consts.PARAMETER_FP; + while (paramMask <= EC_Consts.PARAMETER_S) { + short paramValue = (short) (paramMask & params); + if (paramValue != 0) { + switch (paramValue) { + case EC_Consts.PARAMETER_FP: + ps.add("P"); + break; + case EC_Consts.PARAMETER_F2M: + ps.add("2^M"); + break; + case EC_Consts.PARAMETER_A: + ps.add("A"); + break; + case EC_Consts.PARAMETER_B: + ps.add("B"); + break; + case EC_Consts.PARAMETER_G: + ps.add("G"); + break; + case EC_Consts.PARAMETER_R: + ps.add("R"); + break; + case EC_Consts.PARAMETER_K: + ps.add("K"); + break; + case EC_Consts.PARAMETER_W: + ps.add("W"); + break; + case EC_Consts.PARAMETER_S: + ps.add("S"); + break; + } + } + paramMask = (short) (paramMask << 1); + } + + if (ps.size() != 0) { + return "[" + String.join(",", ps) + "]"; + } else { + return "unknown"; + } + } + + public static String getTransformation(short transformationType) { + if (transformationType == 0) { + return "NONE"; + } + List<String> names = new LinkedList<>(); + short transformationMask = 1; + while (transformationMask <= EC_Consts.TRANSFORMATION_04_MASK) { + short transformationValue = (short) (transformationMask & transformationType); + if (transformationValue != 0) { + switch (transformationValue) { + case EC_Consts.TRANSFORMATION_FIXED: + names.add("FIXED"); + break; + case EC_Consts.TRANSFORMATION_ONE: + names.add("ONE"); + break; + case EC_Consts.TRANSFORMATION_ZERO: + names.add("ZERO"); + break; + case EC_Consts.TRANSFORMATION_ONEBYTERANDOM: + names.add("ONE_BYTE_RANDOM"); + break; + case EC_Consts.TRANSFORMATION_FULLRANDOM: + names.add("FULL_RANDOM"); + break; + case EC_Consts.TRANSFORMATION_INCREMENT: + names.add("INCREMENT"); + break; + case EC_Consts.TRANSFORMATION_INFINITY: + names.add("INFINITY"); + break; + case EC_Consts.TRANSFORMATION_COMPRESS: + names.add("COMPRESSED"); + break; + case EC_Consts.TRANSFORMATION_COMPRESS_HYBRID: + names.add("HYBRID"); + break; + case EC_Consts.TRANSFORMATION_04_MASK: + names.add("MASK(O4)"); + break; + case EC_Consts.TRANSFORMATION_MAX: + names.add("MAX"); + break; + } + } + transformationMask = (short) ((transformationMask) << 1); + } + if (names.size() != 0) { + return String.join(" + ", names); + } else { + return "unknown"; } } public static String getKATypeString(byte kaType) { switch (kaType) { - case KeyAgreement_ALG_EC_SVDP_DH: + case EC_Consts.KeyAgreement_ALG_EC_SVDP_DH: return "ALG_EC_SVDP_DH"; - case KeyAgreement_ALG_EC_SVDP_DH_PLAIN: + case EC_Consts.KeyAgreement_ALG_EC_SVDP_DH_PLAIN: return "ALG_EC_SVDP_DH_PLAIN"; - case KeyAgreement_ALG_EC_PACE_GM: + case EC_Consts.KeyAgreement_ALG_EC_PACE_GM: return "ALG_EC_PACE_GM"; - case KeyAgreement_ALG_EC_SVDP_DH_PLAIN_XY: + case EC_Consts.KeyAgreement_ALG_EC_SVDP_DH_PLAIN_XY: return "ALG_EC_SVDP_DH_PLAIN_XY"; - case KeyAgreement_ALG_EC_SVDP_DHC: + case EC_Consts.KeyAgreement_ALG_EC_SVDP_DHC: return "ALG_EC_SVDP_DHC"; - case KeyAgreement_ALG_EC_SVDP_DHC_PLAIN: + case EC_Consts.KeyAgreement_ALG_EC_SVDP_DHC_PLAIN: return "ALG_EC_SVDP_DHC_PLAIN"; default: return "unknown"; @@ -200,17 +311,17 @@ public class CardUtil { public static byte getKAType(String kaTypeString) { switch (kaTypeString) { case "ALG_EC_SVDP_DH": - return KeyAgreement_ALG_EC_SVDP_DH; + return EC_Consts.KeyAgreement_ALG_EC_SVDP_DH; case "ALG_EC_SVDP_DH_PLAIN": - return KeyAgreement_ALG_EC_SVDP_DH_PLAIN; + return EC_Consts.KeyAgreement_ALG_EC_SVDP_DH_PLAIN; case "ALG_EC_PACE_GM": - return KeyAgreement_ALG_EC_PACE_GM; + return EC_Consts.KeyAgreement_ALG_EC_PACE_GM; case "ALG_EC_SVDP_DH_PLAIN_XY": - return KeyAgreement_ALG_EC_SVDP_DH_PLAIN_XY; + return EC_Consts.KeyAgreement_ALG_EC_SVDP_DH_PLAIN_XY; case "ALG_EC_SVDP_DHC": - return KeyAgreement_ALG_EC_SVDP_DHC; + return EC_Consts.KeyAgreement_ALG_EC_SVDP_DHC; case "ALG_EC_SVDP_DHC_PLAIN": - return KeyAgreement_ALG_EC_SVDP_DHC_PLAIN; + return EC_Consts.KeyAgreement_ALG_EC_SVDP_DHC_PLAIN; default: return 0; } @@ -228,15 +339,15 @@ public class CardUtil { public static String getSigTypeString(byte sigType) { switch (sigType) { - case Signature_ALG_ECDSA_SHA: + case EC_Consts.Signature_ALG_ECDSA_SHA: return "ALG_ECDSA_SHA"; - case Signature_ALG_ECDSA_SHA_224: + case EC_Consts.Signature_ALG_ECDSA_SHA_224: return "ALG_ECDSA_SHA_224"; - case Signature_ALG_ECDSA_SHA_256: + case EC_Consts.Signature_ALG_ECDSA_SHA_256: return "ALG_ECDSA_SHA_256"; - case Signature_ALG_ECDSA_SHA_384: + case EC_Consts.Signature_ALG_ECDSA_SHA_384: return "ALG_ECDSA_SHA_384"; - case Signature_ALG_ECDSA_SHA_512: + case EC_Consts.Signature_ALG_ECDSA_SHA_512: return "ALG_ECDSA_SHA_512"; default: return "unknown"; @@ -246,15 +357,15 @@ public class CardUtil { public static byte getSigType(String sigTypeString) { switch (sigTypeString) { case "ALG_ECDSA_SHA": - return Signature_ALG_ECDSA_SHA; + return EC_Consts.Signature_ALG_ECDSA_SHA; case "ALG_ECDSA_SHA_224": - return Signature_ALG_ECDSA_SHA_224; + return EC_Consts.Signature_ALG_ECDSA_SHA_224; case "ALG_ECDSA_SHA_256": - return Signature_ALG_ECDSA_SHA_256; + return EC_Consts.Signature_ALG_ECDSA_SHA_256; case "ALG_ECDSA_SHA_384": - return Signature_ALG_ECDSA_SHA_384; + return EC_Consts.Signature_ALG_ECDSA_SHA_384; case "ALG_ECDSA_SHA_512": - return Signature_ALG_ECDSA_SHA_512; + return EC_Consts.Signature_ALG_ECDSA_SHA_512; default: return 0; } @@ -269,4 +380,31 @@ public class CardUtil { } return sigType; } + + public static String getKeyTypeString(byte keyClass) { + switch (keyClass) { + case KeyPair.ALG_EC_FP: + return "ALG_EC_FP"; + case KeyPair.ALG_EC_F2M: + return "ALG_EC_F2M"; + default: + return ""; + } + } + + public static String getParameterString(short params) { + String what = ""; + if (params == EC_Consts.PARAMETERS_DOMAIN_F2M || params == EC_Consts.PARAMETERS_DOMAIN_FP) { + what = "curve"; + } else if (params == EC_Consts.PARAMETER_W) { + what = "pubkey"; + } else if (params == EC_Consts.PARAMETER_S) { + what = "privkey"; + } else if (params == EC_Consts.PARAMETERS_KEYPAIR) { + what = "keypair"; + } else { + what = getParams(params); + } + return what; + } } diff --git a/src/cz/crcs/ectester/common/util/ECUtil.java b/src/cz/crcs/ectester/common/util/ECUtil.java index 973b813..0979d91 100644 --- a/src/cz/crcs/ectester/common/util/ECUtil.java +++ b/src/cz/crcs/ectester/common/util/ECUtil.java @@ -146,16 +146,22 @@ public class ECUtil { alpha = alpha.add(x.multiply(a)); alpha = alpha.add(b); + if(!isResidue(alpha, p)) { + throw new IllegalArgumentException(); + } + BigInteger beta = modSqrt(alpha, p); if (beta.getLowestSetBit() == 0) { // rightmost bit is one if (data[0] == 0x02) { - beta = beta.negate(); + // yp is 0 + beta = p.subtract(beta); } } else { // rightmost bit is zero if (data[0] == 0x03) { - beta = beta.negate(); + // yp is 1 + beta = p.subtract(beta); } } diff --git a/src/cz/crcs/ectester/common/util/FileUtil.java b/src/cz/crcs/ectester/common/util/FileUtil.java new file mode 100644 index 0000000..790596b --- /dev/null +++ b/src/cz/crcs/ectester/common/util/FileUtil.java @@ -0,0 +1,33 @@ +package cz.crcs.ectester.common.util; + +import cz.crcs.ectester.common.output.TeeOutputStream; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Jan Jancar johny@neuromancer.sk + */ +public class FileUtil { + public static OutputStream openStream(String[] files) throws FileNotFoundException { + if (files == null) { + return null; + } + List<OutputStream> outs = new LinkedList<>(); + for (String fileOut : files) { + outs.add(new FileOutputStream(fileOut)); + } + return new TeeOutputStream(outs.toArray(new OutputStream[0])); + } + + public static OutputStreamWriter openFiles(String[] files) throws FileNotFoundException { + if (files == null) { + return null; + } + return new OutputStreamWriter(openStream(files)); + } +} |
