001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.junit.rebind;
017:
018: import com.google.gwt.core.ext.Generator;
019: import com.google.gwt.core.ext.GeneratorContext;
020: import com.google.gwt.core.ext.TreeLogger;
021: import com.google.gwt.core.ext.UnableToCompleteException;
022: import com.google.gwt.core.ext.typeinfo.JClassType;
023: import com.google.gwt.core.ext.typeinfo.JMethod;
024: import com.google.gwt.core.ext.typeinfo.NotFoundException;
025: import com.google.gwt.core.ext.typeinfo.TypeOracle;
026: import com.google.gwt.core.ext.typeinfo.JParameter;
027: import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
028: import com.google.gwt.user.rebind.SourceWriter;
029:
030: import java.io.PrintWriter;
031: import java.util.Map;
032: import java.util.ArrayList;
033: import java.util.List;
034: import java.util.HashMap;
035:
036: /**
037: * This class generates a stub class for classes that derive from GWTTestCase.
038: * This stub class provides the necessary bridge between our Hosted or Hybrid
039: * mode classes and the JUnit system.
040: *
041: */
042: public class JUnitTestCaseStubGenerator extends Generator {
043:
044: interface MethodFilter {
045: public boolean accept(JMethod method);
046: }
047:
048: private static final String GWT_TESTCASE_CLASS_NAME = "com.google.gwt.junit.client.GWTTestCase";
049:
050: /**
051: * Returns the method names for the set of methods that are strictly JUnit
052: * test methods (have no arguments).
053: *
054: * @param requestedClass
055: */
056: public static String[] getTestMethodNames(JClassType requestedClass) {
057: return getAllMethods(requestedClass, new MethodFilter() {
058: public boolean accept(JMethod method) {
059: return isJUnitTestMethod(method, false);
060: }
061: }).keySet().toArray(new String[] {});
062: }
063:
064: /**
065: * Like JClassType.getMethod( String name ), except:
066: *
067: * <li>it accepts a filter</li>
068: * <li>it searches the inheritance hierarchy (includes subclasses)</li>
069: *
070: * For methods which are overriden, only the most derived implementations are included.
071: *
072: * @param type The type to search. Must not be null
073: * @return Map<String.List<JMethod>> The set of matching methods. Will not be null.
074: */
075: static Map<String, List<JMethod>> getAllMethods(JClassType type,
076: MethodFilter filter) {
077: Map<String, List<JMethod>> methods = new HashMap<String, List<JMethod>>();
078: JClassType cls = type;
079:
080: while (cls != null) {
081: JMethod[] clsDeclMethods = cls.getMethods();
082:
083: // For every method, include it iff our filter accepts it
084: // and we don't already have a matching method
085: for (int i = 0, n = clsDeclMethods.length; i < n; ++i) {
086:
087: JMethod declMethod = clsDeclMethods[i];
088:
089: if (!filter.accept(declMethod)) {
090: continue;
091: }
092:
093: List<JMethod> list = methods.get(declMethod.getName());
094:
095: if (list == null) {
096: list = new ArrayList<JMethod>();
097: methods.put(declMethod.getName(), list);
098: list.add(declMethod);
099: continue;
100: }
101:
102: JParameter[] declParams = declMethod.getParameters();
103:
104: for (int j = 0; j < list.size(); ++j) {
105: JMethod method = list.get(j);
106: JParameter[] parameters = method.getParameters();
107: if (!equals(declParams, parameters)) {
108: list.add(declMethod);
109: }
110: }
111: }
112: cls = cls.getSuperclass();
113: }
114:
115: return methods;
116: }
117:
118: /**
119: * Returns true if the method is considered to be a valid JUnit test method.
120: * The criteria are that the method's name begin with "test" and have public
121: * access. The method may be static. You must choose to include or exclude
122: * methods which have arguments.
123: *
124: */
125: static boolean isJUnitTestMethod(JMethod method, boolean acceptArgs) {
126: if (!method.getName().startsWith("test")) {
127: return false;
128: }
129:
130: if (!method.isPublic()) {
131: return false;
132: }
133:
134: return acceptArgs || method.getParameters().length == 0
135: && !acceptArgs;
136: }
137:
138: /**
139: * Returns true iff the two sets of parameters are of the same lengths and types.
140: *
141: * @param params1 must not be null
142: * @param params2 must not be null
143: */
144: private static boolean equals(JParameter[] params1,
145: JParameter[] params2) {
146: if (params1.length != params2.length) {
147: return false;
148: }
149: for (int i = 0; i < params1.length; ++i) {
150: if (params1[i].getType() != params2[i].getType()) {
151: return false;
152: }
153: }
154: return true;
155: }
156:
157: String qualifiedStubClassName;
158: String simpleStubClassName;
159: String typeName;
160: TreeLogger logger;
161: String packageName;
162:
163: private JClassType requestedClass;
164: private SourceWriter sourceWriter;
165: private TypeOracle typeOracle;
166:
167: /**
168: * Create a new type that statisfies the rebind request.
169: */
170: @Override
171: public String generate(TreeLogger logger, GeneratorContext context,
172: String typeName) throws UnableToCompleteException {
173:
174: if (!init(logger, context, typeName)) {
175: return qualifiedStubClassName;
176: }
177:
178: writeSource();
179: sourceWriter.commit(logger);
180:
181: return qualifiedStubClassName;
182: }
183:
184: public JClassType getRequestedClass() {
185: return requestedClass;
186: }
187:
188: public SourceWriter getSourceWriter() {
189: return sourceWriter;
190: }
191:
192: public TypeOracle getTypeOracle() {
193: return typeOracle;
194: }
195:
196: boolean init(TreeLogger logger, GeneratorContext context,
197: String typeName) throws UnableToCompleteException {
198:
199: this .typeName = typeName;
200: this .logger = logger;
201: typeOracle = context.getTypeOracle();
202: assert typeOracle != null;
203:
204: try {
205: requestedClass = typeOracle.getType(typeName);
206: } catch (NotFoundException e) {
207: logger
208: .log(
209: TreeLogger.ERROR,
210: "Could not find type '"
211: + typeName
212: + "'; please see the log, as this usually indicates a previous error ",
213: e);
214: throw new UnableToCompleteException();
215: }
216:
217: // Get the stub class name, and see if its source file exists.
218: //
219: simpleStubClassName = getSimpleStubClassName(requestedClass);
220: packageName = requestedClass.getPackage().getName();
221: qualifiedStubClassName = packageName + "."
222: + simpleStubClassName;
223:
224: sourceWriter = getSourceWriter(logger, context, packageName,
225: simpleStubClassName, requestedClass
226: .getQualifiedSourceName());
227:
228: return sourceWriter != null;
229: }
230:
231: @SuppressWarnings("unused")
232: void writeSource() throws UnableToCompleteException {
233: String[] testMethods = getTestMethodNames(requestedClass);
234: writeGetNewTestCase(simpleStubClassName, sourceWriter);
235: writeDoRunTestMethod(testMethods, sourceWriter);
236: writeGetTestName(typeName, sourceWriter);
237: }
238:
239: /**
240: * Gets the name of the native stub class.
241: */
242: private String getSimpleStubClassName(JClassType baseClass) {
243: return "__" + baseClass.getSimpleSourceName() + "_unitTestImpl";
244: }
245:
246: private SourceWriter getSourceWriter(TreeLogger logger,
247: GeneratorContext ctx, String packageName, String className,
248: String super className) {
249:
250: PrintWriter printWriter = ctx.tryCreate(logger, packageName,
251: className);
252: if (printWriter == null) {
253: return null;
254: }
255:
256: ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(
257: packageName, className);
258:
259: composerFactory.setSuperclass(super className);
260:
261: return composerFactory.createSourceWriter(ctx, printWriter);
262: }
263:
264: private void writeDoRunTestMethod(String[] testMethodNames,
265: SourceWriter sw) {
266: sw.println();
267: sw
268: .println("protected final void doRunTest(String name) throws Throwable {");
269: sw.indent();
270: for (int i = 0, n = testMethodNames.length; i < n; ++i) {
271: String methodName = testMethodNames[i];
272:
273: if (i > 0) {
274: sw.print("else ");
275: }
276:
277: sw.println("if (name.equals(\"" + methodName + "\")) {");
278: sw.indentln(methodName + "();");
279: sw.println("}");
280: }
281: sw.outdent();
282: sw.println("}"); // finish doRunTest();
283: }
284:
285: /**
286: * Create the appMain method that is the main entry point for the GWT
287: * application.
288: */
289: private void writeGetNewTestCase(String stubClassName,
290: SourceWriter sw) {
291: sw.println();
292: sw.println("public final " + GWT_TESTCASE_CLASS_NAME
293: + " getNewTestCase() {");
294: sw.indent();
295: sw.println("return new " + stubClassName + "();");
296: sw.outdent();
297: sw.println("}"); // finish getNewTestCase();
298: }
299:
300: /**
301: * Create the appMain method that is the main entry point for the GWT
302: * application.
303: */
304: private void writeGetTestName(String testClassName, SourceWriter sw) {
305: sw.println();
306: sw.println("public final String getTestName() {");
307: sw.indent();
308: sw.println("return \"" + testClassName + "\";");
309: sw.outdent();
310: sw.println("}"); // finish getNewTestCase();
311: }
312: }
|