chromium/ash/webui/diagnostics_ui/resources/routine_list_executor.ts

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert} from 'chrome://resources/js/assert.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.js';

import {RoutineResult, RoutineResultInfo, RoutineRunnerReceiver, RoutineType, SystemRoutineControllerInterface} from './system_routine_controller.mojom-webui.js';

/**
 * Represents the execution progress of a test routine.
 */
export enum ExecutionProgress {
  NOT_STARTED,
  RUNNING,
  COMPLETED,
  CANCELLED,
  SKIPPED,
  WARNING,
}

/**
 * Represents the status of the test suite.
 */
export enum TestSuiteStatus {
  NOT_RUNNING,
  RUNNING,
  COMPLETED,
}

/**
 * Represents the input to a single routine-result-entry in a
 * routine-result-list.
 */
export class ResultStatusItem {
  routine: RoutineType;
  progress: ExecutionProgress;
  result: RoutineResult|null = null;

  constructor(routine: RoutineType, progress = ExecutionProgress.NOT_STARTED) {
    this.routine = routine;
    this.progress = progress;
  }
}

/**
 * The type of the status callback function.
 */
export type StatusCallbackFunction = (arg0: ResultStatusItem) => void;

type InitialRunRoutineStatus =
    void|ExecutionProgress.CANCELLED|ExecutionProgress.COMPLETED;
type RunRoutineStatus = Exclude<InitialRunRoutineStatus, void>;


/**
 * Implements the RoutineRunnerInterface remote. Creates a resolver and resolves
 * it when the onRoutineResult function is called.
 */
class ExecutionContext {
  routineRunner: RoutineRunnerReceiver = new RoutineRunnerReceiver(this);
  private resolver: PromiseResolver<RoutineResultInfo|null> =
      new PromiseResolver();

  /**
   * Implements RoutineRunnerInterface.onRoutineResult.
   */
  onRoutineResult(result: RoutineResultInfo): void {
    this.resolver.resolve(result);
    this.close();
  }

  whenComplete(): Promise<RoutineResultInfo|null> {
    return this.resolver.promise;
  }

  close(): void {
    this.routineRunner.$.close();
  }

  cancel(): void {
    this.resolver.resolve(null);
  }
}

/**
 * Executes a list of test routines, firing a status callback with a
 * ResultStatusItem before and after each test. The resulting ResultStatusItem
 * maps directly to an entry in a routine-result-list.
 */
export class RoutineListExecutor {
  private routineController: SystemRoutineControllerInterface;
  private currentExecutionContext: ExecutionContext|null = null;
  private routinesCancelled: boolean = false;
  constructor(routineController: SystemRoutineControllerInterface) {
    this.routineController = routineController;
  }

  /**
   * Executes a list of routines providing a status callback as each test
   * starts and finishes. The return promise will resolve when all tests are
   * completed.
   */
  runRoutines(routines: RoutineType[], statusCallback: StatusCallbackFunction):
      Promise<RunRoutineStatus> {
    assert(routines.length > 0);

    // Create a chain of promises that each schedule the next routine when
    // they complete, firing the status callback before and after each test.
    let promise: Promise<InitialRunRoutineStatus> = Promise.resolve();
    routines.forEach((name) => {
      promise = promise.then(() => {
        // Notify the status callback of the test status.
        if (this.routinesCancelled) {
          statusCallback(
              new ResultStatusItem(name, ExecutionProgress.CANCELLED));
          return ExecutionProgress.CANCELLED;
        }
        statusCallback(new ResultStatusItem(name, ExecutionProgress.RUNNING));

        this.currentExecutionContext = new ExecutionContext();
        // Create a new remote and execute the next test.
        this.routineController.runRoutine(
            name,
            this.currentExecutionContext.routineRunner.$
                .bindNewPipeAndPassRemote());

        // When the test completes, notify the status callback of the
        // result.
        return this.currentExecutionContext.whenComplete().then(
            (info: RoutineResultInfo|null) => {
              let progress = ExecutionProgress.CANCELLED;
              let result = null;

              if (info !== null) {
                assert(info.type === name);
                progress = ExecutionProgress.COMPLETED;
                result = info.result;
              }

              const status = new ResultStatusItem(name, progress);
              status.result = result;
              statusCallback(status);
              return progress;
            });
      });
    });

    return promise as Promise<RunRoutineStatus>;
  }

  close(): void {
    if (this.currentExecutionContext) {
      this.currentExecutionContext.close();
    }
  }

  cancel(): void {
    if (this.currentExecutionContext) {
      this.routinesCancelled = true;
      this.currentExecutionContext.cancel();
    }
  }
}