001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.netbeans.modules.apisupport.project.ui;
042:
043: import java.awt.event.ActionEvent;
044: import java.io.File;
045: import java.io.IOException;
046: import java.util.ArrayList;
047: import java.util.Arrays;
048: import java.util.Iterator;
049: import java.util.List;
050: import java.util.Properties;
051: import java.util.logging.Logger;
052: import javax.swing.AbstractAction;
053: import javax.swing.Action;
054: import javax.swing.JComponent;
055: import javax.swing.JMenu;
056: import javax.swing.JMenuItem;
057: import javax.swing.JSeparator;
058: import org.apache.tools.ant.module.api.AntProjectCookie;
059: import org.apache.tools.ant.module.api.support.ActionUtils;
060: import org.apache.tools.ant.module.api.support.TargetLister;
061: import org.netbeans.api.project.Project;
062: import org.netbeans.modules.apisupport.project.NbModuleProject;
063: import org.openide.ErrorManager;
064: import org.openide.awt.Actions;
065: import org.openide.awt.Mnemonics;
066: import org.openide.filesystems.FileObject;
067: import org.openide.filesystems.FileStateInvalidException;
068: import org.openide.loaders.DataObject;
069: import org.openide.loaders.DataObjectNotFoundException;
070: import org.openide.util.ContextAwareAction;
071: import org.openide.util.Lookup;
072: import org.openide.util.Task;
073: import org.openide.util.TaskListener;
074: import org.openide.util.actions.Presenter;
075: import org.openide.util.NbBundle;
076: import org.openide.awt.DynamicMenuContent;
077: import org.openide.awt.HtmlBrowser;
078: import org.openide.execution.ExecutorTask;
079:
080: /**
081: * Defines XTest actions for top level project logical node. Inspired by CVS
082: * sub menu org.netbeans.modules.versioning.system.cvss.ui.actions.ProjectCvsMenuItem.
083: * Used also in xtest/module.
084: *
085: * <pre>
086: * XTest >
087: * Clean
088: * --------------------------
089: * Build unit Tests
090: * Run unit Tests
091: * Measure unit Test Coverage
092: * --------------------------
093: * Build qa-functional Tests
094: * Run qa-functional Tests
095: * Measure qa-functional Test Coverage
096: * </pre>
097: *
098: * <p>The menu is available only for projects that contains some tests.
099: *
100: * @author Jiri.Skrivanek@sun.com
101: */
102: public final class XTestProjectMenuItem extends AbstractAction
103: implements Presenter.Popup, ContextAwareAction {
104:
105: public static final String NAME = NbBundle.getBundle(
106: XTestProjectMenuItem.class).getString("CTL_MenuItem_XTest");
107: private static final String RUN_TEST_DIST = "runtests-dist";
108: private Lookup context;
109:
110: /** Creates XTest sub menu. */
111: public XTestProjectMenuItem() {
112: this (null);
113: }
114:
115: /** Creates XTest sub menu.
116: * @param actionContext context of action
117: */
118: public XTestProjectMenuItem(Lookup actionContext) {
119: super (NAME);
120: this .context = actionContext;
121: }
122:
123: /** No action for sub menu holder.
124: * @param ignore action event
125: */
126: public void actionPerformed(ActionEvent ignore) {
127: // empty
128: }
129:
130: /** Returns created popup menu.
131: * @return created popup menu.
132: */
133: public JMenuItem getPopupPresenter() {
134: return new XTestProjectMenuItems();
135: }
136:
137: public Action createContextAwareInstance(Lookup actionContext) {
138: return new XTestProjectMenuItem(actionContext);
139: }
140:
141: /** Sub menu items. */
142: class XTestProjectMenuItems extends JMenuItem implements
143: DynamicMenuContent {
144:
145: public JComponent[] getMenuPresenters() {
146: Action[] actions = actions();
147: if (actions == null) {
148: // hide sub menu
149: return new JComponent[0];
150: }
151: final JMenu menu = new JMenu();
152: Mnemonics.setLocalizedText(menu, NAME);
153: for (int i = 0; i < actions.length; i++) {
154: Action action = actions[i];
155: if (action == null) {
156: menu.add(new JSeparator());
157: } else {
158: JMenuItem item = new JMenuItem();
159: Actions.connect(item, actions[i], false);
160: menu.add(item);
161: }
162: }
163: return new JComponent[] { menu };
164: }
165:
166: public JComponent[] synchMenuPresenters(JComponent[] items) {
167: return items;
168: }
169: }
170:
171: /** Returns array of actions for all test types in selected project. */
172: private Action[] actions() {
173: Project project = context.lookup(Project.class);
174: // It should not happen but issue 86977 claims it happens.
175: if (project == null) {
176: return null;
177: }
178: if (project.getLookup().lookup(ModuleActions.class) == null) {
179: // do not create sub menu for non-NbModuleProject
180: return null;
181: }
182: List<Action> actions = new ArrayList<Action>();
183:
184: String[] testTypes = findTestTypes(project);
185: if (testTypes.length > 0) {
186: actions.add(createAction(NbBundle.getMessage(
187: XTestProjectMenuItem.class, "CTL_MenuItem_Clean"),
188: project, "", new String[] { "realclean" }, false));
189: actions.add(null);
190: } else {
191: // hide XTest submenu
192: return null;
193: // or
194: //actions.add(createAction("Create XTest Infrastructure", project, "", new String[] {""}, false));
195: // and open new file wizard Testing Tools|XTest Infrastructure (enable it always)
196: }
197: for (int i = 0; i < testTypes.length; i++) {
198: // "Build "+testTypes[i]+" Tests"
199: actions.add(createAction(
200: NbBundle.getMessage(XTestProjectMenuItem.class,
201: "CTL_MenuItem_BuildTests", testTypes[i]), // NOI18N
202: project, testTypes[i],
203: new String[] { "buildtests" }, false)); // NOI18N
204: // "Run "+testTypes[i]+" Tests"
205: actions.add(createAction(
206: NbBundle.getMessage(XTestProjectMenuItem.class,
207: "CTL_MenuItem_RunTests", testTypes[i]), // NOI18N
208: project, testTypes[i], new String[] { "runtests" },
209: true)); // NOI18N
210: actions.add(createAction(
211: NbBundle.getMessage(XTestProjectMenuItem.class,
212: "CTL_MenuItem_RunTestsDist", testTypes[i]), // NOI18N
213: project, testTypes[i],
214: new String[] { RUN_TEST_DIST }, true)); // NOI18N
215: if (targetExists(findTestBuildXml(project), "coverage")) { //NOI18N
216: // "Measure "+testTypes[i]+" Tests Coverage"
217: actions.add(createAction(
218: NbBundle.getMessage(XTestProjectMenuItem.class,
219: "CTL_MenuItem_MeasureCoverage",
220: testTypes[i]), // NOI18N
221: project, testTypes[i],
222: new String[] { "coverage" }, true)); // NOI18N
223: }
224: // add separator
225: if (testTypes.length - 1 > i) {
226: actions.add(null);
227: }
228: }
229: return actions.toArray(new Action[actions.size()]);
230: }
231:
232: private AbstractAction createAction(String displayName,
233: final Project project, final String testType,
234: final String[] targets, final boolean showResults) {
235:
236: return new AbstractAction(displayName) {
237: /** Enabled only if one project is selected and test/build.xml exists. */
238: @Override
239: public boolean isEnabled() {
240: if (RUN_TEST_DIST.equals(targets[0])) {
241: NbModuleProject nbprj = project.getLookup().lookup(
242: NbModuleProject.class);
243: return (nbprj != null)
244: && nbprj.getNbrootFile("nbbuild") != null; // NOI18N
245: }
246: // enable only if one project is selected
247: if (context.lookup(
248: new Lookup.Template<Project>(Project.class))
249: .allInstances().size() == 1) {
250: return findTestBuildXml(project) != null;
251: }
252: return false;
253: }
254:
255: /** Run given target and show test results if requested. */
256: public void actionPerformed(ActionEvent ignore) {
257: Properties props = new Properties();
258: props.setProperty("xtest.testtype", testType); // NOI18N
259: try {
260: if (targets[0].equals(RUN_TEST_DIST)) {
261: runTestDist();
262: } else {
263: ExecutorTask task = ActionUtils.runTarget(
264: findTestBuildXml(project), targets,
265: props);
266: task.addTaskListener(new TaskListener() {
267: public void taskFinished(Task task) {
268: if (((ExecutorTask) task).result() == 0
269: && showResults) {
270: if (targets[0].equals("coverage")) { //NOI18N
271: showCoverageResults(project);
272: } else {
273: showTestResults(project);
274: }
275: }
276: }
277: });
278: }
279: } catch (IOException e) {
280: ErrorManager.getDefault().notify(e);
281: }
282: }
283:
284: /** Run tests from binary test distribution
285: */
286: private void runTestDist() throws IOException {
287: // build tests
288: Properties props = new Properties();
289: final NbModuleProject nbprj = project.getLookup()
290: .lookup(NbModuleProject.class);
291: FileObject buildXml = nbprj
292: .getNbrootFileObject("nbbuild/build.xml"); // NOI18N
293: props.put("config.modules.test", nbprj
294: .getPathWithinNetBeansOrg()); // NOI18N
295: if (buildXml != null) {
296: ExecutorTask task = ActionUtils.runTarget(buildXml,
297: new String[] { "build-test-dist" }, props); // NOI18N
298: task.waitFinished();
299: if (task.result() == 0) {
300: // run tests
301: String path = "nbbuild/build/testdist/"
302: + testType + "/xtest-all-" + testType
303: + ".xml"; // NOI18N
304: props.clear();
305: File nbDestDir = nbprj
306: .getNbrootFile("nbbuild/netbeans"); // NOI18N
307: props.put("netbeans.dest.dir", nbDestDir
308: .getCanonicalPath()); // NOI18N
309: task = ActionUtils.runTarget(nbprj
310: .getNbrootFileObject(path),
311: new String[] { "all" }, props); // NOI18N
312: task.addTaskListener(new TaskListener() {
313: public void taskFinished(Task task) {
314: showBrowser(nbprj
315: .getNbrootFileObject("nbbuild/build/testdist/"
316: + testType
317: + "/results/index.html")); // NOI18N
318: }
319: });
320: }
321: }
322: }
323: };
324: }
325:
326: /** Returns true if target is available in build script. */
327: private static boolean targetExists(FileObject buildXml,
328: String targetName) {
329: if (buildXml == null) {
330: return false;
331: }
332: DataObject d;
333: try {
334: d = DataObject.find(buildXml);
335: } catch (DataObjectNotFoundException ex) {
336: ErrorManager.getDefault().notify(ex);
337: return false;
338: }
339: AntProjectCookie apc = d.getCookie(AntProjectCookie.class);
340: Iterator iter;
341: try {
342: iter = TargetLister.getTargets(apc).iterator();
343: } catch (IOException ex) {
344: // something wrong in build.xml => target not found
345: Logger.getAnonymousLogger().fine(ex.getMessage());
346: return false;
347: }
348: while (iter.hasNext()) {
349: if (((TargetLister.Target) iter.next()).getName().equals(
350: targetName)) {
351: return true;
352: }
353: }
354: return false;
355: }
356:
357: /** Returns FileObject representing test/build.xml or null if it doesn't exist. */
358: private static FileObject findTestBuildXml(Project project) {
359: return project.getProjectDirectory().getFileObject(
360: "test/build.xml"); // NOI18N
361: }
362:
363: /** Opens test/results/index.html in browser of specified project.
364: * @param project project to open test results for
365: */
366: private static void showTestResults(Project project) {
367: showBrowser(project.getProjectDirectory().getFileObject(
368: "test/results/index.html")); // NOI18N
369: }
370:
371: /** Opens test/coverage/coverage.html in browser.
372: * @param project project to open test results for
373: */
374: private static void showCoverageResults(Project project) {
375: showBrowser(project.getProjectDirectory().getFileObject(
376: "test/coverage/coverage.html")); // NOI18N
377: }
378:
379: /** Opens location in browser.
380: * @param resultsFO FileObject to be opened in browser
381: */
382: private static void showBrowser(FileObject resultsFO) {
383: if (resultsFO != null) {
384: try {
385: HtmlBrowser.URLDisplayer.getDefault().showURL(
386: resultsFO.getURL());
387: } catch (FileStateInvalidException ex) {
388: ErrorManager.getDefault().notify(ex);
389: }
390: }
391: }
392:
393: /** Returns sorted array of available test types in given project. It searches
394: * for pairs build-testtype.xml, cfg-testtype.xml and returns array
395: * of available test types. */
396: private static String[] findTestTypes(Project project) {
397: FileObject testFO = project.getProjectDirectory()
398: .getFileObject("test"); // NOI18N
399: if (testFO == null) {
400: return new String[0];
401: }
402: FileObject[] fos = testFO.getChildren(); // NOI18N
403: List<String> testTypes = new ArrayList<String>();
404: for (int i = 0; i < fos.length; i++) {
405: if (fos[i].getExt().equalsIgnoreCase("xml")
406: && fos[i].getName().matches("build-.*")) { // NOI18N
407: String testType = fos[i].getName().substring(
408: fos[i].getName().indexOf('-') + 1);
409: if (project.getProjectDirectory().getFileObject(
410: "test/cfg-" + testType + ".xml") != null) { // NOI18N
411: testTypes.add(testType);
412: }
413: }
414: }
415: String[] result = testTypes
416: .toArray(new String[testTypes.size()]);
417: Arrays.sort(result);
418: return result;
419: }
420: }
|