public final class

TestPlatformListener

extends RunListener

 java.lang.Object

↳RunListener

↳androidx.test.internal.events.client.TestPlatformListener

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

This is for Android-based JUnit clients to speak with services that use the protocol.

Summary

Constructors
publicTestPlatformListener(TestPlatformEventService notificationService)

Creates the TestPlatformListener to communicate with the remote test platform event service.

Methods
public booleanreportProcessCrash(java.lang.Throwable t)

Reports the process crash event with a given exception.

public voidtestAssumptionFailure(Failure failure)

public voidtestFailure(Failure failure)

public voidtestFinished(Description description)

public voidtestIgnored(Description description)

public voidtestRunFinished(Result result)

public voidtestRunStarted(Description description)

public voidtestStarted(Description description)

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

Constructors

public TestPlatformListener(TestPlatformEventService notificationService)

Creates the TestPlatformListener to communicate with the remote test platform event service.

Parameters:

notificationService: the remote service to send test run events to

Methods

public void testRunStarted(Description description)

public void testRunFinished(Result result)

public void testStarted(Description description)

public void testFinished(Description description)

public void testFailure(Failure failure)

public void testAssumptionFailure(Failure failure)

public void testIgnored(Description description)

public boolean reportProcessCrash(java.lang.Throwable t)

Reports the process crash event with a given exception. It is assumed that AJUR is crashing and not recovering from this. This will inform all clients that:

  1. A test has encountered an error (or the run has encountered an error if no test is in progress)
  2. The currently running test has finished (if it didn't already finished normally)
  3. The test run has finished.

Source

/*
 * Copyright (C) 2021 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.events.client;

import static androidx.test.internal.util.Checks.checkNotNull;

import android.util.Log;
import androidx.annotation.NonNull;
import androidx.test.services.events.ErrorInfo;
import androidx.test.services.events.ParcelableConverter;
import androidx.test.services.events.TestCaseInfo;
import androidx.test.services.events.TestEventException;
import androidx.test.services.events.TestRunInfo;
import androidx.test.services.events.TestStatus;
import androidx.test.services.events.TestStatus.Status;
import androidx.test.services.events.TimeStamp;
import androidx.test.services.events.platform.TestCaseErrorEvent;
import androidx.test.services.events.platform.TestCaseFinishedEvent;
import androidx.test.services.events.platform.TestCaseStartedEvent;
import androidx.test.services.events.platform.TestPlatformEvent;
import androidx.test.services.events.platform.TestRunErrorEvent;
import androidx.test.services.events.platform.TestRunFinishedEvent;
import androidx.test.services.events.platform.TestRunStartedEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.runner.Description;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;

/**
 * This {@link RunListener} is for Android-based JUnit clients to speak with services that use the
 * {@link TestPlatformEvent} protocol.
 */
public final class TestPlatformListener extends RunListener {
  private static final String TAG = "TestPlatformListener";
  private static final String INIT_ERROR = "initializationError";
  private final TestPlatformEventService notificationService;
  private Map<Description, Status> testCaseToStatus;
  private Set<Description> foundTestCases;
  private Set<Description> finishedTestCases;
  private Set<Description> startedTestCases;
  // The {@link Description} for the parent TestRunner/TestSuite. May contain many nested
  // {@link Description}s for other {@link Runner}s and individual test methods.
  private Description testRunDescription = Description.EMPTY;
  private final AtomicReference<Description> currentTestCase =
      new AtomicReference<>(Description.EMPTY);
  private TestRunInfo memoizedTestRun;
  private final AtomicBoolean processCrashed = new AtomicBoolean(false);
  /*
   * ongoingResult and ongoingResultListener enable us to generate a final test run result in the
   * event of an application crash.  This is a bit messy, but Result's internal state is completely
   * dependent on its listener.  If this API changes, we can just subclass Result directly and
   * populate it throughout this RunListener.
   */
  private final AtomicReference<Result> ongoingResult = new AtomicReference<>(new Result());
  private final AtomicReference<RunListener> ongoingResultListener =
      new AtomicReference<>(ongoingResult.get().createListener());

  /**
   * Creates the {@link TestPlatformListener} to communicate with the remote test platform event
   * service.
   *
   * @param notificationService the remote service to send test run events to
   */
  public TestPlatformListener(@NonNull TestPlatformEventService notificationService) {
    super();
    // Instantiates everything on creation so that we can correctly report errors before
    // {@link #testRunStarted} is called.
    initListener();
    this.notificationService =
        checkNotNull(notificationService, "notificationService cannot be null");
  }

  /** {@inheritDoc} */
  @Override
  public void testRunStarted(Description description) throws Exception {
    initListener();
    ongoingResultListener.get().testRunStarted(description);
    setRunDescription(description);
    List<Description> testCases =
        JUnitDescriptionParser.getAllTestCaseDescriptions(testRunDescription);
    for (Description testCase : testCases) {
      foundTestCases.add(testCase);
      // Tests are considered passed if nothing changes their status.
      testCaseToStatus.put(testCase, Status.PASSED);
    }
    try {
      memoizedTestRun = convertToTestRun(testRunDescription);
      notificationService.send(new TestRunStartedEvent(memoizedTestRun, TimeStamp.now()));
    } catch (TestEventException e) {
      Log.e(TAG, "Unable to send TestRunStartedEvent to Test Platform", e);
    }
  }

  /** {@inheritDoc} */
  @Override
  public void testRunFinished(Result result) throws Exception {
    ongoingResultListener.get().testRunFinished(result);
    Status status = result.wasSuccessful() ? Status.PASSED : Status.FAILED;
    // If the process crashed at any point, this is failed.
    status = processCrashed.get() ? Status.FAILED : status;
    // Mark all test cases that haven't run as CANCELLED or ABORTED (if started)
    if (foundTestCases.size() > finishedTestCases.size()) {
      // This was aborted mid test run. Mark it if this isn't already failing for some other
      // reason.
      status = status.equals(Status.PASSED) ? Status.ABORTED : status;
      for (Description test :
          JUnitDescriptionParser.getAllTestCaseDescriptions(testRunDescription)) {
        if (!finishedTestCases.contains(test)) {
          if (startedTestCases.contains(test)) {
            // The test isn't completed, but it was started and not finished.
            testCaseToStatus.put(test, Status.ABORTED);
          } else {
            // The test was supposed to be run but was never finished
            testCaseToStatus.put(test, Status.CANCELLED);
          }
          testFinishedInternal(test, TimeStamp.now());
        }
      }
    }
    try {
      notificationService.send(
          new TestRunFinishedEvent(memoizedTestRun, new TestStatus(status), TimeStamp.now()));
    } catch (TestEventException e) {
      Log.e(TAG, "Unable to send TestRunFinishedEvent to Test Platform", e);
    }
  }

  /** {@inheritDoc} */
  @Override
  public void testStarted(Description description) throws Exception {
    if (isInitError(description)) {
      return; // This isn't a real test method, don't send an update to the service
    }
    ongoingResultListener.get().testStarted(description);
    startedTestCases.add(description);
    currentTestCase.set(description); // Caches the test description in case of a crash
    try {
      notificationService.send(
          new TestCaseStartedEvent(convertToTestCase(description), TimeStamp.now()));
    } catch (TestEventException e) {
      Log.e(TAG, "Unable to send TestStartedEvent to Test Platform", e);
    }
  }

  /** {@inheritDoc} */
  @Override
  public void testFinished(Description description) throws Exception {
    testFinishedInternal(description, TimeStamp.now());
  }

  // If the test is marked as finished during the test run finish, we use the same timestamp
  private void testFinishedInternal(Description description, TimeStamp timeStamp) throws Exception {
    if (isInitError(description)) {
      return; // This isn't a real test method, don't send an update to the service
    }
    ongoingResultListener.get().testFinished(description);
    finishedTestCases.add(description);
    try {
      notificationService.send(
          new TestCaseFinishedEvent(
              convertToTestCase(description),
              new TestStatus(testCaseToStatus.get(description)),
              timeStamp));
    } catch (TestEventException e) {
      Log.e(TAG, "Unable to send TestFinishedEvent to Test Platform", e);
    } finally {
      // reset test case
      currentTestCase.set(Description.EMPTY);
    }
  }

  /** {@inheritDoc} */
  @Override
  public void testFailure(Failure failure) throws Exception {
    Description description = failure.getDescription();
    ongoingResultListener.get().testFailure(failure);
    if (description.isTest() && !isInitError(description)) {
      testCaseToStatus.put(description, Status.FAILED);
    }
    try {
      TestPlatformEvent event = createErrorEvent(failure, TimeStamp.now());
      notificationService.send(event);
    } catch (TestEventException e) {
      throw new IllegalStateException("Unable to send error event", e);
    }
  }

  /** {@inheritDoc} */
  @Override
  public void testAssumptionFailure(Failure failure) {
    ongoingResultListener.get().testAssumptionFailure(failure);
    if (failure.getDescription().isTest()) {
      testCaseToStatus.put(failure.getDescription(), Status.SKIPPED);
    }
    try {
      TestPlatformEvent event = createErrorEvent(failure, TimeStamp.now());
      notificationService.send(event);
    } catch (TestEventException e) {
      Log.e(TAG, "Unable to send TestAssumptionFailureEvent to Test Platform", e);
    }
  }

  /** {@inheritDoc} */
  @Override
  public void testIgnored(Description description) throws Exception {
    ongoingResultListener.get().testIgnored(description);
    Log.i(
        TAG,
        "TestIgnoredEvent("
            + description.getDisplayName()
            + "): "
            + description.getClassName()
            + "#"
            + description.getMethodName());
    testCaseToStatus.put(description, Status.IGNORED);
    testFinishedInternal(description, TimeStamp.now());
  }

  /**
   * Reports the process crash event with a given exception. It is assumed that AJUR is crashing and
   * not recovering from this. This will inform all clients that:
   *
   * <ol>
   *   <li>A test has encountered an error (or the run has encountered an error if no test is in
   *       progress)
   *   <li>The currently running test has finished (if it didn't already finished normally)
   *   <li>The test run has finished.
   * </ol>
   */
  public boolean reportProcessCrash(Throwable t) {
    processCrashed.set(true);
    boolean isTestCase = true;
    Description failingDescription = currentTestCase.get();
    if (failingDescription.equals(Description.EMPTY)) {
      isTestCase = false;
      failingDescription = testRunDescription;
    }
    try {
      Log.e("TestPlatformListener", "reporting crash as testfailure", t);
      testFailure(new Failure(failingDescription, t));
      if (isTestCase) {
        testFinished(failingDescription);
      }
      testRunFinished(ongoingResult.get());
    } catch (Exception e) {
      Log.e(TAG, "An exception was encountered while reporting the process crash", e);
      return false;
    }
    return true;
  }

  private void initListener() {
    finishedTestCases = new HashSet<>();
    foundTestCases = new HashSet<>();
    startedTestCases = new HashSet<>();
    testCaseToStatus = new HashMap<>();
    currentTestCase.set(Description.EMPTY);
    testRunDescription = Description.EMPTY;
    memoizedTestRun = null;
    processCrashed.set(false);
    ongoingResult.set(new Result());
    ongoingResultListener.set(ongoingResult.get().createListener());
  }

  private void setRunDescription(Description description) {
    testRunDescription = description;
    // Ignore around the "null" top-level Runner Description in AJUR or any unnecessarily nested
    // Runner structures.
    while (testRunDescription.getDisplayName().equals("null")
        && testRunDescription.getChildren().size() == 1) {
      testRunDescription = testRunDescription.getChildren().get(0);
    }
  }

  private static TestCaseInfo convertToTestCase(Description testCase) throws TestEventException {
    return ParcelableConverter.getTestCaseFromDescription(testCase);
  }

  private static TestRunInfo convertToTestRun(Description testRun) throws TestEventException {
    List<TestCaseInfo> testCases = new ArrayList<>();
    for (Description testCase : JUnitDescriptionParser.getAllTestCaseDescriptions(testRun)) {
      testCases.add(convertToTestCase(testCase));
    }
    return new TestRunInfo(testRun.getDisplayName(), testCases);
  }

  private static boolean isInitError(Description description) {
    return description.getMethodName() != null && description.getMethodName().equals(INIT_ERROR);
  }

  private TestPlatformEvent createErrorEvent(Failure failure, TimeStamp timeStamp)
      throws TestEventException {
    Description descriptionToUse = failure.getDescription();
    if (!descriptionToUse.isTest() || isInitError(descriptionToUse)) {
      descriptionToUse = testRunDescription;
    }
    ErrorInfo errorInfo = ErrorInfo.createFromFailure(failure);
    // If the description is a run description, report a run error. Otherwise report a test error.
    if (!descriptionToUse.equals(testRunDescription)) {
      try {
        return new TestCaseErrorEvent(convertToTestCase(descriptionToUse), errorInfo, timeStamp);
      } catch (TestEventException e) {
        Log.e(TAG, "Unable to create TestCaseErrorEvent", e);
      }
    }
    if (memoizedTestRun == null) {
      Log.d(TAG, "No test run info. Reporting an error before test run has ever started.");
      memoizedTestRun = convertToTestRun(Description.EMPTY);
    }
    return new TestRunErrorEvent(memoizedTestRun, errorInfo, timeStamp);
  }

}