Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace enrol_lti\local\ltiadvantage\task;
  18  
  19  use enrol_lti\helper;
  20  use Packback\Lti1p3\LtiAssignmentsGradesService;
  21  use Packback\Lti1p3\LtiGrade;
  22  use Packback\Lti1p3\LtiLineitem;
  23  use core\task\manager;
  24  use phpunit_util;
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  global $CFG;
  29  
  30  require_once (__DIR__ . '/../lti_advantage_testcase.php');
  31  
  32  /**
  33   * Tests for the enrol_lti\local\ltiadvantage\task\sync_tool_grades adhoc task.
  34   *
  35   * @package enrol_lti
  36   * @copyright 2023 David Pesce <david.pesce@exputo.com>
  37   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   * @coversDefaultClass \enrol_lti\local\ltiadvantage\task\sync_tool_grades
  39   */
  40  class sync_tool_grades_test extends \lti_advantage_testcase {
  41      /**
  42       * Get a task which has a mocked ags instance injected, meaning no real calls will be made to the platform.
  43       *
  44       * This allows us to test the behaviour of the task (in terms of which users are in scope and which grades are sent)
  45       * without needing to deal with any auth.
  46       *
  47       * @param string $statuscode the HTTP status code to simulate.
  48       * @param bool $mockexception whether to simulate an exception during the service call or not.
  49       * @return sync_grades instance of the task with a mocked ags instance inside.
  50       */
  51      protected function get_task_with_mocked_grade_service($statuscode = '200', $mockexception = false): sync_tool_grades {
  52          $mockgradeservice = $this->getMockBuilder(LtiAssignmentsGradesService::class)
  53              ->disableOriginalConstructor()
  54              ->onlyMethods(['putGrade'])
  55              ->getMock();
  56          $mockgradeservice->method('putGrade')->willReturnCallback(function() use ($statuscode, $mockexception) {
  57              if ($mockexception) {
  58                  throw new \Exception();
  59              }
  60              return ['headers' => ['httpstatus' => "HTTP/2 $statuscode OK"], 'body' => '', 'status' => $statuscode];
  61          });
  62          // Get a mock task, with the method 'get_ags()' mocked to return the mocked AGS instance.
  63          $mocktask = $this->getMockBuilder(sync_tool_grades::class)
  64              ->onlyMethods(['get_ags'])
  65              ->getMock();
  66          $mocktask->method('get_ags')->willReturn($mockgradeservice);
  67          return $mocktask;
  68      }
  69  
  70      /**
  71       * Helper function to set a grade for a user.
  72       *
  73       * @param int $userid the id of the user being graded.
  74       * @param float $grade the grade value, out of 100, to set for the user.
  75       * @param \stdClass $resource the published resource object.
  76       * @return float the fractional grade value expected to be used during sync.
  77       */
  78      protected function set_user_grade_for_resource(int $userid, float $grade, \stdClass $resource): float {
  79  
  80          global $CFG;
  81          require_once($CFG->libdir . '/accesslib.php');
  82          require_once($CFG->libdir . '/gradelib.php');
  83          $context = \context::instance_by_id($resource->contextid);
  84  
  85          if ($context->contextlevel == CONTEXT_COURSE) {
  86              $gi = \grade_item::fetch_course_item($resource->courseid);
  87          } else if ($context->contextlevel == CONTEXT_MODULE) {
  88              $cm = get_coursemodule_from_id('assign', $context->instanceid);
  89  
  90              $gi = \grade_item::fetch([
  91                  'itemtype' => 'mod',
  92                  'itemmodule' => 'assign',
  93                  'iteminstance' => $cm->instance,
  94                  'courseid' => $resource->courseid
  95              ]);
  96          }
  97  
  98          if ($ggrade = \grade_grade::fetch(['itemid' => $gi->id, 'userid' => $userid])) {
  99              $ggrade->finalgrade = $grade;
 100              $ggrade->rawgrade = $grade;
 101              $ggrade->update();
 102          } else {
 103              $ggrade = new \grade_grade();
 104              $ggrade->itemid = $gi->id;
 105              $ggrade->userid = $userid;
 106              $ggrade->rawgrade = $grade;
 107              $ggrade->finalgrade = $grade;
 108              $ggrade->rawgrademax = 100;
 109              $ggrade->rawgrademin = 0;
 110              $ggrade->timecreated = time();
 111              $ggrade->timemodified = time();
 112              $ggrade->insert();
 113          }
 114          return floatval($ggrade->finalgrade / $gi->grademax);
 115      }
 116  
 117      /**
 118       * Helper to set the completion status for published course or module.
 119       *
 120       * @param \stdClass $resource the resource - either a course or module.
 121       * @param int $userid the id of the user to override the completion status for.
 122       * @param bool $complete whether the resource is deemed complete or not.
 123       */
 124      protected function override_resource_completion_status_for_user(\stdClass $resource, int $userid,
 125              bool $complete): void {
 126  
 127          global $CFG;
 128          require_once($CFG->libdir . '/accesslib.php');
 129          require_once($CFG->libdir . '/completionlib.php');
 130          require_once($CFG->libdir . '/datalib.php');
 131          $this->setAdminUser();
 132          $context = \context::instance_by_id($resource->contextid);
 133          $completion = new \completion_info(get_course($resource->courseid));
 134          if ($context->contextlevel == CONTEXT_COURSE) {
 135              $ccompletion = new \completion_completion(['userid' => $userid, 'course' => $resource->courseid]);
 136              if ($complete) {
 137                  $ccompletion->mark_complete();
 138              } else {
 139                  $completion->clear_criteria();
 140              }
 141          } else if ($context->contextlevel == CONTEXT_MODULE) {
 142              $completion->update_state(
 143                  get_coursemodule_from_id('assign', $context->instanceid),
 144                  $complete ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE,
 145                  $userid,
 146                  true
 147              );
 148          }
 149      }
 150  
 151      /**
 152       * Data provider for test_grade_sync_positive_case.
 153       *
 154       * @return array
 155       */
 156      public static function grade_sync_positive_cases(): array {
 157          return [
 158              [200],
 159              [201],
 160              [202],
 161              [204],
 162          ];
 163      }
 164  
 165      /**
 166       * Test the sync grades task works correct when platform responses with given status code.
 167       *
 168       * @covers ::execute
 169       * @param string $statuscode the response status code with which the job should work correctly
 170       * @dataProvider grade_sync_positive_cases
 171       */
 172      public function test_grade_sync_positive_case($statuscode): void {
 173          $this->resetAfterTest();
 174  
 175          [$course, $resource] = $this->create_test_environment();
 176          $launchservice = $this->get_tool_launch_service();
 177          $task = $this->get_task_with_mocked_grade_service($statuscode);
 178  
 179          // Launch the resource for an instructor which will create the domain objects needed for service calls.
 180          $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
 181          $instructoruser = $this->getDataGenerator()->create_user();
 182          [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
 183  
 184          // Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
 185          $studentusers = $this->get_mock_launch_users_with_ids(['2'], false,
 186              'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
 187  
 188          $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]);
 189          $student1user = $this->getDataGenerator()->create_user();
 190          [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
 191  
 192          // Grade student1.
 193          $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
 194  
 195          // Sync and verify that only student1's grade is sent.
 196          ob_start();
 197          $task->set_custom_data($resource);
 198          $task->execute();
 199          $ob = ob_get_contents();
 200          ob_end_clean();
 201          $expectedtraces = [
 202              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 203              "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
 204                  "'$course->id'.",
 205              "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
 206                  "'$resource->id' and the course '$course->id' was sent.",
 207              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 208                  "Processed 2 users; sent 1 grades."
 209          ];
 210          foreach ($expectedtraces as $expectedtrace) {
 211              $this->assertStringContainsString($expectedtrace, $ob);
 212          }
 213      }
 214  
 215      /**
 216       * Test the sync grades task during several runs and for a series of grade changes.
 217       *
 218       * @covers ::execute
 219       */
 220      public function test_grade_sync_chronological_syncs() {
 221          $this->resetAfterTest();
 222  
 223          [$course, $resource] = $this->create_test_environment();
 224          $launchservice = $this->get_tool_launch_service();
 225          $task = $this->get_task_with_mocked_grade_service();
 226  
 227          // Launch the resource for an instructor which will create the domain objects needed for service calls.
 228          $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
 229          $instructoruser = $this->getDataGenerator()->create_user();
 230          [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
 231  
 232          // Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
 233          $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
 234              'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
 235  
 236          $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]);
 237          $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1]);
 238          $student1user = $this->getDataGenerator()->create_user();
 239          $student2user = $this->getDataGenerator()->create_user();
 240          [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
 241          [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch);
 242  
 243          // Grade student1 only.
 244          $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
 245  
 246          // Sync and verify that only student1's grade is sent.
 247          ob_start();
 248          $task->set_custom_data($resource);
 249          $task->execute();
 250          $ob = ob_get_contents();
 251          ob_end_clean();
 252  
 253          $expectedtraces = [
 254              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 255              "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
 256                  "'$course->id'.",
 257              "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
 258                  "'$resource->id' and the course '$course->id' was sent.",
 259              "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
 260                  "'$course->id'.",
 261              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 262                  "Processed 3 users; sent 1 grades."
 263          ];
 264          foreach ($expectedtraces as $expectedtrace) {
 265              $this->assertStringContainsString($expectedtrace, $ob);
 266          }
 267  
 268          // Sync again, verifying no grades are sent because nothing has changed.
 269          ob_start();
 270          $task->execute();
 271          $ob = ob_get_contents();
 272          ob_end_clean();
 273          $expectedtraces = [
 274              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 275              "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
 276              "'$course->id'.",
 277              "Not sent - The grade for the user '$student1id', for the resource '$resource->id' and the course ".
 278                  "'$course->id' was not sent as the grades are the same.",
 279              "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
 280                  "'$course->id'.",
 281              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 282              "Processed 3 users; sent 0 grades."
 283          ];
 284          foreach ($expectedtraces as $expectedtrace) {
 285              $this->assertStringContainsString($expectedtrace, $ob);
 286          }
 287  
 288          // Change student1's grade and add a grade for student2.
 289          $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 68.5, $resource);
 290          $expectedstudent2grade = $this->set_user_grade_for_resource($student2id, 44.5, $resource);
 291  
 292          // Sync again, verifying both grade changes are sent.
 293          ob_start();
 294          $task->execute();
 295          $ob = ob_get_contents();
 296          ob_end_clean();
 297  
 298          $expectedtraces = [
 299              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 300              "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
 301                  "'$course->id'.",
 302              "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
 303                  "'$resource->id' and the course '$course->id' was sent.",
 304              "Success - The grade '$expectedstudent2grade' for the user '$student2id', for the resource ".
 305                  "'$resource->id' and the course '$course->id' was sent.",
 306              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 307                  "Processed 3 users; sent 2 grades."
 308          ];
 309          foreach ($expectedtraces as $expectedtrace) {
 310              $this->assertStringContainsString($expectedtrace, $ob);
 311          }
 312      }
 313  
 314      /**
 315       * Test a grade sync when there are more than one resource link for the resource.
 316       *
 317       * @covers ::execute
 318       */
 319      public function test_grade_sync_multiple_resource_links() {
 320          $this->resetAfterTest();
 321  
 322          [$course, $resource] = $this->create_test_environment();
 323          $launchservice = $this->get_tool_launch_service();
 324          $task = $this->get_task_with_mocked_grade_service();
 325  
 326          // Launch the resource first for an instructor using the default resource link in the platform.
 327          $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
 328          $instructoruser = $this->getDataGenerator()->create_user();
 329          [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
 330  
 331          // Launch again as the instructor, this time from a different resource link in the platform.
 332          $teachermocklaunch2 = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], 'RLID-2');
 333          $launchservice->user_launches_tool($instructoruser, $teachermocklaunch2);
 334  
 335          // Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
 336          $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
 337              'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
 338  
 339          $student1reslink1launch = $this->get_mock_launch($resource, $studentusers[0]);
 340          $student2reslink1launch = $this->get_mock_launch($resource, $studentusers[1]);
 341          $student1reslink2launch = $this->get_mock_launch($resource, $studentusers[1], 'RLID-2');
 342          $student1user = $this->getDataGenerator()->create_user();
 343          $student2user = $this->getDataGenerator()->create_user();
 344          [$student1id] = $launchservice->user_launches_tool($student1user, $student1reslink1launch);
 345          [$student2id] = $launchservice->user_launches_tool($student2user, $student2reslink1launch);
 346          $launchservice->user_launches_tool($student1user, $student1reslink2launch);
 347  
 348          // Grade student1 only.
 349          $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
 350  
 351          // Sync and verify that only student1's grade is sent but that it's sent for BOTH resource links.
 352          ob_start();
 353          $task->set_custom_data($resource);
 354          $task->execute();
 355          $ob = ob_get_contents();
 356          ob_end_clean();
 357  
 358          $expectedtraces = [
 359              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 360              "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
 361              "'$course->id'.",
 362              "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
 363              "'$resource->id' and the course '$course->id' was sent.",
 364              "Found 2 resource link(s) for the user '$student1id', for the resource ".
 365              "'$resource->id' and the course '$course->id'. Attempting to sync grades for all.",
 366              "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
 367              "'$course->id'.",
 368              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 369              "Processed 3 users; sent 1 grades."
 370          ];
 371          foreach ($expectedtraces as $expectedtrace) {
 372              $this->assertStringContainsString($expectedtrace, $ob);
 373          }
 374  
 375          // Verify that the grade was reported as being synced twice - once for each resource link.
 376          $expected = "/Found 2 resource link\(s\) for the user '$student1id', for the resource ".
 377              "'$resource->id' and the course '$course->id'. Attempting to sync grades for all.\n".
 378              "Processing resource link '.*'.\n".
 379              "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
 380              "'$resource->id' and the course '$course->id' was sent.\n".
 381              "Processing resource link '.*'.\n".
 382              "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
 383              "'$resource->id' and the course '$course->id' was sent./";
 384          $this->assertMatchesRegularExpression($expected, $ob);
 385      }
 386  
 387      /**
 388       * Test the grade sync task when the launch data doesn't include the AGS support.
 389       *
 390       * @covers ::execute
 391       */
 392      public function test_sync_grades_no_service_endpoint() {
 393          $this->resetAfterTest();
 394          [$course, $resource] = $this->create_test_environment();
 395          $launchservice = $this->get_tool_launch_service();
 396  
 397          // Launch the resource for an instructor which will create the domain objects needed for service calls.
 398          $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0],
 399              null, null);
 400          $instructoruser = $this->getDataGenerator()->create_user();
 401          [$userid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
 402  
 403          $task = $this->get_task_with_mocked_grade_service();
 404          $task->set_custom_data($resource);
 405          $this->expectOutputRegex(
 406              "/Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.\n".
 407              "Found 1 resource link\(s\) for the user '$userid', for the resource '$resource->id' and the ".
 408              "course '$course->id'. Attempting to sync grades for all.\n".
 409              "Processing resource link '.*'.\n".
 410              "Skipping - No grade service found for the user '$userid', for the resource '$resource->id' and the ".
 411              "course '$course->id'.\n".
 412              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. Processed 1 users; ".
 413              "sent 0 grades./"
 414          );
 415          $task->execute();
 416      }
 417  
 418      /**
 419       * Test syncing grades when the enrolment instance is disabled.
 420       *
 421       * @covers ::execute
 422       */
 423      public function test_sync_grades_disabled_instance() {
 424          $this->resetAfterTest();
 425          global $DB;
 426  
 427          [$course, $resource, $resource2, $resource3] = $this->create_test_environment();
 428  
 429          // Disable resource 1.
 430          $enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_DISABLED];
 431          $DB->update_record('enrol', $enrol);
 432  
 433          // Delete the activity being shared by resource 2, leaving resource 2 disabled as a result.
 434          $modcontext = \context::instance_by_id($resource2->contextid);
 435          course_delete_module($modcontext->instanceid);
 436  
 437          // Only the enabled resource 3 should sync grades.
 438          $task = $this->get_task_with_mocked_grade_service();
 439          $task->set_custom_data($resource3);
 440          $this->expectOutputRegex(
 441              "/^Starting - LTI Advantage grade sync for shared resource '$resource3->id' in course '$course->id'.\n".
 442              "Completed - Synced grades for tool '$resource3->id' in the course '$course->id'. Processed 0 users; ".
 443              "sent 0 grades.\n$/"
 444          );
 445          $task->execute();
 446      }
 447  
 448      /**
 449       * Test the grade sync when the context has been deleted in between launch and when the grade sync task is run.
 450       *
 451       * @covers ::execute
 452       */
 453      public function test_sync_grades_deleted_context() {
 454          $this->resetAfterTest();
 455          global $DB;
 456  
 457          [$course, $resource] = $this->create_test_environment();
 458          $launchservice = $this->get_tool_launch_service();
 459  
 460          // Launch the resource for an instructor which will create the domain objects needed for service calls.
 461          $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
 462          $instructoruser = $this->getDataGenerator()->create_user();
 463          [$userid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
 464  
 465          // Delete the activity, then enable the enrolment method (it is disabled during activity deletion).
 466          $modcontext = \context::instance_by_id($resource->contextid);
 467          course_delete_module($modcontext->instanceid);
 468          $enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_ENABLED];
 469          $DB->update_record('enrol', $enrol);
 470  
 471          $task = $this->get_task_with_mocked_grade_service();
 472          $task->set_custom_data($resource);
 473          $this->expectOutputRegex(
 474              "/Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.\n".
 475              "Found 1 resource link\(s\) for the user '$userid', for the resource '$resource->id' and the ".
 476              "course '$course->id'. Attempting to sync grades for all.\n".
 477              "Processing resource link '.*'.\n".
 478              "Failed - Invalid contextid '$resource->contextid' for the resource '$resource->id'.\n".
 479              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. Processed 1 users; ".
 480              "sent 0 grades./"
 481          );
 482          $task->execute();
 483      }
 484  
 485      /**
 486       * Test grade sync when completion is required for the activity before sync takes place.
 487       *
 488       * @covers ::execute
 489       */
 490      public function test_sync_grades_completion_required() {
 491          $this->resetAfterTest();
 492          global $CFG;
 493          require_once($CFG->libdir . '/completionlib.php');
 494  
 495          [
 496              $course,
 497              $resource,
 498              $resource2,
 499              $publishedcourse
 500          ] = $this->create_test_environment(true, true, false, helper::MEMBER_SYNC_ENROL_AND_UNENROL, true, true);
 501          $launchservice = $this->get_tool_launch_service();
 502          $task = $this->get_task_with_mocked_grade_service();
 503  
 504          // Launch the resource for an instructor which will create the domain objects needed for service calls.
 505          $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
 506          $instructoruser = $this->getDataGenerator()->create_user();
 507          [$teacherid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
 508  
 509          // Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
 510          $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
 511              'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
 512          $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]);
 513          $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1]);
 514          $student1user = $this->getDataGenerator()->create_user();
 515          $student2user = $this->getDataGenerator()->create_user();
 516          [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
 517          [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch);
 518  
 519          // Launch the published course as student2.
 520          $student2mockcourselaunch = $this->get_mock_launch($publishedcourse, $studentusers[1], '23456');
 521          $launchservice->user_launches_tool($student2user, $student2mockcourselaunch);
 522  
 523          // Grade student1 in the assign resource.
 524          $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
 525  
 526          // And student2 in the course resource.
 527          $expectedstudent2grade = $this->set_user_grade_for_resource($student2id, 55.5, $publishedcourse);
 528  
 529          // Since adhoc tasks are queued via sync_grades scheduled task, we need to create a queue.
 530          $allitems = array($resource, $resource2, $publishedcourse);
 531  
 532          // Sync and verify that no grades are sent because resource and published course are both not yet complete.
 533          ob_start();
 534          foreach ($allitems as $item) {
 535              $task->set_custom_data($item);
 536              $task->execute();
 537          }
 538          $ob = ob_get_contents();
 539          ob_end_clean();
 540          $expectedtraces = [
 541              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 542              "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
 543                  "'$course->id'.",
 544              "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ".
 545                  "'$course->id'.",
 546              "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
 547                  "'$course->id'.",
 548              "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ".
 549                  "the course '$course->id'.",
 550              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 551                  "Processed 3 users; sent 0 grades."
 552          ];
 553          foreach ($expectedtraces as $expectedtrace) {
 554              $this->assertStringContainsString($expectedtrace, $ob);
 555          }
 556  
 557          // Complete the resource for student1.
 558          $this->override_resource_completion_status_for_user($resource, $student1id, true);
 559  
 560          // Run the sync again, this time confirming the grade for student1 is sent.
 561          ob_start();
 562          foreach ($allitems as $item) {
 563              $task->set_custom_data($item);
 564              $task->execute();
 565          }
 566          $ob = ob_get_contents();
 567          ob_end_clean();
 568          $expectedtraces = [
 569              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 570              "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
 571                  "'$course->id'.",
 572              "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
 573                  "'$resource->id' and the course '$course->id' was sent.",
 574              "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
 575                  "'$course->id'.",
 576              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 577                  "Processed 3 users; sent 1 grades.",
 578              "Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.",
 579              "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ".
 580                  "the course '$course->id'.",
 581              "Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ".
 582                  "Processed 1 users; sent 0 grades.",
 583          ];
 584          foreach ($expectedtraces as $expectedtrace) {
 585              $this->assertStringContainsString($expectedtrace, $ob);
 586          }
 587  
 588          // Fail completion for student1 and confirm no grade is sent, even despite it being changed.
 589          $this->set_user_grade_for_resource($student1id, 33.3, $resource);
 590          $this->override_resource_completion_status_for_user($resource, $student1id, false);
 591  
 592          ob_start();
 593          foreach ($allitems as $item) {
 594              $task->set_custom_data($item);
 595              $task->execute();
 596          }
 597          $ob = ob_get_contents();
 598          ob_end_clean();
 599          $expectedtraces = [
 600              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 601              "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
 602                  "'$course->id'.",
 603              "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ".
 604                  "'$course->id'.",
 605              "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
 606                  "'$course->id'.",
 607              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 608                  "Processed 3 users; sent 0 grades.",
 609              "Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.",
 610              "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ".
 611                  "the course '$course->id'.",
 612              "Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ".
 613                  "Processed 1 users; sent 0 grades.",
 614          ];
 615          foreach ($expectedtraces as $expectedtrace) {
 616              $this->assertStringContainsString($expectedtrace, $ob);
 617          }
 618  
 619          // Complete the course for student2 and verify the grade is now sent.
 620          $this->override_resource_completion_status_for_user($publishedcourse, $student2id, true);
 621  
 622          ob_start();
 623          foreach ($allitems as $item) {
 624              $task->set_custom_data($item);
 625              $task->execute();
 626          }
 627          $ob = ob_get_contents();
 628          ob_end_clean();
 629          $expectedtraces = [
 630              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 631              "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
 632                  "'$course->id'.",
 633              "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ".
 634                  "'$course->id'.",
 635              "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
 636                  "'$course->id'.",
 637              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 638                  "Processed 3 users; sent 0 grades.",
 639              "Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.",
 640              "Success - The grade '$expectedstudent2grade' for the user '$student2id', for the resource ".
 641                  "'$publishedcourse->id' and the course '$course->id' was sent.",
 642              "Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ".
 643                  "Processed 1 users; sent 1 grades.",
 644  
 645          ];
 646          foreach ($expectedtraces as $expectedtrace) {
 647              $this->assertStringContainsString($expectedtrace, $ob);
 648          }
 649  
 650          // Mark the course as in progress again for student2 and verify any new grade changes are not sent.
 651          $this->set_user_grade_for_resource($student2id, 78.8, $publishedcourse);
 652          $this->override_resource_completion_status_for_user($publishedcourse, $student2id, false);
 653  
 654          ob_start();
 655          foreach ($allitems as $item) {
 656              $task->set_custom_data($item);
 657              $task->execute();
 658          }
 659          $ob = ob_get_contents();
 660          ob_end_clean();
 661          $expectedtraces = [
 662              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 663              "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
 664                  "'$course->id'.",
 665              "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ".
 666                  "'$course->id'.",
 667              "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
 668                  "'$course->id'.",
 669              "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ".
 670                  "the course '$course->id'.",
 671              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 672                  "Processed 3 users; sent 0 grades."
 673          ];
 674          foreach ($expectedtraces as $expectedtrace) {
 675              $this->assertStringContainsString($expectedtrace, $ob);
 676          }
 677      }
 678  
 679      /**
 680       * Test grade sync when the attempt to call the service returns an exception or a bad HTTP response code.
 681       *
 682       * @covers ::execute
 683       */
 684      public function test_sync_grades_failed_service_call() {
 685          $this->resetAfterTest();
 686          [$course, $resource] = $this->create_test_environment();
 687          $launchservice = $this->get_tool_launch_service();
 688          $task = $this->get_task_with_mocked_grade_service('200', true);
 689  
 690          // Launch the resource for an instructor which will create the domain objects needed for service calls.
 691          $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
 692          $instructoruser = $this->getDataGenerator()->create_user();
 693          [$teacherid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
 694  
 695          // Launch the resource for a student, creating the enrolment and allowing grading to take place.
 696          $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
 697              'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
 698          $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]);
 699          $student1user = $this->getDataGenerator()->create_user();
 700          [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
 701  
 702          // Grade student1 in the assign resource.
 703          $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
 704  
 705          // Run the sync, verifying that the response error causes a 'Failed' trace but that the task completes.
 706          ob_start();
 707          $task->set_custom_data($resource);
 708          $task->execute();
 709          $ob = ob_get_contents();
 710          ob_end_clean();
 711          $expectedtraces = [
 712              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 713              "Failed - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
 714                  "'$resource->id' and the course '$course->id' failed to send.",
 715              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 716                  "Processed 2 users; sent 0 grades."
 717          ];
 718          foreach ($expectedtraces as $expectedtrace) {
 719              $this->assertStringContainsString($expectedtrace, $ob);
 720          }
 721  
 722          // Now run the sync again, this time with a bad http response code.
 723          $task = $this->get_task_with_mocked_grade_service('400');
 724          ob_start();
 725          $task->set_custom_data($resource);
 726          $task->execute();
 727          $ob = ob_get_contents();
 728          ob_end_clean();
 729          $expectedtraces = [
 730              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 731              "Failed - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
 732              "'$resource->id' and the course '$course->id' failed to send.",
 733              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 734              "Processed 2 users; sent 0 grades."
 735          ];
 736          foreach ($expectedtraces as $expectedtrace) {
 737              $this->assertStringContainsString($expectedtrace, $ob);
 738          }
 739      }
 740  
 741      /**
 742       * Test the sync when only the lineitem URL is provided and when lineitem creation/query isn't expected.
 743       *
 744       * @covers ::execute
 745       */
 746      public function test_sync_grades_coupled_lineitem() {
 747          $this->resetAfterTest();
 748  
 749          [$course, $resource] = $this->create_test_environment();
 750          $launchservice = $this->get_tool_launch_service();
 751  
 752          // The launches use a coupled line item. Only scores can be posted. Line items and results cannot be created or queried.
 753          $agsclaim = [
 754              "scope" => ["https://purl.imsglobal.org/spec/lti-ags/scope/score"],
 755              "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 756          ];
 757  
 758          // Launch the resource for an instructor which will create the domain objects needed for service calls.
 759          $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null,
 760              $agsclaim);
 761          $instructoruser = $this->getDataGenerator()->create_user();
 762          [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
 763  
 764          // Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
 765          $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
 766              'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
 767  
 768          $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim);
 769          $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim);
 770          $student1user = $this->getDataGenerator()->create_user();
 771          $student2user = $this->getDataGenerator()->create_user();
 772          [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
 773          [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch);
 774  
 775          // Grade student1 only.
 776          $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
 777  
 778          // Mock task, asserting that score posting to an existing line item takes place, via a mock grade service object.
 779          $mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class);
 780          $mockgradeservice->method('putGrade')->willReturnCallback(function() {
 781              return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200];
 782          });
 783          $mockgradeservice->expects($this->never())
 784              ->method('findOrCreateLineitem');
 785          $mockgradeservice->expects($this->once())
 786              ->method('putGrade')
 787              ->with($this->isInstanceOf(LtiGrade::class));
 788          $mocktask = $this->getMockBuilder(sync_tool_grades::class)
 789              ->onlyMethods(['get_ags'])
 790              ->getMock();
 791          $mocktask->method('get_ags')->willReturn($mockgradeservice);
 792  
 793          // Sync and verify that only student1's grade is sent.
 794          ob_start();
 795          $mocktask->set_custom_data($resource);
 796          $mocktask->execute();
 797          $ob = ob_get_contents();
 798          ob_end_clean();
 799          $expectedtraces = [
 800              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 801              "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
 802              "'$course->id'.",
 803              "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
 804              "'$resource->id' and the course '$course->id' was sent.",
 805              "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
 806              "'$course->id'.",
 807              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 808              "Processed 3 users; sent 1 grades."
 809          ];
 810          foreach ($expectedtraces as $expectedtrace) {
 811              $this->assertStringContainsString($expectedtrace, $ob);
 812          }
 813      }
 814  
 815      /**
 816       * Test the sync for an activity context when only the lineitems URL is provided and when line item creation/query is expected.
 817       *
 818       * @covers ::execute
 819       */
 820      public function test_sync_grades_none_or_many_lineitems_activity_context() {
 821          $this->resetAfterTest();
 822  
 823          [$course, $resource] = $this->create_test_environment();
 824          $launchservice = $this->get_tool_launch_service();
 825  
 826          // The launches omit the 'lineitem' claim, meaning the item may have none (or many) line items.
 827          $agsclaim = [
 828              "scope" => [
 829                  "https://purl.imsglobal.org/spec/lti-ags/scope/score",
 830                  "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
 831              ],
 832              "lineitems" => "https://platform.example.com/10/lineitems"
 833          ];
 834  
 835          // Launch the resource for an instructor which will create the domain objects needed for service calls.
 836          $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null,
 837              $agsclaim);
 838          $instructoruser = $this->getDataGenerator()->create_user();
 839          [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
 840  
 841          // Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
 842          $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
 843              'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
 844  
 845          $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim);
 846          $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim);
 847          $student1user = $this->getDataGenerator()->create_user();
 848          $student2user = $this->getDataGenerator()->create_user();
 849          [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
 850          [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch);
 851  
 852          // Grade student1 only.
 853          $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
 854  
 855          // Mock task, asserting that line item creation takes place via a mock grade service object.
 856          $mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class);
 857          $mockgradeservice->method('putGrade')->willReturnCallback(function() {
 858              return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200];
 859          });
 860          $mockgradeservice->expects($this->once())
 861              ->method('findOrCreateLineitem');
 862          $mockgradeservice->expects($this->once())
 863              ->method('putGrade')
 864              ->with($this->isInstanceOf(LtiGrade::class), $this->isInstanceOf(LtiLineitem::class));
 865          $mocktask = $this->getMockBuilder(sync_tool_grades::class)
 866              ->onlyMethods(['get_ags'])
 867              ->getMock();
 868          $mocktask->method('get_ags')->willReturn($mockgradeservice);
 869  
 870          // Sync and verify that only student1's grade is sent.
 871          ob_start();
 872          $mocktask->set_custom_data($resource);
 873          $mocktask->execute();
 874          $ob = ob_get_contents();
 875          ob_end_clean();
 876          $expectedtraces = [
 877              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 878              "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
 879              "'$course->id'.",
 880              "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
 881              "'$resource->id' and the course '$course->id' was sent.",
 882              "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
 883              "'$course->id'.",
 884              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 885              "Processed 3 users; sent 1 grades."
 886          ];
 887          foreach ($expectedtraces as $expectedtrace) {
 888              $this->assertStringContainsString($expectedtrace, $ob);
 889          }
 890      }
 891  
 892      /**
 893       * Test the sync for a course context when only the lineitems URL is provided and when line item creation/query is expected.
 894       *
 895       * @covers ::execute
 896       */
 897      public function test_sync_grades_none_or_many_lineitems_course_context() {
 898          $this->resetAfterTest();
 899  
 900          [$course, $tool1, $tool2, $resource] = $this->create_test_environment();
 901          $launchservice = $this->get_tool_launch_service();
 902  
 903          // The launches omit the 'lineitem' claim, meaning the item may have none (or many) line items.
 904          $agsclaim = [
 905              "scope" => [
 906                  "https://purl.imsglobal.org/spec/lti-ags/scope/score",
 907                  "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
 908              ],
 909              "lineitems" => "https://platform.example.com/10/lineitems"
 910          ];
 911  
 912          // Launch the resource for an instructor which will create the domain objects needed for service calls.
 913          $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null,
 914              $agsclaim);
 915          $instructoruser = $this->getDataGenerator()->create_user();
 916          [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
 917  
 918          // Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
 919          $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
 920              'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
 921  
 922          $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim);
 923          $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim);
 924          $student1user = $this->getDataGenerator()->create_user();
 925          $student2user = $this->getDataGenerator()->create_user();
 926          [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
 927          [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch);
 928  
 929          // Grade student1 only.
 930          $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
 931  
 932          // Mock task, asserting that line item creation takes place via a mock grade service object.
 933          $mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class);
 934          $mockgradeservice->method('putGrade')->willReturnCallback(function() {
 935              return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200];
 936          });
 937          $mockgradeservice->expects($this->once())
 938              ->method('findOrCreateLineitem');
 939          $mockgradeservice->expects($this->once())
 940              ->method('putGrade')
 941              ->with($this->isInstanceOf(LtiGrade::class), $this->isInstanceOf(LtiLineitem::class));
 942          $mocktask = $this->getMockBuilder(sync_tool_grades::class)
 943              ->onlyMethods(['get_ags'])
 944              ->getMock();
 945          $mocktask->method('get_ags')->willReturn($mockgradeservice);
 946  
 947          // Sync and verify that only student1's grade is sent.
 948          ob_start();
 949          $mocktask->set_custom_data($resource);
 950          $mocktask->execute();
 951          $ob = ob_get_contents();
 952          ob_end_clean();
 953          $expectedtraces = [
 954              "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
 955              "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
 956              "'$course->id'.",
 957              "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
 958              "'$resource->id' and the course '$course->id' was sent.",
 959              "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
 960              "'$course->id'.",
 961              "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
 962              "Processed 3 users; sent 1 grades."
 963          ];
 964          foreach ($expectedtraces as $expectedtrace) {
 965              $this->assertStringContainsString($expectedtrace, $ob);
 966          }
 967      }
 968  }