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: * Portions Copyrighted 2007 Sun Microsystems, Inc.
027: */
028: package org.netbeans.modules.ruby.hints;
029:
030: import java.util.Collections;
031: import org.netbeans.modules.gsf.api.OffsetRange;
032: import org.netbeans.editor.BaseDocument;
033: import org.netbeans.editor.Utilities;
034: import org.netbeans.modules.ruby.RubyTestBase;
035: import java.util.Map;
036: import org.netbeans.modules.gsf.api.CompilationInfo;
037: import java.util.ArrayList;
038: import java.util.Comparator;
039: import java.util.HashMap;
040: import java.util.HashSet;
041: import java.util.List;
042: import java.util.Map;
043: import java.util.Set;
044: import java.util.prefs.Preferences;
045: import javax.swing.text.Document;
046: import org.jruby.ast.Node;
047: import org.netbeans.modules.gsf.api.CompilationInfo;
048: import org.netbeans.modules.gsf.api.OffsetRange;
049: import org.netbeans.api.ruby.platform.TestUtil;
050: import org.netbeans.editor.BaseDocument;
051: import org.netbeans.editor.Utilities;
052: import org.netbeans.junit.NbTestCase;
053: import org.netbeans.modules.ruby.AstUtilities;
054: import org.netbeans.modules.ruby.hints.options.HintsSettings;
055: import org.netbeans.modules.ruby.hints.spi.AstRule;
056: import org.netbeans.modules.ruby.hints.infrastructure.RubyHintsProvider;
057: import org.netbeans.modules.ruby.hints.infrastructure.RulesManager;
058: import org.netbeans.modules.ruby.hints.spi.ErrorRule;
059: import org.netbeans.modules.ruby.hints.spi.HintSeverity;
060: import org.netbeans.modules.ruby.hints.spi.Rule;
061: import org.netbeans.modules.ruby.hints.spi.SelectionRule;
062: import org.netbeans.modules.ruby.hints.spi.UserConfigurableRule;
063: import org.netbeans.spi.editor.hints.ErrorDescription;
064: import org.netbeans.spi.editor.hints.Fix;
065: import org.netbeans.spi.editor.hints.LazyFixList;
066: import org.openide.filesystems.FileObject;
067: import org.openide.filesystems.FileUtil;
068:
069: /**
070: * Common utility methods for testing a hint
071: *
072: * @author Tor Norbye
073: */
074: public abstract class HintTestBase extends RubyTestBase {
075:
076: public HintTestBase(String testName) {
077: super (testName);
078: }
079:
080: private static final String[] JRUBY_BIG_FILES = {
081: // Biggest files in the standard library
082: "lib/ruby/1.8/drb/drb.rb",
083: "lib/ruby/1.8/rdoc/parsers/parse_rb.rb",
084: "lib/ruby/1.8/rdoc/parsers/parse_f95.rb",
085: "lib/ruby/1.8/net/http.rb",
086: "lib/ruby/1.8/cgi.rb",
087: "lib/ruby/1.8/net/imap.rb",
088: // Biggest files in Rails
089: "lib/ruby/gems/1.8/gems/activerecord-2.0.2/test/associations_test.rb",
090: "lib/ruby/gems/1.8/gems/actionmailer-2.0.2/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb",
091: "lib/ruby/gems/1.8/gems/actionpack-2.0.2/test/controller/routing_test.rb",
092: "lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations.rb",
093: "lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb",
094: "lib/ruby/gems/1.8/gems/actionpack-2.0.2/test/template/date_helper_test.rb", };
095:
096: protected List<FileObject> getBigSourceFiles() {
097: FileObject jruby = TestUtil.getXTestJRubyHomeFO();
098:
099: List<FileObject> files = new ArrayList<FileObject>();
100: for (String relative : JRUBY_BIG_FILES) {
101: FileObject f = jruby.getFileObject(relative);
102: assertNotNull(relative, f);
103: files.add(f);
104: }
105:
106: return files;
107: }
108:
109: private String annotate(BaseDocument doc,
110: List<ErrorDescription> result, int caretOffset)
111: throws Exception {
112: Map<OffsetRange, List<ErrorDescription>> posToDesc = new HashMap<OffsetRange, List<ErrorDescription>>();
113: Set<OffsetRange> ranges = new HashSet<OffsetRange>();
114: for (ErrorDescription desc : result) {
115: int start = desc.getRange().getBegin().getOffset();
116: int end = desc.getRange().getEnd().getOffset();
117: OffsetRange range = new OffsetRange(start, end);
118: List<ErrorDescription> l = posToDesc.get(range);
119: if (l == null) {
120: l = new ArrayList<ErrorDescription>();
121: posToDesc.put(range, l);
122: }
123: l.add(desc);
124: ranges.add(range);
125: }
126: StringBuilder sb = new StringBuilder();
127: String text = doc.getText(0, doc.getLength());
128: Map<Integer, OffsetRange> starts = new HashMap<Integer, OffsetRange>(
129: 100);
130: Map<Integer, OffsetRange> ends = new HashMap<Integer, OffsetRange>(
131: 100);
132: for (OffsetRange range : ranges) {
133: starts.put(range.getStart(), range);
134: ends.put(range.getEnd(), range);
135: }
136:
137: int index = 0;
138: int length = text.length();
139: while (index < length) {
140: int lineStart = Utilities.getRowStart(doc, index);
141: int lineEnd = Utilities.getRowEnd(doc, index);
142: OffsetRange lineRange = new OffsetRange(lineStart, lineEnd);
143: boolean skipLine = true;
144: for (OffsetRange range : ranges) {
145: if (lineRange.containsInclusive(range.getStart())
146: || lineRange.containsInclusive(range.getEnd())) {
147: skipLine = false;
148: }
149: }
150: if (!skipLine) {
151: List<ErrorDescription> descsOnLine = null;
152: int underlineStart = -1;
153: int underlineEnd = -1;
154: for (int i = lineStart; i <= lineEnd; i++) {
155: if (i == caretOffset) {
156: sb.append("^");
157: }
158: if (starts.containsKey(i)) {
159: if (descsOnLine == null) {
160: descsOnLine = new ArrayList<ErrorDescription>();
161: }
162: underlineStart = i - lineStart;
163: OffsetRange range = starts.get(i);
164: if (posToDesc.get(range) != null) {
165: for (ErrorDescription desc : posToDesc
166: .get(range)) {
167: descsOnLine.add(desc);
168: }
169: }
170: }
171: if (ends.containsKey(i)) {
172: underlineEnd = i - lineStart;
173: }
174: sb.append(text.charAt(i));
175: }
176: if (underlineStart != -1) {
177: for (int i = 0; i < underlineStart; i++) {
178: sb.append(" ");
179: }
180: for (int i = underlineStart; i < underlineEnd; i++) {
181: sb.append("-");
182: }
183: sb.append("\n");
184: }
185: if (descsOnLine != null) {
186: Collections.sort(descsOnLine,
187: new Comparator<ErrorDescription>() {
188: public int compare(
189: ErrorDescription arg0,
190: ErrorDescription arg1) {
191: return arg0
192: .getDescription()
193: .compareTo(
194: arg1
195: .getDescription());
196: }
197: });
198: for (ErrorDescription desc : descsOnLine) {
199: sb.append("HINT:");
200: sb.append(desc.getDescription());
201: sb.append("\n");
202: LazyFixList list = desc.getFixes();
203: if (list != null) {
204: List<Fix> fixes = list.getFixes();
205: if (fixes != null) {
206: for (Fix fix : fixes) {
207: sb.append("FIX:");
208: sb.append(fix.getText());
209: sb.append("\n");
210: }
211: }
212: }
213: }
214: }
215: }
216: index = lineEnd + 1;
217: }
218:
219: return sb.toString();
220: }
221:
222: protected boolean parseErrorsOk;
223:
224: protected ComputedHints getHints(NbTestCase test, Rule hint,
225: String relFilePath, FileObject fileObject, String caretLine)
226: throws Exception {
227: assert relFilePath == null || fileObject == null;
228: UserConfigurableRule ucr = null;
229: if (hint instanceof UserConfigurableRule) {
230: ucr = (UserConfigurableRule) hint;
231: }
232:
233: // Make sure the hint is enabled
234: if (ucr != null && !HintsSettings.isEnabled(ucr)) {
235: Preferences p = RulesManager.getInstance().getPreferences(
236: ucr, HintsSettings.getCurrentProfileId());
237: HintsSettings.setEnabled(p, true);
238: }
239:
240: CompilationInfo info = fileObject != null ? getInfo(fileObject)
241: : getInfo(relFilePath);
242: Node root = AstUtilities.getRoot(info);
243: if (root == null && !(hint instanceof ErrorRule)) { // only expect testcase source errors in error tests
244: if (parseErrorsOk) {
245: List<ErrorDescription> result = new ArrayList<ErrorDescription>();
246: int caretOffset = 0;
247: return new ComputedHints(info, result, caretOffset);
248: }
249: assertNotNull("Unexpected parse error in test case "
250: + FileUtil.getFileDisplayName(info.getFileObject())
251: + "\nErrors = " + info.getErrors(), root);
252: }
253:
254: String text = info.getText();
255:
256: int caretOffset = -1;
257: if (caretLine != null) {
258: int caretDelta = caretLine.indexOf("^");
259: assertTrue(caretDelta != -1);
260: caretLine = caretLine.substring(0, caretDelta)
261: + caretLine.substring(caretDelta + 1);
262: int lineOffset = text.indexOf(caretLine);
263: assertTrue("NOT FOUND: " + info.getFileObject().getName()
264: + ":" + caretLine, lineOffset != -1);
265:
266: caretOffset = lineOffset + caretDelta;
267: }
268:
269: RubyHintsProvider provider = new RubyHintsProvider();
270:
271: List<ErrorDescription> result = new ArrayList<ErrorDescription>();
272: if (hint instanceof ErrorRule) {
273: // It's an error!
274: // Create a hint registry which contains ONLY our hint (so other registered
275: // hints don't interfere with the test)
276: Map<String, List<ErrorRule>> testHints = new HashMap<String, List<ErrorRule>>();
277: if (hint.appliesTo(info)) {
278: ErrorRule errorRule = (ErrorRule) hint;
279: for (String key : errorRule.getCodes()) {
280: testHints.put(key, Collections
281: .singletonList(errorRule));
282: }
283: }
284: provider.setTestingHints(null, null, testHints, null);
285: provider.computeErrors(info, result);
286: } else if (hint instanceof SelectionRule) {
287: SelectionRule rule = (SelectionRule) hint;
288: List<SelectionRule> testHints = new ArrayList<SelectionRule>();
289: testHints.add(rule);
290:
291: provider.setTestingHints(null, null, null, testHints);
292:
293: if (caretLine != null) {
294: int start = text.indexOf(caretLine);
295: int end = start + caretLine.length();
296: provider
297: .computeSelectionHints(info, result, start, end);
298: }
299: } else {
300: assert hint instanceof AstRule && ucr != null;
301: AstRule astRule = (AstRule) hint;
302: // Create a hint registry which contains ONLY our hint (so other registered
303: // hints don't interfere with the test)
304: Map<Integer, List<AstRule>> testHints = new HashMap<Integer, List<AstRule>>();
305: if (hint.appliesTo(info)) {
306: for (int nodeId : astRule.getKinds()) {
307: testHints.put(nodeId, Collections
308: .singletonList(astRule));
309: }
310: }
311: if (RulesManager.getInstance().getSeverity(ucr) == HintSeverity.CURRENT_LINE_WARNING) {
312: provider.setTestingHints(null, testHints, null, null);
313: provider.computeSuggestions(info, result, caretOffset);
314: } else {
315: provider.setTestingHints(testHints, null, null, null);
316: provider.computeHints(info, result);
317: }
318: }
319:
320: return new ComputedHints(info, result, caretOffset);
321: }
322:
323: protected void assertNoJRubyMatches(Rule hint,
324: Set<String> exceptions) throws Exception {
325: List<FileObject> files = findJRubyRubyFiles();
326: assertTrue(files.size() > 0);
327:
328: Set<String> fails = new HashSet<String>();
329: for (FileObject fileObject : files) {
330: ComputedHints r = getHints(this , hint, null, fileObject,
331: null);
332: CompilationInfo info = r.info;
333: List<ErrorDescription> result = r.hints;
334: int caretOffset = r.caretOffset;
335: if (hint.getDefaultSeverity() == HintSeverity.CURRENT_LINE_WARNING
336: && hint instanceof AstRule) {
337: result = new ArrayList<ErrorDescription>(result);
338: Set<Integer> nodeTypes = ((AstRule) hint).getKinds();
339: Node root = AstUtilities.getRoot(info);
340: List<Node> nodes = new ArrayList<Node>();
341: int[] nodeIds = new int[nodeTypes.size()];
342: int index = 0;
343: for (int id : nodeTypes) {
344: nodeIds[index++] = id;
345: }
346: AstUtilities.addNodesByType(root, nodeIds, nodes);
347: BaseDocument doc = (BaseDocument) info.getDocument();
348: for (Node n : nodes) {
349: int start = AstUtilities.getRange(n).getStart();
350: int lineStart = Utilities.getRowFirstNonWhite(doc,
351: start);
352: int lineEnd = Utilities.getRowEnd(doc, start);
353: String first = doc.getText(lineStart, start
354: - lineStart);
355: String last = doc.getText(start, lineEnd - start);
356: if (first.indexOf("^") == -1
357: && last.indexOf("^") == -1) {
358: String caretLine = first + "^" + last;
359: ComputedHints r2 = getHints(this , hint, null,
360: fileObject, caretLine);
361: result.addAll(r.hints);
362: }
363: }
364: }
365:
366: String annotatedSource = annotate((BaseDocument) info
367: .getDocument(), result, caretOffset);
368:
369: if (annotatedSource.length() > 0) {
370: // Check if there's an exception
371: String name = fileObject.getNameExt();
372: if (exceptions.contains(name)) {
373: continue;
374: }
375:
376: fails.add(fileObject.getNameExt());
377: }
378: }
379:
380: assertTrue(fails.toString(), fails.size() == 0);
381: }
382:
383: // TODO - rename to "checkHints"
384: protected void findHints(NbTestCase test, Rule hint,
385: String relFilePath, String caretLine) throws Exception {
386: findHints(test, hint, relFilePath, null, caretLine);
387: }
388:
389: protected void findHints(Rule hint, String relFilePath,
390: String selStartLine, String selEndLine) throws Exception {
391: FileObject fo = getTestFile(relFilePath);
392: String text = read(fo);
393:
394: assert selStartLine != null;
395: assert selEndLine != null;
396:
397: int selStartOffset = -1;
398: int lineDelta = selStartLine.indexOf("^");
399: assertTrue(lineDelta != -1);
400: selStartLine = selStartLine.substring(0, lineDelta)
401: + selStartLine.substring(lineDelta + 1);
402: int lineOffset = text.indexOf(selStartLine);
403: assertTrue(lineOffset != -1);
404:
405: selStartOffset = lineOffset + lineDelta;
406:
407: int selEndOffset = -1;
408: lineDelta = selEndLine.indexOf("^");
409: assertTrue(lineDelta != -1);
410: selEndLine = selEndLine.substring(0, lineDelta)
411: + selEndLine.substring(lineDelta + 1);
412: lineOffset = text.indexOf(selEndLine);
413: assertTrue(lineOffset != -1);
414:
415: selEndOffset = lineOffset + lineDelta;
416:
417: String caretLine = text.substring(selStartOffset, selEndOffset)
418: + "^";
419:
420: findHints(this , hint, relFilePath, caretLine);
421: }
422:
423: // TODO - rename to "checkHints"
424: protected void findHints(NbTestCase test, Rule hint,
425: FileObject fileObject, String caretLine) throws Exception {
426: findHints(test, hint, null, fileObject, caretLine);
427: }
428:
429: // TODO - rename to "checkHints"
430: protected void findHints(NbTestCase test, Rule hint,
431: String relFilePath, FileObject fileObject, String caretLine)
432: throws Exception {
433: ComputedHints r = getHints(test, hint, relFilePath, fileObject,
434: caretLine);
435: CompilationInfo info = r.info;
436: List<ErrorDescription> result = r.hints;
437: int caretOffset = r.caretOffset;
438:
439: String annotatedSource = annotate((BaseDocument) info
440: .getDocument(), result, caretOffset);
441:
442: if (fileObject != null) {
443: assertDescriptionMatches(fileObject, annotatedSource, true,
444: ".hints");
445: } else {
446: assertDescriptionMatches(relFilePath, annotatedSource,
447: true, ".hints");
448: }
449: }
450:
451: protected void applyHint(NbTestCase test, Rule hint,
452: String relFilePath, String selStartLine, String selEndLine,
453: String fixDesc) throws Exception {
454: FileObject fo = getTestFile(relFilePath);
455: String text = read(fo);
456:
457: assert selStartLine != null;
458: assert selEndLine != null;
459:
460: int selStartOffset = -1;
461: int lineDelta = selStartLine.indexOf("^");
462: assertTrue(lineDelta != -1);
463: selStartLine = selStartLine.substring(0, lineDelta)
464: + selStartLine.substring(lineDelta + 1);
465: int lineOffset = text.indexOf(selStartLine);
466: assertTrue(lineOffset != -1);
467:
468: selStartOffset = lineOffset + lineDelta;
469:
470: int selEndOffset = -1;
471: lineDelta = selEndLine.indexOf("^");
472: assertTrue(lineDelta != -1);
473: selEndLine = selEndLine.substring(0, lineDelta)
474: + selEndLine.substring(lineDelta + 1);
475: lineOffset = text.indexOf(selEndLine);
476: assertTrue(lineOffset != -1);
477:
478: selEndOffset = lineOffset + lineDelta;
479:
480: String caretLine = text.substring(selStartOffset, selEndOffset)
481: + "^";
482:
483: applyHint(test, hint, relFilePath, caretLine, fixDesc);
484: }
485:
486: protected void applyHint(NbTestCase test, Rule hint,
487: String relFilePath, String caretLine, String fixDesc)
488: throws Exception {
489: initializeRegistry();
490: ComputedHints r = getHints(test, hint, relFilePath, null,
491: caretLine);
492: CompilationInfo info = r.info;
493:
494: Fix fix = findApplicableFix(r, fixDesc);
495: assertNotNull(fix);
496: fix.implement();
497:
498: Document doc = info.getDocument();
499: String fixed = doc.getText(0, doc.getLength());
500:
501: assertDescriptionMatches(relFilePath, fixed, true, ".fixed");
502: }
503:
504: public void ensureRegistered(AstRule hint) throws Exception {
505: Map<Integer, List<AstRule>> hints = RulesManager.getInstance()
506: .getHints();
507: Set<Integer> kinds = hint.getKinds();
508: for (int nodeType : kinds) {
509: List<AstRule> rules = hints.get(nodeType);
510: assertNotNull(rules);
511: boolean found = false;
512: for (AstRule rule : rules) {
513: if (rule instanceof BlockVarReuse) {
514: found = true;
515: break;
516: }
517: }
518:
519: assertTrue(found);
520: }
521: }
522:
523: private Fix findApplicableFix(ComputedHints r, String text) {
524: boolean substringMatch = true;
525: if (text.endsWith("\n")) {
526: text = text.substring(0, text.length() - 1);
527: substringMatch = false;
528: }
529: int caretOffset = r.caretOffset;
530: for (ErrorDescription desc : r.hints) {
531: int start = desc.getRange().getBegin().getOffset();
532: int end = desc.getRange().getEnd().getOffset();
533: OffsetRange range = new OffsetRange(start, end);
534: if (range.containsInclusive(caretOffset)
535: || caretOffset == range.getEnd() + 1) { // special case for wrong JRuby offsets
536: // Optionally make sure the text is the one we're after such that
537: // tests can disambiguate among multiple fixes
538: LazyFixList list = desc.getFixes();
539: assertNotNull(list);
540: for (Fix fix : list.getFixes()) {
541: if (text == null
542: || (substringMatch && fix.getText()
543: .indexOf(text) != -1)
544: || (!substringMatch && fix.getText()
545: .equals(text))) {
546: return fix;
547: }
548: }
549: }
550: }
551:
552: return null;
553: }
554:
555: private static class ComputedHints {
556: ComputedHints(CompilationInfo info,
557: List<ErrorDescription> hints, int caretOffset) {
558: this .info = info;
559: this .hints = hints;
560: this .caretOffset = caretOffset;
561: }
562:
563: CompilationInfo info;
564: List<ErrorDescription> hints;
565: int caretOffset;
566: }
567: }
|