/** * Reset contents of all database tables to initial values, reset caches, etc. * * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care! * * @static * @param bool $detectchanges * true - changes in global state and database are reported as errors * false - no errors reported * null - only critical problems are reported as errors * @return void */ public static function reset_all_data($detectchanges = false) { global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION; // Stop any message redirection. phpunit_util::stop_message_redirection(); // Stop any message redirection. phpunit_util::stop_phpmailer_redirection(); // Stop any message redirection. phpunit_util::stop_event_redirection(); // We used to call gc_collect_cycles here to ensure desctructors were called between tests. // This accounted for 25% of the total time running phpunit - so we removed it. // Show any unhandled debugging messages, the runbare() could already reset it. self::display_debugging_messages(); self::reset_debugging(); // reset global $DB in case somebody mocked it $DB = self::get_global_backup('DB'); if ($DB->is_transaction_started()) { // we can not reset inside transaction $DB->force_transaction_rollback(); } $resetdb = self::reset_database(); $warnings = array(); if ($detectchanges === true) { if ($resetdb) { $warnings[] = 'Warning: unexpected database modification, resetting DB state'; } $oldcfg = self::get_global_backup('CFG'); $oldsite = self::get_global_backup('SITE'); foreach ($CFG as $k => $v) { if (!property_exists($oldcfg, $k)) { $warnings[] = 'Warning: unexpected new $CFG->' . $k . ' value'; } else { if ($oldcfg->{$k} !== $CFG->{$k}) { $warnings[] = 'Warning: unexpected change of $CFG->' . $k . ' value'; } } unset($oldcfg->{$k}); } if ($oldcfg) { foreach ($oldcfg as $k => $v) { $warnings[] = 'Warning: unexpected removal of $CFG->' . $k; } } if ($USER->id != 0) { $warnings[] = 'Warning: unexpected change of $USER'; } if ($COURSE->id != $oldsite->id) { $warnings[] = 'Warning: unexpected change of $COURSE'; } } if (ini_get('max_execution_time') != 0) { // This is special warning for all resets because we do not want any // libraries to mess with timeouts unintentionally. // Our PHPUnit integration is not supposed to change it either. if ($detectchanges !== false) { $warnings[] = 'Warning: max_execution_time was changed to ' . ini_get('max_execution_time'); } set_time_limit(0); } // restore original globals $_SERVER = self::get_global_backup('_SERVER'); $CFG = self::get_global_backup('CFG'); $SITE = self::get_global_backup('SITE'); $_GET = array(); $_POST = array(); $_FILES = array(); $_REQUEST = array(); $COURSE = $SITE; // reinitialise following globals $OUTPUT = new bootstrap_renderer(); $PAGE = new moodle_page(); $FULLME = null; $ME = null; $SCRIPT = null; // Empty sessison and set fresh new not-logged-in user. \core\session\manager::init_empty_session(); // reset all static caches \core\event\manager::phpunit_reset(); accesslib_clear_all_caches(true); get_string_manager()->reset_caches(true); reset_text_filters_cache(true); events_get_handlers('reset'); core_text::reset_caches(); get_message_processors(false, true); filter_manager::reset_caches(); // Reset internal users. core_user::reset_internal_users(); //TODO MDL-25290: add more resets here and probably refactor them to new core function // Reset course and module caches. if (class_exists('format_base')) { // If file containing class is not loaded, there is no cache there anyway. format_base::reset_course_cache(0); } get_fast_modinfo(0, 0, true); // Reset other singletons. if (class_exists('core_plugin_manager')) { core_plugin_manager::reset_caches(true); } if (class_exists('\\core\\update\\checker')) { \core\update\checker::reset_caches(true); } if (class_exists('\\core\\update\\deployer')) { \core\update\deployer::reset_caches(true); } // purge dataroot directory self::reset_dataroot(); // restore original config once more in case resetting of caches changed CFG $CFG = self::get_global_backup('CFG'); // inform data generator self::get_data_generator()->reset(); // fix PHP settings error_reporting($CFG->debug); // verify db writes just in case something goes wrong in reset if (self::$lastdbwrites != $DB->perf_get_writes()) { error_log('Unexpected DB writes in phpunit_util::reset_all_data()'); self::$lastdbwrites = $DB->perf_get_writes(); } if ($warnings) { $warnings = implode("\n", $warnings); trigger_error($warnings, E_USER_WARNING); } }
/** * Remove all event handlers and queued events * * @category event * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results' */ function events_uninstall($component) { $cachedhandlers = events_get_cached($component); events_cleanup($component, $cachedhandlers); events_get_handlers('reset'); }
public function test_legacy() { global $DB; $this->resetAfterTest(true); $observers = array(array('eventname' => '\\core_tests\\event\\unittest_executed', 'callback' => '\\core_tests\\event\\unittest_observer::observe_one'), array('eventname' => '*', 'callback' => '\\core_tests\\event\\unittest_observer::observe_all', 'includefile' => null, 'internal' => 1, 'priority' => 9999)); $DB->delete_records('log', array()); events_update_definition('unittest'); $DB->delete_records_select('events_handlers', "component <> 'unittest'"); events_get_handlers('reset'); $this->assertEquals(3, $DB->count_records('events_handlers')); set_config('loglifetime', 60 * 60 * 24 * 5); \core\event\manager::phpunit_replace_observers($observers); \core_tests\event\unittest_observer::reset(); $event1 = \core_tests\event\unittest_executed::create(array('courseid' => 1, 'context' => \context_system::instance(), 'other' => array('sample' => 5, 'xx' => 10))); $event1->trigger(); $event2 = \core_tests\event\unittest_executed::create(array('courseid' => 2, 'context' => \context_system::instance(), 'other' => array('sample' => 6, 'xx' => 11))); $event2->nest = true; $event2->trigger(); $this->assertSame(array('observe_all-1', 'observe_one-1', 'legacy_handler-1', 'observe_all-nesting-2', 'legacy_handler-3', 'observe_one-2', 'observe_all-3', 'observe_one-3', 'legacy_handler-2'), \core_tests\event\unittest_observer::$info); $this->assertSame($event1, \core_tests\event\unittest_observer::$event[0]); $this->assertSame($event1, \core_tests\event\unittest_observer::$event[1]); $this->assertSame(array(1, 5), \core_tests\event\unittest_observer::$event[2]); $logs = $DB->get_records('log', array(), 'id ASC'); $this->assertCount(3, $logs); $log = array_shift($logs); $this->assertEquals(1, $log->course); $this->assertSame('core_unittest', $log->module); $this->assertSame('view', $log->action); $log = array_shift($logs); $this->assertEquals(2, $log->course); $this->assertSame('core_unittest', $log->module); $this->assertSame('view', $log->action); $log = array_shift($logs); $this->assertEquals(3, $log->course); $this->assertSame('core_unittest', $log->module); $this->assertSame('view', $log->action); }
/** * Do not call directly, this is intended to be used from new event base only. * * @private * @deprecated since Moodle 3.1 * @param string $eventname name of the event * @param mixed $eventdata event data object * @return int number of failed events */ function events_trigger_legacy($eventname, $eventdata) { global $CFG, $USER, $DB; $failedcount = 0; // number of failed events. // pull out all registered event handlers if ($handlers = events_get_handlers($eventname)) { foreach ($handlers as $handler) { $errormessage = ''; if ($handler->schedule === 'instant') { if ($handler->status) { //check if previous pending events processed if (!$DB->record_exists('events_queue_handlers', array('handlerid' => $handler->id))) { // ok, queue is empty, lets reset the status back to 0 == ok $handler->status = 0; $DB->set_field('events_handlers', 'status', 0, array('id' => $handler->id)); // reset static handler cache events_get_handlers('reset'); } } // dispatch the event only if instant schedule and status ok if ($handler->status or !$handler->internal and $DB->is_transaction_started()) { // increment the error status counter $handler->status++; $DB->set_field('events_handlers', 'status', $handler->status, array('id' => $handler->id)); // reset static handler cache events_get_handlers('reset'); } else { $errormessage = 'Unknown error'; $result = events_dispatch($handler, $eventdata, $errormessage); if ($result === true) { // everything is fine - event dispatched continue; } else { if ($result === false) { // retry later - set error count to 1 == send next instant into cron queue $DB->set_field('events_handlers', 'status', 1, array('id' => $handler->id)); // reset static handler cache events_get_handlers('reset'); } else { // internal problem - ignore the event completely $failedcount++; continue; } } } // update the failed counter $failedcount++; } else { if ($handler->schedule === 'cron') { //ok - use queueing of events only } else { // unknown schedule - ignore event completely debugging("Unknown handler schedule type: {$handler->schedule}"); $failedcount++; continue; } } // if even type is not instant, or dispatch asked for retry, queue it $event = new stdClass(); $event->userid = $USER->id; $event->eventdata = base64_encode(serialize($eventdata)); $event->timecreated = time(); if (debugging()) { $dump = ''; $callers = debug_backtrace(); foreach ($callers as $caller) { if (!isset($caller['line'])) { $caller['line'] = '?'; } if (!isset($caller['file'])) { $caller['file'] = '?'; } $dump .= 'line ' . $caller['line'] . ' of ' . substr($caller['file'], strlen($CFG->dirroot) + 1); if (isset($caller['function'])) { $dump .= ': call to '; if (isset($caller['class'])) { $dump .= $caller['class'] . $caller['type']; } $dump .= $caller['function'] . '()'; } $dump .= "\n"; } $event->stackdump = $dump; } else { $event->stackdump = ''; } $event->id = $DB->insert_record('events_queue', $event); events_queue_handler($handler, $event, $errormessage); } } else { // No handler found for this event name - this is ok! } return $failedcount; }
public function test_legacy() { global $DB, $CFG; $this->resetAfterTest(true); $observers = array(array('eventname' => '\\core_tests\\event\\unittest_executed', 'callback' => '\\core_tests\\event\\unittest_observer::observe_one'), array('eventname' => '*', 'callback' => '\\core_tests\\event\\unittest_observer::observe_all', 'includefile' => null, 'internal' => 1, 'priority' => 9999)); $DB->delete_records('log', array()); events_update_definition('unittest'); $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER); $DB->delete_records_select('events_handlers', "component <> 'unittest'"); events_get_handlers('reset'); $this->assertEquals(3, $DB->count_records('events_handlers')); set_config('loglifetime', 60 * 60 * 24 * 5); \core\event\manager::phpunit_replace_observers($observers); \core_tests\event\unittest_observer::reset(); $event1 = \core_tests\event\unittest_executed::create(array('context' => \context_system::instance(), 'other' => array('sample' => 5, 'xx' => 10))); $event1->trigger(); $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER); $event2 = \core_tests\event\unittest_executed::create(array('context' => \context_system::instance(), 'other' => array('sample' => 6, 'xx' => 11))); $event2->nest = true; $event2->trigger(); $this->assertDebuggingCalledCount(2, array(self::DEBUGGING_MSG, self::DEBUGGING_MSG), array(DEBUG_DEVELOPER, DEBUG_DEVELOPER)); $this->assertSame(array('observe_all-5', 'observe_one-5', 'legacy_handler-0', 'observe_all-nesting-6', 'legacy_handler-0', 'observe_one-6', 'observe_all-666', 'observe_one-666', 'legacy_handler-0'), \core_tests\event\unittest_observer::$info); $this->assertSame($event1, \core_tests\event\unittest_observer::$event[0]); $this->assertSame($event1, \core_tests\event\unittest_observer::$event[1]); $this->assertSame(array(0, 5), \core_tests\event\unittest_observer::$event[2]); $logs = $DB->get_records('log', array(), 'id ASC'); $this->assertCount(0, $logs); }
/** * Reset contents of all database tables to initial values, reset caches, etc. * * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care! * * @static * @param bool $logchanges log changes in global state and database in error log * @return void */ public static function reset_all_data($logchanges = false) { global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $GROUPLIB_CACHE; // reset global $DB in case somebody mocked it $DB = self::get_global_backup('DB'); if ($DB->is_transaction_started()) { // we can not reset inside transaction $DB->force_transaction_rollback(); } $resetdb = self::reset_database(); $warnings = array(); if ($logchanges) { if ($resetdb) { $warnings[] = 'Warning: unexpected database modification, resetting DB state'; } $oldcfg = self::get_global_backup('CFG'); $oldsite = self::get_global_backup('SITE'); foreach ($CFG as $k => $v) { if (!property_exists($oldcfg, $k)) { $warnings[] = 'Warning: unexpected new $CFG->' . $k . ' value'; } else { if ($oldcfg->{$k} !== $CFG->{$k}) { $warnings[] = 'Warning: unexpected change of $CFG->' . $k . ' value'; } } unset($oldcfg->{$k}); } if ($oldcfg) { foreach ($oldcfg as $k => $v) { $warnings[] = 'Warning: unexpected removal of $CFG->' . $k; } } if ($USER->id != 0) { $warnings[] = 'Warning: unexpected change of $USER'; } if ($COURSE->id != $oldsite->id) { $warnings[] = 'Warning: unexpected change of $COURSE'; } } // restore original globals $_SERVER = self::get_global_backup('_SERVER'); $CFG = self::get_global_backup('CFG'); $SITE = self::get_global_backup('SITE'); $COURSE = $SITE; // reinitialise following globals $OUTPUT = new bootstrap_renderer(); $PAGE = new moodle_page(); $FULLME = null; $ME = null; $SCRIPT = null; $SESSION = new stdClass(); $_SESSION['SESSION'] =& $SESSION; // set fresh new not-logged-in user $user = new stdClass(); $user->id = 0; $user->mnethostid = $CFG->mnet_localhost_id; session_set_user($user); // reset all static caches accesslib_clear_all_caches(true); get_string_manager()->reset_caches(); events_get_handlers('reset'); textlib::reset_caches(); if (class_exists('repository')) { repository::reset_caches(); } $GROUPLIB_CACHE = null; //TODO MDL-25290: add more resets here and probably refactor them to new core function // purge dataroot directory self::reset_dataroot(); // restore original config once more in case resetting of caches changed CFG $CFG = self::get_global_backup('CFG'); // inform data generator self::get_data_generator()->reset(); // fix PHP settings error_reporting($CFG->debug); // verify db writes just in case something goes wrong in reset if (self::$lastdbwrites != $DB->perf_get_writes()) { error_log('Unexpected DB writes in phpunit_util::reset_all_data()'); self::$lastdbwrites = $DB->perf_get_writes(); } if ($warnings) { $warnings = implode("\n", $warnings); trigger_error($warnings, E_USER_WARNING); } }
/** * Reset contents of all database tables to initial values, reset caches, etc. * * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care! * * @static * @param bool $logchanges log changes in global state and database in error log * @return void */ public static function reset_all_data($logchanges = false) { global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION; // Stop any message redirection. phpunit_util::stop_message_redirection(); // Release memory and indirectly call destroy() methods to release resource handles, etc. gc_collect_cycles(); // Show any unhandled debugging messages, the runbare() could already reset it. self::display_debugging_messages(); self::reset_debugging(); // reset global $DB in case somebody mocked it $DB = self::get_global_backup('DB'); if ($DB->is_transaction_started()) { // we can not reset inside transaction $DB->force_transaction_rollback(); } $resetdb = self::reset_database(); $warnings = array(); if ($logchanges) { if ($resetdb) { $warnings[] = 'Warning: unexpected database modification, resetting DB state'; } $oldcfg = self::get_global_backup('CFG'); $oldsite = self::get_global_backup('SITE'); foreach ($CFG as $k => $v) { if (!property_exists($oldcfg, $k)) { $warnings[] = 'Warning: unexpected new $CFG->' . $k . ' value'; } else { if ($oldcfg->{$k} !== $CFG->{$k}) { $warnings[] = 'Warning: unexpected change of $CFG->' . $k . ' value'; } } unset($oldcfg->{$k}); } if ($oldcfg) { foreach ($oldcfg as $k => $v) { $warnings[] = 'Warning: unexpected removal of $CFG->' . $k; } } if ($USER->id != 0) { $warnings[] = 'Warning: unexpected change of $USER'; } if ($COURSE->id != $oldsite->id) { $warnings[] = 'Warning: unexpected change of $COURSE'; } } // restore original globals $_SERVER = self::get_global_backup('_SERVER'); $CFG = self::get_global_backup('CFG'); $SITE = self::get_global_backup('SITE'); $COURSE = $SITE; // reinitialise following globals $OUTPUT = new bootstrap_renderer(); $PAGE = new moodle_page(); $FULLME = null; $ME = null; $SCRIPT = null; $SESSION = new stdClass(); $_SESSION['SESSION'] =& $SESSION; // set fresh new not-logged-in user $user = new stdClass(); $user->id = 0; $user->mnethostid = $CFG->mnet_localhost_id; session_set_user($user); // reset all static caches accesslib_clear_all_caches(true); get_string_manager()->reset_caches(true); reset_text_filters_cache(true); events_get_handlers('reset'); textlib::reset_caches(); if (class_exists('repository')) { repository::reset_caches(); } filter_manager::reset_caches(); //TODO MDL-25290: add more resets here and probably refactor them to new core function // Reset course and module caches. if (class_exists('format_base')) { // If file containing class is not loaded, there is no cache there anyway. format_base::reset_course_cache(0); } get_fast_modinfo(0, 0, true); // Reset other singletons. if (class_exists('plugin_manager')) { plugin_manager::reset_caches(true); } if (class_exists('available_update_checker')) { available_update_checker::reset_caches(true); } if (class_exists('available_update_deployer')) { available_update_deployer::reset_caches(true); } // purge dataroot directory self::reset_dataroot(); // restore original config once more in case resetting of caches changed CFG $CFG = self::get_global_backup('CFG'); // inform data generator self::get_data_generator()->reset(); // fix PHP settings error_reporting($CFG->debug); // verify db writes just in case something goes wrong in reset if (self::$lastdbwrites != $DB->perf_get_writes()) { error_log('Unexpected DB writes in phpunit_util::reset_all_data()'); self::$lastdbwrites = $DB->perf_get_writes(); } if ($warnings) { $warnings = implode("\n", $warnings); trigger_error($warnings, E_USER_WARNING); } }
/** * Reset contents of all database tables to initial values, reset caches, etc. * * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care! * * @static * @param bool $logchanges log changes in global state and database in error log * @return void */ public static function reset_all_data($logchanges = false) { global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $GROUPLIB_CACHE; // Release memory and indirectly call destroy() methods to release resource handles, etc. gc_collect_cycles(); // reset global $DB in case somebody mocked it $DB = self::get_global_backup('DB'); if ($DB->is_transaction_started()) { // we can not reset inside transaction $DB->force_transaction_rollback(); } $resetdb = self::reset_database(); $warnings = array(); if ($logchanges) { if ($resetdb) { $warnings[] = 'Warning: unexpected database modification, resetting DB state'; } $oldcfg = self::get_global_backup('CFG'); $oldsite = self::get_global_backup('SITE'); foreach($CFG as $k=>$v) { if (!property_exists($oldcfg, $k)) { $warnings[] = 'Warning: unexpected new $CFG->'.$k.' value'; } else if ($oldcfg->$k !== $CFG->$k) { $warnings[] = 'Warning: unexpected change of $CFG->'.$k.' value'; } unset($oldcfg->$k); } if ($oldcfg) { foreach($oldcfg as $k=>$v) { $warnings[] = 'Warning: unexpected removal of $CFG->'.$k; } } if ($USER->id != 0) { $warnings[] = 'Warning: unexpected change of $USER'; } if ($COURSE->id != $oldsite->id) { $warnings[] = 'Warning: unexpected change of $COURSE'; } } if (ini_get('max_execution_time') != 0) { // This is special warning for all resets because we do not want any // libraries to mess with timeouts unintentionally. // Our PHPUnit integration is not supposed to change it either. // TODO: MDL-38912 uncomment and fix all + somehow resolve timeouts in failed tests. //$warnings[] = 'Warning: max_execution_time was changed.'; set_time_limit(0); } // restore original globals $_SERVER = self::get_global_backup('_SERVER'); $CFG = self::get_global_backup('CFG'); $SITE = self::get_global_backup('SITE'); $COURSE = $SITE; // reinitialise following globals $OUTPUT = new bootstrap_renderer(); $PAGE = new moodle_page(); $FULLME = null; $ME = null; $SCRIPT = null; $SESSION = new stdClass(); $_SESSION['SESSION'] =& $SESSION; // set fresh new not-logged-in user $user = new stdClass(); $user->id = 0; $user->mnethostid = $CFG->mnet_localhost_id; session_set_user($user); // reset all static caches accesslib_clear_all_caches(true); get_string_manager()->reset_caches(); events_get_handlers('reset'); textlib::reset_caches(); if (class_exists('repository')) { repository::reset_caches(); } $GROUPLIB_CACHE = null; //TODO MDL-25290: add more resets here and probably refactor them to new core function // Reset course and module caches. $reset = 'reset'; get_fast_modinfo($reset); // Reset other singletons. if (class_exists('plugin_manager')) { plugin_manager::reset_caches(true); } if (class_exists('available_update_checker')) { available_update_checker::reset_caches(true); } // purge dataroot directory self::reset_dataroot(); // restore original config once more in case resetting of caches changed CFG $CFG = self::get_global_backup('CFG'); // inform data generator self::get_data_generator()->reset(); // fix PHP settings error_reporting($CFG->debug); // verify db writes just in case something goes wrong in reset if (self::$lastdbwrites != $DB->perf_get_writes()) { error_log('Unexpected DB writes in phpunit_util::reset_all_data()'); self::$lastdbwrites = $DB->perf_get_writes(); } if ($warnings) { $warnings = implode("\n", $warnings); trigger_error($warnings, E_USER_WARNING); } }
/** * Function to call all eventhandlers when triggering an event * @param eventname - name of the event * @param eventdata - event data object (without magic quotes) * @return number of failed events * * PUBLIC */ function events_trigger($eventname, $eventdata) { global $CFG, $USER; $failedcount = 0; // number of failed events. $event = false; // pull out all registered event handlers if ($handlers = events_get_handlers($eventname)) { foreach ($handlers as $handler) { $errormessage = ''; if ($handler->schedule == 'instant') { if ($handler->status) { //check if previous pending events processed if (!record_exists('events_queue_handlers', 'handlerid', $handler->id)) { // ok, queue is empty, lets reset the status back to 0 == ok $handler->status = 0; set_field('events_handlers', 'status', 0, 'id', $handler->id); // reset static handler cache events_get_handlers('reset'); } } // dispatch the event only if instant schedule and status ok if (!$handler->status) { $errormessage = 'Unknown error'; if (events_dispatch($handler, $eventdata, $errormessage)) { continue; } // set error count to 1 == send next instant into cron queue set_field('events_handlers', 'status', 1, 'id', $handler->id); // reset static handler cache events_get_handlers('reset'); } else { // increment the error status counter $handler->status++; set_field('events_handlers', 'status', $handler->status, 'id', $handler->id); // reset static handler cache events_get_handlers('reset'); } // update the failed counter $failedcount++; } else { if ($handler->schedule == 'cron') { //ok - use queuing of events only } else { // unknown schedule - fallback to cron type debugging("Unknown handler schedule type: {$handler->schedule}"); } } // if even type is not instant, or dispatch failed, queue it if ($event === false) { $event = new object(); $event->userid = $USER->id; $event->eventdata = addslashes(serialize($eventdata)); $event->timecreated = time(); if (debugging()) { $dump = ''; $callers = debug_backtrace(); foreach ($callers as $caller) { $dump .= 'line ' . $caller['line'] . ' of ' . substr($caller['file'], strlen($CFG->dirroot) + 1); if (isset($caller['function'])) { $dump .= ': call to '; if (isset($caller['class'])) { $dump .= $caller['class'] . $caller['type']; } $dump .= $caller['function'] . '()'; } $dump .= "\n"; } $event->stackdump = addslashes($dump); } else { $event->stackdump = ''; } $event->id = insert_record('events_queue', $event); } events_queue_handler($handler, $event, $errormessage); } } else { //debugging("No handler found for event: $eventname"); } return $failedcount; }