Bug 323738 - Minitest support

Initial support for minitest.
Recognize minitest, display in tests view and etc. All with
bugs/limitations but good enough for being able to run/view resulst
inside tests view. Aka good enough to base improvements on.



Change-Id: I83cd751e04f41587eee5c20e103b715971ad8ae3
Signed-off-by: Alexander Kurtakov <akurtako@redhat.com>
diff --git a/plugins/org.eclipse.dltk.ruby.testing/build.properties b/plugins/org.eclipse.dltk.ruby.testing/build.properties
index c35ac1e..a15eb58 100644
--- a/plugins/org.eclipse.dltk.ruby.testing/build.properties
+++ b/plugins/org.eclipse.dltk.ruby.testing/build.properties
@@ -7,4 +7,6 @@
                icons/,\
                testing/dltk-rspec-runner.rb,\
                testing/dltk-testunit-runner.rb,\
-               about.html
+               about.html,\
+               testing/minitest/dltk_plugin.rb,\
+               testing/dltk-minitest-runner.rb
diff --git a/plugins/org.eclipse.dltk.ruby.testing/plugin.xml b/plugins/org.eclipse.dltk.ruby.testing/plugin.xml
index f8707f0..a3cf698 100644
--- a/plugins/org.eclipse.dltk.ruby.testing/plugin.xml
+++ b/plugins/org.eclipse.dltk.ruby.testing/plugin.xml
@@ -2,6 +2,13 @@
 <?eclipse version="3.2"?>
 <plugin>
    <extension point="org.eclipse.dltk.testing.engine">
+    <testingEngine
+         class="org.eclipse.dltk.ruby.testing.internal.miniunit.MiniTestingEngine"
+         id="org.eclipse.dltk.ruby.testing.minitest"
+         name="Minitest"
+         nature="org.eclipse.dltk.ruby.core.nature"
+         priority="30">
+   </testingEngine>
    <testingEngine
          class="org.eclipse.dltk.ruby.testing.internal.testunit.TestUnitTestingEngine"
          id="org.eclipse.dltk.ruby.testing.testunit"
diff --git a/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/Messages.java b/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/Messages.java
index a22bd8b..9913143 100644
--- a/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/Messages.java
+++ b/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/Messages.java
@@ -34,6 +34,8 @@
 	public static String validate_probablyTestUnit;
 	public static String validate_notRSpec;
 	public static String validate_probablyRSpec;
+	public static String validate_notMinitest;
+	public static String validate_probablyMinitest;
 	public static String validate_runtimeError;
 	public static String validate_sourceErrors;
 	static {
diff --git a/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/messages.properties b/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/messages.properties
index 29535fc..2a608eb 100644
--- a/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/messages.properties
+++ b/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/messages.properties
@@ -7,6 +7,8 @@
 validate_probablyTestUnit=This module could be Test::Unit test
 validate_notRSpec=This module does not look like RSpec test
 validate_probablyRSpec=This module could be RSpec test
+validate_notMinitest=This module does not look like Minitest test
+validate_probablyMinitest=This module could be Minitest test
 validate_runtimeError=Error while evaluating: {0}
 validate_sourceErrors=Could not evaluate due to errors in source code
 RubyTestingLaunchShortcut_testLaunch=Test Launch
diff --git a/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/miniunit/MiniTestRunnerUI.java b/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/miniunit/MiniTestRunnerUI.java
new file mode 100644
index 0000000..90c0700
--- /dev/null
+++ b/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/miniunit/MiniTestRunnerUI.java
@@ -0,0 +1,245 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Red Hat, Inc. and others
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Red Hat, Inc. - initial API and Implementation
+ *******************************************************************************/
+package org.eclipse.dltk.ruby.testing.internal.miniunit;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.dltk.core.IMethod;
+import org.eclipse.dltk.core.IProjectFragment;
+import org.eclipse.dltk.core.IScriptProject;
+import org.eclipse.dltk.core.IType;
+import org.eclipse.dltk.core.ModelException;
+import org.eclipse.dltk.core.environment.EnvironmentPathUtils;
+import org.eclipse.dltk.core.search.IDLTKSearchConstants;
+import org.eclipse.dltk.core.search.IDLTKSearchScope;
+import org.eclipse.dltk.core.search.SearchEngine;
+import org.eclipse.dltk.core.search.SearchMatch;
+import org.eclipse.dltk.core.search.SearchParticipant;
+import org.eclipse.dltk.core.search.SearchPattern;
+import org.eclipse.dltk.core.search.SearchRequestor;
+import org.eclipse.dltk.ruby.core.utils.RubySyntaxUtils;
+import org.eclipse.dltk.ruby.internal.debug.ui.console.RubyConsoleSourceModuleLookup;
+import org.eclipse.dltk.ruby.testing.internal.AbstractRubyTestRunnerUI;
+import org.eclipse.dltk.ruby.testing.internal.AbstractRubyTestingEngine;
+import org.eclipse.dltk.ruby.testing.internal.ResolverUtils;
+import org.eclipse.dltk.ruby.testing.internal.RubyTestingPlugin;
+import org.eclipse.dltk.testing.DLTKTestingMessages;
+import org.eclipse.dltk.testing.TestElementResolution;
+import org.eclipse.dltk.testing.model.ITestCaseElement;
+import org.eclipse.dltk.testing.model.ITestSuiteElement;
+import org.eclipse.osgi.util.NLS;
+
+public class MiniTestRunnerUI extends AbstractRubyTestRunnerUI {
+
+	private static final char CLASS_BEGIN = '(';
+	private static final char CLASS_END = ')';
+
+	/**
+	 * @param testingEngine
+	 */
+	public MiniTestRunnerUI(AbstractRubyTestingEngine testingEngine, IScriptProject project) {
+		super(testingEngine, project);
+	}
+
+	@Override
+	public String getTestCaseLabel(ITestCaseElement caseElement, boolean full) {
+		final String testName = caseElement.getTestName();
+		int index = testName.lastIndexOf(CLASS_BEGIN);
+		if (index > 0) {
+			final int braceIndex = index;
+			while (index > 0 && Character.isWhitespace(testName.charAt(index - 1))) {
+				--index;
+			}
+			if (full) {
+				int end = testName.length();
+				if (end > braceIndex + 1 && testName.charAt(end - 1) == CLASS_END) {
+					--end;
+				}
+				final String template = DLTKTestingMessages.TestSessionLabelProvider_testMethodName_className;
+				return NLS.bind(template, testName.substring(braceIndex + 1, end), testName.substring(0, index));
+			} else {
+				return testName.substring(0, index);
+			}
+		} else {
+			return testName;
+		}
+	}
+
+	@Override
+	public String getTestStartedMessage(ITestCaseElement caseElement) {
+		final String testName = caseElement.getTestName();
+		int index = testName.lastIndexOf(CLASS_BEGIN);
+		if (index > 0) {
+			int end = testName.length();
+			if (end > index && testName.charAt(end - 1) == CLASS_END) {
+				--end;
+			}
+			final String className = testName.substring(index + 1, end);
+			while (index > 0 && Character.isWhitespace(testName.charAt(index - 1))) {
+				--index;
+			}
+			final String method = testName.substring(0, index);
+			return NLS.bind(DLTKTestingMessages.TestRunnerViewPart_message_started, className, method);
+		} else {
+			return testName;
+		}
+	}
+
+	@Override
+	protected TestElementResolution resolveTestCase(ITestCaseElement testCase) {
+		final String testName = testCase.getTestName();
+		if (testName.length() == 0) {
+			return null;
+		}
+		final int pos = testName.lastIndexOf(CLASS_BEGIN);
+		if (!(pos > 0 && testName.charAt(testName.length() - 1) == CLASS_END)) {
+			return null;
+		}
+		final String className = testName.substring(pos + 1, testName.length() - 1);
+		if (!RubySyntaxUtils.isValidClass(className)) {
+			return null;
+		}
+		final String methodName = testName.substring(0, pos).trim();
+		if (RubySyntaxUtils.isRubyMethodName(methodName)) {
+			final IMethod method = findMethod(className, methodName);
+			if (method != null) {
+				return new TestElementResolution(method, ResolverUtils.getSourceRange(method));
+			}
+		}
+		final List<IType> types = findClasses(className);
+		if (types == null) {
+			return null;
+		}
+		return null;
+	}
+
+	@Override
+	protected TestElementResolution resolveTestSuite(ITestSuiteElement element) {
+		final String className = element.getSuiteTypeName();
+		if (RubySyntaxUtils.isValidClass(className)) {
+			final List<IType> types = findClasses(className);
+			if (types != null) {
+				final IType type = types.get(0);
+				return new TestElementResolution(type, ResolverUtils.getSourceRange(type));
+			}
+		}
+		return null;
+	}
+
+	private static final class TypeSearchRequestor extends SearchRequestor {
+		final List<IType> types = new ArrayList<IType>();
+
+		@Override
+		public void acceptSearchMatch(SearchMatch match) throws CoreException {
+			types.add((IType) match.getElement());
+		}
+	}
+
+	private static final class MethodRequestor extends SearchRequestor {
+		IMethod method = null;
+
+		@Override
+		public void acceptSearchMatch(SearchMatch match) throws CoreException {
+			method = (IMethod) match.getElement();
+		}
+	}
+
+	/**
+	 * @param className
+	 * @param methodName
+	 * @return
+	 */
+	private IMethod findMethod(String className, String methodName) {
+		final IDLTKSearchScope scope = getSearchScope();
+		final String sPattern = className + "::" + methodName; //$NON-NLS-1$
+		SearchPattern pattern = SearchPattern.createPattern(sPattern, IDLTKSearchConstants.METHOD,
+				IDLTKSearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE,
+				scope.getLanguageToolkit());
+		try {
+			final MethodRequestor requestor = new MethodRequestor();
+			new SearchEngine().search(pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() },
+					scope, requestor, null);
+			return requestor.method;
+		} catch (CoreException e) {
+			final String msg = "Error in findMethod({0}::{1})"; //$NON-NLS-1$
+			RubyTestingPlugin.error(NLS.bind(msg, className, methodName), e);
+		}
+		return null;
+	}
+
+	/**
+	 * @param className
+	 */
+	private List<IType> findClasses(String className) {
+		final IDLTKSearchScope scope = getSearchScope();
+		SearchPattern pattern = SearchPattern.createPattern(className, IDLTKSearchConstants.TYPE,
+				IDLTKSearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE,
+				scope.getLanguageToolkit());
+		try {
+			final TypeSearchRequestor requestor = new TypeSearchRequestor();
+			new SearchEngine().search(pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() },
+					scope, requestor, null);
+			if (!requestor.types.isEmpty()) {
+				return requestor.types;
+			}
+		} catch (CoreException e) {
+			final String msg = "Error in findClasses({0})"; //$NON-NLS-1$
+			RubyTestingPlugin.error(NLS.bind(msg, className), e);
+		}
+		return null;
+	}
+
+	private static final String[] TEST_UNIT = { "test", "unit" }; //$NON-NLS-1$ //$NON-NLS-2$
+
+	private boolean testFragmentPath(IPath fragmentPath, IPath path) {
+		if (pathEquality.isPrefixOf(fragmentPath, path)
+				&& path.segmentCount() > fragmentPath.segmentCount() + TEST_UNIT.length) {
+			for (int j = 0; j < TEST_UNIT.length; ++j) {
+				if (!TEST_UNIT[j].equals(path.segment(fragmentPath.segmentCount() + j))) {
+					return false;
+				}
+			}
+			return true;
+		}
+		return false;
+	}
+
+	@Override
+	protected boolean selectLine(String line) {
+		final String filename = extractFileName(line);
+		if (filename == null) {
+			return true;
+		}
+		if (filename.endsWith(MiniTestingEngine.MINITEST_RUNNER)) {
+			return false;
+		}
+		final IPath path = new Path(filename);
+		try {
+			final IProjectFragment[] fragments = project.getProjectFragments();
+			for (int i = 0; i < fragments.length; ++i) {
+				final IProjectFragment fragment = fragments[i];
+				if (fragment.isExternal()
+						&& testFragmentPath(EnvironmentPathUtils.getLocalPath(fragment.getPath()), path)
+						&& RubyConsoleSourceModuleLookup.isIncluded(fragment, path)) {
+					return false;
+				}
+			}
+		} catch (ModelException e) {
+			return true;
+		}
+		return true;
+	}
+}
diff --git a/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/miniunit/MiniTestingEngine.java b/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/miniunit/MiniTestingEngine.java
new file mode 100644
index 0000000..10ff374
--- /dev/null
+++ b/plugins/org.eclipse.dltk.ruby.testing/src/org/eclipse/dltk/ruby/testing/internal/miniunit/MiniTestingEngine.java
@@ -0,0 +1,160 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Red Hat Inc. and others
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Red Hat, Inc. - initial API and Implementation
+ *******************************************************************************/
+package org.eclipse.dltk.ruby.testing.internal.miniunit;
+
+import java.io.File;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.debug.core.ILaunch;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.dltk.ast.ASTNode;
+import org.eclipse.dltk.ast.declarations.MethodDeclaration;
+import org.eclipse.dltk.ast.declarations.ModuleDeclaration;
+import org.eclipse.dltk.ast.expressions.CallExpression;
+import org.eclipse.dltk.compiler.util.Util;
+import org.eclipse.dltk.core.DLTKCore;
+import org.eclipse.dltk.core.IModelElement;
+import org.eclipse.dltk.core.IScriptProject;
+import org.eclipse.dltk.core.ISourceModule;
+import org.eclipse.dltk.core.environment.IEnvironment;
+import org.eclipse.dltk.launching.InterpreterConfig;
+import org.eclipse.dltk.ruby.ast.RubyClassDeclaration;
+import org.eclipse.dltk.ruby.testing.internal.AbstractRubyTestingEngine;
+import org.eclipse.dltk.ruby.testing.internal.AbstractTestingEngineValidateVisitor;
+import org.eclipse.dltk.ruby.testing.internal.Messages;
+import org.eclipse.dltk.ruby.testing.internal.ResolverUtils;
+import org.eclipse.dltk.ruby.testing.internal.RubyTestingLaunchConfigurationDelegate;
+import org.eclipse.dltk.testing.DLTKTestingConstants;
+import org.eclipse.dltk.testing.ITestRunnerUI;
+import org.eclipse.osgi.util.NLS;
+
+public class MiniTestingEngine extends AbstractRubyTestingEngine {
+
+	static class TestUnitValidateVisitor extends AbstractTestingEngineValidateVisitor {
+
+		private static final String MINITEST_AUTORUN = "minitest/autorun"; //$NON-NLS-1$
+		private static final String MINITEST_TEST = "Minitest::Test"; //$NON-NLS-1$
+		private static final String TEST = "test"; //$NON-NLS-1$
+
+		private ISourceModule module;
+		private int testUnitWeight = 0;
+
+		static final int REQUIRE_WEIGHT = 10;
+		static final int TESTCASE_WEIGHT = 10;
+		static final int METHOD_WEIGHT = 1;
+
+		public TestUnitValidateVisitor(ISourceModule module) {
+			this.module = module;
+		}
+
+		@Override
+		public boolean visitGeneral(ASTNode node) throws Exception {
+			if (node instanceof CallExpression) {
+				final CallExpression call = (CallExpression) node;
+				if (isRequire(call, MINITEST_AUTORUN)) {
+					testUnitWeight += REQUIRE_WEIGHT;
+				}
+			} else if (node instanceof RubyClassDeclaration) {
+				if (isSuperClassOf(module, (RubyClassDeclaration) node, MINITEST_TEST)) {
+					testUnitWeight += TESTCASE_WEIGHT;
+				}
+			} else if (node instanceof MethodDeclaration) {
+				if (isNodeOnStack(RubyClassDeclaration.class) && isMethodPrefix((MethodDeclaration) node, TEST)) {
+					testUnitWeight += METHOD_WEIGHT;
+				}
+			}
+			return super.visitGeneral(node);
+		}
+
+		public IStatus getStatus() {
+			if (testUnitWeight >= Math.min(REQUIRE_WEIGHT, TESTCASE_WEIGHT)) {
+				return Status.OK_STATUS;
+			}
+			if (testUnitWeight >= METHOD_WEIGHT) {
+				return createStatus(IStatus.INFO, Messages.validate_probablyMinitest);
+			}
+			return createStatus(IStatus.WARNING, Messages.validate_notMinitest);
+		}
+	}
+
+	@Override
+	public IStatus validateSourceModule(ISourceModule module) {
+		final ModuleDeclaration declaration = ResolverUtils.parse(module);
+		if (declaration == null) {
+			return createStatus(IStatus.WARNING, Messages.validate_sourceErrors);
+		}
+		final TestUnitValidateVisitor visitor = new TestUnitValidateVisitor(module);
+		try {
+			declaration.traverse(visitor);
+		} catch (Exception e) {
+			return createStatus(IStatus.WARNING, NLS.bind(Messages.validate_runtimeError, e.getMessage()));
+		}
+		return visitor.getStatus();
+	}
+
+	static final String MINITEST_RUNNER = "dltk-minitest-runner.rb"; //$NON-NLS-1$
+
+	@Override
+	public void configureLaunch(InterpreterConfig config, ILaunchConfiguration configuration, ILaunch launch)
+			throws CoreException {
+		// select port number
+		final String strPort = String.valueOf(allocatePort());
+		launch.setAttribute(DLTKTestingConstants.ATTR_PORT, strPort);
+		config.addEnvVar(RUBY_TESTING_PORT, strPort);
+		// add runner
+		if (!RubyTestingLaunchConfigurationDelegate.isContainerMode(configuration)) {
+			if (config.getEnvironment().isLocal()) {
+				final String runnerName = MINITEST_RUNNER;
+				if (!isDevelopmentMode(config, runnerName)) {
+					final File runnerFile = getRunnerFile(getBundle(), RUNNER_PATH, runnerName);
+					config.addInterpreterArg("-r"); //$NON-NLS-1$
+					config.addInterpreterArg(runnerFile.getPath());
+				}
+			}
+		} else {
+			final String containerHandle = configuration.getAttribute(DLTKTestingConstants.ATTR_TEST_CONTAINER,
+					Util.EMPTY_STRING);
+			Assert.isLegal(containerHandle.length() != 0);
+			IModelElement element = DLTKCore.create(containerHandle);
+			Assert.isNotNull(element);
+			IResource resource = element.getUnderlyingResource();
+			Assert.isNotNull(resource);
+			final IPath path = resource.getProjectRelativePath();
+			if (path.isEmpty()) {
+				config.addEnvVar(RUBY_TESTING_PATH, "."); //$NON-NLS-1$
+			} else {
+				config.addEnvVar(RUBY_TESTING_PATH, path.toOSString());
+			}
+		}
+	}
+
+	@Override
+	public String getMainScriptPath(ILaunchConfiguration configuration, IEnvironment scriptEnvironment)
+			throws CoreException {
+		if (RubyTestingLaunchConfigurationDelegate.isContainerMode(configuration)) {
+			return getRunnerFile(getBundle(), RUNNER_PATH, MINITEST_RUNNER).getPath();
+		} else {
+			return null;
+		}
+	}
+
+	@Override
+	public ITestRunnerUI getTestRunnerUI(IScriptProject project, ILaunchConfiguration configuration) {
+		return new MiniTestRunnerUI(this, project);
+	}
+
+}
diff --git a/plugins/org.eclipse.dltk.ruby.testing/testing/dltk-minitest-runner.rb b/plugins/org.eclipse.dltk.ruby.testing/testing/dltk-minitest-runner.rb
new file mode 100644
index 0000000..641b3ef
--- /dev/null
+++ b/plugins/org.eclipse.dltk.ruby.testing/testing/dltk-minitest-runner.rb
@@ -0,0 +1,13 @@
+#
+# Copyright (c) 2016 Red Hat Inc. and others
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+#
+# Contributors:
+#    Red Hat Inc. - initial API and Implementation
+#
+
+$:.unshift File.dirname(__FILE__)
\ No newline at end of file
diff --git a/plugins/org.eclipse.dltk.ruby.testing/testing/minitest/dltk_plugin.rb b/plugins/org.eclipse.dltk.ruby.testing/testing/minitest/dltk_plugin.rb
new file mode 100644
index 0000000..ac48f03
--- /dev/null
+++ b/plugins/org.eclipse.dltk.ruby.testing/testing/minitest/dltk_plugin.rb
@@ -0,0 +1,172 @@
+#
+# Copyright (c) 2016 Red Hat Inc. and others
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+#
+# Contributors:
+#    Red Hat Inc. - initial API and Implementation
+#
+
+require 'minitest/autorun'
+require 'minitest/spec'
+require 'socket'
+
+module Minitest
+  module EnvVars
+    # environment variable name to pass communication port number
+    # to the launched script
+    PORT = "RUBY_TESTING_PORT"
+    PATH = "RUBY_TESTING_PATH"
+  end
+
+  module MessageIds
+    # Notification that a test run has started.
+    # MessageIds.TEST_RUN_START + testCount.toString + " " + version
+    TEST_RUN_START = "%TESTC  "
+
+    # Notification that a test run has ended.
+    # TEST_RUN_END + elapsedTime.toString().
+    TEST_RUN_END   = "%RUNTIME"
+
+    # Notification about a test inside the test suite.
+    # TEST_TREE + testId + "," + testName + "," + isSuite + "," + testcount
+    # isSuite = "true" or "false"
+    TEST_TREE      = "%TSTTREE"
+
+    #Notification that a test has started.
+    # MessageIds.TEST_START + testID + "," + testName
+    TEST_START     = "%TESTS  "
+
+    # Notification that a test has ended.
+    # TEST_END + testID + "," + testName
+    TEST_END       = "%TESTE  "
+
+    # Notification that a test had a error.
+    # TEST_ERROR + testID + "," + testName.
+    # After the notification follows the stack trace.
+    TEST_ERROR     = "%ERROR  "
+
+    # Notification that a test had a failure.
+    # TEST_FAILED + testID + "," + testName.
+    # After the notification follows the stack trace.
+    TEST_FAILED    = "%FAILED "
+
+    # Notification that a test trace has started.
+    # The end of the trace is signaled by a TRACE_END
+    # message. In between the TRACE_START and TRACE_END
+    # the stack trace is submitted as multiple lines.
+    TRACE_START    = "%TRACES "
+
+    # Notification that a trace ends.
+    TRACE_END      = "%TRACEE "
+
+    # Notification that the expected result has started.
+    # The end of the expected result is signaled by a EXPECTED_END.
+    EXPECTED_START = "%EXPECTS"
+
+    # Notification that an expected result ends.
+    EXPECTED_END   = "%EXPECTE"
+
+    # Notification that the actual result has started.
+    # The end of the actual result is signaled by a ACTUAL_END.
+    ACTUAL_START   = "%ACTUALS"
+
+    # Notification that an actual result ends.
+    ACTUAL_END     = "%ACTUALE"
+
+    #Test identifier prefix for ignored tests.
+    IGNORED_TEST_PREFIX = "@Ignore: "
+
+  end # of MessageIds
+
+  def self.plugin_dltk_init(options)
+    Minitest.reporter.reporters.clear
+    self.reporter << DLTKReporter.new(options[:io], options)
+  end
+
+  class DLTKReporter < Minitest::StatisticsReporter
+    def start
+      connectSocket ENV[EnvVars::PORT].to_i
+      @testsByName = {}
+      DLTKReporter.sendMessage MessageIds::TEST_RUN_START + 0.to_s + " v2"
+      super
+    end
+
+    def record(result)
+      notifyTestError result if result.error?
+      notifyTestFailure result if result.failure and !result.error?
+      DLTKReporter.sendMessage MessageIds::TEST_END + result.class.name+result.name + "," + result.name
+    end
+
+    def report
+      super
+      DLTKReporter.sendMessage MessageIds::TEST_RUN_END + (@total_time.to_i * 1000).to_s
+    end
+
+    def connectSocket(port)
+      return false unless port > 0
+      #debug "Opening socket on #{port}"
+      for i in 1..10
+        #debug "Iteration #{i}"
+        begin
+          @@socket = TCPSocket.new('localhost', port)
+          #debug "Socket opened"
+          return true
+        rescue
+          #debug $!.to_s
+        end
+        sleep 1
+      end
+      false
+    end
+
+    def disconnect(result)
+      if @@socket
+        #debug "Closing socket"
+        begin
+          @@socket.close
+        rescue
+          debug $!.to_s
+        end
+        @@socket = nil
+        #debug "Socket closed"
+      end
+    end
+
+    def notifyTestFailure(result)
+      DLTKReporter.sendMessage MessageIds::TEST_FAILED + result.class.name+result.name + "," + result.name
+      DLTKReporter.sendMessage MessageIds::TRACE_START
+      DLTKReporter.sendMessage result.failure.to_s
+      DLTKReporter.sendMessage result.failure.location
+      DLTKReporter.sendMessage MessageIds::TRACE_END
+    end
+
+    def notifyTestError(result)
+      location, line = result.method(result.name).source_location
+      DLTKReporter.sendMessage MessageIds::TEST_ERROR + result.class.name+result.name + "," + result.name
+      DLTKReporter.sendMessage MessageIds::TRACE_START
+      DLTKReporter.sendMessage result.failure.to_s
+      DLTKReporter.sendMessage result.failure.location
+      DLTKReporter.sendMessage MessageIds::TRACE_END
+    end
+
+    def self.sendMessage(message)
+      #debug message
+      if @@socket
+        @@socket.puts message
+      end
+    end
+
+  end
+
+  class Runnable
+    def self.run_one_method klass, method_name, reporter
+      DLTKReporter.sendMessage MessageIds::TEST_START + klass.name+method_name + ',' + method_name
+      reporter.record Minitest.run_one_method(klass, method_name)
+    end
+  end
+
+end
\ No newline at end of file