From 8549003236db60c0c70cdd61387293c5fe543616 Mon Sep 17 00:00:00 2001 From: "isbadawi@google.com" Date: Sat, 14 Jul 2012 01:12:24 +0000 Subject: [PATCH] Add instrumentation for collecting client-side code coverage. Review at http://gwt-code-reviews.appspot.com/1764803 Review by: cromwellian@google.com git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@11164 8db76d5a-ed1c-0410-87a9-c151d255dfc7 --- .../gwt/dev/jjs/JavaToJavaScriptCompiler.java | 20 +- .../gwt/dev/js/BaselineCoverageGatherer.java | 123 +++++++++ .../gwt/dev/js/CoverageInstrumentor.java | 242 ++++++++++++++++++ .../google/gwt/dev/js/CoverageVisitor.java | 161 ++++++++++++ .../gwt/dev/js/CoverageInstrumentorTest.java | 107 ++++++++ 5 files changed, 651 insertions(+), 2 deletions(-) create mode 100644 dev/core/src/com/google/gwt/dev/js/BaselineCoverageGatherer.java create mode 100644 dev/core/src/com/google/gwt/dev/js/CoverageInstrumentor.java create mode 100644 dev/core/src/com/google/gwt/dev/js/CoverageVisitor.java create mode 100644 dev/core/test/com/google/gwt/dev/js/CoverageInstrumentorTest.java diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java index 231d823b83b..c561dfc2a14 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java +++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java @@ -67,8 +67,8 @@ import com.google.gwt.dev.jjs.impl.AstDumper; import com.google.gwt.dev.jjs.impl.CastNormalizer; import com.google.gwt.dev.jjs.impl.CatchBlockNormalizer; -import com.google.gwt.dev.jjs.impl.CodeSplitter.MultipleDependencyGraphRecorder; import com.google.gwt.dev.jjs.impl.CodeSplitter; +import com.google.gwt.dev.jjs.impl.CodeSplitter.MultipleDependencyGraphRecorder; import com.google.gwt.dev.jjs.impl.CodeSplitter2; import com.google.gwt.dev.jjs.impl.ControlFlowAnalyzer; import com.google.gwt.dev.jjs.impl.DeadCodeElimination; @@ -103,7 +103,9 @@ import com.google.gwt.dev.jjs.impl.UnifyAst; import com.google.gwt.dev.jjs.impl.VerifySymbolMap; import com.google.gwt.dev.jjs.impl.gflow.DataflowOptimizer; +import com.google.gwt.dev.js.BaselineCoverageGatherer; import com.google.gwt.dev.js.ClosureJsRunner; +import com.google.gwt.dev.js.CoverageInstrumentor; import com.google.gwt.dev.js.EvalFunctionsAtTopScope; import com.google.gwt.dev.js.JsBreakUpLargeVarStatements; import com.google.gwt.dev.js.JsCoerceIntShift; @@ -147,6 +149,7 @@ import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event; import com.google.gwt.soyc.SoycDashboard; import com.google.gwt.soyc.io.ArtifactsOutputDirectory; +import com.google.gwt.thirdparty.guava.common.collect.Multimap; import org.xml.sax.SAXException; @@ -290,6 +293,12 @@ public static PermutationResult compilePermutation(TreeLogger logger, UnifiedAst ResolveRebinds.exec(jprogram, permutation.getOrderedRebindAnswers()); + // Traverse the AST to figure out which lines are instrumentable for + // coverage. This has to happen before optimizations because functions might + // be optimized out; we want those marked as "not executed", not "not + // instrumentable". + Multimap instrumentableLines = BaselineCoverageGatherer.exec(jprogram); + // (4) Optimize the normalized Java AST for each permutation. int optimizationLevel = options.getOptimizationLevel(); if (optimizationLevel == OptionOptimize.OPTIMIZE_LEVEL_DRAFT) { @@ -347,6 +356,11 @@ public static PermutationResult compilePermutation(TreeLogger logger, UnifiedAst */ JsStackEmulator.exec(jprogram, jsProgram, propertyOracles, jjsmap); + /* + * If coverage is enabled, instrument the AST to record location info. + */ + CoverageInstrumentor.exec(jsProgram, instrumentableLines); + /* * Work around Safari 5 bug by rewriting a >> b as ~~a >> b. * @@ -1271,7 +1285,9 @@ public void endVisit(JsNameRef x, JsContext ctx) { // variable names, some of the are property. At least this // this give us a safe approximation. Ideally we need // the code removal passes to remove stuff in the scope objects. - nameUsed.add(x.getName().getIdent()); + if (x.isResolved()) { + nameUsed.add(x.getName().getIdent()); + } } @Override diff --git a/dev/core/src/com/google/gwt/dev/js/BaselineCoverageGatherer.java b/dev/core/src/com/google/gwt/dev/js/BaselineCoverageGatherer.java new file mode 100644 index 00000000000..df8a7545faf --- /dev/null +++ b/dev/core/src/com/google/gwt/dev/js/BaselineCoverageGatherer.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012 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.dev.js; + +import com.google.gwt.dev.jjs.InternalCompilerException; +import com.google.gwt.dev.jjs.SourceInfo; +import com.google.gwt.dev.jjs.ast.Context; +import com.google.gwt.dev.jjs.ast.JClassLiteral; +import com.google.gwt.dev.jjs.ast.JExpression; +import com.google.gwt.dev.jjs.ast.JMethodCall; +import com.google.gwt.dev.jjs.ast.JProgram; +import com.google.gwt.dev.jjs.ast.JThisRef; +import com.google.gwt.dev.jjs.ast.JVisitor; +import com.google.gwt.dev.jjs.ast.js.JsniMethodBody; +import com.google.gwt.dev.js.ast.JsContext; +import com.google.gwt.dev.js.ast.JsExpression; +import com.google.gwt.thirdparty.guava.common.base.Charsets; +import com.google.gwt.thirdparty.guava.common.collect.HashMultimap; +import com.google.gwt.thirdparty.guava.common.collect.Multimap; +import com.google.gwt.thirdparty.guava.common.collect.Sets; +import com.google.gwt.thirdparty.guava.common.io.Files; + +import java.io.File; +import java.io.IOException; +import java.util.Set; + +/** + * Build up a collection of all instrumentable lines, useful for generating + * coverage reports. + */ +public class BaselineCoverageGatherer { + public static Multimap exec(JProgram jProgram) { + if (System.getProperty("gwt.coverage") == null) { + return null; + } + return new BaselineCoverageGatherer(jProgram, getCoveredSourceFiles()).execImpl(); + } + + private static Set getCoveredSourceFiles() { + String filename = System.getProperty("gwt.coverage"); + File instrumentationFile = new File(filename); + try { + return Sets.newHashSet(Files.readLines(instrumentationFile, Charsets.UTF_8)); + } catch (IOException e) { + throw new InternalCompilerException("Could not open " + filename, e); + } + } + + private Multimap instrumentableLines = HashMultimap.create(); + private Set instrumentedFiles; + private JProgram jProgram; + + private BaselineCoverageGatherer(JProgram jProgram, Set instrumentedFiles) { + this.jProgram = jProgram; + this.instrumentedFiles = instrumentedFiles; + } + + private void cover(SourceInfo info) { + if (instrumentedFiles.contains(info.getFileName())) { + instrumentableLines.put(info.getFileName(), info.getStartLine()); + } + } + + private Multimap execImpl() { + /** + * Figure out which lines are executable. This is mostly straightforward + * except that we have to avoid some synthetic nodes introduced earlier, + * otherwise e.g. class declarations will be visited. + */ + new JVisitor() { + @Override public void endVisit(JMethodCall x, Context ctx) { + // this is a bit of a hack. The compiler inserts no-arg super calls, but + // there isn't really a way to detect that they're synthetic, and the + // strategy below of comparing source info with that of the enclosing type + // doesn't work because the enclosing type is set to be that of the superclass. + if (x.getTarget().isSynthetic() || x.toSource().equals("super()")) { + return; + } + endVisit((JExpression) x, ctx); + } + + @Override public void endVisit(JThisRef x, Context ctx) { + if (x.getSourceInfo().equals(x.getClassType().getSourceInfo())) { + return; + } + endVisit((JExpression) x, ctx); + } + + @Override public void endVisit(JClassLiteral x, Context ctx) { + if (x.getSourceInfo().equals(x.getRefType().getSourceInfo())) { + return; + } + endVisit((JExpression) x, ctx); + } + + @Override public void endVisit(JExpression x, Context ctx) { + cover(x.getSourceInfo()); + } + + @Override public void endVisit(JsniMethodBody x, Context ctx) { + new CoverageVisitor(instrumentedFiles) { + @Override public void endVisit(JsExpression x, JsContext ctx) { + cover(x.getSourceInfo()); + } + }.accept(x.getFunc()); + } + }.accept(jProgram); + return instrumentableLines; + } +} \ No newline at end of file diff --git a/dev/core/src/com/google/gwt/dev/js/CoverageInstrumentor.java b/dev/core/src/com/google/gwt/dev/js/CoverageInstrumentor.java new file mode 100644 index 00000000000..8af6e88f6e0 --- /dev/null +++ b/dev/core/src/com/google/gwt/dev/js/CoverageInstrumentor.java @@ -0,0 +1,242 @@ +/* + * Copyright 2012 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.dev.js; + +import com.google.gwt.dev.jjs.InternalCompilerException; +import com.google.gwt.dev.jjs.SourceInfo; +import com.google.gwt.dev.jjs.SourceOrigin; +import com.google.gwt.dev.js.ast.JsArrayAccess; +import com.google.gwt.dev.js.ast.JsBinaryOperation; +import com.google.gwt.dev.js.ast.JsBinaryOperator; +import com.google.gwt.dev.js.ast.JsContext; +import com.google.gwt.dev.js.ast.JsExprStmt; +import com.google.gwt.dev.js.ast.JsExpression; +import com.google.gwt.dev.js.ast.JsForIn; +import com.google.gwt.dev.js.ast.JsFunction; +import com.google.gwt.dev.js.ast.JsModVisitor; +import com.google.gwt.dev.js.ast.JsName; +import com.google.gwt.dev.js.ast.JsNameRef; +import com.google.gwt.dev.js.ast.JsNumberLiteral; +import com.google.gwt.dev.js.ast.JsObjectLiteral; +import com.google.gwt.dev.js.ast.JsParameter; +import com.google.gwt.dev.js.ast.JsProgram; +import com.google.gwt.dev.js.ast.JsPropertyInitializer; +import com.google.gwt.dev.js.ast.JsStatement; +import com.google.gwt.dev.js.ast.JsStringLiteral; +import com.google.gwt.dev.js.ast.JsVars; +import com.google.gwt.dev.js.ast.JsVars.JsVar; +import com.google.gwt.dev.js.ast.JsVisitor; +import com.google.gwt.thirdparty.guava.common.collect.Multimap; + +import java.io.StringReader; +import java.util.List; + +/** + * Instruments the generated JavaScript to record code coverage information + * about the original Java source. + * + * We maintain a global coverage object, whose keys are Java source filenames + * and whose values are objects mapping line numbers to 1 (executed) or 0 (not + * executed). + */ +public class CoverageInstrumentor { + /** + * This class does the actual instrumentation. It replaces + * {@code expr} with {@code ($coverage[file][line] = 1, expr)}. + */ + private class Instrumentor extends CoverageVisitor { + public Instrumentor() { + super(instrumentableLines.keySet()); + } + + @Override + public void endVisit(JsExpression x, JsContext ctx) { + SourceInfo info = x.getSourceInfo(); + if (!instrumentableLines.containsEntry(info.getFileName(), info.getStartLine())) { + return; + } + JsStringLiteral fileName = new JsStringLiteral(info, info.getFileName()); + JsNumberLiteral lineNumber = new JsNumberLiteral(info, info.getStartLine()); + JsArrayAccess lhs = new JsArrayAccess(info, new JsArrayAccess(info, + coverageObject.makeRef(info), fileName), lineNumber); + JsBinaryOperation update = new JsBinaryOperation(info, JsBinaryOperator.ASG, + lhs, new JsNumberLiteral(info, 1)); + ctx.replaceMe(new JsBinaryOperation(info, JsBinaryOperator.COMMA, update, x)); + } + } + + public static void exec(JsProgram jsProgram, Multimap instrumentableLines) { + if (instrumentableLines == null) { + return; + } + new CoverageInstrumentor(jsProgram, instrumentableLines).execImpl(); + } + + private Multimap instrumentableLines; + private JsProgram jsProgram; + private JsName coverageObject; + + private CoverageInstrumentor(JsProgram jsProgram, Multimap instrumentableLines) { + this.instrumentableLines = instrumentableLines; + this.jsProgram = jsProgram; + } + + private void addBeforeUnloadListener() { + JsFunction handler = function(new StringBuilder() + .append("function() {") + .append(" var coverage = JSON.parse(localStorage.getItem('gwt_coverage'));") + .append(" if (coverage !== null)") + .append(" merge_coverage($coverage, coverage);") + .append(" localStorage.setItem('gwt_coverage', JSON.stringify($coverage));") + .append("}").toString()); + SourceInfo info = dummySourceInfo(); + JsNameRef lhs = qualifiedRef(info, "window", "onbeforeunload"); + JsBinaryOperation asg = new JsBinaryOperation(info, JsBinaryOperator.ASG, lhs, handler); + makeGlobal(asg.makeStmt()); + } + + /** + * Creates the baseline coverage object, with an entry mapping to 0 for every + * instrumented line. + */ + private JsObjectLiteral baselineCoverage() { + SourceInfo info = dummySourceInfo(); + JsObjectLiteral baseline = new JsObjectLiteral(info); + List properties = baseline.getPropertyInitializers(); + for (String filename : instrumentableLines.keySet()) { + JsPropertyInitializer pair = new JsPropertyInitializer(info); + pair.setLabelExpr(new JsStringLiteral(info, filename)); + JsObjectLiteral lines = new JsObjectLiteral(info); + List coverage = lines.getPropertyInitializers(); + for (int line : instrumentableLines.get(filename)) { + coverage.add(new JsPropertyInitializer(info, + new JsNumberLiteral(info, line), new JsNumberLiteral(info, 0))); + } + pair.setValueExpr(lines); + properties.add(pair); + } + return baseline; + } + + private SourceInfo dummySourceInfo() { + return jsProgram.createSourceInfoSynthetic(getClass()); + } + + private void execImpl() { + coverageObject = global("$coverage", baselineCoverage()); + new JsModVisitor() { + @Override + public void endVisit(JsFunction x, JsContext ctx) { + new Instrumentor().accept(x.getBody()); + } + }.accept(jsProgram); + global("merge", mergeFunction()); + global("merge_coverage", mergeCoverageFunction()); + addBeforeUnloadListener(); + } + + /** + * Create a function object from a string. Names introduced inside the function + * are not obfuscatable. + */ + private JsFunction function(String code) { + try { + List stmts = + JsParser.parse(SourceOrigin.UNKNOWN, jsProgram.getScope(), new StringReader(code)); + JsExprStmt stmt = (JsExprStmt) stmts.get(0); + JsFunction f = (JsFunction) stmt.getExpression(); + new JsVisitor() { + @Override public void endVisit(JsParameter x, JsContext ctx) { + x.getName().setObfuscatable(false); + } + + @Override public void endVisit(JsForIn x, JsContext ctx) { + x.getIterVarName().setObfuscatable(false); + } + + @Override public void endVisit(JsVar x, JsContext ctx) { + x.getName().setObfuscatable(false); + } + }.accept(f); + return f; + } catch (Exception e) { + throw new InternalCompilerException("Unexpected exception parsing '" + code + "'", e); + } + } + + /** + * Declares a global variable with the given name initialized to the given + * expression. Returns a reference to the name object, useful for making + * references to the variable. + */ + private JsName global(String name, JsExpression initExpr) { + JsName jsName = jsProgram.getScope().declareName(name); + jsName.setObfuscatable(false); + JsVar var = new JsVar(dummySourceInfo(), jsName); + var.setInitExpr(initExpr); + makeGlobal(var); + return jsName; + } + + private void makeGlobal(JsVar var) { + JsVars vars; + JsStatement first = jsProgram.getGlobalBlock().getStatements().get(0); + if (first instanceof JsVars) { + vars = (JsVars) first; + } else { + vars = new JsVars(dummySourceInfo()); + jsProgram.getGlobalBlock().getStatements().add(0, vars); + } + vars.add(var); + } + + private void makeGlobal(JsStatement statement) { + jsProgram.getGlobalBlock().getStatements().add(0, statement); + } + + private JsFunction mergeCoverageFunction() { + return function(new StringBuilder() + .append("function (x, y) {") + .append(" merge(x, y, function(u, v) {") + .append(" return merge(u, v, Math.max);") + .append(" });") + .append("}").toString()); + } + + private JsFunction mergeFunction() { + return function(new StringBuilder() + .append("function (x, y, merger) {") + .append(" for (var key in y)") + .append(" if (x.hasOwnProperty(key))") + .append(" x[key] = merger(x[key], y[key]);") + .append(" else") + .append(" x[key] = y[key];") + .append(" return x;") + .append("}").toString()); + } + + private JsNameRef qualifiedRef(SourceInfo info, String qualifier, String name) { + JsNameRef qualified = ref(info, name); + qualified.setQualifier(ref(info, qualifier)); + return qualified; + } + + private JsNameRef ref(SourceInfo info, String name) { + JsName jsName = jsProgram.getScope().declareName(name); + jsName.setObfuscatable(false); + return jsName.makeRef(info); + } +} \ No newline at end of file diff --git a/dev/core/src/com/google/gwt/dev/js/CoverageVisitor.java b/dev/core/src/com/google/gwt/dev/js/CoverageVisitor.java new file mode 100644 index 00000000000..29d0bbbba6a --- /dev/null +++ b/dev/core/src/com/google/gwt/dev/js/CoverageVisitor.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012 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.dev.js; + +import com.google.gwt.dev.js.ast.JsArrayAccess; +import com.google.gwt.dev.js.ast.JsBinaryOperation; +import com.google.gwt.dev.js.ast.JsContext; +import com.google.gwt.dev.js.ast.JsExpression; +import com.google.gwt.dev.js.ast.JsFor; +import com.google.gwt.dev.js.ast.JsInvocation; +import com.google.gwt.dev.js.ast.JsModVisitor; +import com.google.gwt.dev.js.ast.JsNameRef; +import com.google.gwt.dev.js.ast.JsNew; +import com.google.gwt.dev.js.ast.JsNode; +import com.google.gwt.dev.js.ast.JsPostfixOperation; +import com.google.gwt.dev.js.ast.JsPrefixOperation; +import com.google.gwt.dev.js.ast.JsUnaryOperator; +import com.google.gwt.dev.js.ast.JsWhile; +import com.google.gwt.thirdparty.guava.common.collect.Sets; + +import java.util.Set; + +/** + * A visitor that visits every location in the AST where instrumentation is + * desirable. + */ +public abstract class CoverageVisitor extends JsModVisitor { + private int lastLine = -1; + private String lastFile = ""; + private Set instrumentedFiles; + + /** + * Nodes in this set are used in a context that expects a reference, not + * just an arbitrary expression. For example, delete takes a + * reference. These are tracked because it wouldn't be safe to rewrite + * delete foo.bar to delete (line='123',foo).bar. + */ + private final Set nodesInRefContext = Sets.newHashSet(); + + public CoverageVisitor(Set instrumentedFiles) { + this.instrumentedFiles = instrumentedFiles; + } + + @Override public void endVisit(JsArrayAccess x, JsContext ctx) { + visitExpression(x, ctx); + } + + @Override public void endVisit(JsBinaryOperation x, JsContext ctx) { + visitExpression(x, ctx); + } + + @Override public void endVisit(JsInvocation x, JsContext ctx) { + nodesInRefContext.remove(x.getQualifier()); + visitExpression(x, ctx); + } + + @Override public void endVisit(JsNameRef x, JsContext ctx) { + visitExpression(x, ctx); + } + + @Override public void endVisit(JsNew x, JsContext ctx) { + visitExpression(x, ctx); + } + + @Override public void endVisit(JsPostfixOperation x, JsContext ctx) { + visitExpression(x, ctx); + } + + @Override public void endVisit(JsPrefixOperation x, JsContext ctx) { + visitExpression(x, ctx); + nodesInRefContext.remove(x.getArg()); + } + + /** + * This is essentially a hacked-up version of JsFor.traverse to account for + * flow control differing from visitation order. It resets lastFile and + * lastLine before the condition and increment expressions in the for loop + * so that location data will be recorded correctly. + */ + @Override public boolean visit(JsFor x, JsContext ctx) { + if (x.getInitExpr() != null) { + x.setInitExpr(accept(x.getInitExpr())); + } else if (x.getInitVars() != null) { + x.setInitVars(accept(x.getInitVars())); + } + + if (x.getCondition() != null) { + resetPosition(); + x.setCondition(accept(x.getCondition())); + } + + if (x.getIncrExpr() != null) { + resetPosition(); + x.setIncrExpr(accept(x.getIncrExpr())); + } + accept(x.getBody()); + return false; + } + + @Override public boolean visit(JsInvocation x, JsContext ctx) { + nodesInRefContext.add(x.getQualifier()); + return true; + } + + @Override public boolean visit(JsPrefixOperation x, JsContext ctx) { + if (x.getOperator() == JsUnaryOperator.DELETE + || x.getOperator() == JsUnaryOperator.TYPEOF) { + nodesInRefContext.add(x.getArg()); + } + return true; + } + + /** + * Similar to JsFor, this resets the current location information before + * evaluating the condition. + */ + @Override public boolean visit(JsWhile x, JsContext ctx) { + resetPosition(); + x.setCondition(accept(x.getCondition())); + accept(x.getBody()); + return false; + } + + protected abstract void endVisit(JsExpression x, JsContext ctx); + + private void resetPosition() { + lastFile = ""; + lastLine = -1; + } + + private void visitExpression(JsExpression x, JsContext ctx) { + if (ctx.isLvalue()) { + // Assignments to comma expressions aren't legal + return; + } else if (nodesInRefContext.contains(x)) { + // Don't modify references into non-references + return; + } else if (!instrumentedFiles.contains(x.getSourceInfo().getFileName())) { + return; + } else if (x.getSourceInfo().getStartLine() == lastLine + && (x.getSourceInfo().getFileName().equals(lastFile))) { + return; + } + lastLine = x.getSourceInfo().getStartLine(); + lastFile = x.getSourceInfo().getFileName(); + endVisit(x, ctx); + } +} \ No newline at end of file diff --git a/dev/core/test/com/google/gwt/dev/js/CoverageInstrumentorTest.java b/dev/core/test/com/google/gwt/dev/js/CoverageInstrumentorTest.java new file mode 100644 index 00000000000..23289518acd --- /dev/null +++ b/dev/core/test/com/google/gwt/dev/js/CoverageInstrumentorTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012 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.dev.js; + +import com.google.gwt.dev.jjs.SourceInfo; +import com.google.gwt.dev.js.ast.JsBlock; +import com.google.gwt.dev.js.ast.JsContext; +import com.google.gwt.dev.js.ast.JsExprStmt; +import com.google.gwt.dev.js.ast.JsExpression; +import com.google.gwt.dev.js.ast.JsFunction; +import com.google.gwt.dev.js.ast.JsProgram; +import com.google.gwt.dev.js.ast.JsStatement; +import com.google.gwt.thirdparty.guava.common.base.Splitter; +import com.google.gwt.thirdparty.guava.common.collect.HashMultimap; +import com.google.gwt.thirdparty.guava.common.collect.LinkedHashMultimap; +import com.google.gwt.thirdparty.guava.common.collect.Multimap; + +import junit.framework.TestCase; + +import java.io.StringReader; + +/** + * Tests for CoverageInstrumentor. + */ +public class CoverageInstrumentorTest extends TestCase { + private JsProgram program; + private JsBlock functionBody; + + @Override + public void setUp() { + program = new JsProgram(); + SourceInfo info = program.createSourceInfo(1, "Test.java"); + JsBlock globalBlock = program.getGlobalBlock(); + JsFunction function = new JsFunction(info, program.getScope()); + functionBody = new JsBlock(info); + function.setBody(functionBody); + globalBlock.getStatements().add(new JsExprStmt(info, function)); + } + + private String instrument(String code) throws Exception { + functionBody.getStatements().clear(); + CoverageInstrumentor.exec(program, parse(code)); + return functionBody.toSource().trim().replaceAll("\\s+", " "); + } + + private String instrumentedProgram(Multimap baseline) { + CoverageInstrumentor.exec(program, baseline); + return program.toSource(); + } + + private Multimap parse(String code) throws Exception { + Iterable lines = Splitter.on('\n').split(code); + Multimap instrumentableLines = HashMultimap.create(); + int i = 0; + for (String line : lines) { + instrumentableLines.put("Test.java", ++i); + } + JsParser.parseInto(functionBody.getSourceInfo(), program.getScope(), + functionBody, new StringReader(code)); + i = 0; + for (JsStatement statement : functionBody.getStatements()) { + final SourceInfo info = program.createSourceInfo(++i, "Test.java"); + statement.setSourceInfo(info); + new CoverageVisitor(instrumentableLines.keySet()) { + @Override public void endVisit(JsExpression x, JsContext ctx) { + x.setSourceInfo(info); + } + }.accept(statement); + } + return instrumentableLines; + } + + public void testBaselineCoverage() throws Exception { + Multimap baselineCoverage = LinkedHashMultimap.create(); + for (int i = 1; i < 6; i++) { + baselineCoverage.put("A.java", i); + baselineCoverage.put("B.java", i); + } + assertTrue(instrumentedProgram(baselineCoverage).contains(new StringBuilder() + .append("var $coverage = {'A.java':{1:0, 2:0, 3:0, 4:0, 5:0}, ") + .append("'B.java':{1:0, 2:0, 3:0, 4:0, 5:0}}"))); + } + + public void testSimpleInstrumentation() throws Exception { + assertEquals("{ $coverage['Test.java'][1] = 1 , f(); }", instrument("f()")); + assertEquals("{ $coverage['Test.java'][1] = 1 , f(); $coverage['Test.java'][2] = 1 , g(); }", + instrument("f() \n g()")); + } + + public void testPreserveLiterals() throws Exception { + assertEquals("{ 'x'; }", instrument("'x'")); + } +} \ No newline at end of file