Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(149)

Unified Diff: dev/core/src/com/google/gwt/dev/js/CoverageInstrumentor.java

Issue 1764803: Add instrumentation for collecting client-side code coverage. Base URL: http://google-web-toolkit.googlecode.com/svn/
Patch Set: Ignore some syntethic AST nodes while figuring out baseline coverage Created 10 months, 2 weeks ago
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: dev/core/src/com/google/gwt/dev/js/CoverageInstrumentor.java
===================================================================
--- dev/core/src/com/google/gwt/dev/js/CoverageInstrumentor.java (revision 0)
+++ dev/core/src/com/google/gwt/dev/js/CoverageInstrumentor.java (revision 0)
@@ -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<String, Integer> instrumentableLines) {
+ if (instrumentableLines == null) {
+ return;
+ }
+ new CoverageInstrumentor(jsProgram, instrumentableLines).execImpl();
+ }
+
+ private Multimap<String, Integer> instrumentableLines;
+ private JsProgram jsProgram;
+ private JsName coverageObject;
+
+ private CoverageInstrumentor(JsProgram jsProgram, Multimap<String, Integer> 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<JsPropertyInitializer> 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<JsPropertyInitializer> 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<JsStatement> 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);
+ }
+}

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld revision f51cb906c4ad+