public class

InstrumentationCoverageReporter

extends java.lang.Object

 java.lang.Object

↳androidx.test.internal.runner.coverage.InstrumentationCoverageReporter

Gradle dependencies

compile group: 'androidx.test', name: 'runner', version: '1.5.0-alpha03'

  • groupId: androidx.test
  • artifactId: runner
  • version: 1.5.0-alpha03

Artifact androidx.test:runner:1.5.0-alpha03 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.test:runner com.android.support.test:runner

Overview

A class that generates the JaCoCo execution data in Android Instrumentation tests.

Summary

Constructors
publicInstrumentationCoverageReporter(Instrumentation instrumentation, PlatformTestStorage testStorage)

Constructor.

Methods
public booleangenerateCoverageInternal(java.lang.String coverageFilePath, java.io.PrintStream instrumentationResultWriter)

Uses the JaCoCo agent to dump the execution data file to the given file path.

public java.lang.StringgenerateCoverageReport(java.lang.String coverageFilePath, java.io.PrintStream instrumentationResultWriter)

Generates the JaCoCo execution data report in the specified file path.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public InstrumentationCoverageReporter(Instrumentation instrumentation, PlatformTestStorage testStorage)

Constructor.

Parameters:

instrumentation: the instrumentation instance. Must not be null.
testStorage: the PlatformTestStorage to dump the coverage execution data onto the device.

Methods

public java.lang.String generateCoverageReport(java.lang.String coverageFilePath, java.io.PrintStream instrumentationResultWriter)

Generates the JaCoCo execution data report in the specified file path. A default file path will be used if no file path was provided, depending on whether the test storage service is available:

  • If the test storage service is available, the coverage file will be generated as coverage.ec under the test storage managed internal directory.
  • Otherwise, the coverage file will be generated under the test app's file folder, i.e. /data/data//files/coverage.ec.

Note, when the test storage service is not available, the caller of this method is responsible to make sure the given file path is writable.

Parameters:

coverageFilePath: the file path to generate the coverage data report. This is a relative path when the test storage service is available, otherwise an absolute path on the device. Can be null.
instrumentationResultWriter: the writer that can be used to write to the Instrumentation summary result.

Returns:

the actual file path the coverage report was written to, when the provided path is null.

public boolean generateCoverageInternal(java.lang.String coverageFilePath, java.io.PrintStream instrumentationResultWriter)

Uses the JaCoCo agent to dump the execution data file to the given file path.

Returns:

true if the coverage data was successfully generated, false otherwise.

Source

/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * 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 androidx.test.internal.runner.coverage;

import android.app.Instrumentation;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.test.platform.io.PlatformTestStorage;
import androidx.test.services.storage.TestStorage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

/** A class that generates the JaCoCo execution data in Android Instrumentation tests. */
public class InstrumentationCoverageReporter {
  private static final String TAG = InstrumentationCoverageReporter.class.getSimpleName();

  private static final String EMMA_RUNTIME_CLASS = "com.vladium.emma.rt.RT";
  private static final String DEFAULT_COVERAGE_FILE_NAME = "coverage.ec";

  private final Instrumentation instrumentation;
  private final PlatformTestStorage testStorage;

  /**
   * Constructor.
   *
   * @param instrumentation the instrumentation instance. Must not be {@code null}.
   * @param testStorage the {@code PlatformTestStorage} to dump the coverage execution data onto the
   *     device.
   */
  public InstrumentationCoverageReporter(
      Instrumentation instrumentation, PlatformTestStorage testStorage) {
    this.instrumentation = instrumentation;
    this.testStorage = testStorage;
  }

  /**
   * Generates the JaCoCo execution data report in the specified file path. A default file path will
   * be used if no file path was provided, depending on whether the test storage service is
   * available:
   *
   * <ul>
   *   <li>If the test storage service is available, the coverage file will be generated as
   *       coverage.ec under the test storage managed internal directory.
   *   <li>Otherwise, the coverage file will be generated under the test app's file folder, i.e.
   *       /data/data/<app-package>/files/coverage.ec.
   * </ul>
   *
   * <p>Note, when the test storage service is not available, the caller of this method is
   * responsible to make sure the given file path is writable.
   *
   * @param coverageFilePath the file path to generate the coverage data report. This is a relative
   *     path when the test storage service is available, otherwise an absolute path on the device.
   *     Can be {@code null}.
   * @param instrumentationResultWriter the writer that can be used to write to the Instrumentation
   *     summary result.
   * @return the actual file path the coverage report was written to, when the provided path is
   *     {@code null}.
   */
  public String generateCoverageReport(
      @Nullable String coverageFilePath, PrintStream instrumentationResultWriter) {
    // Unfortunately, the JaCoCo (Emma-compatible) API only supports dumping the execution data to a
    // `File`, rather than accepting an `OutputStream`. Worth looking JaCoCo's newer API [1] which
    // supports obtaining the execution data directly, and writing using the `PlatformTestStorage`
    // without inspecting its implementation/instance.
    // [1]
    // https://www.jacoco.org/jacoco/trunk/doc/api/org/jacoco/agent/rt/IAgent.html#getExecutionData(boolean).
    if (testStorage instanceof TestStorage) {
      coverageFilePath = dumpCoverageToTestStorage(coverageFilePath, instrumentationResultWriter);
    } else {
      coverageFilePath = dumpCoverageToFile(coverageFilePath, instrumentationResultWriter);
    }
    Log.d(TAG, "Coverage file was generated to " + coverageFilePath);
    // Also outputs a more user friendly message.
    instrumentationResultWriter.format("\nGenerated code coverage data to %s", coverageFilePath);
    return coverageFilePath;
  }

  /**
   * Directly write the coverage execution data to file when the test storage service is not
   * installed on the device.
   */
  private String dumpCoverageToFile(
      String coverageFilePath, PrintStream instrumentationResultWriter) {
    if (coverageFilePath == null) {
      Log.d(TAG, "No coverage file path was specified. Dumps to the default file path.");
      coverageFilePath =
          instrumentation.getTargetContext().getFilesDir().getAbsolutePath()
              + File.separator
              + DEFAULT_COVERAGE_FILE_NAME;
    }

    if (!generateCoverageInternal(coverageFilePath, instrumentationResultWriter)) {
      Log.w(
          TAG,
          "Failed to generate the coverage data file. Please refer to the instrumentation result"
              + " for more info.");
    }
    return coverageFilePath;
  }

  /**
   * Dumps the coverage execution data to file and then moves it to the test storage internal
   * folder.
   */
  private String dumpCoverageToTestStorage(
      String coverageFilePath, PrintStream instrumentationResultWriter) {
    if (coverageFilePath == null) {
      Log.d(
          TAG,
          "No coverage file path was specified. Dumps to the default coverage file using test"
              + " storage.");
      coverageFilePath = DEFAULT_COVERAGE_FILE_NAME;
    }

    String tempCoverageFilePath =
        instrumentation.getTargetContext().getFilesDir().getAbsolutePath()
            + File.separator
            + DEFAULT_COVERAGE_FILE_NAME;
    if (!generateCoverageInternal(tempCoverageFilePath, instrumentationResultWriter)) {
      Log.w(
          TAG,
          "Failed to generate the coverage data file. Please refer to the instrumentation result"
              + " for more info.");
    }

    try {
      Log.d(
          TAG,
          "Test service is available. Moving the coverage data file to be managed by the storage"
              + " service.");
      moveFileToTestStorage(tempCoverageFilePath, coverageFilePath);
      return coverageFilePath;
    } catch (IOException e) {
      reportEmmaError(instrumentationResultWriter, e);
    }
    return null;
  }

  private void moveFileToTestStorage(String srcFilePath, String destFilePath) throws IOException {
    File srcFile = new File(srcFilePath);
    if (srcFile.exists()) {
      Log.d(
          TAG,
          String.format(
              "Moving coverage file [%s] to the internal test storage [%s].",
              srcFilePath, destFilePath));
      try (OutputStream outputStream = testStorage.openInternalOutputFile(destFilePath);
          FileChannel srcChannel = new FileInputStream(srcFilePath).getChannel();
          WritableByteChannel destChannel = Channels.newChannel(outputStream)) {
        srcChannel.transferTo(0 /* position */, srcChannel.size() /* count */, destChannel);
      }
      if (!srcFile.delete()) {
        Log.e(
            TAG,
            String.format(
                "Failed to delete original coverage file [%s]", srcFile.getAbsolutePath()));
      }
    }
  }

  /**
   * Uses the JaCoCo agent to dump the execution data file to the given file path.
   *
   * @return true if the coverage data was successfully generated, false otherwise.
   */
  // Has to be `public` so that Mockito could properly stub it in testing.
  @VisibleForTesting
  public boolean generateCoverageInternal(
      String coverageFilePath, PrintStream instrumentationResultWriter) {
    java.io.File coverageFile = new java.io.File(coverageFilePath);

    try {
      // In case the target and instrumentation contexts are different, prioritize coverage from the
      // target context. If the target context classloader implements a delegate-first strategy
      // for org.jacoco.agent.rt and com.vladium.emma.rt, it will still be possible to get coverage
      // from either, or both, contexts.
      Class<?> emmaRTClass;
      try {
        emmaRTClass =
            Class.forName(
                EMMA_RUNTIME_CLASS, true, instrumentation.getTargetContext().getClassLoader());
      } catch (ClassNotFoundException e) {
        emmaRTClass =
            Class.forName(EMMA_RUNTIME_CLASS, true, instrumentation.getContext().getClassLoader());
        String msg = "Generating coverage for alternate test context.";
        Log.w(TAG, msg);
        instrumentationResultWriter.format("\nWarning: %s", msg);
      }

      // Uses reflection to call emma dump coverage method, to avoid always statically compiling
      // against the JaCoCo jar. The test infrastructure should make sure the JaCoCo library is
      // available when collecting the coverage data.
      Method dumpCoverageMethod =
          emmaRTClass.getMethod(
              "dumpCoverageData", coverageFile.getClass(), boolean.class, boolean.class);
      dumpCoverageMethod.invoke(null, coverageFile, false, false);
      return true;
    } catch (ClassNotFoundException e) {
      reportEmmaError(instrumentationResultWriter, "Is Emma/JaCoCo jar on classpath?", e);
    } catch (SecurityException
        | NoSuchMethodException
        | IllegalArgumentException
        | IllegalAccessException
        | InvocationTargetException e) {
      reportEmmaError(instrumentationResultWriter, e);
    }
    return false;
  }

  private void reportEmmaError(PrintStream writer, Exception e) {
    reportEmmaError(writer, "", e);
  }

  private void reportEmmaError(PrintStream writer, String hint, Exception e) {
    String msg = "Failed to generate Emma/JaCoCo coverage. " + hint;
    Log.e(TAG, msg, e);
    writer.format("\nError: %s", msg);
  }
}