//
// Copyright 2019 Ettus Research, A National Instruments Company
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: PkgTestExec
//
// Description: This package provides infrastructure for tracking the state of
// testbench execution and the results of each test.
//



package PkgTestExec;

  timeunit 1ns;
  timeprecision 1ps;

  typedef enum { SEV_INFO, SEV_WARNING, SEV_ERROR, SEV_FATAL } severity_t;

  typedef std::process timeout_t;

  class TestExec;

    string tb_name;                   // Name of the testbench
    string current_test;              // Name of the current test being run
    int    num_started, num_finished; // Number of tests started and finished
    int    num_passed;                // Number of tests that have passed
    int    num_assertions;            // Number of assertions checked for the current test
    time   start_time, end_time;      // Start and end time of the testbench
    bit    stop_on_error = 1;         // Configuration option to stop when an error occurs
    bit    done = 0;                  // Flag that sets when tb is finished
    logic  test_status[$] = { 1'bX }; // Pass/fail of each test where index matches test number
    int    start_tb_count = 0;        // Total number of start_tb calls
    int    end_tb_count = 0;          // Total number of end_tb completions

    timeout_t tb_timeout;             // Handle to timeout for the overall testbench
    timeout_t test_timeout;           // Handle to timeout for current test

    semaphore tb_count_sem;           // Semaphore for testbench counters
    semaphore test_sem;               // Testbench semaphore


    function new();
      $timeformat(-9, 0, " ns", 12);
      this.tb_count_sem = new(1);
      this.test_sem = new(1);
    endfunction : new


    // Call start_tb() at the start of a testbench to initialize state tracking
    // properties.
    //
    //    time_limit:  Max simulation time allowed before at timeout occurs.
    //                 This is a catch-all, in case things go very wrong during
    //                 simulation and cause it to never end.
    //
    task start_tb(string tb_name, realtime time_limit = 10ms);
      // Increment start_tb_count to track how many TBs we expect to complete.
      // Use a semaphore to protect the read-modify-write operation, which
      // could be called from multiple threads.
      tb_count_sem.get();
      start_tb_count++;
      tb_count_sem.put();
      // Get the sempahore, to prevent multiple overlapping instances of the
      // same testbench.
      test_sem.get();
      done = 0;
      $display("========================================================");
      $display("TESTBENCH STARTED: %s", tb_name);
      $display("========================================================");
      this.tb_name = tb_name;
      start_time   = $time;
      test_status  = { 1'bX };  // There is no test 0, so set it to X
      num_started  = 0;
      num_finished = 0;
      num_passed   = 0;
      start_timeout(
        tb_timeout,
        time_limit,
        $sformatf("Testbench \"%s\" time limit exceeded", tb_name),
        SEV_FATAL
      );
    endtask : start_tb


    // Call end_tb() at the end of a testbench to report final statistics and,
    // optionally, end simulation.
    //
    //    finish:  Set to 1 (default) to cause $finish() to be called at the
    //             end of simulation, cuasing the simulator to close.
    //
    function void end_tb(bit finish = 1);
      assert (num_started == num_finished) else begin
        $fatal(1, "Testbench ended before test completed");
      end
      end_time = $time;
      $display("========================================================");
      $display("TESTBENCH FINISHED: %s", tb_name);
      $display(" - Time elapsed:  %0t", end_time - start_time);
      $display(" - Tests Run:     %0d", num_finished);
      $display(" - Tests Passed:  %0d", num_passed);
      $display(" - Tests Failed:  %0d", num_started - num_passed);
      $display("Result: %s",
        (num_started == num_passed) && (num_finished > 0)
        ? "PASSED   " : "FAILED!!!");
      $display("========================================================");

      end_timeout(tb_timeout);

      done = 1;

      // Increment end_tb_count so we know when all the TBs have completed. Do
      // this before releasing the semaphore so that only one thread can be
      // doing this read-modify-write operation.
      end_tb_count++;

      // Release the semaphore to allow new instances of the testbench to run
      test_sem.put();

      if (finish) $finish();
    endfunction : end_tb


    // Call at the start of each test with the name of the test.
    //
    //   test_name:  String name for the test to be started
    //
    task start_test(string test_name, realtime time_limit = 0);
      // Make sure a there isn't already a test running
      assert (num_started == num_finished) else begin
        $fatal(1, "Test started before completing previous test");
      end

      // Create a timeout for this test
      if (time_limit > 0) begin
        start_timeout(
          test_timeout,
          time_limit,
          $sformatf("Test \"%s\" time limit exceeded", test_name),
          SEV_FATAL
        );
      end

      current_test = test_name;
      num_started++;
      $display("[TEST CASE %3d] (t = %t) BEGIN: %s...", num_started, $time, test_name);
      test_status.push_back(1);   // Set status to 1 (passed) initially
      num_assertions = 0;
    endtask : start_test


    // Call end_test() at the end of each test.
    //
    //   test_result:  Optional value to indicate the overall pass/fail result
    //                 of the test. Use non-zero for pass, 0 for fail.
    //
    task end_test(int test_result = 1);
      bit passed;

      assert (num_started == num_finished + 1) else begin
        $fatal(1, "No test running");
      end

      end_timeout(test_timeout);

      passed = test_status[num_started] && test_result;
      num_finished++;
      $display("[TEST CASE %3d] (t = %t) DONE... %s",
               num_started, $time, passed ? "Passed" : "FAILED");

      if (passed) num_passed++;

      current_test = "";
    endtask : end_test


    // Wait until all testbenches have completed. This assumes that start_tb is
    // called for all testbenches at time 0, but the testbenches may finish at
    // different times.
    //
    //   finish: If 1, this task calls $finish() once all testbenches have
    //          completed. Otherwise, it returns.
    //
    task wait_all_tb(bit finish = 0);
      // Wait until the first testbench has completed
      wait (end_tb_count > 0);

      // By this time, all the testbenches should have started, so we wait
      // until the end count catches up to the start count.
      wait (end_tb_count == start_tb_count);

      if (finish) $finish();
    endtask


    // Assert the given expression and call $error() if it fails. Simulation
    // will also be stopped (using $stop) if stop_on_error is true.
    //
    //   expr:     The expression value to be asserted
    //   message:  String to report if the assertion fails
    //
    function void assert_error(int expr, string message = "");
      num_assertions++;
      assert (expr) else begin
        test_status[num_started] = 0;
        $error(message);
        if (stop_on_error) $stop;
      end
    endfunction : assert_error


    // Assert the given expression and call $fatal() if it fails.
    //
    //   expr:     The expression value to be asserted
    //   message:  String to report if the assertion fails
    //
    function void assert_fatal(int expr, string message = "");
      num_assertions++;
      assert (expr) else begin
        test_status[num_started] = 0;
        $fatal(1, message);
      end
    endfunction : assert_fatal


    // Assert the given expression and call $warning() if it fails.
    //
    //   expr:     The expression value to be asserted
    //   message:  String to report if the assertion fails
    //
    function void assert_warning(int expr, string message = "");
      num_assertions++;
      assert (expr) else begin
        $warning(message);
      end
    endfunction : assert_warning


    // Assert the given expression and call the appropriate severity or fatal
    // task if the expression fails.
    //
    //   expr:      The expression value to be asserted
    //   message:   String to report if the assertion fails
    //   severity:  Indicates the type of severity task that should be used if
    //              the assertion fails ($info, $warning, $error, $fatal).
    //              Default value is SEV_ERROR.
    //
    function void assert_sev(
      int        expr,
      string     message = "",
      severity_t severity = SEV_ERROR
    );
      num_assertions++;
      assert (expr) else begin
        case (severity)
          SEV_INFO: begin
            $info(message);
          end
          SEV_WARNING: begin
            $warning(message);
          end
          SEV_ERROR: begin
            $error(message);
            test_status[num_started] = 0;
            if (stop_on_error) $stop;
          end
          default: begin  // SEV_FATAL
            test_status[num_started] = 0;
            $fatal(1, message);
          end
        endcase
      end
    endfunction : assert_sev


    // Create a timeout that will expire after the indicated delay, causing an
    // error if the timeout is not canceled before the delay has elapsed.
    //
    //   handle:         A handle to the timeout process created. Use this
    //                   handle to end the timeout.
    //   timeout_delay:  Amount of time to wait before the timeout expires.
    //   message:        String message to display if the timeout expires.
    //   severity:       Indicates the type of severity task that should be
    //                   used if the timeout expires. Default is SEV_ERROR.
    //
    task start_timeout(
      output timeout_t  handle,
      input  realtime   timeout_delay,
      input  string     message = "Timeout",
      input  severity_t severity = SEV_ERROR
    );
      mailbox #(std::process) pid = new(1);
      fork
        begin : timeout
          pid.put(process::self());
          #(timeout_delay);
          assert_sev(0, {"Timeout: ", message}, severity);
        end
      join_none
      #0;   // Start the timeout process so we can get its process ID

      // Return the process ID
      pid.get(handle);
    endtask


    // Cancel the timeout with the given handle. This kills the process
    // running the timeout delay.
    //
    //   handle:  Handle created by start_timeout() for the timeout process.
    //
    function void end_timeout(timeout_t handle);
      if (handle != null)
        if (handle.status != process::FINISHED) handle.kill();
    endfunction;

  endclass : TestExec


  //---------------------------------------------------------------------------
  // Test Object
  //---------------------------------------------------------------------------
  //
  // This is the default TestExec object instance shared by all instances of
  // the running the testbench.
  //
  //---------------------------------------------------------------------------

  TestExec test = new();

endpackage : PkgTestExec

