/* * Copyright 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.tools.apichecker; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.core.ext.typeinfo.NotFoundException; import com.google.gwt.dev.javac.testing.impl.StaticJavaResource; import com.google.gwt.dev.resource.Resource; import com.google.gwt.dev.util.log.AbstractTreeLogger; import com.google.gwt.dev.util.log.PrintWriterTreeLogger; import junit.framework.TestCase; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Base class for all the ApiCompatibility Testing. Encapsulates two api * containers and a test method. * */ public class ApiCompatibilityUnitTest extends TestCase { /** * Mock class to test if the correct ApiChange(s) are being returned. * */ static class MockApiElement implements ApiElement { final String signature; public MockApiElement(String signature) { this.signature = signature; } public String getRelativeSignature() { return signature; } } private static class FinalKeywordRefactoring { private static String getFirstApiSourceForObject() { StringBuffer sb = new StringBuffer(); sb.append("package java.lang;\n"); sb.append("public final class Object {\n"); sb.append("\tpublic Object foo;\n"); sb.append("\tpublic void bar() {}\n"); sb.append("}\n"); return sb.toString(); } private static String getSecondApiSourceForObject() { StringBuffer sb = new StringBuffer(); sb.append("package java.lang;\n"); sb.append("public class Object {\n"); sb.append("\tpublic final Object foo;\n"); sb.append("\tpublic final void bar() {}\n"); sb.append("}\n"); return sb.toString(); } void testBothWays() throws NotFoundException, UnableToCompleteException { Map<String, String> firstApi = new HashMap<String, String>(); firstApi.put("java.lang.Object", getFirstApiSourceForObject()); Map<String, String> secondApi = new HashMap<String, String>(); secondApi.put("java.lang.Object", getSecondApiSourceForObject()); // firstApi is the reference Api Collection<ApiChange> apiChanges = getApiChanges(firstApi, secondApi); assertEquals(Arrays.asList(new ApiChange[] {new ApiChange(new MockApiElement( "java.lang.Object::foo"), ApiChange.Status.FINAL_ADDED),}), apiChanges); // secondApi is the reference Api apiChanges = getApiChanges(secondApi, firstApi); assertEquals(Arrays.asList(new ApiChange[] {new ApiChange(new MockApiElement( "java.lang.Object"), ApiChange.Status.FINAL_ADDED),}), apiChanges); } } /** * Test when constructor overloading results in Api incompatibilities. * <p> * Imagine a class Foo had a constructor Foo(String ..). If in the new Api, a * constructor Foo(Integer ..) is added, ApiChecker should output a * OVERLOADED_METHOD_CALL Api change (because Foo(null) cannot be compiled). * However, if Foo(Object ..) is added, it should be okay since JLS matches * from the most specific to the least specific. */ private static class OverloadedConstructorRefactoring { private static String getFirstApiSourceForObject() { StringBuffer sb = new StringBuffer(); sb.append("package java.lang;\n"); sb.append("public class Object {\n"); sb.append("\tpublic static class Foo extends java.lang.Object {\n"); sb.append("\tpublic Foo(Foo x){}\n"); sb.append("\t}\n"); sb.append("\tpublic static class Bar extends java.lang.Object {\n"); sb.append("\tpublic Bar(Bar y){}\n"); sb.append("\t}\n"); sb.append("}\n"); return sb.toString(); } private static String getSecondApiSourceForObject() { StringBuffer sb = new StringBuffer(); sb.append("package java.lang;\n"); sb.append("public class Object {\n"); sb.append("\tpublic static class Foo extends java.lang.Object {\n"); sb.append("\tpublic Foo(Foo x){}\n"); sb.append("\tpublic Foo(Object x){}\n"); sb.append("\t}\n"); sb.append("\tpublic static class Bar extends java.lang.Object {\n"); sb.append("\tpublic Bar(Bar y){}\n"); sb.append("\tpublic Bar(Foo y){}\n"); sb.append("\t}\n"); sb.append("}\n"); return sb.toString(); } void testBothWays() throws NotFoundException, UnableToCompleteException { Map<String, String> firstApi = new HashMap<String, String>(); firstApi.put("java.lang.Object", getFirstApiSourceForObject()); Map<String, String> secondApi = new HashMap<String, String>(); secondApi.put("java.lang.Object", getSecondApiSourceForObject()); // firstApi is the reference Api Collection<ApiChange> apiChanges = getApiChanges(firstApi, secondApi); assertEquals(Arrays.asList(new ApiChange[] {new ApiChange(new MockApiElement( "java.lang.Object.Bar::Bar(Ljava/lang/Object$Bar;)"), ApiChange.Status.OVERLOADED_METHOD_CALL),}), apiChanges); // secondApi is the reference Api apiChanges = getApiChanges(secondApi, firstApi); assertEquals(Arrays.asList(new ApiChange[] { new ApiChange(new MockApiElement("java.lang.Object.Foo::Foo(Ljava/lang/Object;)"), ApiChange.Status.MISSING), new ApiChange(new MockApiElement("java.lang.Object.Bar::Bar(Ljava/lang/Object$Foo;)"), ApiChange.Status.MISSING),}), apiChanges); } } /** * Test when method overloading results in Api incompatibilities. * <p> * Imagine a class Foo had a method foo(String ..). If in the new Api, a * method foo(Integer ..) is added, ApiChecker should output a * OVERLOADED_METHOD_CALL Api change (because foo(null) cannot be compiled). * However, if foo(Object ..) is added, it should be okay since JLS matches * from the most specific to the least specific. */ private static class OverloadedMethodRefactoring { private static String getFirstApiSourceForObject() { StringBuffer sb = new StringBuffer(); sb.append("package java.lang;\n"); sb.append("public class Object {\n"); sb.append("\tstatic class Foo extends java.lang.Object {\n"); sb.append("\t}\n"); sb.append("\tstatic class Bar extends java.lang.Object {\n"); sb.append("\t}\n"); sb.append("\tpublic void fooObject(Foo x){}\n"); sb.append("\tpublic void fooBar(Foo y){}\n"); sb.append("}\n"); return sb.toString(); } private static String getSecondApiSourceForObject() { StringBuffer sb = new StringBuffer(); sb.append("package java.lang;\n"); sb.append("public class Object {\n"); sb.append("\tstatic class Foo extends java.lang.Object {\n"); sb.append("\t}\n"); sb.append("\tstatic class Bar extends java.lang.Object {\n"); sb.append("\t}\n"); sb.append("\tpublic void fooObject(Foo x){}\n"); sb.append("\tpublic void fooObject(Object x){}\n"); sb.append("\tpublic void fooBar(Foo y){}\n"); sb.append("\tpublic void fooBar(Bar y){}\n"); sb.append("}\n"); return sb.toString(); } void testBothWays() throws NotFoundException, UnableToCompleteException { Map<String, String> firstApi = new HashMap<String, String>(); firstApi.put("java.lang.Object", getFirstApiSourceForObject()); Map<String, String> secondApi = new HashMap<String, String>(); secondApi.put("java.lang.Object", getSecondApiSourceForObject()); // firstApi is the reference Api Collection<ApiChange> apiChanges = getApiChanges(firstApi, secondApi); assertEquals(Arrays.asList(new ApiChange[] {new ApiChange(new MockApiElement( "java.lang.Object::fooBar(Ljava/lang/Object$Foo;)"), ApiChange.Status.OVERLOADED_METHOD_CALL),}), apiChanges); // secondApi is the reference Api apiChanges = getApiChanges(secondApi, firstApi); assertEquals(Arrays.asList(new ApiChange[] { new ApiChange(new MockApiElement("java.lang.Object::fooBar(Ljava/lang/Object$Bar;)"), ApiChange.Status.MISSING), new ApiChange(new MockApiElement("java.lang.Object::fooObject(Ljava/lang/Object;)"), ApiChange.Status.MISSING),}), apiChanges); } } /** * Test whether the ApiChecker correctly identifies moving fields and methods * to a super class. * */ private static class SuperClassRefactoring { private static String getFirstApiSourceForObject() { StringBuffer sb = new StringBuffer(); sb.append("package java.lang;\n"); sb.append("public class Object {\n"); sb.append("\t\tpublic static void staticMethod(){}\n"); sb.append("\t\tpublic static int staticField = 1;\n"); sb.append("\t\tpublic int instanceField = 2;\n"); sb.append("\tpublic static class Foo extends java.lang.Object {\n"); sb.append("\t}\n"); sb.append("}\n"); return sb.toString(); } private static String getSecondApiSourceForObject() { StringBuffer sb = new StringBuffer(); sb.append("package java.lang;\n"); sb.append("public class Object {\n"); sb.append("\tpublic static class Foo extends java.lang.Object {\n"); sb.append("\t\tpublic static void staticMethod(){}\n"); sb.append("\t\tpublic static int staticField = 1;\n"); sb.append("\t\tpublic int instanceField = 2;\n"); sb.append("\t}\n"); sb.append("}\n"); return sb.toString(); } void testBothWays() throws NotFoundException, UnableToCompleteException { Map<String, String> firstApi = new HashMap<String, String>(); firstApi.put("java.lang.Object", getFirstApiSourceForObject()); Map<String, String> secondApi = new HashMap<String, String>(); secondApi.put("java.lang.Object", getSecondApiSourceForObject()); // firstApi is the reference Api Collection<ApiChange> apiChanges = getApiChanges(firstApi, secondApi); assertEquals(Arrays.asList(new ApiChange[] { new ApiChange(new MockApiElement("java.lang.Object::instanceField"), ApiChange.Status.MISSING), new ApiChange(new MockApiElement("java.lang.Object::staticField"), ApiChange.Status.MISSING), new ApiChange(new MockApiElement("java.lang.Object::staticMethod()"), ApiChange.Status.MISSING),}), apiChanges); // secondApi is the reference Api apiChanges = getApiChanges(secondApi, firstApi); assertEquals(0, apiChanges.size()); } } /** * Assert that two ApiChanges are equal. */ static void assertEquals(ApiChange apiChange1, ApiChange apiChange2) { assert apiChange1 != null; assert apiChange2 != null; assertEquals(apiChange1.getStatus(), apiChange2.getStatus()); assertEquals(apiChange1.getApiElement().getRelativeSignature(), apiChange2.getApiElement() .getRelativeSignature()); } /** * Assert that two sets of ApiChanges are equal. */ static void assertEquals(Collection<ApiChange> collection1, Collection<ApiChange> collection2) { assertEquals(collection1.size(), collection2.size()); List<ApiChange> list1 = new ArrayList<ApiChange>(); list1.addAll(collection1); Collections.sort(list1); List<ApiChange> list2 = new ArrayList<ApiChange>(); list2.addAll(collection2); Collections.sort(list2); for (int i = 0; i < list1.size(); i++) { assertEquals(list1.get(i), list2.get(i)); } } /** * Returns the apiChanges from moving to an existing api to a new Api. * * @param existingTypesToSourcesMap existing Api * @param newTypesToSourcesMap new Api * @return A collection of ApiChange */ static Collection<ApiChange> getApiChanges(Map<String, String> existingTypesToSourcesMap, Map<String, String> newTypesToSourcesMap) throws UnableToCompleteException, NotFoundException { AbstractTreeLogger logger = new PrintWriterTreeLogger(); logger.setMaxDetail(TreeLogger.ERROR); Set<Resource> set1 = new HashSet<Resource>(); for (Map.Entry<String, String> entry : existingTypesToSourcesMap.entrySet()) { set1.add(new StaticJavaResource(entry.getKey(), entry.getValue())); } Set<String> emptyList = Collections.emptySet(); Set<Resource> set2 = new HashSet<Resource>(); for (String type : existingTypesToSourcesMap.keySet()) { set2.add(new StaticJavaResource(type, newTypesToSourcesMap.get(type))); } ApiContainer existingApi = new ApiContainer("existingApi", set1, emptyList, logger); ApiContainer newApi = new ApiContainer("newApi", set2, emptyList, logger); return ApiCompatibilityChecker.getApiDiff(newApi, existingApi, emptyList); } public void testConstructorOverloading() throws NotFoundException, UnableToCompleteException { new OverloadedConstructorRefactoring().testBothWays(); } public void testFinalKeywordRefactoring() throws NotFoundException, UnableToCompleteException { new FinalKeywordRefactoring().testBothWays(); } public void testMethodOverloading() throws NotFoundException, UnableToCompleteException { new OverloadedMethodRefactoring().testBothWays(); } public void testSuperClassRefactoring() throws NotFoundException, UnableToCompleteException { new SuperClassRefactoring().testBothWays(); } }