OrchestratedInstrumentationListener.java

/*
 * Copyright (C) 2016 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.orchestrator.instrumentationlistener;

import static androidx.test.orchestrator.listeners.OrchestrationListenerManager.KEY_TEST_EVENT;

import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import androidx.test.orchestrator.callback.OrchestratorCallback;
import androidx.test.orchestrator.junit.BundleJUnitUtils;
import androidx.test.orchestrator.listeners.OrchestrationListenerManager.TestEvent;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.runner.Description;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;

/**
 * A {@link RunListener} for the orchestrated instrumentation to communicate information back to the
 * {@link androidx.test.orchestrator.OrchestratorService}. Not to be attached to {@link
 * androidx.test.orchestrator.AndroidTestOrchestrator} itself.
 */
public final class OrchestratedInstrumentationListener extends RunListener {

  private static final String TAG = "OrchestrationListener";

  private static final String ORCHESTRATOR_PACKAGE = "androidx.test.orchestrator";

  private static final String ODO_SERVICE_PACKAGE =
      "androidx.test.orchestrator.OrchestratorService";

  private final OnConnectListener listener;
  private final ConditionVariable testFinishedCondition = new ConditionVariable();
  private final AtomicBoolean isTestFailed = new AtomicBoolean(false);
  // Cached test description
  private Description description = Description.EMPTY;

  OrchestratorCallback odoCallback;

  /** Interface to notify when the listener has connected to the orchestrator. */
  public interface OnConnectListener {
    void onOrchestratorConnect();
  }

  public OrchestratedInstrumentationListener(OnConnectListener listener) {
    super();
    this.listener = listener;
  }

  private final ServiceConnection connection =
      new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName className, IBinder service) {
          odoCallback = OrchestratorCallback.Stub.asInterface(service);
          Log.i(TAG, "OrchestrationListener connected to service");
          listener.onOrchestratorConnect();
        }

        @Override
        public void onServiceDisconnected(ComponentName className) {
          odoCallback = null;
          Log.i(TAG, "OrchestrationListener disconnected from service");
        }
      };

  public void connect(Context context) {
    Intent intent = new Intent(ODO_SERVICE_PACKAGE);
    intent.setPackage(ORCHESTRATOR_PACKAGE);

    if (!context.bindService(intent, connection, Service.BIND_AUTO_CREATE)) {
      throw new RuntimeException("Cannot connect to " + ODO_SERVICE_PACKAGE);
    }
  }

  @Override
  public void testRunStarted(Description description) {
    try {
      sendTestNotification(
          TestEvent.TEST_RUN_STARTED, BundleJUnitUtils.getBundleFromDescription(description));
    } catch (RemoteException e) {
      Log.e(TAG, "Unable to send TestRunStarted Status to Orchestrator", e);
    }
  }

  @Override
  public void testRunFinished(Result result) {
    try {
      sendTestNotification(
          TestEvent.TEST_RUN_FINISHED, BundleJUnitUtils.getBundleFromResult(result));
    } catch (RemoteException e) {
      Log.e(TAG, "Unable to send TestRunFinished Status to Orchestrator", e);
    }
  }

  @Override
  public void testStarted(Description description) {
    testFinishedCondition.close();
    isTestFailed.set(false);
    this.description = description; // Caches the test description in case of a crash
    try {
      sendTestNotification(
          TestEvent.TEST_STARTED, BundleJUnitUtils.getBundleFromDescription(description));
    } catch (RemoteException e) {
      Log.e(TAG, "Unable to send TestStarted Status to Orchestrator", e);
    }
  }

  @Override
  public void testFinished(Description description) {
    try {
      sendTestNotification(
          TestEvent.TEST_FINISHED, BundleJUnitUtils.getBundleFromDescription(description));
      testFinishedCondition.open();
    } catch (RemoteException e) {
      Log.e(TAG, "Unable to send TestFinished Status to Orchestrator", e);
    }
  }

  @Override
  public void testFailure(Failure failure) {
    // This block can be called by the JUnit test framework when a failure happened in the test,
    // or {@link #reportProcessCrash(Throwable)} when we'd like to report a process crash as a
    // failure.
    // We'd like to make sure only one failure gets sent so that the isTestFailed variable is
    // checked and set without possibly racing between two thread calls.
    if (isTestFailed.compareAndSet(false, true)) {
      if (Description.TEST_MECHANISM.equals(failure.getDescription())) {
        // If an internal test runner exception occurred, the Description is "Test mechanism",
        // so replace it with the test description previously received in testStarted().
        failure = new Failure(description, failure.getException());
      }
      Log.d(TAG, "Sending TestFailure event: " + failure.getException().getMessage());
      try {
        sendTestNotification(
            TestEvent.TEST_FAILURE, BundleJUnitUtils.getBundleFromFailure(failure));
      } catch (RemoteException e) {
        throw new IllegalStateException("Unable to send TestFailure status, terminating", e);
      }
    }
  }

  @Override
  public void testAssumptionFailure(Failure failure) {
    try {
      sendTestNotification(
          TestEvent.TEST_ASSUMPTION_FAILURE, BundleJUnitUtils.getBundleFromFailure(failure));
    } catch (RemoteException e) {
      throw new IllegalStateException(
          "Unable to send TestAssumptionFailure status, terminating", e);
    }
  }

  @Override
  public void testIgnored(Description description) {
    try {
      sendTestNotification(
          TestEvent.TEST_IGNORED, BundleJUnitUtils.getBundleFromDescription(description));
    } catch (RemoteException e) {
      Log.e(TAG, "Unable to send TestIgnored Status to Orchestrator", e);
    }
  }

  public void sendTestNotification(TestEvent type, Bundle bundle) throws RemoteException {
    if (null == odoCallback) {
      throw new IllegalStateException("Unable to send notification, callback is null");
    }
    bundle.putString(KEY_TEST_EVENT, type.toString());

    odoCallback.sendTestNotification(bundle);
  }

  public void addTests(Description description) {
    if (description.isEmpty()) {
      return;
    }

    if (description.isTest()) {
      addTest(description.getClassName() + "#" + description.getMethodName());
    } else {
      for (Description child : description.getChildren()) {
        addTests(child);
      }
    }
  }

  public void addTest(String test) {
    if (null == odoCallback) {
      throw new IllegalStateException("Unable to send test, callback is null");
    }

    try {
      odoCallback.addTest(test);
    } catch (RemoteException e) {
      Log.e(TAG, "Unable to send test", e);
    }
  }

  /**
   * Blocks until the test running within this Instrumentation has finished, whether the test
   * succeeds or fails.
   *
   * <p>We consider a test finished when the {@link #testFinished(Description)} method has been
   * called.
   */
  public void waitUntilTestFinished(long timeoutMs) {
    testFinishedCondition.block(timeoutMs);
  }

  /**
   * Returns whether the test has failed.
   *
   * <p>We consider a test failed if the {@link #testFailure(Failure)} method has been called.
   */
  public boolean isTestFailed() {
    return isTestFailed.get();
  }

  /** Reports the process crash event with a given exception. */
  public void reportProcessCrash(Throwable t) {
    testFailure(new Failure(description, t));
    testFinished(description);
  }
}