/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2010 Oracle and/or its affiliates. All rights reserved. * * Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. * * Contributor(s): * * Portions Copyrighted 2008 Sun Microsystems, Inc. */ package org.netbeans.modules.ruby.testrunner; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import org.netbeans.api.extexecution.print.LineConvertors.FileLocator; import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectUtils; import org.netbeans.api.ruby.platform.RubyPlatform; import org.netbeans.modules.gsf.testrunner.api.Manager; import org.netbeans.modules.gsf.testrunner.api.TestSession; import org.netbeans.modules.gsf.testrunner.api.TestSession.SessionType; import org.netbeans.modules.ruby.platform.execution.RubyExecutionDescriptor; import org.netbeans.modules.ruby.rubyproject.RubyBaseProject; import org.netbeans.modules.ruby.rubyproject.RubyProjectUtil; import org.netbeans.modules.ruby.rubyproject.SharedRubyProjectProperties; import org.netbeans.modules.ruby.rubyproject.rake.RakeTask; import org.netbeans.modules.ruby.rubyproject.spi.RakeTaskCustomizer; import org.netbeans.modules.ruby.rubyproject.spi.TestRunner; import org.netbeans.modules.ruby.testrunner.TestRunnerUtilities.DefaultTaskEvaluator; import org.netbeans.modules.ruby.testrunner.ui.RspecHandlerFactory; import org.netbeans.modules.ruby.testrunner.ui.TestRunnerInputProcessorFactory; import org.netbeans.modules.ruby.testrunner.ui.TestRunnerLineConvertor; import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor.Message; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.modules.InstalledFileLocator; import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.netbeans.modules.ruby.codecoverage.RubyCoverageProvider; import org.netbeans.modules.ruby.rubyproject.RubyTestingSettings; /** * Test runner for RSpec tests. * * <i>TODO: get rid of duplication with RSpecSupport, such as finding the rspec binary</i>. * * @author Erno Mononen */ @org.openide.util.lookup.ServiceProviders({@org.openide.util.lookup.ServiceProvider(service=org.netbeans.modules.ruby.rubyproject.spi.RakeTaskCustomizer.class), @org.openide.util.lookup.ServiceProvider(service=org.netbeans.modules.ruby.rubyproject.spi.TestRunner.class)}) public class RspecRunner implements TestRunner, RakeTaskCustomizer { private static final String PLUGIN_SPEC_PATH = "vendor/plugins/rspec/bin/spec"; // NOI18N private static final String SCRIPT_SPEC_PATH = "script/spec"; // NOI18N private static final String SPEC_BIN = "spec"; // NOI18N private static final TestRunner INSTANCE = new RspecRunner(); private static final String SPEC_OPTS = "spec/spec.opts"; // NOI18N private static final String NETBEANS_SPEC_OPTS_SUFFIX = "netbeans"; //NOI18N private static final String NETBEANS_SPEC_OPTS = SPEC_OPTS + "." + NETBEANS_SPEC_OPTS_SUFFIX; // NOI18N public static final String RSPEC_MEDIATOR_SCRIPT = "nb_rspec_mediator.rb"; //NOI18N /** * The name of the system property that specifies whether a warning message should * be displayed when using spec/spec.opts instead of spec/spec.opts.netbeans. * See #101997. */ private static final String SPEC_OPTS_WARN_PROP = "ruby.rspec.specopts.warn"; //NOI18N public TestRunner getInstance() { return INSTANCE; } public boolean supports(TestType type) { return TestType.RSPEC == type; } private static boolean warnWhenUsingSpecOpts() { return Boolean.valueOf(System.getProperty(SPEC_OPTS_WARN_PROP, "true")); //NOI18N } public void runTest(FileObject testFile, boolean debug) { List<String> specFile = new ArrayList<String>(); specFile.add(FileUtil.toFile(testFile).getAbsolutePath()); List<String> additionalArgs = new ArrayList<String>(); additionalArgs.add(FileUtil.toFile(testFile).getAbsolutePath()); run(FileOwnerQuery.getOwner(testFile), additionalArgs, testFile.getName(), debug); } public void runSingleTest(FileObject testFile, String testMethod, boolean debug) { // the testMethod param here actually presents the line number // of the rspec specification in the test file. List<String> additionalArgs = new ArrayList<String>(); additionalArgs.add("--line"); additionalArgs.add(testMethod); additionalArgs.add(FileUtil.toFile(testFile).getAbsolutePath()); run(FileOwnerQuery.getOwner(testFile), additionalArgs, testFile.getName(), debug); } public void runAllTests(Project project, boolean debug) { // collect specs from the test dirs (will be changed to use rake spec in the future) RubyBaseProject baseProject = project.getLookup().lookup(RubyBaseProject.class); FileObject[] testDirs = baseProject.getTestSourceRootFiles(); List<String> specs = new ArrayList<String>(); for (FileObject dir : testDirs) { Enumeration<? extends FileObject> children = dir.getChildren(true); while (children.hasMoreElements()) { FileObject each = children.nextElement(); if (!each.isFolder() && "rb".equals(each.getExt()) && each.getName().endsWith("spec")) { //NOI18N specs.add(FileUtil.toFile(each).getAbsolutePath()); } } } run(project, specs, ProjectUtils.getInformation(project).getDisplayName(), debug); } private void run(Project project, List<String> additionalArgs, String name, boolean debug) { FileLocator locator = project.getLookup().lookup(FileLocator.class); RubyPlatform platform = RubyPlatform.platformFor(project); if (additionalArgs.isEmpty()) { // just display 'no tests run' immediately if there are no files to run TestSession empty = new TestSession(name, project, debug ? SessionType.DEBUG : SessionType.TEST, new RubyTestRunnerNodeFactory()); Manager.getInstance().emptyTestRun(empty); return; } List<String> arguments = new ArrayList<String>(); arguments.add("--require"); //NOI18N arguments.add(getMediatorScript().getAbsolutePath()); arguments.add("--runner"); //NOI18N arguments.add("NbRspecMediator"); //NOI18N FileObject opts = addSpecOpts(project, additionalArgs); arguments.addAll(additionalArgs); RubyExecutionDescriptor desc = null; String charsetName = null; File spec = getSpec(project); if (spec == null) { DialogDisplayer.getDefault().notify( new Message(NbBundle.getMessage(RspecRunner.class, "MSG_SpecNotFound"), Message.ERROR_MESSAGE)); return; } desc = new RubyExecutionDescriptor(platform, name, FileUtil.toFile(project.getProjectDirectory()), getSpec(project).getAbsolutePath()); desc.additionalArgs(arguments.toArray(new String[arguments.size()])); TestRunnerUtilities.addProperties(desc, project); desc.addInitialArgs(RubyProjectUtil.getLoadPath(project)); //NOI18N desc.debug(debug); desc.allowInput(); desc.fileLocator(locator); desc.addStandardRecognizers(); RubyCoverageProvider coverageProvider = RubyCoverageProvider.get(project); if (coverageProvider != null && coverageProvider.isEnabled()) { desc = coverageProvider.wrapWithCoverage(desc, false, null); } TestSession session = new TestSession(name, project, debug ? SessionType.DEBUG : SessionType.TEST, new RubyTestRunnerNodeFactory()); addSpecOptsWarningIfNeeded(session, opts); TestExecutionManager.getInstance().start(desc, new RspecHandlerFactory(), session); } /** * Gets the spec binary to use for the project. Prefers the spec rails plugin * if found. * * @param project * @return */ private File getSpec(Project project) { FileObject projectDir = project.getProjectDirectory(); FileObject specScript = projectDir.getFileObject(SCRIPT_SPEC_PATH); if (specScript != null) { return FileUtil.toFile(specScript); } if (projectDir != null) { FileObject pluginSpec = projectDir.getFileObject(PLUGIN_SPEC_PATH); if (pluginSpec != null) { return FileUtil.toFile(pluginSpec); } } RubyPlatform platform = RubyPlatform.platformFor(project); String spec = platform.findExecutable(SPEC_BIN); //NOI18N if (spec != null) { return new File(spec); } return null; } static File getMediatorScript() { File mediatorScript = InstalledFileLocator.getDefault().locate( RSPEC_MEDIATOR_SCRIPT, "org.netbeans.modules.ruby.testrunner", false); // NOI18N if (mediatorScript == null) { throw new IllegalStateException("Could not locate " + RSPEC_MEDIATOR_SCRIPT); // NOI18N } return mediatorScript; } private static FileObject addSpecOpts(Project project, List<String> additionalArgs) { FileObject specOpts = getSpecOpts(project); if (specOpts != null) { additionalArgs.add("--options"); // NOI18N additionalArgs.add(FileUtil.toFile(specOpts).getAbsolutePath()); } return specOpts; } private static FileObject getSpecOpts(Project project) { // TODO: duplicated in RSpecSupport FileObject projectDir = project.getProjectDirectory(); // First look for a NetBeans-specific options file, in case you want different // options when running under the IDE (for example, no --color since the // color escape codes don't work under our terminal) FileObject specOpts = projectDir.getFileObject(NETBEANS_SPEC_OPTS); if (specOpts == null) { specOpts = projectDir.getFileObject(SPEC_OPTS); } return specOpts; } private String getSpecOptsContent(FileObject specOpts) { StringBuilder result = new StringBuilder(); BufferedReader from = null; try { from = new BufferedReader(new InputStreamReader(specOpts.getInputStream())); String line; while ((line = from.readLine()) != null) { result.append(line); result.append(" "); } } catch (FileNotFoundException ex) { Exceptions.printStackTrace(ex); } catch (IOException ioe) { Exceptions.printStackTrace(ioe); } finally { try { from.close(); } catch (IOException ex) { Exceptions.printStackTrace(ex); } } return result.toString(); } private static void addSpecOptsWarningIfNeeded(TestSession session, FileObject specOpts) { if (specOpts == null || !warnWhenUsingSpecOpts()) { return; } if (!NETBEANS_SPEC_OPTS_SUFFIX.equals(specOpts.getExt())) { //NOI18N session.setStartingMsg(NbBundle.getMessage(RspecRunner.class, "MSG_SpecOptsWarning", SPEC_OPTS, NETBEANS_SPEC_OPTS, SPEC_OPTS_WARN_PROP)); } } public void customize(Project project, RakeTask task, RubyExecutionDescriptor taskDescriptor, boolean debug) { boolean useRunner = RubyTestingSettings.getDefault().useRunner(TestType.RSPEC) && TestRunnerUtilities.useTestRunner(project, SharedRubyProjectProperties.SPEC_TASKS, task, RSpecTaskEvaluator.INSTANCE); if (!useRunner) { return; } TestExecutionManager.getInstance().reset(); String path = getMediatorScript().getAbsolutePath(); if (Utilities.isWindows()) { RubyPlatform platform = RubyPlatform.platformFor(project); if (platform != null && platform.isJRuby()) { // backslashes don't work with JRuby here path = path.replace('\\', '/'); //NOI18N } } String options = "--require '" + path + "' --runner NbRspecMediator"; //NOI18N FileObject specOpts = getSpecOpts(project); if (specOpts != null) { options += " " + getSpecOptsContent(specOpts); } task.addTaskParameters("SPEC_OPTS=" + options); //NOI18N TestSession session = new TestSession(task.getDisplayName(), project, debug ? SessionType.DEBUG : SessionType.TEST, new RubyTestRunnerNodeFactory()); addSpecOptsWarningIfNeeded(session, specOpts); Manager manager = Manager.getInstance(); final TestRunnerLineConvertor convertor = new TestRunnerLineConvertor(manager, session, new RspecHandlerFactory()); TestRunnerUtilities.setUpConvertors(session, taskDescriptor, manager, convertor); TestRunnerUtilities.addProperties(taskDescriptor, project); TestExecutionManager.getInstance().init(taskDescriptor); session.setRerunHandler(TestExecutionManager.getInstance()); } private static class RSpecTaskEvaluator implements DefaultTaskEvaluator { static final RSpecTaskEvaluator INSTANCE = new RSpecTaskEvaluator(); private RSpecTaskEvaluator() { } public boolean isDefault(RakeTask task) { return "spec".equals(task.getTask()); //NOI18N } } }