package com.jetbrains.lang.dart.ide.runner.test; import com.google.gson.*; import com.intellij.execution.testframework.TestConsoleProperties; import com.intellij.execution.testframework.sm.ServiceMessageBuilder; import com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.PathUtil; import com.jetbrains.lang.dart.ide.runner.util.DartTestLocationProvider; import com.jetbrains.lang.dart.util.DartUrlResolver; import gnu.trove.TIntLongHashMap; import jetbrains.buildServer.messages.serviceMessages.ServiceMessageVisitor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.reflect.Type; import java.text.ParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Convert events from JSON format generated by package:test to the string format * expected by the event processor. * NOTE: The test runner runs tests asynchronously. It is possible to get a 'testDone' * event followed some time later by an 'error' event for that same test. That should * convert a successful test into a failure. That case is not being handled. */ public class DartTestEventsConverter extends OutputToGeneralTestEventsConverter { private static final Logger LOG = Logger.getInstance(DartTestEventsConverter.class.getName()); private static final String TYPE_START = "start"; private static final String TYPE_SUITE = "suite"; private static final String TYPE_ERROR = "error"; private static final String TYPE_GROUP = "group"; private static final String TYPE_PRINT = "print"; private static final String TYPE_DONE = "done"; private static final String TYPE_ALL_SUITES = "allSuites"; private static final String TYPE_TEST_START = "testStart"; private static final String TYPE_TEST_DONE = "testDone"; private static final String DEF_GROUP = "group"; private static final String DEF_SUITE = "suite"; private static final String DEF_TEST = "test"; private static final String DEF_METADATA = "metadata"; private static final String JSON_TYPE = "type"; private static final String JSON_NAME = "name"; private static final String JSON_ID = "id"; private static final String JSON_TEST_ID = "testID"; private static final String JSON_SUITE_ID = "suiteID"; private static final String JSON_PARENT_ID = "parentID"; private static final String JSON_GROUP_IDS = "groupIDs"; private static final String JSON_RESULT = "result"; private static final String JSON_MILLIS = "time"; private static final String JSON_COUNT = "count"; private static final String JSON_TEST_COUNT = "testCount"; private static final String JSON_MESSAGE = "message"; private static final String JSON_ERROR_MESSAGE = "error"; private static final String JSON_STACK_TRACE = "stackTrace"; private static final String JSON_IS_FAILURE = "isFailure"; private static final String JSON_PATH = "path"; private static final String JSON_PLATFORM = "platform"; private static final String JSON_LINE = "line"; private static final String JSON_COLUMN = "column"; private static final String JSON_URL = "url"; private static final String RESULT_SUCCESS = "success"; private static final String RESULT_FAILURE = "failure"; private static final String RESULT_ERROR = "error"; private static final String EXPECTED = "Expected: "; private static final Pattern EXPECTED_ACTUAL_RESULT = Pattern.compile("\\nExpected: (.*)\\n Actual: (.*)\\n *\\^\\n Differ.*\\n"); private static final String FILE_URL_PREFIX = "dart_location://"; private static final String LOADING_PREFIX = "loading "; private static final String COMPILING_PREFIX = "compiling "; private static final String SET_UP_ALL_VIRTUAL_TEST_NAME = "(setUpAll)"; private static final String TEAR_DOWN_ALL_VIRTUAL_TEST_NAME = "(tearDownAll)"; private static final Gson GSON = new Gson(); @NotNull private final DartUrlResolver myUrlResolver; private String myLocation; private Key myCurrentOutputType; private ServiceMessageVisitor myCurrentVisitor; private TIntLongHashMap myTestIdToTimestamp; private Map<Integer, Test> myTestData; private Map<Integer, Group> myGroupData; private Map<Integer, Suite> mySuiteData; private int mySuitCount; public DartTestEventsConverter(@NotNull final String testFrameworkName, @NotNull final TestConsoleProperties consoleProperties, @NotNull final DartUrlResolver urlResolver) { super(testFrameworkName, consoleProperties); myUrlResolver = urlResolver; myTestIdToTimestamp = new TIntLongHashMap(); myTestData = new HashMap<>(); myGroupData = new HashMap<>(); mySuiteData = new HashMap<>(); } protected boolean processServiceMessages(final String text, final Key outputType, final ServiceMessageVisitor visitor) throws ParseException { LOG.debug("<<< " + text.trim()); myCurrentOutputType = outputType; myCurrentVisitor = visitor; // service message parser expects line like "##teamcity[ .... ]" without whitespaces in the end. return processEventText(text); } private boolean processEventText(final String text) throws JsonSyntaxException, ParseException { JsonParser jp = new JsonParser(); JsonElement elem; try { elem = jp.parse(text); } catch (JsonSyntaxException ex) { if (text.contains("\"json\" is not an allowed value for option \"reporter\"")) { final ServiceMessageBuilder testStarted = ServiceMessageBuilder.testStarted("Failed to start"); final ServiceMessageBuilder testFailed = ServiceMessageBuilder.testFailed("Failed to start"); testFailed.addAttribute("message", "Please update your pubspec.yaml dependency on package:test to version 0.12.9 or later."); final ServiceMessageBuilder testFinished = ServiceMessageBuilder.testFinished("Failed to start"); return finishMessage(testStarted, 1, 0) & finishMessage(testFailed, 1, 0) & finishMessage(testFinished, 1, 0); } return doProcessServiceMessages(text); } if (elem == null || !elem.isJsonObject()) return false; return process(elem.getAsJsonObject()); } private boolean doProcessServiceMessages(@NotNull final String text) throws ParseException { LOG.debug(">>> " + text); return super.processServiceMessages(text, myCurrentOutputType, myCurrentVisitor); } private boolean process(JsonObject obj) throws JsonSyntaxException, ParseException { String type = obj.get(JSON_TYPE).getAsString(); if (TYPE_TEST_START.equals(type)) { return handleTestStart(obj); } else if (TYPE_TEST_DONE.equals(type)) { return handleTestDone(obj); } else if (TYPE_ERROR.equals(type)) { return handleError(obj); } else if (TYPE_PRINT.equals(type)) { return handlePrint(obj); } else if (TYPE_GROUP.equals(type)) { return handleGroup(obj); } else if (TYPE_SUITE.equals(type)) { return handleSuite(obj); } else if (TYPE_ALL_SUITES.equals(type)) { return handleAllSuites(obj); } else if (TYPE_START.equals(type)) { return handleStart(obj); } else if (TYPE_DONE.equals(type)) { return handleDone(obj); } else { return true; } } private boolean handleTestStart(JsonObject obj) throws ParseException { final JsonObject testObj = obj.getAsJsonObject(DEF_TEST); // Not reached if testObj == null. final Test test = getTest(obj); myTestIdToTimestamp.put(test.getId(), getTimestamp(obj)); if (shouldTestBeHiddenIfPassed(test)) { // Virtual test that represents loading or compiling a test suite. See lib/src/runner/loader.dart -> Loader.loadFile() in pkg/test source code // At this point we do not report anything to the framework, but if error occurs, we'll report it as a normal test String path = ""; if (test.getName().startsWith(LOADING_PREFIX)) { path = test.getName().substring(LOADING_PREFIX.length()); } else if (test.getName().startsWith(COMPILING_PREFIX)) { path = test.getName().substring(COMPILING_PREFIX.length()); } if (path.length() > 0) myLocation = FILE_URL_PREFIX + path; test.myTestStartReported = false; return true; } final ServiceMessageBuilder testStarted = ServiceMessageBuilder.testStarted(test.getBaseName()); test.myTestStartReported = true; addLocationHint(testStarted, test); boolean result = finishMessage(testStarted, test.getId(), test.getValidParentId()); final Metadata metadata = Metadata.from(testObj.getAsJsonObject(DEF_METADATA)); if (metadata.skip) { final ServiceMessageBuilder message = ServiceMessageBuilder.testIgnored(test.getBaseName()); if (metadata.skipReason != null) message.addAttribute("message", metadata.skipReason); result &= finishMessage(message, test.getId(), test.getValidParentId()); } return result; } private static boolean shouldTestBeHiddenIfPassed(@NotNull final Test test) { // There are so called 'virtual' tests that are created for loading test suites, setUpAll(), and tearDownAll(). // They shouldn't be visible when they do not cause problems. But if any error occurs, we'll report it later as a normal test. // See lib/src/runner/loader.dart -> Loader.loadFile() and lib/src/backend/declarer.dart -> Declarer._setUpAll and Declarer._tearDownAll in pkg/test source code final Group group = test.getParent(); return group == null && (test.getName().startsWith(LOADING_PREFIX) || test.getName().startsWith(COMPILING_PREFIX)) || group != null && group.getDoneTestsCount() == 0 && test.getBaseName().equals(SET_UP_ALL_VIRTUAL_TEST_NAME) || group != null && group.getDoneTestsCount() > 0 && test.getBaseName().equals(TEAR_DOWN_ALL_VIRTUAL_TEST_NAME); } private boolean handleTestDone(JsonObject obj) throws ParseException { final Test test = getTest(obj); if (!test.myTestStartReported) return true; String result = getResult(obj); if (!result.equals(RESULT_SUCCESS) && !result.equals(RESULT_FAILURE) && !result.equals(RESULT_ERROR)) { throw new ParseException("Unknown result: " + obj, 0); } test.testDone(); //if (test.getMetadata().skip) return true; // skipped tests are reported as ignored in handleTestStart(). testFinished signal must follow ServiceMessageBuilder testFinished = ServiceMessageBuilder.testFinished(test.getBaseName()); long duration = getTimestamp(obj) - myTestIdToTimestamp.get(test.getId()); testFinished.addAttribute("duration", Long.toString(duration)); return finishMessage(testFinished, test.getId(), test.getValidParentId()) && checkGroupDone(test.getParent()); } private boolean checkGroupDone(@Nullable final Group group) throws ParseException { if (group != null && group.getTestCount() > 0 && group.getDoneTestsCount() == group.getTestCount()) { return processGroupDone(group) && checkGroupDone(group.getParent()); } return true; } private boolean handleGroup(JsonObject obj) throws ParseException { Group group = getGroup(obj.getAsJsonObject(DEF_GROUP)); // From spec: The implicit group at the root of each test suite has null name and parentID attributes. if (group.getParent() == null && group.getTestCount() > 0) { // com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter.MyServiceMessageVisitor.KEY_TESTS_COUNT // and com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter.MyServiceMessageVisitor.ATTR_KEY_TEST_COUNT final ServiceMessageBuilder testCount = new ServiceMessageBuilder("testCount").addAttribute("count", String.valueOf(group.getTestCount())); doProcessServiceMessages(testCount.toString()); } if (group.isArtificial()) return true; // Ignore artificial groups. ServiceMessageBuilder groupMsg = ServiceMessageBuilder.testSuiteStarted(group.getBaseName()); // Possible attributes: "nodeType" "nodeArgs" "running" addLocationHint(groupMsg, group); return finishMessage(groupMsg, group.getId(), group.getValidParentId()); } private boolean handleSuite(JsonObject obj) throws ParseException { Suite suite = getSuite(obj.getAsJsonObject(DEF_SUITE)); if (!suite.hasPath()) { mySuiteData.remove(suite.getId()); } return true; } private boolean handleError(JsonObject obj) throws ParseException { final Test test = getTest(obj); final String message = getErrorMessage(obj); boolean result = true; if (!test.myTestStartReported) { final ServiceMessageBuilder testStarted = ServiceMessageBuilder.testStarted(test.getBaseName()); test.myTestStartReported = true; result = finishMessage(testStarted, test.getId(), test.getValidParentId()); } if (test.myTestErrorReported) { final ServiceMessageBuilder testErrorMessage = ServiceMessageBuilder.testStdErr(test.getBaseName()); testErrorMessage.addAttribute("out", appendLineBreakIfNeeded(message)); result &= finishMessage(testErrorMessage, test.getId(), test.getValidParentId()); } else { final ServiceMessageBuilder testError = ServiceMessageBuilder.testFailed(test.getBaseName()); test.myTestErrorReported = true; String failureMessage = message; int firstExpectedIndex = message.indexOf(EXPECTED); if (firstExpectedIndex >= 0) { Matcher matcher = EXPECTED_ACTUAL_RESULT.matcher(message); if (matcher.find(firstExpectedIndex + EXPECTED.length())) { String expectedText = matcher.group(1); String actualText = matcher.group(2); testError.addAttribute("expected", expectedText); testError.addAttribute("actual", actualText); if (firstExpectedIndex == 0) { failureMessage = "Comparison failed"; } else { failureMessage = message.substring(0, firstExpectedIndex); } } } if (!getBoolean(obj, JSON_IS_FAILURE)) testError.addAttribute("error", "true"); testError.addAttribute("message", appendLineBreakIfNeeded(failureMessage)); result &= finishMessage(testError, test.getId(), test.getValidParentId()); } final String stackTrace = getStackTrace(obj); if (!StringUtil.isEmptyOrSpaces(stackTrace)) { final ServiceMessageBuilder stackTraceMessage = ServiceMessageBuilder.testStdErr(test.getBaseName()); stackTraceMessage.addAttribute("out", appendLineBreakIfNeeded(stackTrace)); result &= finishMessage(stackTraceMessage, test.getId(), test.getValidParentId()); } return result; } @NotNull private static String appendLineBreakIfNeeded(@NotNull final String message) { return message.endsWith("\n") ? message : message + "\n"; } private boolean handleAllSuites(JsonObject obj) { JsonElement elem = obj.get(JSON_COUNT); if (elem == null || !elem.isJsonPrimitive()) return true; mySuitCount = elem.getAsInt(); return true; } private boolean handlePrint(JsonObject obj) throws ParseException { final Test test = getTest(obj); boolean result = true; if (!test.myTestStartReported) { if (test.getBaseName().equals(SET_UP_ALL_VIRTUAL_TEST_NAME) || test.getBaseName().equals(TEAR_DOWN_ALL_VIRTUAL_TEST_NAME)) { return true; // output in successfully passing setUpAll/tearDownAll is not important enough to make these nodes visible } final ServiceMessageBuilder testStarted = ServiceMessageBuilder.testStarted(test.getBaseName()); test.myTestStartReported = true; result = finishMessage(testStarted, test.getId(), test.getValidParentId()); } ServiceMessageBuilder message = ServiceMessageBuilder.testStdOut(test.getBaseName()); message.addAttribute("out", appendLineBreakIfNeeded(getMessage(obj))); return result & finishMessage(message, test.getId(), test.getValidParentId()); } private boolean handleStart(JsonObject obj) throws ParseException { myTestIdToTimestamp.clear(); myTestData.clear(); myGroupData.clear(); mySuiteData.clear(); mySuitCount = 0; return doProcessServiceMessages(new ServiceMessageBuilder("enteredTheMatrix").toString()); } private boolean handleDone(JsonObject obj) throws ParseException { // The test runner has reached the end of the tests. processAllTestsDone(); return true; } private void processAllTestsDone() { // All tests are done. for (Group group : myGroupData.values()) { // For package: test prior to v. 0.12.9 there were no Group.testCount field, so need to finish them all at the end. // AFAIK the order does not matter. A depth-first post-order traversal of the tree would work // if order does matter. Note: Currently, there is no tree representation, just parent links. if (group.getTestCount() == 0 || group.getDoneTestsCount() != group.getTestCount()) { try { processGroupDone(group); } catch (ParseException ex) { // ignore it } } } myTestIdToTimestamp.clear(); myTestData.clear(); myGroupData.clear(); mySuiteData.clear(); mySuitCount = 0; } private boolean processGroupDone(@NotNull final Group group) throws ParseException { if (group.isArtificial()) return true; ServiceMessageBuilder groupMsg = ServiceMessageBuilder.testSuiteFinished(group.getBaseName()); return finishMessage(groupMsg, group.getId(), group.getValidParentId()); } private boolean finishMessage(@NotNull ServiceMessageBuilder msg, int testId, int parentId) throws ParseException { msg.addAttribute("nodeId", String.valueOf(testId)); msg.addAttribute("parentNodeId", String.valueOf(parentId)); return doProcessServiceMessages(msg.toString()); } private void addLocationHint(ServiceMessageBuilder messageBuilder, Item item) { String location = "unknown"; String loc; final VirtualFile file = item.getUrl() == null ? null : myUrlResolver.findFileByDartUrl(item.getUrl()); if (file != null) { loc = FILE_URL_PREFIX + file.getPath(); } else if (item.hasSuite()) { loc = FILE_URL_PREFIX + item.getSuite().getPath(); } else { loc = myLocation; } if (loc != null) { String nameList = GSON.toJson(item.nameList(), DartTestLocationProvider.STRING_LIST_TYPE); location = loc + "," + item.getLine() + "," + item.getColumn() + "," + nameList; } messageBuilder.addAttribute("locationHint", location); } private static long getTimestamp(JsonObject obj) throws ParseException { return getLong(obj, JSON_MILLIS); } private static long getLong(JsonObject obj, String name) throws ParseException { JsonElement val = obj == null ? null : obj.get(name); if (val == null || !val.isJsonPrimitive()) throw new ParseException("Value is not type long: " + val, 0); return val.getAsLong(); } private static boolean getBoolean(JsonObject obj, String name) throws ParseException { JsonElement val = obj == null ? null : obj.get(name); if (val == null || !val.isJsonPrimitive()) throw new ParseException("Value is not type boolean: " + val, 0); return val.getAsBoolean(); } @NotNull private Test getTest(JsonObject obj) throws ParseException { return getItem(obj, myTestData); } @NotNull private Group getGroup(JsonObject obj) throws ParseException { return getItem(obj, myGroupData); } @NotNull private Suite getSuite(JsonObject obj) throws ParseException { return getItem(obj, mySuiteData); } @NotNull private <T extends Item> T getItem(JsonObject obj, Map<Integer, T> items) throws ParseException { if (obj == null) throw new ParseException("Unexpected null json object", 0); T item; JsonElement id = obj.get(JSON_ID); if (id != null) { if (items == myTestData) { @SuppressWarnings("unchecked") T type = (T)Test.from(obj, myGroupData, mySuiteData); item = type; } else if (items == myGroupData) { @SuppressWarnings("unchecked") T group = (T)Group.from(obj, myGroupData, mySuiteData); item = group; } else { @SuppressWarnings("unchecked") T suite = (T)Suite.from(obj); item = suite; } items.put(id.getAsInt(), item); } else { JsonElement testId = obj.get(JSON_TEST_ID); if (testId != null) { int baseId = testId.getAsInt(); item = items.get(baseId); } else { JsonElement testObj = obj.get(DEF_TEST); if (testObj != null) { return getItem(testObj.getAsJsonObject(), items); } else { throw new ParseException("No testId in json object", 0); } } } return item; } @NotNull private static String getErrorMessage(JsonObject obj) { return nonNullJsonValue(obj, JSON_ERROR_MESSAGE, "<no error message>"); } @NotNull private static String getMessage(JsonObject obj) { return nonNullJsonValue(obj, JSON_MESSAGE, "<no message>"); } @NotNull private static String getStackTrace(JsonObject obj) { return nonNullJsonValue(obj, JSON_STACK_TRACE, "<no stack trace>"); } @NotNull private static String getResult(JsonObject obj) { return nonNullJsonValue(obj, JSON_RESULT, "<no result>"); } @NotNull private static String nonNullJsonValue(JsonObject obj, @NotNull String id, @NotNull String def) { JsonElement val = obj == null ? null : obj.get(id); if (val == null || !val.isJsonPrimitive()) return def; return val.getAsString(); } private static class Item { protected static final String NO_NAME = "<no name>"; private final int myId; private final String myName; private final Group myParent; private final Suite mySuite; private final Metadata myMetadata; private final int myLine; private final int myColumn; private final String myUrl; static int extractInt(JsonObject obj, String memberName) { JsonElement elem = obj.get(memberName); if (elem == null || !elem.isJsonPrimitive()) return -1; return elem.getAsInt(); } static String extractString(JsonObject obj, String memberName, String defaultResult) { JsonElement elem = obj.get(memberName); if (elem == null || elem.isJsonNull()) return defaultResult; return elem.getAsString(); } static Metadata extractMetadata(JsonObject obj) { return Metadata.from(obj.get(DEF_METADATA)); } static Suite lookupSuite(JsonObject obj, Map<Integer, Suite> suites) { JsonElement suiteObj = obj.get(JSON_SUITE_ID); Suite suite = null; if (suiteObj != null && suiteObj.isJsonPrimitive()) { int parentId = suiteObj.getAsInt(); suite = suites.get(parentId); } return suite; } Item(int id, String name, Group parent, Suite suite, Metadata metadata, int line, int column, String url) { myId = id; myName = name; myParent = parent; mySuite = suite; myMetadata = metadata; myLine = line; myColumn = column; myUrl = url; } int getId() { return myId; } String getName() { return myName; } String getBaseName() { // Virtual test that represents loading or compiling a test suite. See lib/src/runner/loader.dart -> Loader.loadFile() in pkg/test source code if (this instanceof Test && getParent() == null) { if (myName.startsWith(LOADING_PREFIX)) { return LOADING_PREFIX + PathUtil.getFileName(myName.substring(LOADING_PREFIX.length())); } else if (myName.startsWith(COMPILING_PREFIX)) { return COMPILING_PREFIX + PathUtil.getFileName(myName.substring(COMPILING_PREFIX.length())); } return myName; // can't happen } // file-level group if (this instanceof Group && NO_NAME.equals(myName) && myParent == null && hasSuite()) { return PathUtil.getFileName(getSuite().getPath()); } // top-level group in suite if (this instanceof Group && myParent != null && myParent.getParent() == null && NO_NAME.equals(myParent.getName())) { return myName; } if (hasValidParent()) { final String parentName = getParent().getName(); if (myName.startsWith(parentName + " ")) { return myName.substring(parentName.length() + 1); } } return myName; } boolean hasSuite() { return mySuite != null && mySuite.hasPath(); } Suite getSuite() { return mySuite; } Group getParent() { return myParent; } Metadata getMetadata() { return myMetadata; } boolean isArtificial() { return NO_NAME.equals(myName) && myParent == null && !hasSuite(); } boolean hasValidParent() { return !(myParent == null || myParent.isArtificial()); } int getValidParentId() { if (hasValidParent()) { return getParent().getId(); } else { return 0; } } List<String> nameList() { List<String> names = new ArrayList<>(); addNames(names); return names; } void addNames(List<String> names) { if (this instanceof Group && NO_NAME.equals(myName) && myParent == null) { return; // do not add a name of a file-level group } if (myParent != null) { myParent.addNames(names); } names.add(StringUtil.escapeStringCharacters(getBaseName())); } public int getLine() { return myLine; } public int getColumn() { return myColumn; } public String getUrl() { return myUrl; } public String toString() { return getClass().getSimpleName() + "(" + String.valueOf(myId) + "," + String.valueOf(myName) + ")"; } } private static class Test extends Item { private boolean myTestStartReported = false; private boolean myTestErrorReported = false; static Test from(JsonObject obj, Map<Integer, Group> groups, Map<Integer, Suite> suites) { int[] groupIds = GSON.fromJson(obj.get(JSON_GROUP_IDS), (Type)int[].class); Group parent = null; if (groupIds != null && groupIds.length > 0) { parent = groups.get(groupIds[groupIds.length - 1]); } Suite suite = lookupSuite(obj, suites); final int line = extractInt(obj, JSON_LINE); final int column = extractInt(obj, JSON_COLUMN); return new Test(extractInt(obj, JSON_ID), extractString(obj, JSON_NAME, NO_NAME), parent, suite, extractMetadata(obj), line < 0 ? -1 : line - 1, column < 0 ? -1 : column - 1, extractString(obj, JSON_URL, null)); } Test(int id, String name, Group parent, Suite suite, Metadata metadata, int line, int column, String url) { super(id, name, parent, suite, metadata, line, column, url); } public void testDone() { if (getParent() != null) { getParent().incDoneTestsCount(); } } } private static class Group extends Item { private int myTestCount = 0; private int myDoneTestsCount = 0; static Group from(JsonObject obj, Map<Integer, Group> groups, Map<Integer, Suite> suites) { JsonElement parentObj = obj.get(JSON_PARENT_ID); Group parent = null; if (parentObj != null && parentObj.isJsonPrimitive()) { int parentId = parentObj.getAsInt(); parent = groups.get(parentId); } Suite suite = lookupSuite(obj, suites); final int line = extractInt(obj, JSON_LINE); final int column = extractInt(obj, JSON_COLUMN); return new Group(extractInt(obj, JSON_ID), extractString(obj, JSON_NAME, NO_NAME), parent, suite, extractMetadata(obj), extractInt(obj, JSON_TEST_COUNT), line < 0 ? -1 : line - 1, column < 0 ? -1 : column - 1, extractString(obj, JSON_URL, null)); } Group(int id, String name, Group parent, Suite suite, Metadata metadata, int count, int line, int column, String url) { super(id, name, parent, suite, metadata, line, column, url); myTestCount = count; } int getTestCount() { return myTestCount; } public int getDoneTestsCount() { return myDoneTestsCount; } public void incDoneTestsCount() { myDoneTestsCount++; if (getParent() != null) { getParent().incDoneTestsCount(); } } } private static class Suite extends Item { static Metadata NoMetadata = new Metadata(); static String NONE = "<none>"; static Suite from(JsonObject obj) { return new Suite(extractInt(obj, JSON_ID), extractString(obj, JSON_PATH, NONE), extractString(obj, JSON_PLATFORM, NONE)); } private final String myPlatform; Suite(int id, String path, String platform) { super(id, path, null, null, NoMetadata, -1, -1, "file://" + path); myPlatform = platform; } String getPath() { return getName(); } String getPlatform() { return myPlatform; } boolean hasPath() { return getPath() != NONE; } } private static class Metadata { @SuppressWarnings("unused") private boolean skip; // assigned by GSON via reflection @SuppressWarnings("unused") private String skipReason; // assigned by GSON via reflection static Metadata from(JsonElement elem) { if (elem == null) return new Metadata(); return GSON.fromJson(elem, (Type)Metadata.class); } } }