/* * Copyright 2013 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.core.ext.linker; import com.google.gson.JsonPrimitive; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.soyc.coderef.ClassDescriptor; import com.google.gwt.core.ext.soyc.coderef.EntityDescriptor; import com.google.gwt.core.ext.soyc.coderef.EntityDescriptor.Fragment; import com.google.gwt.core.ext.soyc.coderef.EntityDescriptorJsonTranslator; import com.google.gwt.core.ext.soyc.coderef.EntityRecorder; import com.google.gwt.core.ext.soyc.coderef.MethodDescriptor; import com.google.gwt.dev.Compiler; import com.google.gwt.dev.CompilerOptionsImpl; import com.google.gwt.dev.util.Util; import com.google.gwt.dev.util.log.PrintWriterTreeLogger; import com.google.gwt.thirdparty.debugging.sourcemap.FilePosition; import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapConsumerV3; import com.google.gwt.thirdparty.guava.common.collect.Lists; import com.google.gwt.thirdparty.guava.common.collect.Maps; import com.google.gwt.thirdparty.guava.common.collect.Sets; import com.google.gwt.thirdparty.guava.common.primitives.Ints; import com.google.gwt.thirdparty.json.JSONArray; import com.google.gwt.thirdparty.json.JSONException; import com.google.gwt.thirdparty.json.JSONObject; import com.google.gwt.util.tools.Utility; import junit.framework.TestCase; import org.eclipse.jdt.internal.compiler.problem.ShouldNotImplement; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import java.io.BufferedReader; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.GZIPInputStream; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; /** * Basic tests for Source maps and (new) soyc reports. * */ public class SourceMapTest extends TestCase { private static String stringContent(File filePath) throws IOException { FileReader reader = new FileReader(filePath); char[] content = new char[(int) filePath.length()]; reader.read(content); reader.close(); return new String(content); } private static File[] filterByName(File dir, final String namePattern) { return dir.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { return pathname.getName().matches(namePattern); } }); } /** * This class represents each row in a generated SymbolMap file. Because not all fields are * serialized, such as CastableTypeMap, some methods are not implemented. * */ static final class SimpleSymbolData implements SymbolData { static Map<String, SimpleSymbolData> readSymbolMap(File filePath) throws IOException { Map<String, SimpleSymbolData> sdata = Maps.newLinkedHashMap(); BufferedReader reader = new BufferedReader(new FileReader(filePath)); String line; while ((line = reader.readLine()) != null) { if (line.startsWith("#")) { // reading a comment continue; } SimpleSymbolData symbolData = new SimpleSymbolData(line); String key = symbolData.getJsniIdent(); assertFalse("Duplicate signature <" + key + "> in symbol maps", sdata.containsKey(key)); sdata.put(key,symbolData); } return sdata; } private static final String NOT_IMPLEMENTED_MESSAGE = "Data not available in current serialized SymbolMap"; private String jsName; private String jsniIdent; private String className; private String memberName; private String sourceUri; private int sourceLine; private int fragmentNumber; // counting how many times it was found in the symbol map table private int counter = 0; public SimpleSymbolData(String line) { this.parseFromLine(line); } public int getCounter() { return counter; } @Override public String getClassName() { return this.className; } @Override public int getFragmentNumber() { return this.fragmentNumber; } @Override public String getJsniIdent() { return this.jsniIdent; } @Override public String getMemberName() { return this.memberName; } @Override public String getRuntimeTypeId() { throw new ShouldNotImplement(NOT_IMPLEMENTED_MESSAGE); } @Override public int getSourceLine() { return this.sourceLine; } @Override public String getSourceUri() { return this.sourceUri; } @Override public String getSymbolName() { return this.jsName; } @Override public boolean isClass() { return this.memberName == null || this.memberName.isEmpty(); } @Override public boolean isField() { return !this.isClass() && jsniIdent.indexOf("(") < 0; } @Override public boolean isMethod() { return !this.isClass() && jsniIdent.indexOf("(") >= 0; } public int incCounter() { return ++counter; } @Override public String toString() { return jsniIdent + " -> " + jsName; } private void parseFromLine(String line) { String[] fields = line.split(","); this.jsName = fields[0]; this.jsniIdent = fields[1].isEmpty() ? fields[2] : fields[1]; this.className = fields[2]; this.memberName = fields[3]; // may be empty this.sourceUri = fields[4]; this.sourceLine = Integer.parseInt(fields[5]); this.fragmentNumber = Integer.parseInt(fields[6]); } } private final CompilerOptionsImpl options = new CompilerOptionsImpl(); // maps permutationId to symbolMap content private Map<Integer, Map<String, SimpleSymbolData>> mapping = Maps.newHashMap(); /** * Test the correspondence between old symbol maps and the information (such as range name and * source position) that is now provided by sourcemap. * * The matching is far from perfect. SymbolMaps record 1-1 correspondences between symbols in * JavaScript and the optimized version of the Java source. The symbol mapping information * provided in the sourcemap extensions maps the original Java source symbols to their JavaScript * counterpart but the mapping is no longer 1-1. E.g. A source java method might have two versions * after optimization due to MakeStaticCalls; and each of those versions might have different * JavaScript names. * * Also correspondence on field accesses, class literals can not be tested. * */ private void checkSourceMap(File symbolMap, List<File> sourceMapFiles) throws Exception { final Map<String, SimpleSymbolData> symbolTable = SimpleSymbolData.readSymbolMap(symbolMap); boolean firstIteration = true; for (File sourceMapFile : sourceMapFiles) { SourceMapConsumerV3 sourceMap = new SourceMapConsumerV3(); sourceMap.parse(stringContent(sourceMapFile)); if (firstIteration) { Integer permutationId = ((JsonPrimitive) sourceMap.getExtensions().get("x_gwt_permutation")).getAsInt(); assertNotNull(permutationId); mapping.put(permutationId, symbolTable); firstIteration = false; } sourceMap.visitMappings(new SourceMapConsumerV3.EntryVisitor() { @Override public void visit(String sourceName, String symbolName, FilePosition srcStartPos, FilePosition startPosition,FilePosition endPosition) { if (symbolName == null || symbolName.isEmpty()) { return; } SimpleSymbolData symbolData = symbolTable.get(symbolName); if (symbolData == null) { return; } symbolData.incCounter(); // field declarations will work, but field accesses wont if (!symbolData.isField()) { assertEquals(symbolData.getSourceUri(), sourceName); if (symbolData.isClass()) { if (symbolData.getFragmentNumber() >= 0) { assertEquals(symbolData.getSourceLine() - 1, srcStartPos.getLine()); } // Some classes on fragment -1 (interfaces) wont work. } else { if (symbolData.getCounter() == 0) { assertTrue(Math.abs(symbolData.getSourceLine() - srcStartPos.getLine()) <= 1); // Some methods wont work on source line. They were generated from the // parent SourceInfo } } } } }); } } private void testSymbolMapsCorrespondence(File root) throws Exception { // Testing SourceMaps as SymbolMap replacement // make sure the files have been produced assertTrue(root.exists()); File[] symbolMapFiles = filterByName(root, "(.*)\\.symbolMap") , sourceMapFiles = filterByName(root, "(.*)_sourceMap(\\d+)\\.json"); // At least there is a source map file for each symbol map file assertTrue(symbolMapFiles.length <= sourceMapFiles.length); List<List<File>> sourceMapSets = Lists.newArrayList(); for (int i = 0; i < symbolMapFiles.length; i++) { String name = symbolMapFiles[i].getName().split("\\.")[0]; List<File> set = Lists.newArrayList(); for (File sourceMap : sourceMapFiles) { if (sourceMap.getName().startsWith(name)) { set.add(sourceMap); } } assertTrue(set.size() >= 1); sourceMapSets.add(set); } for (int i = 0; i < symbolMapFiles.length; i++) { checkSourceMap(symbolMapFiles[i], sourceMapSets.get(i)); } } private void testSoycCorrespondence(File root) throws Exception { // Testing SourceMap as Soyc reports replacements assertTrue(root.exists()); for (Integer permutation : mapping.keySet()) { checkSplitPloints( new File(root.getPath() + "/splitPoints" + permutation + ".xml.gz"), new File(root.getPath() + "/fragments" + permutation + ".json")); checkEntities( new File(root.getPath() + "/stories" + permutation + ".xml.gz"), new File(root.getPath() + "/dependencies" + permutation + ".xml.gz"), mapping.get(permutation), new File(root.getPath() + "/" + EntityRecorder.ENTITIES + permutation + ".json")); } } private void checkEntities(File sizeMap, File dependency, Map<String, SimpleSymbolData> symbolTable, File entitiesFile) throws Exception { Map<String, ClassDescriptor> clsMap = EntityDescriptorJsonTranslator.readJson(new JSONObject(stringContent(entitiesFile))) .getAllClassesByName(); SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); parser.reset(); parser.parse(new GZIPInputStream(new FileInputStream(sizeMap)), checkStories(clsMap)); parser.reset(); parser.parse(new GZIPInputStream(new FileInputStream(dependency)), checkDependencies(clsMap)); checkSymbols(symbolTable, clsMap); } private void checkSymbols(Map<String, SimpleSymbolData> symbolTable, Map<String, ClassDescriptor> clsMap) { for (SimpleSymbolData symbol : symbolTable.values()) { if (symbol.getClassName().endsWith("[]")) { // Arrays aren't stored, because they are not entities, ie definable piece of code continue; } ClassDescriptor classDescriptor = clsMap.get(symbol.getClassName()); if (classDescriptor == null) { // Few classes in symbol maps are not presented in the new report. This is because, they // don't contribute to fragment size nor appear in the dependency graph. continue; } if (symbol.isClass()) { assertTrue(classDescriptor.getObfuscatedNames().contains(symbol.getSymbolName())); } else if (symbol.isField()) { assertTrue(classDescriptor.getField(symbol.getMemberName()).getObfuscatedNames() .contains(symbol.getSymbolName())); } else { // method MethodDescriptor mth = classDescriptor.getMethod( unSynthMethodSignature(symbol.getJsniIdent().split("::")[1])); assertTrue(mth.getObfuscatedNames().contains(symbol.getSymbolName())); } } } private static String unSynthMethodSignature(String mthSignature) { if (mthSignature.startsWith("$") && !mthSignature.startsWith("$init()") && !mthSignature.startsWith("$clinit()")) { return mthSignature.replaceFirst("L[^;\\(]*;","").substring(1); } return mthSignature; } private DefaultHandler checkDependencies( final Map<String,ClassDescriptor> classDescriptorByName) { return new DefaultHandler() { Set<Integer> currentDependencies = Sets.newHashSet(); // Compares method names in an "relaxed" manner, to account for the synthetic devirtualized // methods that appear in dependencies.xml but have to match the real methods in the // source map class model. boolean compareMethodNames(String thisName, String thatName) { return (thisName.replaceFirst("\\$*", "").equals(thatName.replaceFirst("\\$*", ""))); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); // "name"/"by" attributes wont include the signature, just the method name if (qName.equals("method")) { currentDependencies.clear(); String[] fullName = attributes.getValue("name").split("::"); for (MethodDescriptor method : classDescriptorByName.get(fullName[0]).getMethods()) { if (compareMethodNames(method.getName(), fullName[1])) { currentDependencies.addAll(Ints.asList(method.getDependentPointers())); } } assertTrue(currentDependencies.size() > 0); } else if (qName.equals("called")) { assertTrue(currentDependencies.size() > 0); String[] fullName = attributes.getValue("by").split("::"); boolean present = false; for (MethodDescriptor method : classDescriptorByName.get(fullName[0]).getMethods()) { if (compareMethodNames(method.getName(), fullName[1])) { if (currentDependencies.contains(method.getUniqueId())) { present = true; break; } } } // We cannot do much, because of the orig dependencies.xml format impressions assertTrue(present); } } }; } private DefaultHandler checkStories(final Map<String, ClassDescriptor> clsMap) { return new DefaultHandler() { int fragment = -1; @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); if (qName.equals("sizemap")) { fragment = Integer.parseInt(attributes.getValue("fragment")); } else if (qName.equals("size")) { assertTrue(fragment > -1); // <size type="type" ref="com.google.gwt.core.client.JavaScriptException" size="25"/> // eg. com.google.gwt.core.client.JavaScriptException::$clinit()V // type := type | method | field | string | var String kind = attributes.getValue("type"); int size = Integer.parseInt(attributes.getValue("size")); String ref = attributes.getValue("ref"); if (kind.equals("type")) { checkInFragments(size, clsMap.get(ref).getFragments()); } else if (kind.equals("method")) { String[] fullName = ref.split("::"); checkInFragments(size, clsMap.get(fullName[0]) .getMethod(unSynthMethodSignature(fullName[1])).getFragments()); } else if (kind.equals("field")) { String[] fullName = ref.split("::"); checkInFragments(size, clsMap.get(fullName[0]).getField(fullName[1]).getFragments()); } // var and string are not recorded in entities } } // Checks that current fragment and size are in the list private void checkInFragments(int size, Collection<Fragment> fragments) { for (EntityDescriptor.Fragment frag : fragments) { if (frag.getId() == fragment && frag.getSize() == size) { return; } } fail("Fragment <" + fragment + "> and size <" + size + "> don't match"); } }; } private void checkSplitPloints(File origSplitPoints, File fragmentsFile) throws Exception { JSONObject jsPoints = new JSONObject(stringContent(fragmentsFile)); final JSONArray initSeq = (JSONArray) jsPoints.opt(EntityRecorder.INITIAL_SEQUENCE); if (initSeq != null) { // Considering stable order on "initial sequence". May be this is too strict, in that case, // we need to store the elements in a list and provide a search method JSONArray fragments = (JSONArray) jsPoints.get(EntityRecorder.FRAGMENTS); final Map<Integer, JSONObject> fragmentById = Maps.newHashMap(); for (int i = 0; i < fragments.length(); i++) { JSONObject spoint = fragments.getJSONObject(i); fragmentById.put(spoint.getInt(EntityRecorder.FRAGMENT_ID), spoint); } SAXParserFactory.newInstance().newSAXParser().parse( new GZIPInputStream(new FileInputStream(origSplitPoints)), new DefaultHandler() { int isIdx = 0; @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); try { if (localName.equals("splipoint")) { JSONArray runAsyncs = fragmentById .get(Integer.parseInt(attributes.getValue("id"))) .getJSONArray(EntityRecorder.FRAGMENT_POINTS); boolean present = false; String runAsync = attributes.getValue("location"); for (int i = 0; i < runAsyncs.length(); i++) { if (runAsyncs.getString(i).equals(runAsync)) { present = true; break; } } assertTrue(present); } else if (localName.equals("splitpointref")) { assertEquals(Integer.parseInt(attributes.getValue("id")), initSeq.getInt(isIdx++)); } } catch (JSONException ex) { fail(ex.getMessage()); } } }); } } public void testSourceMap() throws Exception { String benchmark = "hello"; String module = "com.google.gwt.sample.hello.Hello"; File work = Utility.makeTemporaryDirectory(null, benchmark + "work"); try { options.setSoycEnabled(true); options.setJsonSoycEnabled(true); options.addModuleName(module); options.setWarDir(new File(work, "war")); options.setExtraDir(new File(work, "extra")); PrintWriterTreeLogger logger = new PrintWriterTreeLogger(); logger.setMaxDetail(TreeLogger.ERROR); Compiler.compile(logger, options); // Change parentDir for cached/pre-built reports String parentDir = options.getExtraDir() + "/" + benchmark; testSymbolMapsCorrespondence(new File(parentDir + "/symbolMaps/")); testSoycCorrespondence(new File(parentDir + "/soycReport/")); } finally { Util.recursiveDelete(work, false); } } }