From 727e23505eaf0e0ddd28863d799c39fb4b6882d6 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Tue, 26 May 2026 10:15:38 +1000 Subject: [PATCH 01/43] cache and restore db --- .github/actions/matrix/matrix_includes.yml | 38 +- .github/actions/parse-version/script.php | 27 +- .github/patches/401-403-phpunit-restore.patch | 499 +++++++++++++++++ .github/patches/404-500-phpunit-restore.patch | 515 ++++++++++++++++++ .github/patches/501-501-phpunit-restore.patch | 509 +++++++++++++++++ .github/patches/502-999-phpunit-restore.patch | 499 +++++++++++++++++ .github/patches/README.md | 43 ++ .github/plugin/setup/action.yml | 154 +++++- .github/templates/moodle-config-template.php | 36 ++ .github/workflows/build-dockerfiles.yml | 88 --- .github/workflows/ci.yml | 35 +- .github/workflows/update-snapshots.yml | 163 ++++-- 12 files changed, 2418 insertions(+), 188 deletions(-) create mode 100644 .github/patches/401-403-phpunit-restore.patch create mode 100644 .github/patches/404-500-phpunit-restore.patch create mode 100644 .github/patches/501-501-phpunit-restore.patch create mode 100644 .github/patches/502-999-phpunit-restore.patch create mode 100644 .github/patches/README.md create mode 100644 .github/templates/moodle-config-template.php delete mode 100644 .github/workflows/build-dockerfiles.yml diff --git a/.github/actions/matrix/matrix_includes.yml b/.github/actions/matrix/matrix_includes.yml index 2639fe8f..a4fba8bc 100644 --- a/.github/actions/matrix/matrix_includes.yml +++ b/.github/actions/matrix/matrix_includes.yml @@ -1,26 +1,26 @@ include: # Always include main (issue #18) - disable-able by config by setting 'disable_master' to true - - {moodle-branch: 'main', php: '8.4', node: '22', mariadb-ver: '10.11', pgsql-ver: '16', database: 'pgsql'} + - {moodle-branch: 'main', php: '8.4', node: '22', pgsql-ver: '16', database: 'pgsql'} # Test all variants of 5.2 - - { moodle-branch: 'MOODLE_502_STABLE', php: '8.4', node: '22', mariadb-ver: '10.11', pgsql-ver: '16', database: 'mariadb' } - - { moodle-branch: 'MOODLE_502_STABLE', php: '8.4', node: '22', mariadb-ver: '10.11', pgsql-ver: '16', database: 'pgsql' } - - { moodle-branch: 'MOODLE_502_STABLE', php: '8.3', node: '22', mariadb-ver: '10.11', pgsql-ver: '16', database: 'mariadb' } - - { moodle-branch: 'MOODLE_502_STABLE', php: '8.3', node: '22', mariadb-ver: '10.11', pgsql-ver: '16', database: 'pgsql' } + - { moodle-branch: 'MOODLE_502_STABLE', php: '8.4', node: '22', mariadb-ver: '10.11', database: 'mariadb' } + - { moodle-branch: 'MOODLE_502_STABLE', php: '8.4', node: '22', pgsql-ver: '16', database: 'pgsql' } + - { moodle-branch: 'MOODLE_502_STABLE', php: '8.3', node: '22', mariadb-ver: '10.11', database: 'mariadb' } + - { moodle-branch: 'MOODLE_502_STABLE', php: '8.3', node: '22', pgsql-ver: '16', database: 'pgsql' } # Test all variants of 5.1 - - { moodle-branch: 'MOODLE_501_STABLE', php: '8.4', node: '22', mariadb-ver: '10.11', pgsql-ver: '15', database: 'mariadb' } - - { moodle-branch: 'MOODLE_501_STABLE', php: '8.4', node: '22', mariadb-ver: '10.11', pgsql-ver: '15', database: 'pgsql' } - - { moodle-branch: 'MOODLE_501_STABLE', php: '8.2', node: '22', mariadb-ver: '10.11', pgsql-ver: '15', database: 'mariadb' } - - { moodle-branch: 'MOODLE_501_STABLE', php: '8.2', node: '22', mariadb-ver: '10.11', pgsql-ver: '15', database: 'pgsql' } + - { moodle-branch: 'MOODLE_501_STABLE', php: '8.4', node: '22', mariadb-ver: '10.11', database: 'mariadb' } + - { moodle-branch: 'MOODLE_501_STABLE', php: '8.4', node: '22', pgsql-ver: '15', database: 'pgsql' } + - { moodle-branch: 'MOODLE_501_STABLE', php: '8.2', node: '22', mariadb-ver: '10.11', database: 'mariadb' } + - { moodle-branch: 'MOODLE_501_STABLE', php: '8.2', node: '22', pgsql-ver: '15', database: 'pgsql' } # Test limited variants of 5.0 - - { moodle-branch: 'MOODLE_500_STABLE', php: '8.4', node: '22', mariadb-ver: '10.11', pgsql-ver: '14', database: 'mariadb' } - - { moodle-branch: 'MOODLE_500_STABLE', php: '8.4', node: '22', mariadb-ver: '10.11', pgsql-ver: '14', database: 'pgsql' } + - { moodle-branch: 'MOODLE_500_STABLE', php: '8.4', node: '22', mariadb-ver: '10.11', database: 'mariadb' } + - { moodle-branch: 'MOODLE_500_STABLE', php: '8.4', node: '22', pgsql-ver: '14', database: 'pgsql' } # Test all variants of 4.5. (LTS). - - { moodle-branch: 'MOODLE_405_STABLE', php: '8.3', node: '22', mariadb-ver: '10.6', pgsql-ver: '13', database: 'mariadb' } - - { moodle-branch: 'MOODLE_405_STABLE', php: '8.3', node: '22', mariadb-ver: '10.6', pgsql-ver: '13', database: 'pgsql' } - - { moodle-branch: 'MOODLE_405_STABLE', php: '8.1', node: '22', mariadb-ver: '10.6', pgsql-ver: '13', database: 'mariadb' } - - { moodle-branch: 'MOODLE_405_STABLE', php: '8.1', node: '22', mariadb-ver: '10.6', pgsql-ver: '13', database: 'pgsql' } + - { moodle-branch: 'MOODLE_405_STABLE', php: '8.3', node: '22', mariadb-ver: '10.6', database: 'mariadb' } + - { moodle-branch: 'MOODLE_405_STABLE', php: '8.3', node: '22', pgsql-ver: '13', database: 'pgsql' } + - { moodle-branch: 'MOODLE_405_STABLE', php: '8.1', node: '22', mariadb-ver: '10.6', database: 'mariadb' } + - { moodle-branch: 'MOODLE_405_STABLE', php: '8.1', node: '22', pgsql-ver: '13', database: 'pgsql' } # Test all variants of 4.1 (LTS). - - { moodle-branch: 'MOODLE_401_STABLE', php: '7.4', node: '22', mariadb-ver: '10.5', pgsql-ver: '12', database: 'mariadb' } - - { moodle-branch: 'MOODLE_401_STABLE', php: '7.4', node: '22', mariadb-ver: '10.5', pgsql-ver: '12', database: 'pgsql' } - - { moodle-branch: 'MOODLE_401_STABLE', php: '8.0', node: '22', mariadb-ver: '10.5', pgsql-ver: '12', database: 'mariadb' } - - { moodle-branch: 'MOODLE_401_STABLE', php: '8.0', node: '22', mariadb-ver: '10.5', pgsql-ver: '12', database: 'pgsql' } + - { moodle-branch: 'MOODLE_401_STABLE', php: '7.4', node: '22', mariadb-ver: '10.5', database: 'mariadb' } + - { moodle-branch: 'MOODLE_401_STABLE', php: '7.4', node: '22', pgsql-ver: '12', database: 'pgsql' } + - { moodle-branch: 'MOODLE_401_STABLE', php: '8.0', node: '22', mariadb-ver: '10.5', database: 'mariadb' } + - { moodle-branch: 'MOODLE_401_STABLE', php: '8.0', node: '22', pgsql-ver: '12', database: 'pgsql' } diff --git a/.github/actions/parse-version/script.php b/.github/actions/parse-version/script.php index e4875104..6226dfc8 100644 --- a/.github/actions/parse-version/script.php +++ b/.github/actions/parse-version/script.php @@ -248,22 +248,35 @@ function container_image_name($moodle_branch, $php) { usort($phpVersions, 'version_compare'); $highestPhp = end($phpVersions); output('highest_php', $highestPhp); - // Find the container and node for this combo + // Find the container and node for this combo. + // Prefer the pgsql entry when present so latest_pgsql_ver is reliably populated. $foundNode = ''; + $foundPgsqlVer = ''; foreach ($finalMatrix as $entry) { if ($entry['moodle-branch'] === $highestMoodleBranch && $entry['php'] === $highestPhp) { output('latest_container', $entry['container']); - if (isset($entry['pgsql-ver'])) { - output('latest_pgsql_ver', $entry['pgsql-ver']); - } else { - output('latest_pgsql_ver', ''); - } if (isset($entry['node'])) { $foundNode = $entry['node']; } - break; + if (($entry['database'] ?? '') === 'pgsql' && isset($entry['pgsql-ver'])) { + $foundPgsqlVer = $entry['pgsql-ver']; + break; + } + } + } + + // If no exact highest-php pgsql entry was found, fallback to any pgsql for the highest branch. + if ($foundPgsqlVer === '') { + foreach ($finalMatrix as $entry) { + if ($entry['moodle-branch'] === $highestMoodleBranch && ($entry['database'] ?? '') === 'pgsql' && isset($entry['pgsql-ver'])) { + $foundPgsqlVer = $entry['pgsql-ver']; + break; + } } } + + output('latest_pgsql_ver', $foundPgsqlVer); + // If node version not found above, fallback to highest node version if (!$foundNode && !empty($nodeVersions)) { usort($nodeVersions, 'version_compare'); diff --git a/.github/patches/401-403-phpunit-restore.patch b/.github/patches/401-403-phpunit-restore.patch new file mode 100644 index 00000000..95a079ca --- /dev/null +++ b/.github/patches/401-403-phpunit-restore.patch @@ -0,0 +1,499 @@ +From 6825a9454816483b474a436dcd3bd9536f6ddff4 Mon Sep 17 00:00:00 2001 +From: Abhinav Gandham +Date: Wed, 13 May 2026 15:33:48 +1000 +Subject: [PATCH] MDL-88495 unit tests: Implmented test site upgrade, snapshot, + and restore functionality." + +--- + admin/tool/phpunit/cli/util.php | 30 ++- + lib/phpunit/bootstraplib.php | 10 +- + lib/phpunit/classes/util.php | 353 ++++++++++++++++++++++++++++++++ + lib/testing/classes/util.php | 11 + + 4 files changed, 397 insertions(+), 7 deletions(-) + +diff --git a/admin/tool/phpunit/cli/util.php b/admin/tool/phpunit/cli/util.php +index 1d176900fdf..2fd70820601 100644 +--- a/admin/tool/phpunit/cli/util.php ++++ b/admin/tool/phpunit/cli/util.php +@@ -47,6 +47,9 @@ list($options, $unrecognized) = cli_get_params( + 'buildcomponentconfigs' => false, + 'diag' => false, + 'run' => false, ++ 'upgrade' => false, ++ 'snapshot' => '', ++ 'restore' => '', + 'help' => false, + ), + array( +@@ -61,7 +64,7 @@ if (!file_exists(__DIR__.'/../../../../vendor/phpunit/phpunit/composer.json') || + phpunit_bootstrap_error(PHPUNIT_EXITCODE_PHPUNITMISSING); + } + +-if ($options['install'] or $options['drop']) { ++if ($options['install'] || $options['drop'] || $options['upgrade']) { + define('CACHE_DISABLE_ALL', true); + } + +@@ -102,8 +105,16 @@ $drop = $options['drop']; + $install = $options['install']; + $buildconfig = $options['buildconfig']; + $buildcomponentconfigs = $options['buildcomponentconfigs']; +- +-if ($options['help'] or (!$drop and !$install and !$buildconfig and !$buildcomponentconfigs and !$diag)) { ++$snapshot = $options['snapshot']; ++$restore = $options['restore']; ++$upgrade = $options['upgrade']; ++ ++if ( ++ $options['help'] || ( ++ !$drop && !$install && !$buildconfig && !$buildcomponentconfigs ++ && !$diag && !$upgrade && !$snapshot && !$restore ++ ) ++) { + $help = "Various PHPUnit utility functions + + Options: +@@ -114,6 +125,10 @@ Options: + --buildconfig Build /phpunit.xml from /phpunit.xml.dist that runs all tests + --buildcomponentconfigs + Build distributed phpunit.xml files for each component ++--upgrade Upgrade test site to latest version ++--snapshot Create snapshot of the current test site. Optionally specify name of the snapshot with ++ --snapshot=NAME. ++--restore=NAME Restore snapshot of test site with the specified name. + + -h, --help Print out this help + +@@ -152,4 +167,13 @@ if ($diag) { + } else if ($install) { + phpunit_util::install_site(); + exit(0); ++} else if ($upgrade) { ++ phpunit_util::upgrade_site(); ++ exit(0); ++} else if ($snapshot) { ++ phpunit_util::snapshot_site(is_string($snapshot) ? $snapshot : ''); ++ exit(0); ++} else if ($restore) { ++ phpunit_util::restore_site(is_string($restore) ? $restore : ''); ++ exit(0); + } +diff --git a/lib/phpunit/bootstraplib.php b/lib/phpunit/bootstraplib.php +index a9640994bfd..d91bee4dd92 100644 +--- a/lib/phpunit/bootstraplib.php ++++ b/lib/phpunit/bootstraplib.php +@@ -65,12 +65,14 @@ function phpunit_bootstrap_error($errorcode, $text = '') { + $text = "Moodle PHPUnit environment configuration warning:\n".$text; + break; + case PHPUNIT_EXITCODE_INSTALL: +- $path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php'); +- $text = "Moodle PHPUnit environment is not initialised, please use:\n php $path"; ++ $initpath = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php'); ++ $text = "Moodle PHPUnit environment is not initialised, please use:\n php $initpath"; + break; + case PHPUNIT_EXITCODE_REINSTALL: +- $path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php'); +- $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php $path"; ++ $initpath = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php'); ++ $utilpath = testing_cli_argument_path('/admin/tool/phpunit/cli/util.php'); ++ $text = "Moodle PHPUnit environment was initialised for different version, please use:\n" ++ . " php $initpath\n or php $utilpath --upgrade"; + break; + default: + $text = empty($text) ? '' : ': '.$text; +diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php +index 9d5acc4bae0..5193830ac57 100644 +--- a/lib/phpunit/classes/util.php ++++ b/lib/phpunit/classes/util.php +@@ -495,6 +495,359 @@ class phpunit_util extends testing_util { + self::store_database_state(); + } + ++ /** ++ * Create a snapshot of the current test site database and site data. ++ * ++ * @param string $snapshotname The name of the snapshot. ++ * @return void may terminate execution with exit code ++ */ ++ public static function snapshot_site(string $snapshotname) { ++ global $DB, $CFG; ++ ++ require($CFG->dirroot . '/version.php'); ++ ++ if (!self::is_test_site()) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not snapshot non-test site!!'); ++ } ++ ++ if (empty($DB->get_tables())) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_INSTALL, 'No database tables present, can not snapshot site!!'); ++ } ++ ++ $dataroot = self::get_dataroot(); ++ $dbtype = $CFG->dbtype; ++ $name = $snapshotname ? "$dbtype-$snapshotname-$version" : "$dbtype-snapshot-$version"; ++ $snapshotdir = $dataroot . "/$name"; ++ ++ // Clear old in-memory caches of snapshot data. ++ self::$tabledata = null; ++ self::$tablestructure = null; ++ self::$sequencenames = null; ++ ++ $sitedatapath = $dataroot . DIRECTORY_SEPARATOR . self::get_originaldatafilesjson(); ++ ++ if (file_exists($sitedatapath)) { ++ unlink($sitedatapath); ++ } ++ ++ self::reset_original_data(); ++ self::save_original_data_files(); ++ self::store_database_state(); ++ self::store_versions_hash(); ++ self::$lastdbwrites = $DB->perf_get_writes(); ++ ++ if (is_dir($snapshotdir)) { ++ remove_dir($snapshotdir); ++ } ++ ++ mkdir($snapshotdir); ++ ++ $filedir = $dataroot . DIRECTORY_SEPARATOR . 'filedir'; ++ ++ self::copy_filedir($filedir, $snapshotdir . DIRECTORY_SEPARATOR . 'filedir' . DIRECTORY_SEPARATOR); ++ ++ $datafiles = [ ++ "$dataroot" . DIRECTORY_SEPARATOR . "phpunit" . DIRECTORY_SEPARATOR . "tabledata.ser", ++ "$dataroot" . DIRECTORY_SEPARATOR . "phpunit" . DIRECTORY_SEPARATOR . "tablestructure.ser", ++ "$dataroot" . DIRECTORY_SEPARATOR . "phpunit" . DIRECTORY_SEPARATOR . "versionshash.txt", ++ "$dataroot" . DIRECTORY_SEPARATOR . "originaldatafiles.json", ++ ]; ++ ++ foreach ($datafiles as $file) { ++ if (!file_exists($file)) { ++ continue; ++ } ++ copy($file, $snapshotdir . DIRECTORY_SEPARATOR . basename($file)); ++ } ++ ++ $dirtozip = $dataroot . DIRECTORY_SEPARATOR . "$name"; ++ ++ $zip = new \ZipArchive(); ++ $zip->open("$dirtozip.zip", \ZipArchive::CREATE | \ZipArchive::OVERWRITE); ++ ++ $files = new \RecursiveIteratorIterator( ++ new \RecursiveDirectoryIterator($dirtozip, \RecursiveDirectoryIterator::SKIP_DOTS), ++ \RecursiveIteratorIterator::LEAVES_ONLY ++ ); ++ ++ foreach ($files as $file) { ++ if (!$file->isDir()) { ++ $filepath = $file->getRealPath(); ++ $filepathrelative = substr($filepath, strlen($dirtozip) + 1); ++ $zip->addFile($filepath, $filepathrelative); ++ } ++ } ++ ++ $zip->close(); ++ remove_dir($snapshotdir); ++ ++ echo "Snapshot successfully created in $dirtozip.zip\n"; ++ } ++ ++ /** ++ * Restore the database and dataroot to the state they were in when snapshot_site() was called. ++ * ++ * @param string $snapshot The snapshot to restore. ++ * @return void may terminate execution with exit code ++ */ ++ public static function restore_site(string $snapshot) { ++ global $DB, $CFG; ++ ++ require($CFG->dirroot . '/version.php'); ++ ++ if (!self::is_test_site()) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not restore non-test site!!'); ++ } ++ ++ if (!empty($DB->get_tables())) { ++ echo "Database tables are still present. Run php public/admin/tool/phpunit/cli/util.php --drop before restoring.\n"; ++ return; ++ } ++ ++ if (!$snapshot) { ++ echo "No snapshot name provided. Please provide the snapshot name as an argument.\n"; ++ return; ++ } ++ ++ $dataroot = self::get_dataroot(); ++ $zipfile = $dataroot . DIRECTORY_SEPARATOR . "$snapshot.zip"; ++ $destpath = $dataroot . DIRECTORY_SEPARATOR . "$snapshot"; ++ ++ $zip = new \ZipArchive(); ++ if ($zip->open($zipfile) === true) { ++ $zip->extractTo($destpath); ++ $zip->close(); ++ } else { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Can not open snapshot zip file $zipfile!!"); ++ } ++ ++ $snapshotdir = $dataroot . "/$snapshot"; ++ ++ // Map snapshot files to their active locations. ++ $datafiles = [ ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "tabledata.ser" ++ => $dataroot . DIRECTORY_SEPARATOR . 'phpunit' . DIRECTORY_SEPARATOR . "tabledata.ser", ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "tablestructure.ser" ++ => $dataroot . DIRECTORY_SEPARATOR . 'phpunit' . DIRECTORY_SEPARATOR . "tablestructure.ser", ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "versionshash.txt" ++ => $dataroot . DIRECTORY_SEPARATOR . 'phpunit' . DIRECTORY_SEPARATOR . "versionshash.txt", ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "originaldatafiles.json" ++ => $dataroot . DIRECTORY_SEPARATOR . self::get_originaldatafilesjson(), ++ ]; ++ ++ foreach ($datafiles as $src => $dest) { ++ if (!file_exists($src)) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Required snapshot file $src does not exist!!"); ++ } ++ } ++ ++ self::$lastdbwrites = null; ++ self::$tabledata = null; ++ self::$tablestructure = null; ++ self::$sequencenames = null; ++ ++ echo "Restoring from $snapshot\n"; ++ ++ // Copy snapshot DB control files to their active locations. ++ foreach ($datafiles as $src => $dest) { ++ copy($src, $dest); ++ } ++ ++ if (empty($DB->get_tables(false))) { ++ self::create_tables_from_snapshot($destpath); ++ } ++ ++ echo "Importing snapshot data...\n"; ++ // Restore the database to snapshot state. ++ self::reset_database(); ++ ++ echo "Data imported. Resetting datarooot...\n"; ++ // Clear current dataroot before restoring filedir from snapshot. ++ self::reset_dataroot(); ++ self::reset_original_data(); ++ static::$datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess']; ++ ++ $snapshotfiledir = $snapshotdir . DIRECTORY_SEPARATOR . 'filedir'; ++ $filedir = $dataroot . DIRECTORY_SEPARATOR . 'filedir'; ++ ++ // Copy snapshot filedir back to dataroot. ++ self::copy_filedir($snapshotfiledir, $filedir); ++ ++ initialise_cfg(); ++ ++ // Execute all adhoc tasks. ++ while ($task = \core\task\manager::get_next_adhoc_task(time())) { ++ $task->execute(); ++ \core\task\manager::adhoc_task_complete($task); ++ } ++ ++ set_config('upgraderunning', 0); ++ ++ self::store_database_state(); ++ self::$lastdbwrites = $DB->perf_get_writes(); ++ ++ remove_dir($snapshotdir); ++ ++ echo "Snapshot successfully restored.\n"; ++ } ++ ++ /** ++ * Upgrade the test site and upgrade/install any new plugins that were added. ++ * ++ * @return void may terminate execution with exit code ++ */ ++ public static function upgrade_site() { ++ ++ if (!self::is_test_site()) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not upgrade non-test site!!'); ++ } ++ ++ if (self::is_test_data_updated()) { ++ echo "No database changes detected, skipping upgrade.\n"; ++ return; ++ } ++ ++ initialise_cfg(); ++ upgrade_noncore(true); ++ set_config('upgraderunning', 0); ++ self::store_versions_hash(); ++ self::store_database_state(); ++ } ++ ++ /** ++ * Helper method that copies all contents from one directory to another, creating directories as needed. ++ * ++ * @param string $sourcedir the source directory to copy ++ * @param string $dirtocreate the destination directory to create and copy files to ++ * @return void ++ */ ++ private static function copy_filedir($sourcedir, $dirtocreate) { ++ ++ if (!file_exists($sourcedir)) { ++ echo "Source directory $sourcedir does not exist!\n"; ++ return; ++ } ++ ++ $iterator = new \RecursiveIteratorIterator( ++ new \RecursiveDirectoryIterator($sourcedir, \RecursiveDirectoryIterator::SKIP_DOTS), ++ \RecursiveIteratorIterator::SELF_FIRST ++ ); ++ ++ if (!is_dir($dirtocreate)) { ++ mkdir($dirtocreate, 0777, true); ++ } ++ ++ foreach ($iterator as $item) { ++ $destpath = $dirtocreate . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); ++ if ($item->isDir()) { ++ if (!is_dir($destpath)) { ++ mkdir($destpath, 0777, true); ++ } ++ } else { ++ copy($item->getPathname(), $destpath); ++ } ++ } ++ } ++ ++ /** ++ * Create the database tables based on the stored snapshot structure. ++ * ++ * @param string $snapshotdir the target snapshot directory to create the tables from. ++ * @param bool $showprogress whether to show progress during table creation. ++ * @return void may terminate execution with exit code ++ */ ++ private static function create_tables_from_snapshot(string $snapshotdir, bool $showprogress = true) { ++ global $DB; ++ ++ $dbman = $DB->get_manager(); ++ ++ $structurefile = $snapshotdir . DIRECTORY_SEPARATOR . 'tablestructure.ser'; ++ if (!file_exists($structurefile)) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Snapshot file $structurefile does not exist!!"); ++ } ++ $tablestructure = unserialize(file_get_contents($structurefile)); ++ if (!is_array($tablestructure)) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Invalid tablestructure.ser in snapshot $snapshotdir"); ++ } ++ ++ $dotsonline = 0; ++ foreach ($tablestructure as $tablename => $columns) { ++ // If the table already exists, skip it. ++ if ($dbman->table_exists($tablename)) { ++ continue; ++ } ++ ++ $table = new \xmldb_table($tablename); ++ ++ // Build an xmldb_field for every column. ++ foreach ($columns as $colname => $col) { ++ $field = new \xmldb_field($colname); ++ $sequence = false; ++ // Map each column meta type to the appropriate xmldb type. ++ switch ($col->meta_type) { ++ case 'R': ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength(10); ++ $sequence = true; ++ break; ++ case 'I': ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength($col->max_length ?: 10); ++ break; ++ case 'N': ++ $field->setType(XMLDB_TYPE_NUMBER); ++ $field->setLength($col->max_length ?: 10); ++ $field->setDecimals($col->scale ?: 0); ++ break; ++ case 'C': ++ $field->setType(XMLDB_TYPE_CHAR); ++ $field->setLength($col->max_length ?: 255); ++ break; ++ case 'X': ++ $field->setType(XMLDB_TYPE_TEXT); ++ break; ++ case 'B': ++ $field->setType(XMLDB_TYPE_BINARY); ++ break; ++ case 'L': ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength(1); ++ break; ++ default: ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength($col->max_length ?: 10); ++ } ++ $field->setNotNull($col->not_null); ++ if ($col->has_default && $col->default_value !== '') { ++ $field->setDefault($col->default_value); ++ } ++ if ($sequence) { ++ $field->setSequence(true); ++ } ++ $table->addField($field); ++ } ++ if (isset($columns['id'])) { ++ // Add a primary key for the 'id' column. ++ $key = new \xmldb_key('primary', XMLDB_KEY_PRIMARY, ['id']); ++ $table->addKey($key); ++ } ++ $dbman->create_table($table); ++ ++ if ($dotsonline == 60) { ++ if ($showprogress) { ++ echo "\n"; ++ } ++ $dotsonline = 0; ++ } ++ if ($showprogress) { ++ echo '.'; ++ } ++ $dotsonline += 1; ++ } ++ ++ echo "\nTables successfully created from snapshot.\n"; ++ } ++ + /** + * Builds dirroot/phpunit.xml file using defaults from /phpunit.xml.dist + * @static +diff --git a/lib/testing/classes/util.php b/lib/testing/classes/util.php +index 0c241e6d2b9..f3f4d2000c1 100644 +--- a/lib/testing/classes/util.php ++++ b/lib/testing/classes/util.php +@@ -136,6 +136,13 @@ abstract class testing_util { + return substr($classname, 0, strpos($classname, '_')); + } + ++ /** ++ * Reset the original data. ++ */ ++ protected static function reset_original_data() { ++ self::$originaldatafilesjsonadded = false; ++ } ++ + /** + * Get data generator + * @static +@@ -781,6 +788,10 @@ abstract class testing_util { + // Clean up the dataroot folder. + $handle = opendir(self::get_dataroot()); + while (false !== ($item = readdir($handle))) { ++ // Explicitly check for a snapshot zip and add it to the skip on reset array if there is one. ++ if (str_contains($item, 'snapshot-')) { ++ $childclassname::$datarootskiponreset[] = $item; ++ } + if (in_array($item, $childclassname::$datarootskiponreset)) { + continue; + } +-- +2.43.0 + diff --git a/.github/patches/404-500-phpunit-restore.patch b/.github/patches/404-500-phpunit-restore.patch new file mode 100644 index 00000000..174c5af8 --- /dev/null +++ b/.github/patches/404-500-phpunit-restore.patch @@ -0,0 +1,515 @@ +From d3f3855897edf3c5411ff3633c8e860884a1894d Mon Sep 17 00:00:00 2001 +From: Abhinav Gandham +Date: Wed, 13 May 2026 15:33:48 +1000 +Subject: [PATCH] MDL-88495 unit tests: Implmented test site upgrade, snapshot, + and restore functionality." + +--- + admin/tool/phpunit/cli/util.php | 30 ++- + lib/phpunit/bootstraplib.php | 10 +- + lib/phpunit/classes/util.php | 357 +++++++++++++++++++++++++++++++- + lib/testing/classes/util.php | 20 +- + 4 files changed, 407 insertions(+), 10 deletions(-) + +diff --git a/admin/tool/phpunit/cli/util.php b/admin/tool/phpunit/cli/util.php +index 3cc928a9e5a..8af8dc64531 100644 +--- a/admin/tool/phpunit/cli/util.php ++++ b/admin/tool/phpunit/cli/util.php +@@ -47,6 +47,9 @@ list($options, $unrecognized) = cli_get_params( + 'buildcomponentconfigs' => false, + 'diag' => false, + 'run' => false, ++ 'upgrade' => false, ++ 'snapshot' => '', ++ 'restore' => '', + 'help' => false, + ], + [ +@@ -61,7 +64,7 @@ if (!file_exists(__DIR__.'/../../../../vendor/phpunit/phpunit/composer.json') || + phpunit_bootstrap_error(PHPUNIT_EXITCODE_PHPUNITMISSING); + } + +-if ($options['install'] || $options['drop']) { ++if ($options['install'] || $options['drop'] || $options['upgrade']) { + define('CACHE_DISABLE_ALL', true); + } + +@@ -102,8 +105,16 @@ $drop = $options['drop']; + $install = $options['install']; + $buildconfig = $options['buildconfig']; + $buildcomponentconfigs = $options['buildcomponentconfigs']; +- +-if ($options['help'] || (!$drop && !$install && !$buildconfig && !$buildcomponentconfigs && !$diag)) { ++$snapshot = $options['snapshot']; ++$restore = $options['restore']; ++$upgrade = $options['upgrade']; ++ ++if ( ++ $options['help'] || ( ++ !$drop && !$install && !$buildconfig && !$buildcomponentconfigs ++ && !$diag && !$upgrade && !$snapshot && !$restore ++ ) ++) { + $help = "Various PHPUnit utility functions + + Options: +@@ -114,6 +125,10 @@ Options: + --buildconfig Build /phpunit.xml from /phpunit.xml.dist that runs all tests + --buildcomponentconfigs + Build distributed phpunit.xml files for each component ++--upgrade Upgrade test site to latest version ++--snapshot Create snapshot of the current test site. Optionally specify name of the snapshot with ++ --snapshot=NAME. ++--restore=NAME Restore snapshot of test site with the specified name. + + -h, --help Print out this help + +@@ -155,4 +170,13 @@ if ($diag) { + } else if ($install) { + phpunit_util::install_site(); + exit(0); ++} else if ($upgrade) { ++ phpunit_util::upgrade_site(); ++ exit(0); ++} else if ($snapshot) { ++ phpunit_util::snapshot_site(is_string($snapshot) ? $snapshot : ''); ++ exit(0); ++} else if ($restore) { ++ phpunit_util::restore_site(is_string($restore) ? $restore : ''); ++ exit(0); + } +diff --git a/lib/phpunit/bootstraplib.php b/lib/phpunit/bootstraplib.php +index a9640994bfd..bf3112cf1f5 100644 +--- a/lib/phpunit/bootstraplib.php ++++ b/lib/phpunit/bootstraplib.php +@@ -65,12 +65,14 @@ function phpunit_bootstrap_error($errorcode, $text = '') { + $text = "Moodle PHPUnit environment configuration warning:\n".$text; + break; + case PHPUNIT_EXITCODE_INSTALL: +- $path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php'); +- $text = "Moodle PHPUnit environment is not initialised, please use:\n php $path"; ++ $initpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); ++ $text = "Moodle PHPUnit environment is not initialised, please use:\n php $initpath"; + break; + case PHPUNIT_EXITCODE_REINSTALL: +- $path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php'); +- $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php $path"; ++ $initpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); ++ $utilpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/util.php'); ++ $text = "Moodle PHPUnit environment was initialised for different version, please use:\n" ++ . " php $initpath\n or php $utilpath --upgrade"; + break; + default: + $text = empty($text) ? '' : ': '.$text; +diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php +index 86b52503aa3..6406c98a2fb 100644 +--- a/lib/phpunit/classes/util.php ++++ b/lib/phpunit/classes/util.php +@@ -520,8 +520,361 @@ class phpunit_util extends testing_util { + } + + /** +- * Builds dirroot/phpunit.xml file using defaults from /phpunit.xml.dist +- * @static ++ * Create a snapshot of the current test site database and site data. ++ * ++ * @param string $snapshotname The name of the snapshot. ++ * @return void may terminate execution with exit code ++ */ ++ public static function snapshot_site(string $snapshotname) { ++ global $DB, $CFG; ++ ++ require($CFG->dirroot . '/version.php'); ++ ++ if (!self::is_test_site()) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not snapshot non-test site!!'); ++ } ++ ++ if (empty($DB->get_tables())) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_INSTALL, 'No database tables present, can not snapshot site!!'); ++ } ++ ++ $dataroot = self::get_dataroot(); ++ $dbtype = $CFG->dbtype; ++ $name = $snapshotname ? "$dbtype-$snapshotname-$version" : "$dbtype-snapshot-$version"; ++ $snapshotdir = $dataroot . "/$name"; ++ ++ // Clear old in-memory caches of snapshot data. ++ self::$tabledata = null; ++ self::$tablestructure = null; ++ self::$sequencenames = null; ++ ++ $sitedatapath = $dataroot . DIRECTORY_SEPARATOR . self::get_originaldatafilesjson(); ++ ++ if (file_exists($sitedatapath)) { ++ unlink($sitedatapath); ++ } ++ ++ self::reset_original_data(); ++ self::save_original_data_files(); ++ self::store_database_state(); ++ self::store_versions_hash(); ++ self::$lastdbwrites = $DB->perf_get_writes(); ++ ++ if (is_dir($snapshotdir)) { ++ remove_dir($snapshotdir); ++ } ++ ++ mkdir($snapshotdir); ++ ++ $filedir = $dataroot . DIRECTORY_SEPARATOR . 'filedir'; ++ ++ self::copy_filedir($filedir, $snapshotdir . DIRECTORY_SEPARATOR . 'filedir' . DIRECTORY_SEPARATOR); ++ ++ $datafiles = [ ++ "$dataroot" . DIRECTORY_SEPARATOR . "phpunit" . DIRECTORY_SEPARATOR . "tabledata.ser", ++ "$dataroot" . DIRECTORY_SEPARATOR . "phpunit" . DIRECTORY_SEPARATOR . "tablestructure.ser", ++ "$dataroot" . DIRECTORY_SEPARATOR . "phpunit" . DIRECTORY_SEPARATOR . "versionshash.txt", ++ "$dataroot" . DIRECTORY_SEPARATOR . "originaldatafiles.json", ++ ]; ++ ++ foreach ($datafiles as $file) { ++ if (!file_exists($file)) { ++ continue; ++ } ++ copy($file, $snapshotdir . DIRECTORY_SEPARATOR . basename($file)); ++ } ++ ++ $dirtozip = $dataroot . DIRECTORY_SEPARATOR . "$name"; ++ ++ $zip = new \ZipArchive(); ++ $zip->open("$dirtozip.zip", \ZipArchive::CREATE | \ZipArchive::OVERWRITE); ++ ++ $files = new \RecursiveIteratorIterator( ++ new \RecursiveDirectoryIterator($dirtozip, \RecursiveDirectoryIterator::SKIP_DOTS), ++ \RecursiveIteratorIterator::LEAVES_ONLY ++ ); ++ ++ foreach ($files as $file) { ++ if (!$file->isDir()) { ++ $filepath = $file->getRealPath(); ++ $filepathrelative = substr($filepath, strlen($dirtozip) + 1); ++ $zip->addFile($filepath, $filepathrelative); ++ } ++ } ++ ++ $zip->close(); ++ remove_dir($snapshotdir); ++ ++ echo "Snapshot successfully created in $dirtozip.zip\n"; ++ } ++ ++ /** ++ * Restore the database and dataroot to the state they were in when snapshot_site() was called. ++ * ++ * @param string $snapshot The snapshot to restore. ++ * @return void may terminate execution with exit code ++ */ ++ public static function restore_site(string $snapshot) { ++ global $DB, $CFG; ++ ++ require($CFG->dirroot . '/version.php'); ++ ++ if (!self::is_test_site()) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not restore non-test site!!'); ++ } ++ ++ if (!empty($DB->get_tables())) { ++ echo "Database tables are still present. Run php public/admin/tool/phpunit/cli/util.php --drop before restoring.\n"; ++ return; ++ } ++ ++ if (!$snapshot) { ++ echo "No snapshot name provided. Please provide the snapshot name as an argument.\n"; ++ return; ++ } ++ ++ $dataroot = self::get_dataroot(); ++ $zipfile = $dataroot . DIRECTORY_SEPARATOR . "$snapshot.zip"; ++ $destpath = $dataroot . DIRECTORY_SEPARATOR . "$snapshot"; ++ ++ $zip = new \ZipArchive(); ++ if ($zip->open($zipfile) === true) { ++ $zip->extractTo($destpath); ++ $zip->close(); ++ } else { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Can not open snapshot zip file $zipfile!!"); ++ } ++ ++ $snapshotdir = $dataroot . "/$snapshot"; ++ ++ // Map snapshot files to their active locations. ++ $datafiles = [ ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "tabledata.ser" ++ => $dataroot . DIRECTORY_SEPARATOR . 'phpunit' . DIRECTORY_SEPARATOR . "tabledata.ser", ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "tablestructure.ser" ++ => $dataroot . DIRECTORY_SEPARATOR . 'phpunit' . DIRECTORY_SEPARATOR . "tablestructure.ser", ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "versionshash.txt" ++ => $dataroot . DIRECTORY_SEPARATOR . 'phpunit' . DIRECTORY_SEPARATOR . "versionshash.txt", ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "originaldatafiles.json" ++ => $dataroot . DIRECTORY_SEPARATOR . self::get_originaldatafilesjson(), ++ ]; ++ ++ foreach ($datafiles as $src => $dest) { ++ if (!file_exists($src)) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Required snapshot file $src does not exist!!"); ++ } ++ } ++ ++ self::$lastdbwrites = null; ++ self::$tabledata = null; ++ self::$tablestructure = null; ++ self::$sequencenames = null; ++ ++ echo "Restoring from $snapshot\n"; ++ ++ // Copy snapshot DB control files to their active locations. ++ foreach ($datafiles as $src => $dest) { ++ copy($src, $dest); ++ } ++ ++ if (empty($DB->get_tables(false))) { ++ self::create_tables_from_snapshot($destpath); ++ } ++ ++ echo "Importing snapshot data...\n"; ++ // Restore the database to snapshot state. ++ self::reset_database(); ++ ++ echo "Data imported. Resetting datarooot...\n"; ++ // Clear current dataroot before restoring filedir from snapshot. ++ self::reset_dataroot(); ++ self::reset_original_data(); ++ static::$datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess']; ++ ++ $snapshotfiledir = $snapshotdir . DIRECTORY_SEPARATOR . 'filedir'; ++ $filedir = $dataroot . DIRECTORY_SEPARATOR . 'filedir'; ++ ++ // Copy snapshot filedir back to dataroot. ++ self::copy_filedir($snapshotfiledir, $filedir); ++ ++ initialise_cfg(); ++ ++ // Execute all adhoc tasks. ++ while ($task = \core\task\manager::get_next_adhoc_task(time())) { ++ $task->execute(); ++ \core\task\manager::adhoc_task_complete($task); ++ } ++ ++ set_config('upgraderunning', 0); ++ ++ self::store_database_state(); ++ self::$lastdbwrites = $DB->perf_get_writes(); ++ ++ remove_dir($snapshotdir); ++ ++ echo "Snapshot successfully restored.\n"; ++ } ++ ++ /** ++ * Upgrade the test site and upgrade/install any new plugins that were added. ++ * ++ * @return void may terminate execution with exit code ++ */ ++ public static function upgrade_site() { ++ ++ if (!self::is_test_site()) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not upgrade non-test site!!'); ++ } ++ ++ if (self::is_test_data_updated()) { ++ echo "No database changes detected, skipping upgrade.\n"; ++ return; ++ } ++ ++ initialise_cfg(); ++ upgrade_noncore(true); ++ set_config('upgraderunning', 0); ++ self::store_versions_hash(); ++ self::store_database_state(); ++ } ++ ++ /** ++ * Helper method that copies all contents from one directory to another, creating directories as needed. ++ * ++ * @param string $sourcedir the source directory to copy ++ * @param string $dirtocreate the destination directory to create and copy files to ++ * @return void ++ */ ++ private static function copy_filedir($sourcedir, $dirtocreate) { ++ ++ if (!file_exists($sourcedir)) { ++ echo "Source directory $sourcedir does not exist!\n"; ++ return; ++ } ++ ++ $iterator = new \RecursiveIteratorIterator( ++ new \RecursiveDirectoryIterator($sourcedir, \RecursiveDirectoryIterator::SKIP_DOTS), ++ \RecursiveIteratorIterator::SELF_FIRST ++ ); ++ ++ if (!is_dir($dirtocreate)) { ++ mkdir($dirtocreate, 0777, true); ++ } ++ ++ foreach ($iterator as $item) { ++ $destpath = $dirtocreate . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); ++ if ($item->isDir()) { ++ if (!is_dir($destpath)) { ++ mkdir($destpath, 0777, true); ++ } ++ } else { ++ copy($item->getPathname(), $destpath); ++ } ++ } ++ } ++ ++ /** ++ * Create the database tables based on the stored snapshot structure. ++ * ++ * @param string $snapshotdir the target snapshot directory to create the tables from. ++ * @param bool $showprogress whether to show progress during table creation. ++ * @return void may terminate execution with exit code ++ */ ++ private static function create_tables_from_snapshot(string $snapshotdir, bool $showprogress = true) { ++ global $DB; ++ ++ $dbman = $DB->get_manager(); ++ ++ $structurefile = $snapshotdir . DIRECTORY_SEPARATOR . 'tablestructure.ser'; ++ if (!file_exists($structurefile)) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Snapshot file $structurefile does not exist!!"); ++ } ++ $tablestructure = unserialize(file_get_contents($structurefile)); ++ if (!is_array($tablestructure)) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Invalid tablestructure.ser in snapshot $snapshotdir"); ++ } ++ ++ $dotsonline = 0; ++ foreach ($tablestructure as $tablename => $columns) { ++ // If the table already exists, skip it. ++ if ($dbman->table_exists($tablename)) { ++ continue; ++ } ++ ++ $table = new \xmldb_table($tablename); ++ ++ // Build an xmldb_field for every column. ++ foreach ($columns as $colname => $col) { ++ $field = new \xmldb_field($colname); ++ $sequence = false; ++ // Map each column meta type to the appropriate xmldb type. ++ switch ($col->meta_type) { ++ case 'R': ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength(10); ++ $sequence = true; ++ break; ++ case 'I': ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength($col->max_length ?: 10); ++ break; ++ case 'N': ++ $field->setType(XMLDB_TYPE_NUMBER); ++ $field->setLength($col->max_length ?: 10); ++ $field->setDecimals($col->scale ?: 0); ++ break; ++ case 'C': ++ $field->setType(XMLDB_TYPE_CHAR); ++ $field->setLength($col->max_length ?: 255); ++ break; ++ case 'X': ++ $field->setType(XMLDB_TYPE_TEXT); ++ break; ++ case 'B': ++ $field->setType(XMLDB_TYPE_BINARY); ++ break; ++ case 'L': ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength(1); ++ break; ++ default: ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength($col->max_length ?: 10); ++ } ++ $field->setNotNull($col->not_null); ++ if ($col->has_default && $col->default_value !== '') { ++ $field->setDefault($col->default_value); ++ } ++ if ($sequence) { ++ $field->setSequence(true); ++ } ++ $table->addField($field); ++ } ++ if (isset($columns['id'])) { ++ // Add a primary key for the 'id' column. ++ $key = new \xmldb_key('primary', XMLDB_KEY_PRIMARY, ['id']); ++ $table->addKey($key); ++ } ++ $dbman->create_table($table); ++ ++ if ($dotsonline == 60) { ++ if ($showprogress) { ++ echo "\n"; ++ } ++ $dotsonline = 0; ++ } ++ if ($showprogress) { ++ echo '.'; ++ } ++ $dotsonline += 1; ++ } ++ ++ echo "\nTables successfully created from snapshot.\n"; ++ } ++ ++ /** ++ * Builds root/phpunit.xml file using defaults from /phpunit.xml.dist ++ * + * @return bool true means main config file created, false means only dataroot file created + */ + public static function build_config_file() { +diff --git a/lib/testing/classes/util.php b/lib/testing/classes/util.php +index 5315c77c94b..4550e4d4a2d 100644 +--- a/lib/testing/classes/util.php ++++ b/lib/testing/classes/util.php +@@ -122,9 +122,23 @@ abstract class testing_util { + */ + final protected static function get_framework() { + $classname = get_called_class(); ++ $reflectedclass = new \ReflectionClass($classname); ++ ++ if ($reflectedclass->inNamespace()) { ++ $namespaces = explode('\\', $reflectedclass->getNamespaceName()); ++ return array_shift($namespaces); ++ } ++ + return substr($classname, 0, strpos($classname, '_')); + } + ++ /** ++ * Reset the original data. ++ */ ++ protected static function reset_original_data() { ++ self::$originaldatafilesjsonadded = false; ++ } ++ + /** + * Get data generator + * @static +@@ -712,7 +726,11 @@ abstract class testing_util { + // Clean up the dataroot folder. + $handle = opendir(self::get_dataroot()); + while (false !== ($item = readdir($handle))) { +- if (in_array($item, $childclassname::$datarootskiponreset)) { ++ // Explicitly check for a snapshot zip and add it to the skip on reset array if there is one. ++ if (str_contains($item, 'snapshot-')) { ++ static::$datarootskiponreset[] = $item; ++ } ++ if (in_array($item, static::$datarootskiponreset)) { + continue; + } + if (is_dir(self::get_dataroot() . "/$item")) { +-- +2.43.0 + diff --git a/.github/patches/501-501-phpunit-restore.patch b/.github/patches/501-501-phpunit-restore.patch new file mode 100644 index 00000000..561881a8 --- /dev/null +++ b/.github/patches/501-501-phpunit-restore.patch @@ -0,0 +1,509 @@ +From e724df9b1296c1d2daf6ecf5da617aff951a9b79 Mon Sep 17 00:00:00 2001 +From: Abhinav Gandham +Date: Thu, 21 May 2026 22:04:10 +1000 +Subject: [PATCH] phpunit patch 501 + +--- + public/admin/tool/phpunit/cli/util.php | 30 ++- + public/lib/phpunit/bootstraplib.php | 10 +- + public/lib/phpunit/classes/util.php | 353 +++++++++++++++++++++++++ + public/lib/testing/classes/util.php | 15 +- + 4 files changed, 399 insertions(+), 9 deletions(-) + +diff --git a/public/admin/tool/phpunit/cli/util.php b/public/admin/tool/phpunit/cli/util.php +index ff5eb956ddd..658263ab3b6 100644 +--- a/public/admin/tool/phpunit/cli/util.php ++++ b/public/admin/tool/phpunit/cli/util.php +@@ -47,6 +47,9 @@ list($options, $unrecognized) = cli_get_params( + 'buildcomponentconfigs' => false, + 'diag' => false, + 'run' => false, ++ 'upgrade' => false, ++ 'snapshot' => '', ++ 'restore' => '', + 'help' => false, + ], + [ +@@ -61,7 +64,7 @@ if (!file_exists(__DIR__.'/../../../../../vendor/phpunit/phpunit/composer.json') + phpunit_bootstrap_error(PHPUNIT_EXITCODE_PHPUNITMISSING); + } + +-if ($options['install'] || $options['drop']) { ++if ($options['install'] || $options['drop'] || $options['upgrade']) { + define('CACHE_DISABLE_ALL', true); + } + +@@ -102,8 +105,16 @@ $drop = $options['drop']; + $install = $options['install']; + $buildconfig = $options['buildconfig']; + $buildcomponentconfigs = $options['buildcomponentconfigs']; +- +-if ($options['help'] || (!$drop && !$install && !$buildconfig && !$buildcomponentconfigs && !$diag)) { ++$snapshot = $options['snapshot']; ++$restore = $options['restore']; ++$upgrade = $options['upgrade']; ++ ++if ( ++ $options['help'] || ( ++ !$drop && !$install && !$buildconfig && !$buildcomponentconfigs ++ && !$diag && !$upgrade && !$snapshot && !$restore ++ ) ++) { + $help = "Various PHPUnit utility functions + + Options: +@@ -114,6 +125,10 @@ Options: + --buildconfig Build /phpunit.xml from /phpunit.xml.dist that runs all tests + --buildcomponentconfigs + Build distributed phpunit.xml files for each component ++--upgrade Upgrade test site to latest version ++--snapshot Create snapshot of the current test site. Optionally specify name of the snapshot with ++ --snapshot=NAME. ++--restore=NAME Restore snapshot of test site with the specified name. + + -h, --help Print out this help + +@@ -155,4 +170,13 @@ if ($diag) { + } else if ($install) { + phpunit_util::install_site(); + exit(0); ++} else if ($upgrade) { ++ phpunit_util::upgrade_site(); ++ exit(0); ++} else if ($snapshot) { ++ phpunit_util::snapshot_site(is_string($snapshot) ? $snapshot : ''); ++ exit(0); ++} else if ($restore) { ++ phpunit_util::restore_site(is_string($restore) ? $restore : ''); ++ exit(0); + } +diff --git a/public/lib/phpunit/bootstraplib.php b/public/lib/phpunit/bootstraplib.php +index 48c75dfa1dc..bf3112cf1f5 100644 +--- a/public/lib/phpunit/bootstraplib.php ++++ b/public/lib/phpunit/bootstraplib.php +@@ -65,12 +65,14 @@ function phpunit_bootstrap_error($errorcode, $text = '') { + $text = "Moodle PHPUnit environment configuration warning:\n".$text; + break; + case PHPUNIT_EXITCODE_INSTALL: +- $path = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); +- $text = "Moodle PHPUnit environment is not initialised, please use:\n php $path"; ++ $initpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); ++ $text = "Moodle PHPUnit environment is not initialised, please use:\n php $initpath"; + break; + case PHPUNIT_EXITCODE_REINSTALL: +- $path = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); +- $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php $path"; ++ $initpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); ++ $utilpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/util.php'); ++ $text = "Moodle PHPUnit environment was initialised for different version, please use:\n" ++ . " php $initpath\n or php $utilpath --upgrade"; + break; + default: + $text = empty($text) ? '' : ': '.$text; +diff --git a/public/lib/phpunit/classes/util.php b/public/lib/phpunit/classes/util.php +index fcc574117ae..55a31f505ac 100644 +--- a/public/lib/phpunit/classes/util.php ++++ b/public/lib/phpunit/classes/util.php +@@ -524,6 +524,359 @@ class phpunit_util extends testing_util { + self::store_database_state(); + } + ++ /** ++ * Create a snapshot of the current test site database and site data. ++ * ++ * @param string $snapshotname The name of the snapshot. ++ * @return void may terminate execution with exit code ++ */ ++ public static function snapshot_site(string $snapshotname) { ++ global $DB, $CFG; ++ ++ require($CFG->dirroot . '/version.php'); ++ ++ if (!self::is_test_site()) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not snapshot non-test site!!'); ++ } ++ ++ if (empty($DB->get_tables())) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_INSTALL, 'No database tables present, can not snapshot site!!'); ++ } ++ ++ $dataroot = self::get_dataroot(); ++ $dbtype = $CFG->dbtype; ++ $name = $snapshotname ? "$dbtype-$snapshotname-$version" : "$dbtype-snapshot-$version"; ++ $snapshotdir = $dataroot . "/$name"; ++ ++ // Clear old in-memory caches of snapshot data. ++ self::$tabledata = null; ++ self::$tablestructure = null; ++ self::$sequencenames = null; ++ ++ $sitedatapath = $dataroot . DIRECTORY_SEPARATOR . self::get_originaldatafilesjson(); ++ ++ if (file_exists($sitedatapath)) { ++ unlink($sitedatapath); ++ } ++ ++ self::reset_original_data(); ++ self::save_original_data_files(); ++ self::store_database_state(); ++ self::store_versions_hash(); ++ self::$lastdbwrites = $DB->perf_get_writes(); ++ ++ if (is_dir($snapshotdir)) { ++ remove_dir($snapshotdir); ++ } ++ ++ mkdir($snapshotdir); ++ ++ $filedir = $dataroot . DIRECTORY_SEPARATOR . 'filedir'; ++ ++ self::copy_filedir($filedir, $snapshotdir . DIRECTORY_SEPARATOR . 'filedir' . DIRECTORY_SEPARATOR); ++ ++ $datafiles = [ ++ "$dataroot" . DIRECTORY_SEPARATOR . "phpunit" . DIRECTORY_SEPARATOR . "tabledata.ser", ++ "$dataroot" . DIRECTORY_SEPARATOR . "phpunit" . DIRECTORY_SEPARATOR . "tablestructure.ser", ++ "$dataroot" . DIRECTORY_SEPARATOR . "phpunit" . DIRECTORY_SEPARATOR . "versionshash.txt", ++ "$dataroot" . DIRECTORY_SEPARATOR . "originaldatafiles.json", ++ ]; ++ ++ foreach ($datafiles as $file) { ++ if (!file_exists($file)) { ++ continue; ++ } ++ copy($file, $snapshotdir . DIRECTORY_SEPARATOR . basename($file)); ++ } ++ ++ $dirtozip = $dataroot . DIRECTORY_SEPARATOR . "$name"; ++ ++ $zip = new \ZipArchive(); ++ $zip->open("$dirtozip.zip", \ZipArchive::CREATE | \ZipArchive::OVERWRITE); ++ ++ $files = new \RecursiveIteratorIterator( ++ new \RecursiveDirectoryIterator($dirtozip, \RecursiveDirectoryIterator::SKIP_DOTS), ++ \RecursiveIteratorIterator::LEAVES_ONLY ++ ); ++ ++ foreach ($files as $file) { ++ if (!$file->isDir()) { ++ $filepath = $file->getRealPath(); ++ $filepathrelative = substr($filepath, strlen($dirtozip) + 1); ++ $zip->addFile($filepath, $filepathrelative); ++ } ++ } ++ ++ $zip->close(); ++ remove_dir($snapshotdir); ++ ++ echo "Snapshot successfully created in $dirtozip.zip\n"; ++ } ++ ++ /** ++ * Restore the database and dataroot to the state they were in when snapshot_site() was called. ++ * ++ * @param string $snapshot The snapshot to restore. ++ * @return void may terminate execution with exit code ++ */ ++ public static function restore_site(string $snapshot) { ++ global $DB, $CFG; ++ ++ require($CFG->dirroot . '/version.php'); ++ ++ if (!self::is_test_site()) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not restore non-test site!!'); ++ } ++ ++ if (!empty($DB->get_tables())) { ++ echo "Database tables are still present. Run php public/admin/tool/phpunit/cli/util.php --drop before restoring.\n"; ++ return; ++ } ++ ++ if (!$snapshot) { ++ echo "No snapshot name provided. Please provide the snapshot name as an argument.\n"; ++ return; ++ } ++ ++ $dataroot = self::get_dataroot(); ++ $zipfile = $dataroot . DIRECTORY_SEPARATOR . "$snapshot.zip"; ++ $destpath = $dataroot . DIRECTORY_SEPARATOR . "$snapshot"; ++ ++ $zip = new \ZipArchive(); ++ if ($zip->open($zipfile) === true) { ++ $zip->extractTo($destpath); ++ $zip->close(); ++ } else { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Can not open snapshot zip file $zipfile!!"); ++ } ++ ++ $snapshotdir = $dataroot . "/$snapshot"; ++ ++ // Map snapshot files to their active locations. ++ $datafiles = [ ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "tabledata.ser" ++ => $dataroot . DIRECTORY_SEPARATOR . 'phpunit' . DIRECTORY_SEPARATOR . "tabledata.ser", ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "tablestructure.ser" ++ => $dataroot . DIRECTORY_SEPARATOR . 'phpunit' . DIRECTORY_SEPARATOR . "tablestructure.ser", ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "versionshash.txt" ++ => $dataroot . DIRECTORY_SEPARATOR . 'phpunit' . DIRECTORY_SEPARATOR . "versionshash.txt", ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "originaldatafiles.json" ++ => $dataroot . DIRECTORY_SEPARATOR . self::get_originaldatafilesjson(), ++ ]; ++ ++ foreach ($datafiles as $src => $dest) { ++ if (!file_exists($src)) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Required snapshot file $src does not exist!!"); ++ } ++ } ++ ++ self::$lastdbwrites = null; ++ self::$tabledata = null; ++ self::$tablestructure = null; ++ self::$sequencenames = null; ++ ++ echo "Restoring from $snapshot\n"; ++ ++ // Copy snapshot DB control files to their active locations. ++ foreach ($datafiles as $src => $dest) { ++ copy($src, $dest); ++ } ++ ++ if (empty($DB->get_tables(false))) { ++ self::create_tables_from_snapshot($destpath); ++ } ++ ++ echo "Importing snapshot data...\n"; ++ // Restore the database to snapshot state. ++ self::reset_database(); ++ ++ echo "Data imported. Resetting datarooot...\n"; ++ // Clear current dataroot before restoring filedir from snapshot. ++ self::reset_dataroot(); ++ self::reset_original_data(); ++ static::$datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess']; ++ ++ $snapshotfiledir = $snapshotdir . DIRECTORY_SEPARATOR . 'filedir'; ++ $filedir = $dataroot . DIRECTORY_SEPARATOR . 'filedir'; ++ ++ // Copy snapshot filedir back to dataroot. ++ self::copy_filedir($snapshotfiledir, $filedir); ++ ++ initialise_cfg(); ++ ++ // Execute all adhoc tasks. ++ while ($task = \core\task\manager::get_next_adhoc_task(time())) { ++ $task->execute(); ++ \core\task\manager::adhoc_task_complete($task); ++ } ++ ++ set_config('upgraderunning', 0); ++ ++ self::store_database_state(); ++ self::$lastdbwrites = $DB->perf_get_writes(); ++ ++ remove_dir($snapshotdir); ++ ++ echo "Snapshot successfully restored.\n"; ++ } ++ ++ /** ++ * Upgrade the test site and upgrade/install any new plugins that were added. ++ * ++ * @return void may terminate execution with exit code ++ */ ++ public static function upgrade_site() { ++ ++ if (!self::is_test_site()) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not upgrade non-test site!!'); ++ } ++ ++ if (self::is_test_data_updated()) { ++ echo "No database changes detected, skipping upgrade.\n"; ++ return; ++ } ++ ++ initialise_cfg(); ++ upgrade_noncore(true); ++ set_config('upgraderunning', 0); ++ self::store_versions_hash(); ++ self::store_database_state(); ++ } ++ ++ /** ++ * Helper method that copies all contents from one directory to another, creating directories as needed. ++ * ++ * @param string $sourcedir the source directory to copy ++ * @param string $dirtocreate the destination directory to create and copy files to ++ * @return void ++ */ ++ private static function copy_filedir($sourcedir, $dirtocreate) { ++ ++ if (!file_exists($sourcedir)) { ++ echo "Source directory $sourcedir does not exist!\n"; ++ return; ++ } ++ ++ $iterator = new \RecursiveIteratorIterator( ++ new \RecursiveDirectoryIterator($sourcedir, \RecursiveDirectoryIterator::SKIP_DOTS), ++ \RecursiveIteratorIterator::SELF_FIRST ++ ); ++ ++ if (!is_dir($dirtocreate)) { ++ mkdir($dirtocreate, 0777, true); ++ } ++ ++ foreach ($iterator as $item) { ++ $destpath = $dirtocreate . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); ++ if ($item->isDir()) { ++ if (!is_dir($destpath)) { ++ mkdir($destpath, 0777, true); ++ } ++ } else { ++ copy($item->getPathname(), $destpath); ++ } ++ } ++ } ++ ++ /** ++ * Create the database tables based on the stored snapshot structure. ++ * ++ * @param string $snapshotdir the target snapshot directory to create the tables from. ++ * @param bool $showprogress whether to show progress during table creation. ++ * @return void may terminate execution with exit code ++ */ ++ private static function create_tables_from_snapshot(string $snapshotdir, bool $showprogress = true) { ++ global $DB; ++ ++ $dbman = $DB->get_manager(); ++ ++ $structurefile = $snapshotdir . DIRECTORY_SEPARATOR . 'tablestructure.ser'; ++ if (!file_exists($structurefile)) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Snapshot file $structurefile does not exist!!"); ++ } ++ $tablestructure = unserialize(file_get_contents($structurefile)); ++ if (!is_array($tablestructure)) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Invalid tablestructure.ser in snapshot $snapshotdir"); ++ } ++ ++ $dotsonline = 0; ++ foreach ($tablestructure as $tablename => $columns) { ++ // If the table already exists, skip it. ++ if ($dbman->table_exists($tablename)) { ++ continue; ++ } ++ ++ $table = new \xmldb_table($tablename); ++ ++ // Build an xmldb_field for every column. ++ foreach ($columns as $colname => $col) { ++ $field = new \xmldb_field($colname); ++ $sequence = false; ++ // Map each column meta type to the appropriate xmldb type. ++ switch ($col->meta_type) { ++ case 'R': ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength(10); ++ $sequence = true; ++ break; ++ case 'I': ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength($col->max_length ?: 10); ++ break; ++ case 'N': ++ $field->setType(XMLDB_TYPE_NUMBER); ++ $field->setLength($col->max_length ?: 10); ++ $field->setDecimals($col->scale ?: 0); ++ break; ++ case 'C': ++ $field->setType(XMLDB_TYPE_CHAR); ++ $field->setLength($col->max_length ?: 255); ++ break; ++ case 'X': ++ $field->setType(XMLDB_TYPE_TEXT); ++ break; ++ case 'B': ++ $field->setType(XMLDB_TYPE_BINARY); ++ break; ++ case 'L': ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength(1); ++ break; ++ default: ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength($col->max_length ?: 10); ++ } ++ $field->setNotNull($col->not_null); ++ if ($col->has_default && $col->default_value !== '') { ++ $field->setDefault($col->default_value); ++ } ++ if ($sequence) { ++ $field->setSequence(true); ++ } ++ $table->addField($field); ++ } ++ if (isset($columns['id'])) { ++ // Add a primary key for the 'id' column. ++ $key = new \xmldb_key('primary', XMLDB_KEY_PRIMARY, ['id']); ++ $table->addKey($key); ++ } ++ $dbman->create_table($table); ++ ++ if ($dotsonline == 60) { ++ if ($showprogress) { ++ echo "\n"; ++ } ++ $dotsonline = 0; ++ } ++ if ($showprogress) { ++ echo '.'; ++ } ++ $dotsonline += 1; ++ } ++ ++ echo "\nTables successfully created from snapshot.\n"; ++ } ++ + /** + * Builds root/phpunit.xml file using defaults from /phpunit.xml.dist + * +diff --git a/public/lib/testing/classes/util.php b/public/lib/testing/classes/util.php +index 3073cde3bf7..0e4c36d7d29 100644 +--- a/public/lib/testing/classes/util.php ++++ b/public/lib/testing/classes/util.php +@@ -115,7 +115,7 @@ abstract class testing_util { + self::$dataroot = $dataroot; + } + +- /** ++ /** + * Returns the testing framework name + * @static + * @return string +@@ -125,6 +125,13 @@ abstract class testing_util { + return substr($classname, 0, strpos($classname, '_')); + } + ++ /** ++ * Reset the original data. ++ */ ++ protected static function reset_original_data() { ++ self::$originaldatafilesjsonadded = false; ++ } ++ + /** + * Get data generator + * @static +@@ -656,7 +663,11 @@ abstract class testing_util { + // Clean up the dataroot folder. + $handle = opendir(self::get_dataroot()); + while (false !== ($item = readdir($handle))) { +- if (in_array($item, $childclassname::$datarootskiponreset)) { ++ // Explicitly check for a snapshot zip and add it to the skip on reset array if there is one. ++ if (str_contains($item, 'snapshot-')) { ++ static::$datarootskiponreset[] = $item; ++ } ++ if (in_array($item, static::$datarootskiponreset)) { + continue; + } + if (is_dir(self::get_dataroot() . "/$item")) { +-- +2.43.0 + diff --git a/.github/patches/502-999-phpunit-restore.patch b/.github/patches/502-999-phpunit-restore.patch new file mode 100644 index 00000000..e2984cfe --- /dev/null +++ b/.github/patches/502-999-phpunit-restore.patch @@ -0,0 +1,499 @@ +From e9e25c9c214ce86aed65a6ddf1d4a9b179197551 Mon Sep 17 00:00:00 2001 +From: Abhinav Gandham +Date: Wed, 13 May 2026 15:33:48 +1000 +Subject: [PATCH] MDL-88495 unit tests: Implmented test site upgrade, snapshot, + and restore functionality." + +--- + public/admin/tool/phpunit/cli/util.php | 30 +- + .../lib/classes/test/phpunit/phpunit_util.php | 353 ++++++++++++++++++ + public/lib/classes/test/testing_util.php | 11 + + public/lib/phpunit/bootstraplib.php | 10 +- + 4 files changed, 397 insertions(+), 7 deletions(-) + +diff --git a/public/admin/tool/phpunit/cli/util.php b/public/admin/tool/phpunit/cli/util.php +index 0137344ba03..2bc7c5dd290 100644 +--- a/public/admin/tool/phpunit/cli/util.php ++++ b/public/admin/tool/phpunit/cli/util.php +@@ -49,6 +49,9 @@ list($options, $unrecognized) = cli_get_params( + 'buildcomponentconfigs' => false, + 'diag' => false, + 'run' => false, ++ 'upgrade' => false, ++ 'snapshot' => '', ++ 'restore' => '', + 'help' => false, + ], + [ +@@ -74,7 +77,7 @@ foreach ($requiredclasses as $requiredclass) { + } + } + +-if ($options['install'] || $options['drop']) { ++if ($options['install'] || $options['drop'] || $options['upgrade']) { + define('CACHE_DISABLE_ALL', true); + } + +@@ -115,8 +118,16 @@ $drop = $options['drop']; + $install = $options['install']; + $buildconfig = $options['buildconfig']; + $buildcomponentconfigs = $options['buildcomponentconfigs']; +- +-if ($options['help'] || (!$drop && !$install && !$buildconfig && !$buildcomponentconfigs && !$diag)) { ++$snapshot = $options['snapshot']; ++$restore = $options['restore']; ++$upgrade = $options['upgrade']; ++ ++if ( ++ $options['help'] || ( ++ !$drop && !$install && !$buildconfig && !$buildcomponentconfigs ++ && !$diag && !$upgrade && !$snapshot && !$restore ++ ) ++) { + $help = "Various PHPUnit utility functions + + Options: +@@ -127,6 +138,10 @@ Options: + --buildconfig Build /phpunit.xml from /phpunit.xml.dist that runs all tests + --buildcomponentconfigs + Build distributed phpunit.xml files for each component ++--upgrade Upgrade test site to latest version ++--snapshot Create snapshot of the current test site. Optionally specify name of the snapshot with ++ --snapshot=NAME. ++--restore=NAME Restore snapshot of test site with the specified name. + + -h, --help Print out this help + +@@ -168,4 +183,13 @@ if ($diag) { + } else if ($install) { + phpunit_util::install_site(); + exit(0); ++} else if ($upgrade) { ++ phpunit_util::upgrade_site(); ++ exit(0); ++} else if ($snapshot) { ++ phpunit_util::snapshot_site(is_string($snapshot) ? $snapshot : ''); ++ exit(0); ++} else if ($restore) { ++ phpunit_util::restore_site(is_string($restore) ? $restore : ''); ++ exit(0); + } +diff --git a/public/lib/classes/test/phpunit/phpunit_util.php b/public/lib/classes/test/phpunit/phpunit_util.php +index adfd5a61c2c..46fedd65872 100644 +--- a/public/lib/classes/test/phpunit/phpunit_util.php ++++ b/public/lib/classes/test/phpunit/phpunit_util.php +@@ -506,6 +506,359 @@ class phpunit_util extends \core\test\testing_util { + self::store_database_state(); + } + ++ /** ++ * Create a snapshot of the current test site database and site data. ++ * ++ * @param string $snapshotname The name of the snapshot. ++ * @return void may terminate execution with exit code ++ */ ++ public static function snapshot_site(string $snapshotname) { ++ global $DB, $CFG; ++ ++ require($CFG->dirroot . '/version.php'); ++ ++ if (!self::is_test_site()) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not snapshot non-test site!!'); ++ } ++ ++ if (empty($DB->get_tables())) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_INSTALL, 'No database tables present, can not snapshot site!!'); ++ } ++ ++ $dataroot = self::get_dataroot(); ++ $dbtype = $CFG->dbtype; ++ $name = $snapshotname ? "$dbtype-$snapshotname-$version" : "$dbtype-snapshot-$version"; ++ $snapshotdir = $dataroot . "/$name"; ++ ++ // Clear old in-memory caches of snapshot data. ++ self::$tabledata = null; ++ self::$tablestructure = null; ++ self::$sequencenames = null; ++ ++ $sitedatapath = $dataroot . DIRECTORY_SEPARATOR . self::get_originaldatafilesjson(); ++ ++ if (file_exists($sitedatapath)) { ++ unlink($sitedatapath); ++ } ++ ++ self::reset_original_data(); ++ self::save_original_data_files(); ++ self::store_database_state(); ++ self::store_versions_hash(); ++ self::$lastdbwrites = $DB->perf_get_writes(); ++ ++ if (is_dir($snapshotdir)) { ++ remove_dir($snapshotdir); ++ } ++ ++ mkdir($snapshotdir); ++ ++ $filedir = $dataroot . DIRECTORY_SEPARATOR . 'filedir'; ++ ++ self::copy_filedir($filedir, $snapshotdir . DIRECTORY_SEPARATOR . 'filedir' . DIRECTORY_SEPARATOR); ++ ++ $datafiles = [ ++ "$dataroot" . DIRECTORY_SEPARATOR . "phpunit" . DIRECTORY_SEPARATOR . "tabledata.ser", ++ "$dataroot" . DIRECTORY_SEPARATOR . "phpunit" . DIRECTORY_SEPARATOR . "tablestructure.ser", ++ "$dataroot" . DIRECTORY_SEPARATOR . "phpunit" . DIRECTORY_SEPARATOR . "versionshash.txt", ++ "$dataroot" . DIRECTORY_SEPARATOR . "originaldatafiles.json", ++ ]; ++ ++ foreach ($datafiles as $file) { ++ if (!file_exists($file)) { ++ continue; ++ } ++ copy($file, $snapshotdir . DIRECTORY_SEPARATOR . basename($file)); ++ } ++ ++ $dirtozip = $dataroot . DIRECTORY_SEPARATOR . "$name"; ++ ++ $zip = new \ZipArchive(); ++ $zip->open("$dirtozip.zip", \ZipArchive::CREATE | \ZipArchive::OVERWRITE); ++ ++ $files = new \RecursiveIteratorIterator( ++ new \RecursiveDirectoryIterator($dirtozip, \RecursiveDirectoryIterator::SKIP_DOTS), ++ \RecursiveIteratorIterator::LEAVES_ONLY ++ ); ++ ++ foreach ($files as $file) { ++ if (!$file->isDir()) { ++ $filepath = $file->getRealPath(); ++ $filepathrelative = substr($filepath, strlen($dirtozip) + 1); ++ $zip->addFile($filepath, $filepathrelative); ++ } ++ } ++ ++ $zip->close(); ++ remove_dir($snapshotdir); ++ ++ echo "Snapshot successfully created in $dirtozip.zip\n"; ++ } ++ ++ /** ++ * Restore the database and dataroot to the state they were in when snapshot_site() was called. ++ * ++ * @param string $snapshot The snapshot to restore. ++ * @return void may terminate execution with exit code ++ */ ++ public static function restore_site(string $snapshot) { ++ global $DB, $CFG; ++ ++ require($CFG->dirroot . '/version.php'); ++ ++ if (!self::is_test_site()) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not restore non-test site!!'); ++ } ++ ++ if (!empty($DB->get_tables())) { ++ echo "Database tables are still present. Run php public/admin/tool/phpunit/cli/util.php --drop before restoring.\n"; ++ return; ++ } ++ ++ if (!$snapshot) { ++ echo "No snapshot name provided. Please provide the snapshot name as an argument.\n"; ++ return; ++ } ++ ++ $dataroot = self::get_dataroot(); ++ $zipfile = $dataroot . DIRECTORY_SEPARATOR . "$snapshot.zip"; ++ $destpath = $dataroot . DIRECTORY_SEPARATOR . "$snapshot"; ++ ++ $zip = new \ZipArchive(); ++ if ($zip->open($zipfile) === true) { ++ $zip->extractTo($destpath); ++ $zip->close(); ++ } else { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Can not open snapshot zip file $zipfile!!"); ++ } ++ ++ $snapshotdir = $dataroot . "/$snapshot"; ++ ++ // Map snapshot files to their active locations. ++ $datafiles = [ ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "tabledata.ser" ++ => $dataroot . DIRECTORY_SEPARATOR . 'phpunit' . DIRECTORY_SEPARATOR . "tabledata.ser", ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "tablestructure.ser" ++ => $dataroot . DIRECTORY_SEPARATOR . 'phpunit' . DIRECTORY_SEPARATOR . "tablestructure.ser", ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "versionshash.txt" ++ => $dataroot . DIRECTORY_SEPARATOR . 'phpunit' . DIRECTORY_SEPARATOR . "versionshash.txt", ++ "$snapshotdir" . DIRECTORY_SEPARATOR . "originaldatafiles.json" ++ => $dataroot . DIRECTORY_SEPARATOR . self::get_originaldatafilesjson(), ++ ]; ++ ++ foreach ($datafiles as $src => $dest) { ++ if (!file_exists($src)) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Required snapshot file $src does not exist!!"); ++ } ++ } ++ ++ self::$lastdbwrites = null; ++ self::$tabledata = null; ++ self::$tablestructure = null; ++ self::$sequencenames = null; ++ ++ echo "Restoring from $snapshot\n"; ++ ++ // Copy snapshot DB control files to their active locations. ++ foreach ($datafiles as $src => $dest) { ++ copy($src, $dest); ++ } ++ ++ if (empty($DB->get_tables(false))) { ++ self::create_tables_from_snapshot($destpath); ++ } ++ ++ echo "Importing snapshot data...\n"; ++ // Restore the database to snapshot state. ++ self::reset_database(); ++ ++ echo "Data imported. Resetting datarooot...\n"; ++ // Clear current dataroot before restoring filedir from snapshot. ++ self::reset_dataroot(); ++ self::reset_original_data(); ++ static::$datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess']; ++ ++ $snapshotfiledir = $snapshotdir . DIRECTORY_SEPARATOR . 'filedir'; ++ $filedir = $dataroot . DIRECTORY_SEPARATOR . 'filedir'; ++ ++ // Copy snapshot filedir back to dataroot. ++ self::copy_filedir($snapshotfiledir, $filedir); ++ ++ initialise_cfg(); ++ ++ // Execute all adhoc tasks. ++ while ($task = \core\task\manager::get_next_adhoc_task(time())) { ++ $task->execute(); ++ \core\task\manager::adhoc_task_complete($task); ++ } ++ ++ set_config('upgraderunning', 0); ++ ++ self::store_database_state(); ++ self::$lastdbwrites = $DB->perf_get_writes(); ++ ++ remove_dir($snapshotdir); ++ ++ echo "Snapshot successfully restored.\n"; ++ } ++ ++ /** ++ * Upgrade the test site and upgrade/install any new plugins that were added. ++ * ++ * @return void may terminate execution with exit code ++ */ ++ public static function upgrade_site() { ++ ++ if (!self::is_test_site()) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not upgrade non-test site!!'); ++ } ++ ++ if (self::is_test_data_updated()) { ++ echo "No database changes detected, skipping upgrade.\n"; ++ return; ++ } ++ ++ initialise_cfg(); ++ upgrade_noncore(true); ++ set_config('upgraderunning', 0); ++ self::store_versions_hash(); ++ self::store_database_state(); ++ } ++ ++ /** ++ * Helper method that copies all contents from one directory to another, creating directories as needed. ++ * ++ * @param string $sourcedir the source directory to copy ++ * @param string $dirtocreate the destination directory to create and copy files to ++ * @return void ++ */ ++ private static function copy_filedir($sourcedir, $dirtocreate) { ++ ++ if (!file_exists($sourcedir)) { ++ echo "Source directory $sourcedir does not exist!\n"; ++ return; ++ } ++ ++ $iterator = new \RecursiveIteratorIterator( ++ new \RecursiveDirectoryIterator($sourcedir, \RecursiveDirectoryIterator::SKIP_DOTS), ++ \RecursiveIteratorIterator::SELF_FIRST ++ ); ++ ++ if (!is_dir($dirtocreate)) { ++ mkdir($dirtocreate, 0777, true); ++ } ++ ++ foreach ($iterator as $item) { ++ $destpath = $dirtocreate . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); ++ if ($item->isDir()) { ++ if (!is_dir($destpath)) { ++ mkdir($destpath, 0777, true); ++ } ++ } else { ++ copy($item->getPathname(), $destpath); ++ } ++ } ++ } ++ ++ /** ++ * Create the database tables based on the stored snapshot structure. ++ * ++ * @param string $snapshotdir the target snapshot directory to create the tables from. ++ * @param bool $showprogress whether to show progress during table creation. ++ * @return void may terminate execution with exit code ++ */ ++ private static function create_tables_from_snapshot(string $snapshotdir, bool $showprogress = true) { ++ global $DB; ++ ++ $dbman = $DB->get_manager(); ++ ++ $structurefile = $snapshotdir . DIRECTORY_SEPARATOR . 'tablestructure.ser'; ++ if (!file_exists($structurefile)) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Snapshot file $structurefile does not exist!!"); ++ } ++ $tablestructure = unserialize(file_get_contents($structurefile)); ++ if (!is_array($tablestructure)) { ++ phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, "Invalid tablestructure.ser in snapshot $snapshotdir"); ++ } ++ ++ $dotsonline = 0; ++ foreach ($tablestructure as $tablename => $columns) { ++ // If the table already exists, skip it. ++ if ($dbman->table_exists($tablename)) { ++ continue; ++ } ++ ++ $table = new \xmldb_table($tablename); ++ ++ // Build an xmldb_field for every column. ++ foreach ($columns as $colname => $col) { ++ $field = new \xmldb_field($colname); ++ $sequence = false; ++ // Map each column meta type to the appropriate xmldb type. ++ switch ($col->meta_type) { ++ case 'R': ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength(10); ++ $sequence = true; ++ break; ++ case 'I': ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength($col->max_length ?: 10); ++ break; ++ case 'N': ++ $field->setType(XMLDB_TYPE_NUMBER); ++ $field->setLength($col->max_length ?: 10); ++ $field->setDecimals($col->scale ?: 0); ++ break; ++ case 'C': ++ $field->setType(XMLDB_TYPE_CHAR); ++ $field->setLength($col->max_length ?: 255); ++ break; ++ case 'X': ++ $field->setType(XMLDB_TYPE_TEXT); ++ break; ++ case 'B': ++ $field->setType(XMLDB_TYPE_BINARY); ++ break; ++ case 'L': ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength(1); ++ break; ++ default: ++ $field->setType(XMLDB_TYPE_INTEGER); ++ $field->setLength($col->max_length ?: 10); ++ } ++ $field->setNotNull($col->not_null); ++ if ($col->has_default && $col->default_value !== '') { ++ $field->setDefault($col->default_value); ++ } ++ if ($sequence) { ++ $field->setSequence(true); ++ } ++ $table->addField($field); ++ } ++ if (isset($columns['id'])) { ++ // Add a primary key for the 'id' column. ++ $key = new \xmldb_key('primary', XMLDB_KEY_PRIMARY, ['id']); ++ $table->addKey($key); ++ } ++ $dbman->create_table($table); ++ ++ if ($dotsonline == 60) { ++ if ($showprogress) { ++ echo "\n"; ++ } ++ $dotsonline = 0; ++ } ++ if ($showprogress) { ++ echo '.'; ++ } ++ $dotsonline += 1; ++ } ++ ++ echo "\nTables successfully created from snapshot.\n"; ++ } ++ + /** + * Builds root/phpunit.xml file using defaults from /phpunit.xml.dist + * +diff --git a/public/lib/classes/test/testing_util.php b/public/lib/classes/test/testing_util.php +index c3aa2a8f3cb..d19aa0ad446 100644 +--- a/public/lib/classes/test/testing_util.php ++++ b/public/lib/classes/test/testing_util.php +@@ -140,6 +140,13 @@ abstract class testing_util { + return substr($classname, 0, strpos($classname, '_')); + } + ++ /** ++ * Reset the original data. ++ */ ++ protected static function reset_original_data() { ++ self::$originaldatafilesjsonadded = false; ++ } ++ + /** + * Get data generator + * +@@ -665,6 +672,10 @@ abstract class testing_util { + // Clean up the dataroot folder. + $handle = opendir(self::get_dataroot()); + while (false !== ($item = readdir($handle))) { ++ // Explicitly check for a snapshot zip and add it to the skip on reset array if there is one. ++ if (str_contains($item, 'snapshot-')) { ++ static::$datarootskiponreset[] = $item; ++ } + if (in_array($item, static::$datarootskiponreset)) { + continue; + } +diff --git a/public/lib/phpunit/bootstraplib.php b/public/lib/phpunit/bootstraplib.php +index 48c75dfa1dc..bf3112cf1f5 100644 +--- a/public/lib/phpunit/bootstraplib.php ++++ b/public/lib/phpunit/bootstraplib.php +@@ -65,12 +65,14 @@ function phpunit_bootstrap_error($errorcode, $text = '') { + $text = "Moodle PHPUnit environment configuration warning:\n".$text; + break; + case PHPUNIT_EXITCODE_INSTALL: +- $path = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); +- $text = "Moodle PHPUnit environment is not initialised, please use:\n php $path"; ++ $initpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); ++ $text = "Moodle PHPUnit environment is not initialised, please use:\n php $initpath"; + break; + case PHPUNIT_EXITCODE_REINSTALL: +- $path = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); +- $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php $path"; ++ $initpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); ++ $utilpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/util.php'); ++ $text = "Moodle PHPUnit environment was initialised for different version, please use:\n" ++ . " php $initpath\n or php $utilpath --upgrade"; + break; + default: + $text = empty($text) ? '' : ': '.$text; +-- +2.43.0 + diff --git a/.github/patches/README.md b/.github/patches/README.md new file mode 100644 index 00000000..dab3fbc5 --- /dev/null +++ b/.github/patches/README.md @@ -0,0 +1,43 @@ +# Shared Core Patches + +Patch files in this directory are applied to the Moodle source tree during CI setup, before +`moodle-plugin-ci install` runs. They are applied in alphabetical filename order using `git am`. + +## Naming convention + +``` +{min}-{max}-{description}.patch +``` + +| Part | Description | +|---|---| +| `min` | Lowest Moodle branch number this patch applies to (e.g. `500` for `MOODLE_500_STABLE`) | +| `max` | Highest Moodle branch number this patch applies to (e.g. `999` for `main`) | +| `description` | Freeform identifier (hyphens recommended) | + +The branch number is the `XXX` from `MOODLE_XXX_STABLE`. The `main` branch is treated as `999`. + +A patch is applied when `min <= current_branch_number <= max`. Patch files whose names do not +match the `NNN-NNN-` prefix are skipped with a warning. + +## Examples + +| Filename | Applies to | +|---|---| +| `500-999-phpunit-restore.patch` | Moodle 5.0 → main | +| `401-405-phpunit-restore.patch` | Moodle 4.1 → 4.5 | + +## Authoring patches + +Patches must be in `git format-patch` / `git am` format (i.e. include a commit header). To create +one: + +```bash +# Make your changes inside a Moodle checkout, then: +git add +git commit -m "Brief description of the patch" +git format-patch HEAD~1 --stdout > NNN-NNN-description.patch +``` + +The patch is applied with `git am --whitespace=nowarn`, so trailing-whitespace issues are ignored. +If `git am` fails the CI job fails immediately (fail-fast per matrix entry). diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 7195b2b6..ec0bd637 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -6,7 +6,8 @@ inputs: required: true node: description: 'Node.js version to use' - required: true + required: false + default: '' extra_php_extensions: description: 'List of additional php packages to install' extra_plugin_runners: @@ -17,6 +18,14 @@ inputs: description: 'Database type (e.g. pgsql, mariadb). Leave empty to skip installation.' required: false default: '' + run_plugin_ci_install: + description: 'Whether to run moodle-plugin-ci install for database-enabled jobs.' + required: false + default: 'true' + use_phpunit_snapshot: + description: 'Whether to restore Moodle tree and phpunit DB snapshot artifacts.' + required: false + default: 'false' runs: using: "composite" steps: @@ -34,6 +43,7 @@ runs: path: plugin submodules: true - name: Install node ${{ inputs.node }} + if: ${{ inputs.node != '' }} uses: actions/setup-node@v2 with: node-version: ${{ inputs.node }} @@ -59,68 +69,168 @@ runs: composer self-update ${{ steps.install-composer1.outputs.COMPOSER_VERSION }} shell: bash - - name: Initialise moodle-plugin-ci + - name: Install moodle-plugin-ci + if: ${{ inputs.use_phpunit_snapshot != 'true' }} run: | - # Initialise moodle-plugin-ci (install via composer) - echo "::group::Initialise moodle-plugin-ci" + START_TS=$(date +%s) + echo "::group::Install moodle-plugin-ci" composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci m-ci ^4 + + END_TS=$(date +%s) + echo "Install moodle-plugin-ci took $((END_TS - START_TS))s" + echo "::endgroup::" + shell: bash + + - name: Add moodle-plugin-ci binaries to PATH + run: | + START_TS=$(date +%s) + echo "::group::Add moodle-plugin-ci binaries to PATH" + # Add dirs to $PATH echo $(cd m-ci/bin; pwd) >> $GITHUB_PATH echo $(cd m-ci/vendor/bin; pwd) >> $GITHUB_PATH + + END_TS=$(date +%s) + echo "Add moodle-plugin-ci binaries to PATH took $((END_TS - START_TS))s" + echo "::endgroup::" + shell: bash + + - name: Generate en_AU locale + run: | + START_TS=$(date +%s) + echo "::group::Generate en_AU locale" + # PHPUnit depends on en_AU.UTF-8 locale sudo locale-gen en_AU.UTF-8 + END_TS=$(date +%s) + echo "Generate en_AU locale took $((END_TS - START_TS))s" echo "::endgroup::" shell: bash - name: Install dependencies if: ${{ inputs.extra_plugin_runners }} run: | + START_TS=$(date +%s) # Install dependencies echo "::group::Install dependencies" ${{ inputs.extra_plugin_runners }} + END_TS=$(date +%s) + echo "Install dependencies took $((END_TS - START_TS))s" echo "::endgroup::" shell: bash - - name: Clone Moodle - # Clone to a temporary directory + - name: Restore snapshot artifacts + if: ${{ inputs.database != '' && inputs.use_phpunit_snapshot == 'true' }} run: | - echo "::group::Moodle clone output" + START_TS=$(date +%s) + echo "::group::Restore snapshot artifacts" + + BRANCH="${{ inputs.moodle_branch }}" + DB="${{ inputs.database }}" + # Build image name: snapshot-{branch}-{db}, lowercased, underscores->hyphens. + IMAGE="ghcr.io/catalyst/snapshot-${BRANCH}-${DB}" + IMAGE="${IMAGE//_/-}" + IMAGE="${IMAGE,,}:latest" + + echo "Pulling snapshot image $IMAGE..." + docker pull "$IMAGE" + + # Extract snapshot artifacts from the image. + CID=$(docker create "$IMAGE") + if docker cp "$CID:/phpunit-snapshot.zip" ./phpunit-snapshot.zip; then + echo "Found phpunit snapshot archive in image." + else + echo "No phpunit snapshot archive found in image." + docker rm "$CID" + exit 1 + fi + if docker cp "$CID:/moodle.tar.gz" ./moodle.tar.gz; then + echo "Found Moodle snapshot tree archive in image." + else + echo "No Moodle snapshot tree archive found in image." + docker rm "$CID" + exit 1 + fi + if docker cp "$CID:/m-ci.tar.gz" ./m-ci.tar.gz; then + echo "Found moodle-plugin-ci archive in image." + else + echo "No moodle-plugin-ci archive found in image." + docker rm "$CID" + exit 1 + fi + docker rm "$CID" + + tar -C "$GITHUB_WORKSPACE" -xzf "$GITHUB_WORKSPACE/m-ci.tar.gz" - git clone https://github.com/moodle/moodle.git --branch $MOODLE_BRANCH $GITHUB_WORKSPACE/moodletemp + rm -rf "$GITHUB_WORKSPACE/moodle" + tar -C "$GITHUB_WORKSPACE" -xzf "$GITHUB_WORKSPACE/moodle.tar.gz" + END_TS=$(date +%s) + echo "Restore snapshot artifacts took $((END_TS - START_TS))s" echo "::endgroup::" shell: bash - env: - DB: ${{ inputs.database }} - MOODLE_BRANCH: ${{ inputs.moodle_branch }} - - name: Install Core Patches - if: ${{ always() }} - # Install core patches to moodle in temporary directory + - name: Apply plugin branch patch + if: ${{ inputs.database != '' && inputs.use_phpunit_snapshot == 'true' }} run: | + START_TS=$(date +%s) + echo "::group::Apply plugin branch patch" + git config --global user.email "test@test.com" git config --global user.name "Test" - ((test -f plugin/patch/${{ inputs.moodle_branch }}.diff && cd $GITHUB_WORKSPACE/moodletemp && git am --whitespace=nowarn < ../plugin/patch/${{ inputs.moodle_branch }}.diff) || echo No patch found;) + + TARGET_REPO="$GITHUB_WORKSPACE/moodle" + + ((test -f plugin/patch/${{ inputs.moodle_branch }}.diff && cd "$TARGET_REPO" && git am --whitespace=nowarn < "$GITHUB_WORKSPACE/plugin/patch/${{ inputs.moodle_branch }}.diff") || echo No plugin branch patch found;) + + END_TS=$(date +%s) + echo "Apply plugin branch patch took $((END_TS - START_TS))s" + echo "::endgroup::" shell: bash - name: Install Moodle and Plugin - if: ${{ inputs.database != '' }} - # Install moodle, but use our temporary directory to include any potential core patches that have been applied. + if: ${{ inputs.database != '' && inputs.run_plugin_ci_install == 'true' && inputs.use_phpunit_snapshot != 'true' }} run: | + START_TS=$(date +%s) # Install moodle commands echo "::group::Moodle install output" - # This is a workaround for https://github.com/moodlehq/moodle-plugin-ci/issues/306 - we disable the git repository validation to allow folder cloning. - sed -i '/public function gitUrl($url)/{n; s/{/& return $url;/}' $GITHUB_WORKSPACE/m-ci/src/Validate.php - - moodle-plugin-ci install -vvv --plugin ./plugin --db-host=127.0.0.1 --repo $GITHUB_WORKSPACE/moodletemp + moodle-plugin-ci install -vvv --plugin ./plugin --db-host=127.0.0.1 + END_TS=$(date +%s) + echo "Moodle install output took $((END_TS - START_TS))s" echo "::endgroup::" shell: bash env: DB: ${{ inputs.database }} - MOODLE_BRANCH: ${{ inputs.moodle_branch }} \ No newline at end of file + MOODLE_BRANCH: ${{ inputs.moodle_branch }} + + - name: Restore PHPUnit DB snapshot + if: ${{ inputs.database != '' && inputs.use_phpunit_snapshot == 'true' }} + run: | + START_TS=$(date +%s) + echo "::group::Restore PHPUnit DB snapshot" + + if [ ! -f "$GITHUB_WORKSPACE/phpunit-snapshot.zip" ]; then + echo "No phpunit snapshot archive present; can not restore phpunit DB snapshot." + exit 1 + fi + + cd "$GITHUB_WORKSPACE/moodle" + + PHPUNIT_UTIL=$(php -r "define('CLI_SCRIPT', true); require 'config.php'; echo \$CFG->dirroot . '/admin/tool/phpunit/cli/util.php';") + + PHPUNIT_DATAROOT=$(php -r "define('CLI_SCRIPT', true); require 'config.php'; echo \$CFG->phpunit_dataroot;") + mkdir -p "$PHPUNIT_DATAROOT" + + cp "$GITHUB_WORKSPACE/phpunit-snapshot.zip" "$PHPUNIT_DATAROOT/ci-phpunit.zip" + php "$PHPUNIT_UTIL" --restore=ci-phpunit + + END_TS=$(date +%s) + echo "Restore PHPUnit DB snapshot took $((END_TS - START_TS))s" + echo "::endgroup::" + shell: bash \ No newline at end of file diff --git a/.github/templates/moodle-config-template.php b/.github/templates/moodle-config-template.php new file mode 100644 index 00000000..08761f55 --- /dev/null +++ b/.github/templates/moodle-config-template.php @@ -0,0 +1,36 @@ +dbtype = '{{DBTYPE}}'; +$CFG->dblibrary = 'native'; +$CFG->dbhost = '127.0.0.1'; +$CFG->dbname = 'moodle'; +$CFG->dbuser = '{{DBTYPE}}' === 'pgsql' ? 'postgres' : 'root'; +$CFG->dbpass = ''; +$CFG->prefix = 'mdl_'; +$CFG->dboptions = [ + 'dbpersist' => 0, + 'dbport' => '', + 'dbsocket' => '', + 'dbcollation' => 'utf8mb4_unicode_ci', +]; + +// config.php lives at moodle/config.php; dataroot is a sibling of moodle/. +$CFG->wwwroot = 'http://localhost'; +$CFG->dataroot = dirname(__DIR__) . '/moodledata'; +$CFG->admin = 'admin'; +$CFG->directorypermissions = 02777; + +$CFG->phpunit_prefix = 'phpu_'; +$CFG->phpunit_dataroot = dirname(__DIR__) . '/moodledata/phpunit'; + +// Support both classic layout (lib/setup.php) and public/ layout (public/lib/setup.php). +if (file_exists(__DIR__ . '/public/lib/setup.php')) { + require_once(__DIR__ . '/public/lib/setup.php'); +} else { + require_once(__DIR__ . '/lib/setup.php'); +} diff --git a/.github/workflows/build-dockerfiles.yml b/.github/workflows/build-dockerfiles.yml deleted file mode 100644 index e5cbff88..00000000 --- a/.github/workflows/build-dockerfiles.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Build Dockerfiles - -on: - workflow_dispatch: - push: - branches: - - main - paths: - - '.github/actions/matrix/matrix_includes.yml' - - 'docker/**' - - '.github/workflows/build-dockerfiles.yml' - -jobs: - build-matrix: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python (for YAML parsing) - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - - name: Install PyYAML - run: pip install pyyaml - - - name: Extract unique moodle-branch/php matrix - id: set-matrix - run: | - import yaml - import json - with open('.github/actions/matrix/matrix_includes.yml') as f: - data = yaml.safe_load(f) - pairs = set((row['moodle-branch'], row['php']) for row in data['include']) - - def container_name(branch, php): - name = f"catalyst-moodle-workflows-{branch}-{php}" - name = name.replace('_', '-').lower() - return f"ghcr.io/catalyst/{name}:latest" - - def find_node(branch, php): - for row in data['include']: - if row['moodle-branch'] == branch and row['php'] == php: - return row.get('node', 20) - return 20 - - matrix = [ - { - 'moodle-branch': b, - 'php': p, - 'node': find_node(b, p), - 'container': container_name(b, p) - } - for b, p in sorted(pairs) - ] - print('::set-output name=matrix::' + json.dumps(matrix)) - shell: python - - build: - name: ${{ matrix.container }} - needs: build-matrix - runs-on: ubuntu-latest - strategy: - matrix: - include: ${{fromJson(needs.build-matrix.outputs.matrix)}} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build Docker image - run: | - docker build \ - --build-arg MOODLE_BRANCH=${{ matrix.moodle-branch }} \ - --build-arg PHP_VERSION=${{ matrix.php }} \ - --build-arg NODE_VERSION=${{ matrix.node }} \ - -t ${{ matrix.container }} \ - -f docker/Dockerfile docker/ - - - name: Push Docker image to GHCR - run: docker push ${{ matrix.container }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5a4afd5..0a035b1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -235,7 +235,7 @@ jobs: needs: prepare_matrix services: pgsql: - image: "postgres:${{ needs.prepare_matrix.outputs.latest_pgsql_ver }}" + image: ${{ needs.prepare_matrix.outputs.latest_pgsql_ver && format('postgres:{0}', needs.prepare_matrix.outputs.latest_pgsql_ver) || '' }} env: POSTGRES_USER: 'postgres' POSTGRES_HOST_AUTH_METHOD: 'trust' @@ -297,7 +297,7 @@ jobs: needs: prepare_matrix services: pgsql: - image: "postgres:${{ needs.prepare_matrix.outputs.latest_pgsql_ver }}" + image: ${{ needs.prepare_matrix.outputs.latest_pgsql_ver && format('postgres:{0}', needs.prepare_matrix.outputs.latest_pgsql_ver) || '' }} env: POSTGRES_USER: 'postgres' POSTGRES_HOST_AUTH_METHOD: 'trust' @@ -335,7 +335,7 @@ jobs: needs: prepare_matrix services: pgsql: - image: "postgres:${{ needs.prepare_matrix.outputs.latest_pgsql_ver }}" + image: ${{ needs.prepare_matrix.outputs.latest_pgsql_ver && format('postgres:{0}', needs.prepare_matrix.outputs.latest_pgsql_ver) || '' }} env: POSTGRES_USER: 'postgres' POSTGRES_HOST_AUTH_METHOD: 'trust' @@ -446,7 +446,7 @@ jobs: runs-on: 'ubuntu-latest' services: postgres: - image: "postgres:${{ matrix.pgsql-ver }}" + image: ${{ matrix.pgsql-ver && format('postgres:{0}', matrix.pgsql-ver) || '' }} env: POSTGRES_USER: 'postgres' POSTGRES_HOST_AUTH_METHOD: 'trust' @@ -458,7 +458,7 @@ jobs: ports: - 5432:5432 mariadb: - image: "mariadb:${{ matrix.mariadb-ver }}" + image: ${{ matrix.mariadb-ver && format('mariadb:{0}', matrix.mariadb-ver) || '' }} env: MYSQL_USER: 'root' MYSQL_ALLOW_EMPTY_PASSWORD: "true" @@ -483,15 +483,32 @@ jobs: uses: ./ci/.github/plugin/setup with: php: ${{ matrix.php }} - node: ${{ matrix.node }} extra_php_extensions: ${{ inputs.extra_php_extensions }} extra_plugin_runners: ${{ inputs.extra_plugin_runners }} moodle_branch: ${{ matrix.moodle-branch }} database: ${{ matrix.database }} + use_phpunit_snapshot: 'true' + - name: Install plugin into Moodle and upgrade phpunit site + shell: bash + run: | + TARGET_DIR=$(php -r "define('CLI_SCRIPT', true); require 'moodle/config.php'; list(\$type, \$name) = core_component::normalize_component('${{ needs.prepare_matrix.outputs.component }}'); \$dir = core_component::get_plugin_directory(\$type, \$name); if (empty(\$dir)) { fwrite(STDERR, 'Could not resolve plugin path for component ${{ needs.prepare_matrix.outputs.component }}\n'); exit(1); } echo \$dir;") + + if [ -z "$TARGET_DIR" ]; then + echo "Resolved plugin target directory is empty" + exit 1 + fi + + rm -rf "$TARGET_DIR" + mkdir -p "$TARGET_DIR" + cp -a plugin/. "$TARGET_DIR/" + + PHPUNIT_UTIL=$(php -r "define('CLI_SCRIPT', true); require 'moodle/config.php'; echo \$CFG->dirroot . '/admin/tool/phpunit/cli/util.php';") + + php "$PHPUNIT_UTIL" --upgrade - name: Run phpunit run: | - moodle-plugin-ci phpunit cd moodle + vendor/bin/phpunit --fail-on-risky --disallow-test-output --testsuite ${{ needs.prepare_matrix.outputs.component }}_testsuite vendor/bin/phpunit --fail-on-risky --disallow-test-output --filter tool_dataprivacy_metadata_registry_testcase vendor/bin/phpunit --fail-on-risky --disallow-test-output --testsuite core_privacy_testsuite --filter provider_testcase shell: bash @@ -510,7 +527,7 @@ jobs: runs-on: 'ubuntu-latest' services: postgres: - image: "postgres:${{ matrix.pgsql-ver }}" + image: ${{ matrix.pgsql-ver && format('postgres:{0}', matrix.pgsql-ver) || '' }} env: POSTGRES_USER: 'postgres' POSTGRES_HOST_AUTH_METHOD: 'trust' @@ -522,7 +539,7 @@ jobs: ports: - 5432:5432 mariadb: - image: "mariadb:${{ matrix.mariadb-ver }}" + image: ${{ matrix.mariadb-ver && format('mariadb:{0}', matrix.mariadb-ver) || '' }} env: MYSQL_USER: 'root' MYSQL_ALLOW_EMPTY_PASSWORD: "true" diff --git a/.github/workflows/update-snapshots.yml b/.github/workflows/update-snapshots.yml index 2bb1ca7e..77947141 100644 --- a/.github/workflows/update-snapshots.yml +++ b/.github/workflows/update-snapshots.yml @@ -4,6 +4,11 @@ on: workflow_dispatch: schedule: - cron: '0 0 * * 0' # Every Sunday at midnight UTC + push: + branches: + - main + paths: + - .github/actions/matrix/matrix_includes.yml permissions: packages: write @@ -111,10 +116,9 @@ jobs: with: path: ci - - name: Run setup (PHP, Node, moodle-plugin-ci, Moodle clone, shared patches) - # database is intentionally empty — we handle the Moodle install manually below - # so we can point moodle-plugin-ci at our minimal snapshot-plugin instead of - # this CI repo (which the composite action would check out as ./plugin/). + - name: Run setup (PHP, Node, moodle-plugin-ci bootstrap) + # database is intentionally empty; this workflow performs clone/patch + # directly and builds snapshot artifacts from the installed tree. uses: ./ci/.github/plugin/setup with: php: ${{ matrix.php }} @@ -122,58 +126,129 @@ jobs: moodle_branch: ${{ matrix.moodle-branch }} database: '' - - name: Install Moodle with snapshot placeholder plugin + - name: Clone Moodle for snapshot shell: bash + run: | + git clone https://github.com/moodle/moodle.git --branch "$MOODLE_BRANCH" "$GITHUB_WORKSPACE/moodle" env: - DB: ${{ matrix.database }} MOODLE_BRANCH: ${{ matrix.moodle-branch }} + + - name: Apply shared core patches + shell: bash + run: | + git config --global user.email "test@test.com" + git config --global user.name "Test" + + MOODLE_BRANCH="${{ matrix.moodle-branch }}" + if [[ "$MOODLE_BRANCH" == "main" ]]; then + BRANCH_NUM=999 + elif [[ "$MOODLE_BRANCH" =~ MOODLE_([0-9]+)_STABLE ]]; then + BRANCH_NUM=${BASH_REMATCH[1]} + else + echo "Could not parse branch number from '$MOODLE_BRANCH', skipping shared patches." + exit 0 + fi + + PATCHES_DIR="${GITHUB_WORKSPACE}/ci/.github/patches" + if [ ! -d "$PATCHES_DIR" ]; then + echo "No shared patches directory found at $PATCHES_DIR, skipping." + exit 0 + fi + + shopt -s nullglob + PATCH_FILES=( "$PATCHES_DIR"/*.patch ) + if [ ${#PATCH_FILES[@]} -eq 0 ]; then + echo "No patch files found in $PATCHES_DIR" + exit 0 + fi + + for PATCHFILE in "${PATCH_FILES[@]}"; do + BASENAME=$(basename "$PATCHFILE") + if [[ "$BASENAME" =~ ^([0-9]+)-([0-9]+)- ]]; then + MIN_VER=${BASH_REMATCH[1]} + MAX_VER=${BASH_REMATCH[2]} + if (( BRANCH_NUM < MIN_VER || BRANCH_NUM > MAX_VER )); then + echo "Skipping $BASENAME (branch $BRANCH_NUM not in range $MIN_VER-$MAX_VER)" + continue + fi + else + echo "WARNING: patch $BASENAME has no version prefix (expected NNN-NNN-name.patch), skipping." + continue + fi + + echo "Applying shared patch $BASENAME to branch $MOODLE_BRANCH..." + cd "$GITHUB_WORKSPACE/moodle" + git am --whitespace=nowarn < "$PATCHFILE" + done + + - name: Install Moodle composer dependencies + shell: bash run: | - # The composite setup action checked out this CI repo into ./plugin/. - # Replace it with a minimal inline Moodle plugin so that - # moodle-plugin-ci has a valid plugin to install against. - rm -rf ./plugin - mkdir -p ./plugin/lang/en - printf 'component = "local_snapshottest";\n$plugin->version = 2026010100;\n$plugin->requires = 2021051700;\n' \ - > ./plugin/version.php - printf ' ./plugin/lang/en/local_snapshottest.php - - # Workaround for https://github.com/moodlehq/moodle-plugin-ci/issues/306 - # (disables git URL validation to allow folder-based repo cloning). - sed -i '/public function gitUrl($url)/{n; s/{/& return $url;/}' \ - "$GITHUB_WORKSPACE/m-ci/src/Validate.php" - - moodle-plugin-ci install -vvv \ - --plugin ./plugin \ - --db-host=127.0.0.1 \ - --repo "$GITHUB_WORKSPACE/moodletemp" - - - name: Initialise PHPUnit and export database + composer --working-dir="moodle" install --no-interaction --prefer-dist + + - name: Install PHPUnit DB for snapshot (no plugin install) shell: bash env: DB: ${{ matrix.database }} run: | - # Moodle 5.0+ uses a public/ webroot subdirectory; 4.x does not. - if [ -d moodle/public ]; then - UTIL="moodle/public/admin/tool/phpunit/cli/util.php" + if [ "$DB" = "pgsql" ]; then + DBTYPE="pgsql" + psql -h 127.0.0.1 -U postgres -d postgres -c "DROP DATABASE IF EXISTS moodle;" + psql -h 127.0.0.1 -U postgres -d postgres -c "CREATE DATABASE moodle;" else - UTIL="moodle/admin/tool/phpunit/cli/util.php" + DBTYPE="mariadb" + mysql -h 127.0.0.1 -u root -e "DROP DATABASE IF EXISTS moodle; CREATE DATABASE moodle DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" fi - echo "Initialising PHPUnit test site..." - php "$UTIL" --install + # Render config.php from template — only DBTYPE varies. + sed "s|{{DBTYPE}}|$DBTYPE|g" \ + "$GITHUB_WORKSPACE/ci/.github/templates/moodle-config-template.php" > moodle/config.php - # Read the PHPUnit database name from Moodle's config. - PHPUNIT_DBNAME=$(php -r "define('CLI_SCRIPT', true); require 'moodle/config.php'; echo \$CFG->phpunit_dbname;") - echo "Exporting PHPUnit database '$PHPUNIT_DBNAME'..." - - if [ "$DB" = "pgsql" ]; then - pg_dump -h 127.0.0.1 -U postgres "$PHPUNIT_DBNAME" > snapshot.sql + # Detect phpunit CLI location (differs between classic and public/ layouts). + if [ -f "moodle/public/admin/tool/phpunit/cli/init.php" ]; then + PHPUNIT_CLI="moodle/public/admin/tool/phpunit/cli" + elif [ -f "moodle/admin/tool/phpunit/cli/init.php" ]; then + PHPUNIT_CLI="moodle/admin/tool/phpunit/cli" else - mysqldump -h 127.0.0.1 -u root "$PHPUNIT_DBNAME" > snapshot.sql + echo "Could not locate Moodle phpunit init script in moodle/ or moodle/public/." + exit 1 fi - echo "SNAPSHOT_SQL=$GITHUB_WORKSPACE/snapshot.sql" >> "$GITHUB_ENV" + mkdir -p "$GITHUB_WORKSPACE/moodledata/phpunit" + php "$PHPUNIT_CLI/init.php" + + - name: Archive phpunit-ready Moodle tree + shell: bash + run: | + rm -rf moodle/.git + tar -C "$GITHUB_WORKSPACE" -czf "$GITHUB_WORKSPACE/moodle.tar.gz" moodle + echo "MOODLE_ARCHIVE=$GITHUB_WORKSPACE/moodle.tar.gz" >> "$GITHUB_ENV" + + - name: Export phpunit snapshot + shell: bash + run: | + cd moodle + + PHPUNIT_UTIL=$(php -r "define('CLI_SCRIPT', true); require 'config.php'; echo \$CFG->dirroot . '/admin/tool/phpunit/cli/util.php';") + + php "$PHPUNIT_UTIL" --snapshot=ci-phpunit + + PHPUNIT_DATAROOT=$(php -r "define('CLI_SCRIPT', true); require 'config.php'; echo \$CFG->phpunit_dataroot;") + PHPUNIT_SNAPSHOT=$(find "$PHPUNIT_DATAROOT" -maxdepth 1 -type f -name '*-ci-phpunit-*.zip' | head -n1) + + if [ -z "$PHPUNIT_SNAPSHOT" ]; then + echo "Could not find phpunit snapshot archive in $PHPUNIT_DATAROOT" + exit 1 + fi + + cp "$PHPUNIT_SNAPSHOT" "$GITHUB_WORKSPACE/phpunit-snapshot.zip" + echo "PHPUNIT_SNAPSHOT=$GITHUB_WORKSPACE/phpunit-snapshot.zip" >> "$GITHUB_ENV" + + - name: Archive moodle-plugin-ci + shell: bash + run: | + tar -C "$GITHUB_WORKSPACE" -czf "$GITHUB_WORKSPACE/m-ci.tar.gz" m-ci + echo "MCI_ARCHIVE=$GITHUB_WORKSPACE/m-ci.tar.gz" >> "$GITHUB_ENV" - name: Log in to GitHub Container Registry uses: docker/login-action@v3 @@ -188,8 +263,10 @@ jobs: IMAGE="${{ matrix.snapshot-image }}" BUILD_DIR=$(mktemp -d) - cp "$SNAPSHOT_SQL" "$BUILD_DIR/snapshot.sql" - printf 'FROM alpine:3\nCOPY snapshot.sql /snapshot.sql\n' > "$BUILD_DIR/Dockerfile" + cp "$PHPUNIT_SNAPSHOT" "$BUILD_DIR/phpunit-snapshot.zip" + cp "$MOODLE_ARCHIVE" "$BUILD_DIR/moodle.tar.gz" + cp "$MCI_ARCHIVE" "$BUILD_DIR/m-ci.tar.gz" + printf 'FROM alpine:3\nCOPY phpunit-snapshot.zip /phpunit-snapshot.zip\nCOPY moodle.tar.gz /moodle.tar.gz\nCOPY m-ci.tar.gz /m-ci.tar.gz\n' > "$BUILD_DIR/Dockerfile" docker build -t "$IMAGE" "$BUILD_DIR" docker push "$IMAGE" From 0a121698b5e1ef2d0ada21cab401304487d4d6c9 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Thu, 21 May 2026 09:23:07 +1000 Subject: [PATCH 02/43] add version bump enforcement --- .github/workflows/ci.yml | 68 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a035b1b..32e1bc18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,6 +167,74 @@ jobs: filter: ${{ inputs.moodle_branches }} min_php: ${{ inputs.min_php }} + version_bump_check: + name: Enforce version bump on PRs + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Check out plugin code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Validate plugin version bump + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + + php <<'PHP' + version\\s*=\\s*([0-9]+(?:\\.[0-9]+)?)\\s*;/', $content, $matches)) { + fwrite(STDERR, "Could not find numeric \\$plugin->version in version.php at commit {$sha}.\n"); + exit(1); + } + + return $matches[1]; + } + + $baseVersion = load_version_from_sha($baseSha); + $headVersion = load_version_from_sha($headSha); + + echo "Base branch version: {$baseVersion}\n"; + echo "Feature branch version: {$headVersion}\n"; + + $bump = (float)$headVersion - (float)$baseVersion; + + if ($bump <= 0.0) { + fwrite(STDERR, "Error: version.php must include a version bump in pull requests.\n"); + exit(1); + } + + if ($bump > 1.0) { + fwrite(STDERR, "Error: version bump must be <= 1.0.\n"); + fwrite(STDERR, sprintf("Computed bump was %.10f.\n", $bump)); + exit(1); + } + + echo sprintf("Computed bump: %.10f\n", $bump); + PHP + + echo "Version bump check passed." + shell: bash + codechecker: if: needs.pre_job.outputs.should_skip != 'true' runs-on: ubuntu-latest From 02f0180fa100634f6e5740db5b6d797921699c18 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Thu, 21 May 2026 09:28:57 +1000 Subject: [PATCH 03/43] add flag to disable version checks --- .github/workflows/ci.yml | 6 +++++- README.md | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32e1bc18..8511d81f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,10 @@ on: disable_ci_validate: type: boolean default: false + disable_version_bump_check: + description: 'If true, this will skip enforcing version bump checks on pull requests' + type: boolean + default: false release_branches: description: 'Required if the branch that should process releases is in a non-standard format (e.g. main or MOODLE_XX_STABLE)' type: string @@ -169,7 +173,7 @@ jobs: version_bump_check: name: Enforce version bump on PRs - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && inputs.disable_version_bump_check != true runs-on: ubuntu-latest steps: - name: Check out plugin code diff --git a/README.md b/README.md index a2743676..aaffc8cf 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ Below lists the available inputs which are _all optional_: | disable_master | If `true`, this will skip testing against moodle/master branch | | disable_release | If `true`, this will skip the release job | | disable_ci_validate | If `true`, this will skip moodle-plugin-ci validate checks | +| disable_version_bump_check | If `true`, this will skip pull-request version bump enforcement checks | | enable_phpmd | If `true`, to enable phpmd | | release_branches | Name of the non-standardly named branch which should run the release job | | moodle_branches | Specify the MOODLE_XX_STABLE branch you specifically want to test against. This is _not_ recommended, and instead you should configuring a supported range. | From 32de370a54c5c2174a54d0335eea103b69a40a00 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Thu, 21 May 2026 09:37:44 +1000 Subject: [PATCH 04/43] cleanup, enforce ints instead of floats --- .github/workflows/ci.yml | 43 ++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8511d81f..d9edfae3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,13 +173,19 @@ jobs: version_bump_check: name: Enforce version bump on PRs - if: github.event_name == 'pull_request' && inputs.disable_version_bump_check != true + needs: pre_job + if: needs.pre_job.outputs.should_skip != 'true' && github.event_name == 'pull_request' && inputs.disable_version_bump_check != true runs-on: ubuntu-latest steps: - name: Check out plugin code uses: actions/checkout@v3 with: fetch-depth: 0 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none - name: Validate plugin version bump env: BASE_SHA: ${{ github.event.pull_request.base.sha }} @@ -197,30 +203,45 @@ jobs: exit(1); } - function load_version_from_sha(string $sha): string { + function load_version_from_sha(string $sha, bool $allowFloat): string { $target = escapeshellarg($sha . ':version.php'); - $content = shell_exec("git show {$target}"); + $outputLines = []; + $exitCode = 0; + exec("git show {$target} 2>&1", $outputLines, $exitCode); - if ($content === null) { - fwrite(STDERR, "Failed to read version.php for commit {$sha}.\n"); + if ($exitCode !== 0) { + $outputText = trim(implode("\n", $outputLines)); + fwrite(STDERR, "Failed to read version.php for commit {$sha} (git show exit code {$exitCode}).\n"); + if ($outputText !== '') { + fwrite(STDERR, "git show output:\n{$outputText}\n"); + } exit(1); } - if (!preg_match('/\\$plugin->version\\s*=\\s*([0-9]+(?:\\.[0-9]+)?)\\s*;/', $content, $matches)) { - fwrite(STDERR, "Could not find numeric \\$plugin->version in version.php at commit {$sha}.\n"); + $content = implode("\n", $outputLines); + + $pattern = $allowFloat + ? '/\\$plugin->version\\s*=\\s*([0-9]+(?:\\.[0-9]+)?)\\s*;/' + : '/\\$plugin->version\\s*=\\s*([0-9]+)\\s*;/'; + + if (!preg_match($pattern, $content, $matches)) { + $expectedType = $allowFloat ? 'numeric (int or float)' : 'integer'; + fwrite(STDERR, "Could not find {$expectedType} \\$plugin->version in version.php at commit {$sha}.\n"); exit(1); } return $matches[1]; } - $baseVersion = load_version_from_sha($baseSha); - $headVersion = load_version_from_sha($headSha); + $baseVersion = load_version_from_sha($baseSha, true); + $headVersion = load_version_from_sha($headSha, false); echo "Base branch version: {$baseVersion}\n"; echo "Feature branch version: {$headVersion}\n"; - $bump = (float)$headVersion - (float)$baseVersion; + $baseVersionFloat = (float)$baseVersion; + $headVersionFloat = (float)$headVersion; + $bump = $headVersionFloat - $baseVersionFloat; if ($bump <= 0.0) { fwrite(STDERR, "Error: version.php must include a version bump in pull requests.\n"); @@ -228,7 +249,7 @@ jobs: } if ($bump > 1.0) { - fwrite(STDERR, "Error: version bump must be <= 1.0.\n"); + fwrite(STDERR, "Error: version bump must be <= 1.\n"); fwrite(STDERR, sprintf("Computed bump was %.10f.\n", $bump)); exit(1); } From 447cabc312ed4bf1f53d5df5025396bbd0418dbd Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Thu, 21 May 2026 09:42:38 +1000 Subject: [PATCH 05/43] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9edfae3..0da904ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,7 +180,15 @@ jobs: - name: Check out plugin code uses: actions/checkout@v3 with: - fetch-depth: 0 + fetch-depth: 1 + - name: Fetch PR base and head commits + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + git fetch --no-tags --depth=1 origin "${BASE_SHA}" + git fetch --no-tags --depth=1 origin "${HEAD_SHA}" - name: Install PHP uses: shivammathur/setup-php@v2 with: From 98ff2d8306207f6c085f572838d365e1c4cc7ddf Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Thu, 21 May 2026 12:44:10 +1000 Subject: [PATCH 06/43] change is run logic to run on PR's and pushes to protected branches --- .github/workflows/ci.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0da904ba..4a63c428 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,7 @@ jobs: runs-on: ubuntu-latest # Map a step output to a job output outputs: - should_skip: ${{ steps.skip_check.outputs.should_skip }} + should_skip: ${{ steps.decide.outputs.should_skip }} steps: - id: skip_check # docs: https://github.com/fkirc/skip-duplicate-actions#skip-concurrent-workflow-runs @@ -89,6 +89,20 @@ jobs: skip_after_successful_duplicate: false # Ensure should_skip is true only if 2 concurrent workflows are working on the same files (content) concurrent_skipping: ${{ inputs.concurrent_skipping }} + - id: decide + name: Decide whether CI should run + env: + EVENT_NAME: ${{ github.event_name }} + REF_PROTECTED: ${{ github.ref_protected }} + DUPLICATE_SHOULD_SKIP: ${{ steps.skip_check.outputs.should_skip }} + run: | + should_skip=true + + if [[ "${EVENT_NAME}" == "pull_request" || ("${EVENT_NAME}" == "push" && "${REF_PROTECTED}" == "true") ]]; then + should_skip="${DUPLICATE_SHOULD_SKIP}" + fi + + echo "should_skip=${should_skip}" >> "${GITHUB_OUTPUT}" prepare_matrix: needs: pre_job if: needs.pre_job.outputs.should_skip != 'true' From 3eb0e0feb759daa345c28104a79c0c53228dac61 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Thu, 21 May 2026 12:47:34 +1000 Subject: [PATCH 07/43] swap de-duplicate order --- .github/workflows/ci.yml | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a63c428..039a2d29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,9 +79,24 @@ jobs: runs-on: ubuntu-latest # Map a step output to a job output outputs: - should_skip: ${{ steps.decide.outputs.should_skip }} + should_skip: ${{ steps.finalize.outputs.should_skip }} steps: + - id: eligibility + name: Check branch/event eligibility + env: + EVENT_NAME: ${{ github.event_name }} + REF_PROTECTED: ${{ github.ref_protected }} + run: | + eligible=false + + if [[ "${EVENT_NAME}" == "pull_request" || ("${EVENT_NAME}" == "push" && "${REF_PROTECTED}" == "true") ]]; then + eligible=true + fi + + echo "eligible=${eligible}" >> "${GITHUB_OUTPUT}" + - id: skip_check + if: steps.eligibility.outputs.eligible == 'true' # docs: https://github.com/fkirc/skip-duplicate-actions#skip-concurrent-workflow-runs uses: fkirc/skip-duplicate-actions@04a1aebece824b56e6ad6a401d015479cd1c50b3 # Oct 9, 2024 commit (current latest) with: @@ -89,16 +104,16 @@ jobs: skip_after_successful_duplicate: false # Ensure should_skip is true only if 2 concurrent workflows are working on the same files (content) concurrent_skipping: ${{ inputs.concurrent_skipping }} - - id: decide + + - id: finalize name: Decide whether CI should run env: - EVENT_NAME: ${{ github.event_name }} - REF_PROTECTED: ${{ github.ref_protected }} + ELIGIBLE: ${{ steps.eligibility.outputs.eligible }} DUPLICATE_SHOULD_SKIP: ${{ steps.skip_check.outputs.should_skip }} run: | should_skip=true - if [[ "${EVENT_NAME}" == "pull_request" || ("${EVENT_NAME}" == "push" && "${REF_PROTECTED}" == "true") ]]; then + if [[ "${ELIGIBLE}" == "true" ]]; then should_skip="${DUPLICATE_SHOULD_SKIP}" fi From c66662088d30c1951e20084900232125e1c9e561 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Thu, 21 May 2026 13:06:24 +1000 Subject: [PATCH 08/43] remove de-duplicate step, causing issues --- .github/workflows/ci.yml | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 039a2d29..286ea50e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,42 +79,18 @@ jobs: runs-on: ubuntu-latest # Map a step output to a job output outputs: - should_skip: ${{ steps.finalize.outputs.should_skip }} + should_skip: ${{ steps.eligibility.outputs.should_skip }} steps: - id: eligibility name: Check branch/event eligibility env: EVENT_NAME: ${{ github.event_name }} REF_PROTECTED: ${{ github.ref_protected }} - run: | - eligible=false - - if [[ "${EVENT_NAME}" == "pull_request" || ("${EVENT_NAME}" == "push" && "${REF_PROTECTED}" == "true") ]]; then - eligible=true - fi - - echo "eligible=${eligible}" >> "${GITHUB_OUTPUT}" - - - id: skip_check - if: steps.eligibility.outputs.eligible == 'true' - # docs: https://github.com/fkirc/skip-duplicate-actions#skip-concurrent-workflow-runs - uses: fkirc/skip-duplicate-actions@04a1aebece824b56e6ad6a401d015479cd1c50b3 # Oct 9, 2024 commit (current latest) - with: - # Do not trust previous successful runs - skip_after_successful_duplicate: false - # Ensure should_skip is true only if 2 concurrent workflows are working on the same files (content) - concurrent_skipping: ${{ inputs.concurrent_skipping }} - - - id: finalize - name: Decide whether CI should run - env: - ELIGIBLE: ${{ steps.eligibility.outputs.eligible }} - DUPLICATE_SHOULD_SKIP: ${{ steps.skip_check.outputs.should_skip }} run: | should_skip=true - if [[ "${ELIGIBLE}" == "true" ]]; then - should_skip="${DUPLICATE_SHOULD_SKIP}" + if [[ "${EVENT_NAME}" == "pull_request" || ( "${EVENT_NAME}" == "push" && "${REF_PROTECTED}" == "true" ) ]]; then + should_skip=false fi echo "should_skip=${should_skip}" >> "${GITHUB_OUTPUT}" From c79470dab4a924be84395a3dbc2d72bfe75520fc Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Thu, 21 May 2026 14:18:13 +1000 Subject: [PATCH 09/43] only run for MOODLE_*_STABLE branches --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 286ea50e..e64c9fbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -179,7 +179,11 @@ jobs: version_bump_check: name: Enforce version bump on PRs needs: pre_job - if: needs.pre_job.outputs.should_skip != 'true' && github.event_name == 'pull_request' && inputs.disable_version_bump_check != true + if: | + needs.pre_job.outputs.should_skip != 'true' && + github.event_name == 'pull_request' && + inputs.disable_version_bump_check != true && + startsWith(github.base_ref, 'MOODLE_') && endsWith(github.base_ref, '_STABLE') runs-on: ubuntu-latest steps: - name: Check out plugin code From 27059948cc6cd5fc9d3aa54ea998de9d5786d142 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Mon, 25 May 2026 16:17:53 +1000 Subject: [PATCH 10/43] Show errors as annotations --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e64c9fbf..989a4835 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -261,13 +261,12 @@ jobs: $bump = $headVersionFloat - $baseVersionFloat; if ($bump <= 0.0) { - fwrite(STDERR, "Error: version.php must include a version bump in pull requests.\n"); + echo "::error file=version.php::version.php must include a version bump in pull requests.\n"; exit(1); } if ($bump > 1.0) { - fwrite(STDERR, "Error: version bump must be <= 1.\n"); - fwrite(STDERR, sprintf("Computed bump was %.10f.\n", $bump)); + echo sprintf("::error file=version.php::version bump must be <= 1 (computed bump was %.10f).\n", $bump); exit(1); } From 80cd1365f3cada64657d58ac9b0aad66a464989a Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Mon, 25 May 2026 16:23:55 +1000 Subject: [PATCH 11/43] Replace errors with annotations --- .github/actions/parse-version/script.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/parse-version/script.php b/.github/actions/parse-version/script.php index 6226dfc8..84595efb 100644 --- a/.github/actions/parse-version/script.php +++ b/.github/actions/parse-version/script.php @@ -79,11 +79,11 @@ function output(string $name, string $value) { $updatesResponse = substr($rawResponse, $headerSize); if ($rawResponse === false || $curlError) { - fwrite(STDERR, "Error: Failed to fetch Moodle updates from $updatesUrl: $curlError\n"); + echo "::error::Failed to fetch Moodle updates from $updatesUrl: $curlError\n"; exit(1); } if ($httpCode !== 200) { - fwrite(STDERR, "Error: Unexpected HTTP $httpCode fetching Moodle updates from $updatesUrl\n"); + echo "::error::Unexpected HTTP $httpCode fetching Moodle updates from $updatesUrl\n"; fwrite(STDERR, "Response headers:\n$responseHeaders\n"); exit(1); } From 87d42fc098b807627652c4032674703a26ffe0b3 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Mon, 25 May 2026 16:50:27 +1000 Subject: [PATCH 12/43] Add time to various cli commands --- .github/plugin/setup/action.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index ec0bd637..700af22e 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -66,7 +66,7 @@ runs: - name: Update Composer if: ${{ inputs.moodle_branch == 'MOODLE_32_STABLE' || inputs.moodle_branch == 'MOODLE_33_STABLE' }} run: | - composer self-update ${{ steps.install-composer1.outputs.COMPOSER_VERSION }} + time composer self-update ${{ steps.install-composer1.outputs.COMPOSER_VERSION }} shell: bash - name: Install moodle-plugin-ci @@ -87,6 +87,7 @@ runs: START_TS=$(date +%s) echo "::group::Add moodle-plugin-ci binaries to PATH" + time composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci m-ci ^4 # Add dirs to $PATH echo $(cd m-ci/bin; pwd) >> $GITHUB_PATH echo $(cd m-ci/vendor/bin; pwd) >> $GITHUB_PATH @@ -135,6 +136,7 @@ runs: IMAGE="ghcr.io/catalyst/snapshot-${BRANCH}-${DB}" IMAGE="${IMAGE//_/-}" IMAGE="${IMAGE,,}:latest" + time git clone https://github.com/moodle/moodle.git --branch $MOODLE_BRANCH $GITHUB_WORKSPACE/moodletemp echo "Pulling snapshot image $IMAGE..." docker pull "$IMAGE" @@ -222,7 +224,7 @@ runs: cd "$GITHUB_WORKSPACE/moodle" - PHPUNIT_UTIL=$(php -r "define('CLI_SCRIPT', true); require 'config.php'; echo \$CFG->dirroot . '/admin/tool/phpunit/cli/util.php';") + time moodle-plugin-ci install -vvv --plugin ./plugin --db-host=127.0.0.1 --repo $GITHUB_WORKSPACE/moodletemp PHPUNIT_DATAROOT=$(php -r "define('CLI_SCRIPT', true); require 'config.php'; echo \$CFG->phpunit_dataroot;") mkdir -p "$PHPUNIT_DATAROOT" From a6f650d49304b7a64d156e22fc24aedaf538ae68 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Mon, 25 May 2026 16:59:09 +1000 Subject: [PATCH 13/43] Remove need to install Moodle for static analysis jobs --- .github/plugin/setup/action.yml | 42 ++++++++++++++++++++++++++++++++- .github/workflows/ci.yml | 31 +++--------------------- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 700af22e..22c010a2 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -221,6 +221,43 @@ runs: echo "No phpunit snapshot archive present; can not restore phpunit DB snapshot." exit 1 fi + - name: Install plugin into Moodle (no database) + if: ${{ inputs.database == '' }} + # For static analysis jobs that don't need a database, copy the plugin into the Moodle + # directory so that tools like mustache and grunt can find it within Moodle's directory tree. + run: | + COMPONENT=$(php -r " + define('MOODLE_INTERNAL', true); + define('MATURITY_ALPHA', 50); + define('MATURITY_BETA', 100); + define('MATURITY_RC', 150); + define('MATURITY_STABLE', 200); + \$plugin = new stdClass(); + include 'plugin/version.php'; + echo \$plugin->component; + ") + TYPE=$(echo "$COMPONENT" | cut -d_ -f1) + NAME=$(echo "$COMPONENT" | cut -d_ -f2-) + TYPE_DIR=$(php -r " + \$components = json_decode(file_get_contents('$GITHUB_WORKSPACE/moodletemp/lib/components.json'), true); + \$type = '$TYPE'; + if (!isset(\$components['plugintypes'][\$type])) { fwrite(STDERR, 'Unknown plugin type: ' . \$type . PHP_EOL); exit(1); } + echo \$components['plugintypes'][\$type]; + ") + PLUGIN_PATH="$GITHUB_WORKSPACE/moodletemp/$TYPE_DIR/$NAME" + mkdir -p "$PLUGIN_PATH" + cp -r plugin/. "$PLUGIN_PATH/" + rm -rf "$PLUGIN_PATH/.git" + echo "MOODLE_DIR=$GITHUB_WORKSPACE/moodletemp" >> "$GITHUB_ENV" + echo "PLUGIN_DIR=$PLUGIN_PATH" >> "$GITHUB_ENV" + shell: bash + + - name: Install Moodle and Plugin + if: ${{ inputs.database != '' }} + # Install moodle, but use our temporary directory to include any potential core patches that have been applied. + run: | + # Install moodle commands + echo "::group::Moodle install output" cd "$GITHUB_WORKSPACE/moodle" @@ -235,4 +272,7 @@ runs: END_TS=$(date +%s) echo "Restore PHPUnit DB snapshot took $((END_TS - START_TS))s" echo "::endgroup::" - shell: bash \ No newline at end of file + shell: bash + env: + DB: ${{ inputs.database }} + MOODLE_BRANCH: ${{ inputs.moodle_branch }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 989a4835..fc763024 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -404,19 +404,6 @@ jobs: if: needs.pre_job.outputs.should_skip != 'true' runs-on: ubuntu-latest needs: prepare_matrix - services: - pgsql: - image: ${{ needs.prepare_matrix.outputs.latest_pgsql_ver && format('postgres:{0}', needs.prepare_matrix.outputs.latest_pgsql_ver) || '' }} - env: - POSTGRES_USER: 'postgres' - POSTGRES_HOST_AUTH_METHOD: 'trust' - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 3 - ports: - - 5432:5432 steps: - name: Check out CI code uses: actions/checkout@v3 @@ -433,7 +420,6 @@ jobs: moodle_branch: ${{ needs.prepare_matrix.outputs.highest_moodle_branch }} php: ${{ needs.prepare_matrix.outputs.highest_php }} node: ${{ needs.prepare_matrix.outputs.highest_node }} - database: 'pgsql' - name: Run mustache if: ${{ inputs.disable_mustache != true }} run: moodle-plugin-ci mustache @@ -442,19 +428,6 @@ jobs: if: needs.pre_job.outputs.should_skip != 'true' runs-on: ubuntu-latest needs: prepare_matrix - services: - pgsql: - image: ${{ needs.prepare_matrix.outputs.latest_pgsql_ver && format('postgres:{0}', needs.prepare_matrix.outputs.latest_pgsql_ver) || '' }} - env: - POSTGRES_USER: 'postgres' - POSTGRES_HOST_AUTH_METHOD: 'trust' - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 3 - ports: - - 5432:5432 steps: - name: Check out CI code uses: actions/checkout@v3 @@ -471,7 +444,9 @@ jobs: extra_php_extensions: ${{ inputs.extra_php_extensions }} extra_plugin_runners: ${{ inputs.extra_plugin_runners }} moodle_branch: ${{ needs.prepare_matrix.outputs.highest_moodle_branch }} - database: 'pgsql' + - name: Install Moodle node dependencies + run: npm install --prefix $MOODLE_DIR + shell: bash - name: Run grunt if: ${{ inputs.disable_grunt != true }} run: | From 911a7224e394e801319b81b2684d5d6b3e93918f Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Mon, 25 May 2026 23:12:14 +1000 Subject: [PATCH 14/43] Remove db install from validate job --- .github/workflows/ci.yml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc763024..b236a7c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -342,19 +342,6 @@ jobs: if: needs.pre_job.outputs.should_skip != 'true' runs-on: ubuntu-latest needs: prepare_matrix - services: - pgsql: - image: ${{ needs.prepare_matrix.outputs.latest_pgsql_ver && format('postgres:{0}', needs.prepare_matrix.outputs.latest_pgsql_ver) || '' }} - env: - POSTGRES_USER: 'postgres' - POSTGRES_HOST_AUTH_METHOD: 'trust' - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 3 - ports: - - 5432:5432 steps: - name: Check out CI code uses: actions/checkout@v3 @@ -371,10 +358,9 @@ jobs: extra_php_extensions: ${{ inputs.extra_php_extensions }} extra_plugin_runners: ${{ inputs.extra_plugin_runners }} moodle_branch: ${{ needs.prepare_matrix.outputs.highest_moodle_branch }} - database: 'pgsql' - name: Run validate if: ${{ inputs.disable_ci_validate != true }} - run: moodle-plugin-ci validate ./plugin + run: moodle-plugin-ci validate shell: bash savepoints: name: Savepoints From c662376b23bc1aedba15364415218cf25a795035 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Mon, 25 May 2026 23:20:52 +1000 Subject: [PATCH 15/43] Create empty config.php file --- .github/plugin/setup/action.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 22c010a2..6bd8fbca 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -248,6 +248,27 @@ runs: mkdir -p "$PLUGIN_PATH" cp -r plugin/. "$PLUGIN_PATH/" rm -rf "$PLUGIN_PATH/.git" + + # Create a minimal config.php so tools that bootstrap Moodle (e.g. validate) can load + # core_component without connecting to a database (ABORT_AFTER_CONFIG is set by moodle-plugin-ci). + cat > "$GITHUB_WORKSPACE/moodletemp/config.php" << 'EOF' +dbtype = 'pgsql'; +$CFG->dblibrary = 'native'; +$CFG->dbhost = 'fakehost'; +$CFG->dbname = 'fakedb'; +$CFG->dbuser = 'fakeuser'; +$CFG->dbpass = ''; +$CFG->prefix = 'mdl_'; +$CFG->dboptions = []; +$CFG->wwwroot = 'http://localhost'; +$CFG->dataroot = '/tmp/moodledata'; +$CFG->admin = 'admin'; +$CFG->directorypermissions = 0777; +require_once(__DIR__ . '/lib/setup.php'); +EOF echo "MOODLE_DIR=$GITHUB_WORKSPACE/moodletemp" >> "$GITHUB_ENV" echo "PLUGIN_DIR=$PLUGIN_PATH" >> "$GITHUB_ENV" shell: bash From 0481c7064932e160ca8c6a14c33945acd4b7f9e6 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Mon, 25 May 2026 23:24:34 +1000 Subject: [PATCH 16/43] Fixed yaml escaping --- .github/plugin/setup/action.yml | 40 ++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 6bd8fbca..b23498b6 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -251,24 +251,28 @@ runs: # Create a minimal config.php so tools that bootstrap Moodle (e.g. validate) can load # core_component without connecting to a database (ABORT_AFTER_CONFIG is set by moodle-plugin-ci). - cat > "$GITHUB_WORKSPACE/moodletemp/config.php" << 'EOF' -dbtype = 'pgsql'; -$CFG->dblibrary = 'native'; -$CFG->dbhost = 'fakehost'; -$CFG->dbname = 'fakedb'; -$CFG->dbuser = 'fakeuser'; -$CFG->dbpass = ''; -$CFG->prefix = 'mdl_'; -$CFG->dboptions = []; -$CFG->wwwroot = 'http://localhost'; -$CFG->dataroot = '/tmp/moodledata'; -$CFG->admin = 'admin'; -$CFG->directorypermissions = 0777; -require_once(__DIR__ . '/lib/setup.php'); -EOF + php -r " + file_put_contents('$GITHUB_WORKSPACE/moodletemp/config.php', + implode(PHP_EOL, [ + 'dbtype = \'pgsql\';', + '\$CFG->dblibrary = \'native\';', + '\$CFG->dbhost = \'fakehost\';', + '\$CFG->dbname = \'fakedb\';', + '\$CFG->dbuser = \'fakeuser\';', + '\$CFG->dbpass = \'\';', + '\$CFG->prefix = \'mdl_\';', + '\$CFG->dboptions = [];', + '\$CFG->wwwroot = \'http://localhost\';', + '\$CFG->dataroot = \'/tmp/moodledata\';', + '\$CFG->admin = \'admin\';', + '\$CFG->directorypermissions = 0777;', + 'require_once(__DIR__ . \'/lib/setup.php\');', + ]) + ); + " echo "MOODLE_DIR=$GITHUB_WORKSPACE/moodletemp" >> "$GITHUB_ENV" echo "PLUGIN_DIR=$PLUGIN_PATH" >> "$GITHUB_ENV" shell: bash From f46de5354d0c77a105a66f73727ac98578fbf6e2 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Mon, 25 May 2026 23:29:02 +1000 Subject: [PATCH 17/43] Ensure dataroot exists --- .github/plugin/setup/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index b23498b6..13b0dc18 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -251,6 +251,7 @@ runs: # Create a minimal config.php so tools that bootstrap Moodle (e.g. validate) can load # core_component without connecting to a database (ABORT_AFTER_CONFIG is set by moodle-plugin-ci). + mkdir -p /tmp/moodledata php -r " file_put_contents('$GITHUB_WORKSPACE/moodletemp/config.php', implode(PHP_EOL, [ From c1f6dd27fd6337c0b4f7f30cb99438da3c8eb1fe Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Tue, 26 May 2026 10:15:38 +1000 Subject: [PATCH 18/43] cache and restore db --- .github/plugin/setup/action.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 13b0dc18..c7d7adbc 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -75,6 +75,7 @@ runs: START_TS=$(date +%s) echo "::group::Install moodle-plugin-ci" + time composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci m-ci ^4 composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci m-ci ^4 END_TS=$(date +%s) @@ -87,7 +88,6 @@ runs: START_TS=$(date +%s) echo "::group::Add moodle-plugin-ci binaries to PATH" - time composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci m-ci ^4 # Add dirs to $PATH echo $(cd m-ci/bin; pwd) >> $GITHUB_PATH echo $(cd m-ci/vendor/bin; pwd) >> $GITHUB_PATH @@ -137,6 +137,12 @@ runs: IMAGE="${IMAGE//_/-}" IMAGE="${IMAGE,,}:latest" time git clone https://github.com/moodle/moodle.git --branch $MOODLE_BRANCH $GITHUB_WORKSPACE/moodletemp + BRANCH="${{ inputs.moodle_branch }}" + DB="${{ inputs.database }}" + # Build image name: snapshot-{branch}-{db}, lowercased, underscores->hyphens. + IMAGE="ghcr.io/catalyst/snapshot-${BRANCH}-${DB}" + IMAGE="${IMAGE//_/-}" + IMAGE="${IMAGE,,}:latest" echo "Pulling snapshot image $IMAGE..." docker pull "$IMAGE" From 2899d4cf07ae55819e0b6c6bdfc510d7dd3d72f8 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Tue, 26 May 2026 10:51:28 +1000 Subject: [PATCH 19/43] merge in brendans changes and use more from snapshot --- .github/plugin/setup/action.yml | 184 ++++++-------------------------- .github/workflows/ci.yml | 1 - 2 files changed, 35 insertions(+), 150 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index c7d7adbc..28bb114b 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -18,14 +18,6 @@ inputs: description: 'Database type (e.g. pgsql, mariadb). Leave empty to skip installation.' required: false default: '' - run_plugin_ci_install: - description: 'Whether to run moodle-plugin-ci install for database-enabled jobs.' - required: false - default: 'true' - use_phpunit_snapshot: - description: 'Whether to restore Moodle tree and phpunit DB snapshot artifacts.' - required: false - default: 'false' runs: using: "composite" steps: @@ -56,32 +48,25 @@ runs: extensions: pgsql, mysqli, zip, gd, xmlrpc, soap, ${{ inputs.extra_php_extensions }} coverage: none - - name: Configure Required Composer Version - id: install-composer - if: ${{ inputs.moodle_branch == 'MOODLE_32_STABLE' || inputs.moodle_branch == 'MOODLE_33_STABLE' }} - run: | - echo "::set-output name=COMPOSER_VERSION::--1" - shell: bash - - - name: Update Composer - if: ${{ inputs.moodle_branch == 'MOODLE_32_STABLE' || inputs.moodle_branch == 'MOODLE_33_STABLE' }} - run: | - time composer self-update ${{ steps.install-composer1.outputs.COMPOSER_VERSION }} + - name: Pull snapshot Docker image and extract artifacts shell: bash - - - name: Install moodle-plugin-ci - if: ${{ inputs.use_phpunit_snapshot != 'true' }} run: | - START_TS=$(date +%s) - echo "::group::Install moodle-plugin-ci" - - time composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci m-ci ^4 - composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci m-ci ^4 + BRANCH="${{ inputs.moodle_branch }}" + # Use provided database for image name; fall back to mariadb for jobs with no database. + DB="${{ inputs.database || 'mariadb' }}" + IMAGE="ghcr.io/catalyst/snapshot-${BRANCH}-${DB}" + IMAGE="${IMAGE//_/-}" + IMAGE="${IMAGE,,}:latest" + echo "Pulling snapshot image $IMAGE..." + docker pull "$IMAGE" + echo "SNAPSHOT_IMAGE=$IMAGE" >> "$GITHUB_ENV" - END_TS=$(date +%s) - echo "Install moodle-plugin-ci took $((END_TS - START_TS))s" - echo "::endgroup::" - shell: bash + CID=$(docker create "$IMAGE") + docker cp "$CID:/m-ci.tar.gz" ./m-ci.tar.gz + docker cp "$CID:/moodle.tar.gz" ./moodle.tar.gz + docker rm "$CID" + tar -C "$GITHUB_WORKSPACE" -xzf "$GITHUB_WORKSPACE/m-ci.tar.gz" + tar -C "$GITHUB_WORKSPACE" -xzf "$GITHUB_WORKSPACE/moodle.tar.gz" - name: Add moodle-plugin-ci binaries to PATH run: | @@ -124,66 +109,7 @@ runs: echo "::endgroup::" shell: bash - - name: Restore snapshot artifacts - if: ${{ inputs.database != '' && inputs.use_phpunit_snapshot == 'true' }} - run: | - START_TS=$(date +%s) - echo "::group::Restore snapshot artifacts" - - BRANCH="${{ inputs.moodle_branch }}" - DB="${{ inputs.database }}" - # Build image name: snapshot-{branch}-{db}, lowercased, underscores->hyphens. - IMAGE="ghcr.io/catalyst/snapshot-${BRANCH}-${DB}" - IMAGE="${IMAGE//_/-}" - IMAGE="${IMAGE,,}:latest" - time git clone https://github.com/moodle/moodle.git --branch $MOODLE_BRANCH $GITHUB_WORKSPACE/moodletemp - BRANCH="${{ inputs.moodle_branch }}" - DB="${{ inputs.database }}" - # Build image name: snapshot-{branch}-{db}, lowercased, underscores->hyphens. - IMAGE="ghcr.io/catalyst/snapshot-${BRANCH}-${DB}" - IMAGE="${IMAGE//_/-}" - IMAGE="${IMAGE,,}:latest" - - echo "Pulling snapshot image $IMAGE..." - docker pull "$IMAGE" - - # Extract snapshot artifacts from the image. - CID=$(docker create "$IMAGE") - if docker cp "$CID:/phpunit-snapshot.zip" ./phpunit-snapshot.zip; then - echo "Found phpunit snapshot archive in image." - else - echo "No phpunit snapshot archive found in image." - docker rm "$CID" - exit 1 - fi - if docker cp "$CID:/moodle.tar.gz" ./moodle.tar.gz; then - echo "Found Moodle snapshot tree archive in image." - else - echo "No Moodle snapshot tree archive found in image." - docker rm "$CID" - exit 1 - fi - if docker cp "$CID:/m-ci.tar.gz" ./m-ci.tar.gz; then - echo "Found moodle-plugin-ci archive in image." - else - echo "No moodle-plugin-ci archive found in image." - docker rm "$CID" - exit 1 - fi - docker rm "$CID" - - tar -C "$GITHUB_WORKSPACE" -xzf "$GITHUB_WORKSPACE/m-ci.tar.gz" - - rm -rf "$GITHUB_WORKSPACE/moodle" - tar -C "$GITHUB_WORKSPACE" -xzf "$GITHUB_WORKSPACE/moodle.tar.gz" - - END_TS=$(date +%s) - echo "Restore snapshot artifacts took $((END_TS - START_TS))s" - echo "::endgroup::" - shell: bash - - name: Apply plugin branch patch - if: ${{ inputs.database != '' && inputs.use_phpunit_snapshot == 'true' }} run: | START_TS=$(date +%s) echo "::group::Apply plugin branch patch" @@ -200,33 +126,28 @@ runs: echo "::endgroup::" shell: bash - - name: Install Moodle and Plugin - if: ${{ inputs.database != '' && inputs.run_plugin_ci_install == 'true' && inputs.use_phpunit_snapshot != 'true' }} + - name: Restore snapshot artifacts + if: ${{ inputs.database != '' }} run: | START_TS=$(date +%s) - # Install moodle commands - echo "::group::Moodle install output" + echo "::group::Restore snapshot artifacts" - moodle-plugin-ci install -vvv --plugin ./plugin --db-host=127.0.0.1 + # Extract phpunit snapshot from the image. + CID=$(docker create "$SNAPSHOT_IMAGE") + if docker cp "$CID:/phpunit-snapshot.zip" ./phpunit-snapshot.zip; then + echo "Found phpunit snapshot archive in image." + else + echo "No phpunit snapshot archive found in image." + docker rm "$CID" + exit 1 + fi + docker rm "$CID" END_TS=$(date +%s) - echo "Moodle install output took $((END_TS - START_TS))s" + echo "Restore snapshot artifacts took $((END_TS - START_TS))s" echo "::endgroup::" shell: bash - env: - DB: ${{ inputs.database }} - MOODLE_BRANCH: ${{ inputs.moodle_branch }} - - - name: Restore PHPUnit DB snapshot - if: ${{ inputs.database != '' && inputs.use_phpunit_snapshot == 'true' }} - run: | - START_TS=$(date +%s) - echo "::group::Restore PHPUnit DB snapshot" - if [ ! -f "$GITHUB_WORKSPACE/phpunit-snapshot.zip" ]; then - echo "No phpunit snapshot archive present; can not restore phpunit DB snapshot." - exit 1 - fi - name: Install plugin into Moodle (no database) if: ${{ inputs.database == '' }} # For static analysis jobs that don't need a database, copy the plugin into the Moodle @@ -245,42 +166,17 @@ runs: TYPE=$(echo "$COMPONENT" | cut -d_ -f1) NAME=$(echo "$COMPONENT" | cut -d_ -f2-) TYPE_DIR=$(php -r " - \$components = json_decode(file_get_contents('$GITHUB_WORKSPACE/moodletemp/lib/components.json'), true); + \$components = json_decode(file_get_contents('$GITHUB_WORKSPACE/moodle/lib/components.json'), true); \$type = '$TYPE'; if (!isset(\$components['plugintypes'][\$type])) { fwrite(STDERR, 'Unknown plugin type: ' . \$type . PHP_EOL); exit(1); } echo \$components['plugintypes'][\$type]; ") - PLUGIN_PATH="$GITHUB_WORKSPACE/moodletemp/$TYPE_DIR/$NAME" + PLUGIN_PATH="$GITHUB_WORKSPACE/moodle/$TYPE_DIR/$NAME" mkdir -p "$PLUGIN_PATH" cp -r plugin/. "$PLUGIN_PATH/" rm -rf "$PLUGIN_PATH/.git" - # Create a minimal config.php so tools that bootstrap Moodle (e.g. validate) can load - # core_component without connecting to a database (ABORT_AFTER_CONFIG is set by moodle-plugin-ci). - mkdir -p /tmp/moodledata - php -r " - file_put_contents('$GITHUB_WORKSPACE/moodletemp/config.php', - implode(PHP_EOL, [ - 'dbtype = \'pgsql\';', - '\$CFG->dblibrary = \'native\';', - '\$CFG->dbhost = \'fakehost\';', - '\$CFG->dbname = \'fakedb\';', - '\$CFG->dbuser = \'fakeuser\';', - '\$CFG->dbpass = \'\';', - '\$CFG->prefix = \'mdl_\';', - '\$CFG->dboptions = [];', - '\$CFG->wwwroot = \'http://localhost\';', - '\$CFG->dataroot = \'/tmp/moodledata\';', - '\$CFG->admin = \'admin\';', - '\$CFG->directorypermissions = 0777;', - 'require_once(__DIR__ . \'/lib/setup.php\');', - ]) - ); - " - echo "MOODLE_DIR=$GITHUB_WORKSPACE/moodletemp" >> "$GITHUB_ENV" + echo "MOODLE_DIR=$GITHUB_WORKSPACE/moodle" >> "$GITHUB_ENV" echo "PLUGIN_DIR=$PLUGIN_PATH" >> "$GITHUB_ENV" shell: bash @@ -291,20 +187,10 @@ runs: # Install moodle commands echo "::group::Moodle install output" - cd "$GITHUB_WORKSPACE/moodle" - - time moodle-plugin-ci install -vvv --plugin ./plugin --db-host=127.0.0.1 --repo $GITHUB_WORKSPACE/moodletemp - - PHPUNIT_DATAROOT=$(php -r "define('CLI_SCRIPT', true); require 'config.php'; echo \$CFG->phpunit_dataroot;") - mkdir -p "$PHPUNIT_DATAROOT" + time moodle-plugin-ci install -vvv --plugin ./plugin --db-host=127.0.0.1 --repo $GITHUB_WORKSPACE/moodle - cp "$GITHUB_WORKSPACE/phpunit-snapshot.zip" "$PHPUNIT_DATAROOT/ci-phpunit.zip" - php "$PHPUNIT_UTIL" --restore=ci-phpunit - - END_TS=$(date +%s) - echo "Restore PHPUnit DB snapshot took $((END_TS - START_TS))s" echo "::endgroup::" shell: bash env: DB: ${{ inputs.database }} - MOODLE_BRANCH: ${{ inputs.moodle_branch }} + MOODLE_BRANCH: ${{ inputs.moodle_branch }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b236a7c5..a8b28949 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -557,7 +557,6 @@ jobs: extra_plugin_runners: ${{ inputs.extra_plugin_runners }} moodle_branch: ${{ matrix.moodle-branch }} database: ${{ matrix.database }} - use_phpunit_snapshot: 'true' - name: Install plugin into Moodle and upgrade phpunit site shell: bash run: | From 2d28070a3ea4fcc5e129c8a6270e06fd6f410ea8 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Tue, 26 May 2026 11:07:02 +1000 Subject: [PATCH 20/43] make data directory, fix patch apply --- .github/plugin/setup/action.yml | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 28bb114b..dafede2c 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -67,6 +67,7 @@ runs: docker rm "$CID" tar -C "$GITHUB_WORKSPACE" -xzf "$GITHUB_WORKSPACE/m-ci.tar.gz" tar -C "$GITHUB_WORKSPACE" -xzf "$GITHUB_WORKSPACE/moodle.tar.gz" + mkdir -p "$GITHUB_WORKSPACE/moodledata" - name: Add moodle-plugin-ci binaries to PATH run: | @@ -114,12 +115,18 @@ runs: START_TS=$(date +%s) echo "::group::Apply plugin branch patch" - git config --global user.email "test@test.com" - git config --global user.name "Test" - TARGET_REPO="$GITHUB_WORKSPACE/moodle" - - ((test -f plugin/patch/${{ inputs.moodle_branch }}.diff && cd "$TARGET_REPO" && git am --whitespace=nowarn < "$GITHUB_WORKSPACE/plugin/patch/${{ inputs.moodle_branch }}.diff") || echo No plugin branch patch found;) + PATCH="$GITHUB_WORKSPACE/plugin/patch/${{ inputs.moodle_branch }}.diff" + + if [ -f "$PATCH" ]; then + # moodle/ was extracted from a tarball (no .git); init a repo just for patch application. + git config --global user.email "test@test.com" + git config --global user.name "Test" + git -C "$TARGET_REPO" init --quiet + git -C "$TARGET_REPO" apply --whitespace=nowarn "$PATCH" + else + echo "No plugin branch patch found for ${{ inputs.moodle_branch }}" + fi END_TS=$(date +%s) echo "Apply plugin branch patch took $((END_TS - START_TS))s" @@ -179,18 +186,3 @@ runs: echo "MOODLE_DIR=$GITHUB_WORKSPACE/moodle" >> "$GITHUB_ENV" echo "PLUGIN_DIR=$PLUGIN_PATH" >> "$GITHUB_ENV" shell: bash - - - name: Install Moodle and Plugin - if: ${{ inputs.database != '' }} - # Install moodle, but use our temporary directory to include any potential core patches that have been applied. - run: | - # Install moodle commands - echo "::group::Moodle install output" - - time moodle-plugin-ci install -vvv --plugin ./plugin --db-host=127.0.0.1 --repo $GITHUB_WORKSPACE/moodle - - echo "::endgroup::" - shell: bash - env: - DB: ${{ inputs.database }} - MOODLE_BRANCH: ${{ inputs.moodle_branch }} \ No newline at end of file From d0f9be9179eb12383d7fbac1e36c3d2cdc330c25 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Tue, 26 May 2026 11:18:41 +1000 Subject: [PATCH 21/43] restore phpunit snapshot --- .github/plugin/setup/action.yml | 41 +++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index dafede2c..23acd6f9 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -133,27 +133,48 @@ runs: echo "::endgroup::" shell: bash - - name: Restore snapshot artifacts + - name: Create database and restore PHPUnit snapshot if: ${{ inputs.database != '' }} + shell: bash + env: + DB: ${{ inputs.database }} run: | START_TS=$(date +%s) - echo "::group::Restore snapshot artifacts" + echo "::group::Create database and restore PHPUnit snapshot" - # Extract phpunit snapshot from the image. + # Extract phpunit snapshot zip from the image — fail if missing. CID=$(docker create "$SNAPSHOT_IMAGE") - if docker cp "$CID:/phpunit-snapshot.zip" ./phpunit-snapshot.zip; then - echo "Found phpunit snapshot archive in image." + docker cp "$CID:/phpunit-snapshot.zip" ./phpunit-snapshot.zip + docker rm "$CID" + + # Create the Moodle database. + if [ "$DB" = "pgsql" ]; then + psql -h 127.0.0.1 -U postgres -d postgres -c "DROP DATABASE IF EXISTS moodle;" + psql -h 127.0.0.1 -U postgres -d postgres -c "CREATE DATABASE moodle;" + else + mysql -h 127.0.0.1 -u root -e "DROP DATABASE IF EXISTS moodle; CREATE DATABASE moodle DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + fi + + # Resolve phpunit_dataroot without a DB connection. + PHPUNIT_DATAROOT=$(php -r "define('ABORT_AFTER_CONFIG', true); require 'moodle/config.php'; echo \$CFG->phpunit_dataroot;") + mkdir -p "$PHPUNIT_DATAROOT" + cp "$GITHUB_WORKSPACE/phpunit-snapshot.zip" "$PHPUNIT_DATAROOT/ci-phpunit.zip" + + # Detect phpunit util location (differs between classic and public/ layouts). + if [ -f "$GITHUB_WORKSPACE/moodle/public/admin/tool/phpunit/cli/util.php" ]; then + PHPUNIT_UTIL="$GITHUB_WORKSPACE/moodle/public/admin/tool/phpunit/cli/util.php" + elif [ -f "$GITHUB_WORKSPACE/moodle/admin/tool/phpunit/cli/util.php" ]; then + PHPUNIT_UTIL="$GITHUB_WORKSPACE/moodle/admin/tool/phpunit/cli/util.php" else - echo "No phpunit snapshot archive found in image." - docker rm "$CID" + echo "Could not locate phpunit util.php" exit 1 fi - docker rm "$CID" + + php "$PHPUNIT_UTIL" --restore=ci-phpunit END_TS=$(date +%s) - echo "Restore snapshot artifacts took $((END_TS - START_TS))s" + echo "Create database and restore PHPUnit snapshot took $((END_TS - START_TS))s" echo "::endgroup::" - shell: bash - name: Install plugin into Moodle (no database) if: ${{ inputs.database == '' }} From daddb7c4bc0c30cbb51ae7582d1d6d02e0f41e07 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Tue, 26 May 2026 11:22:39 +1000 Subject: [PATCH 22/43] always install, upgrade phpunit db --- .github/plugin/setup/action.yml | 57 ++++++++++++++++----------------- .github/workflows/ci.yml | 17 ---------- 2 files changed, 27 insertions(+), 47 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 23acd6f9..57a88b0f 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -133,6 +133,32 @@ runs: echo "::endgroup::" shell: bash + - name: Install plugin into Moodle + shell: bash + run: | + COMPONENT=$(php -r " + define('MOODLE_INTERNAL', true); + define('MATURITY_ALPHA', 50); define('MATURITY_BETA', 100); + define('MATURITY_RC', 150); define('MATURITY_STABLE', 200); + \$plugin = new stdClass(); + include 'plugin/version.php'; + echo \$plugin->component; + ") + PLUGIN_PATH=$(php -r " + define('CLI_SCRIPT', true); + define('ABORT_AFTER_CONFIG', true); + require 'moodle/config.php'; + require_once \$CFG->dirroot . '/lib/classes/component.php'; + list(\$type, \$name) = core_component::normalize_component('$COMPONENT'); + echo core_component::get_plugin_directory(\$type, \$name); + ") + mkdir -p "$PLUGIN_PATH" + cp -r plugin/. "$PLUGIN_PATH/" + rm -rf "$PLUGIN_PATH/.git" + + echo "MOODLE_DIR=$GITHUB_WORKSPACE/moodle" >> "$GITHUB_ENV" + echo "PLUGIN_DIR=$PLUGIN_PATH" >> "$GITHUB_ENV" + - name: Create database and restore PHPUnit snapshot if: ${{ inputs.database != '' }} shell: bash @@ -171,39 +197,10 @@ runs: fi php "$PHPUNIT_UTIL" --restore=ci-phpunit + php "$PHPUNIT_UTIL" --upgrade END_TS=$(date +%s) echo "Create database and restore PHPUnit snapshot took $((END_TS - START_TS))s" echo "::endgroup::" - - name: Install plugin into Moodle (no database) - if: ${{ inputs.database == '' }} - # For static analysis jobs that don't need a database, copy the plugin into the Moodle - # directory so that tools like mustache and grunt can find it within Moodle's directory tree. - run: | - COMPONENT=$(php -r " - define('MOODLE_INTERNAL', true); - define('MATURITY_ALPHA', 50); - define('MATURITY_BETA', 100); - define('MATURITY_RC', 150); - define('MATURITY_STABLE', 200); - \$plugin = new stdClass(); - include 'plugin/version.php'; - echo \$plugin->component; - ") - TYPE=$(echo "$COMPONENT" | cut -d_ -f1) - NAME=$(echo "$COMPONENT" | cut -d_ -f2-) - TYPE_DIR=$(php -r " - \$components = json_decode(file_get_contents('$GITHUB_WORKSPACE/moodle/lib/components.json'), true); - \$type = '$TYPE'; - if (!isset(\$components['plugintypes'][\$type])) { fwrite(STDERR, 'Unknown plugin type: ' . \$type . PHP_EOL); exit(1); } - echo \$components['plugintypes'][\$type]; - ") - PLUGIN_PATH="$GITHUB_WORKSPACE/moodle/$TYPE_DIR/$NAME" - mkdir -p "$PLUGIN_PATH" - cp -r plugin/. "$PLUGIN_PATH/" - rm -rf "$PLUGIN_PATH/.git" - echo "MOODLE_DIR=$GITHUB_WORKSPACE/moodle" >> "$GITHUB_ENV" - echo "PLUGIN_DIR=$PLUGIN_PATH" >> "$GITHUB_ENV" - shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8b28949..1216e52c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -557,23 +557,6 @@ jobs: extra_plugin_runners: ${{ inputs.extra_plugin_runners }} moodle_branch: ${{ matrix.moodle-branch }} database: ${{ matrix.database }} - - name: Install plugin into Moodle and upgrade phpunit site - shell: bash - run: | - TARGET_DIR=$(php -r "define('CLI_SCRIPT', true); require 'moodle/config.php'; list(\$type, \$name) = core_component::normalize_component('${{ needs.prepare_matrix.outputs.component }}'); \$dir = core_component::get_plugin_directory(\$type, \$name); if (empty(\$dir)) { fwrite(STDERR, 'Could not resolve plugin path for component ${{ needs.prepare_matrix.outputs.component }}\n'); exit(1); } echo \$dir;") - - if [ -z "$TARGET_DIR" ]; then - echo "Resolved plugin target directory is empty" - exit 1 - fi - - rm -rf "$TARGET_DIR" - mkdir -p "$TARGET_DIR" - cp -a plugin/. "$TARGET_DIR/" - - PHPUNIT_UTIL=$(php -r "define('CLI_SCRIPT', true); require 'moodle/config.php'; echo \$CFG->dirroot . '/admin/tool/phpunit/cli/util.php';") - - php "$PHPUNIT_UTIL" --upgrade - name: Run phpunit run: | cd moodle From 6ce417157ed6be00b685ffcdd109a08f53cc3c99 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Tue, 26 May 2026 11:26:32 +1000 Subject: [PATCH 23/43] old plugin install --- .github/plugin/setup/action.yml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 57a88b0f..e2d5f313 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -138,20 +138,23 @@ runs: run: | COMPONENT=$(php -r " define('MOODLE_INTERNAL', true); - define('MATURITY_ALPHA', 50); define('MATURITY_BETA', 100); - define('MATURITY_RC', 150); define('MATURITY_STABLE', 200); + define('MATURITY_ALPHA', 50); + define('MATURITY_BETA', 100); + define('MATURITY_RC', 150); + define('MATURITY_STABLE', 200); \$plugin = new stdClass(); include 'plugin/version.php'; echo \$plugin->component; ") - PLUGIN_PATH=$(php -r " - define('CLI_SCRIPT', true); - define('ABORT_AFTER_CONFIG', true); - require 'moodle/config.php'; - require_once \$CFG->dirroot . '/lib/classes/component.php'; - list(\$type, \$name) = core_component::normalize_component('$COMPONENT'); - echo core_component::get_plugin_directory(\$type, \$name); + TYPE=$(echo "$COMPONENT" | cut -d_ -f1) + NAME=$(echo "$COMPONENT" | cut -d_ -f2-) + TYPE_DIR=$(php -r " + \$components = json_decode(file_get_contents('$GITHUB_WORKSPACE/moodle/lib/components.json'), true); + \$type = '$TYPE'; + if (!isset(\$components['plugintypes'][\$type])) { fwrite(STDERR, 'Unknown plugin type: ' . \$type . PHP_EOL); exit(1); } + echo \$components['plugintypes'][\$type]; ") + PLUGIN_PATH="$GITHUB_WORKSPACE/moodle/$TYPE_DIR/$NAME" mkdir -p "$PLUGIN_PATH" cp -r plugin/. "$PLUGIN_PATH/" rm -rf "$PLUGIN_PATH/.git" From 40136e794cea57ad6612e6c16abe77c156a9e0fe Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Tue, 26 May 2026 11:54:29 +1000 Subject: [PATCH 24/43] check for version before install --- .github/plugin/setup/action.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index e2d5f313..211231d0 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -136,6 +136,10 @@ runs: - name: Install plugin into Moodle shell: bash run: | + if [ ! -f "plugin/version.php" ]; then + echo "No plugin/version.php found, skipping plugin install." + exit 0 + fi COMPONENT=$(php -r " define('MOODLE_INTERNAL', true); define('MATURITY_ALPHA', 50); From f8ccd78041f664cfd708689c33f115a3441c4905 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Tue, 26 May 2026 12:00:39 +1000 Subject: [PATCH 25/43] stop running setup for making snapshots --- .github/workflows/update-snapshots.yml | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/update-snapshots.yml b/.github/workflows/update-snapshots.yml index 77947141..a674b8c4 100644 --- a/.github/workflows/update-snapshots.yml +++ b/.github/workflows/update-snapshots.yml @@ -116,15 +116,25 @@ jobs: with: path: ci - - name: Run setup (PHP, Node, moodle-plugin-ci bootstrap) - # database is intentionally empty; this workflow performs clone/patch - # directly and builds snapshot artifacts from the installed tree. - uses: ./ci/.github/plugin/setup + - name: Install node ${{ matrix.node }} + uses: actions/setup-node@v4 with: - php: ${{ matrix.php }} - node: ${{ matrix.node }} - moodle_branch: ${{ matrix.moodle-branch }} - database: '' + node-version: ${{ matrix.node }} + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0 + with: + php-version: ${{ matrix.php }} + ini-values: max_input_vars=5000 + extensions: pgsql, mysqli, zip, gd, xmlrpc, soap + coverage: none + + - name: Install moodle-plugin-ci + shell: bash + run: | + composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci m-ci ^4 + echo "$(cd m-ci/bin; pwd)" >> "$GITHUB_PATH" + echo "$(cd m-ci/vendor/bin; pwd)" >> "$GITHUB_PATH" - name: Clone Moodle for snapshot shell: bash From 22988e666d8ea6d393240c2bcdc4388fee347875 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Tue, 26 May 2026 12:05:00 +1000 Subject: [PATCH 26/43] install locale --- .github/workflows/update-snapshots.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/update-snapshots.yml b/.github/workflows/update-snapshots.yml index a674b8c4..3fd2c9ef 100644 --- a/.github/workflows/update-snapshots.yml +++ b/.github/workflows/update-snapshots.yml @@ -224,6 +224,7 @@ jobs: exit 1 fi + sudo locale-gen en_AU.UTF-8 mkdir -p "$GITHUB_WORKSPACE/moodledata/phpunit" php "$PHPUNIT_CLI/init.php" From 9bd9b3c7852827a686e8e164ce902881022d2dd9 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Tue, 26 May 2026 12:31:16 +1000 Subject: [PATCH 27/43] action fix --- .github/plugin/setup/action.yml | 4 ++-- .github/workflows/update-snapshots.yml | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 211231d0..8147a034 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -188,8 +188,8 @@ runs: mysql -h 127.0.0.1 -u root -e "DROP DATABASE IF EXISTS moodle; CREATE DATABASE moodle DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" fi - # Resolve phpunit_dataroot without a DB connection. - PHPUNIT_DATAROOT=$(php -r "define('ABORT_AFTER_CONFIG', true); require 'moodle/config.php'; echo \$CFG->phpunit_dataroot;") + # phpunit_dataroot is always $GITHUB_WORKSPACE/moodledata/phpunit per the config template. + PHPUNIT_DATAROOT="$GITHUB_WORKSPACE/moodledata/phpunit" mkdir -p "$PHPUNIT_DATAROOT" cp "$GITHUB_WORKSPACE/phpunit-snapshot.zip" "$PHPUNIT_DATAROOT/ci-phpunit.zip" diff --git a/.github/workflows/update-snapshots.yml b/.github/workflows/update-snapshots.yml index 3fd2c9ef..9d599eae 100644 --- a/.github/workflows/update-snapshots.yml +++ b/.github/workflows/update-snapshots.yml @@ -238,13 +238,20 @@ jobs: - name: Export phpunit snapshot shell: bash run: | - cd moodle - - PHPUNIT_UTIL=$(php -r "define('CLI_SCRIPT', true); require 'config.php'; echo \$CFG->dirroot . '/admin/tool/phpunit/cli/util.php';") + # Detect util.php location (differs between classic and public/ layouts). + if [ -f "$GITHUB_WORKSPACE/moodle/public/admin/tool/phpunit/cli/util.php" ]; then + PHPUNIT_UTIL="$GITHUB_WORKSPACE/moodle/public/admin/tool/phpunit/cli/util.php" + elif [ -f "$GITHUB_WORKSPACE/moodle/admin/tool/phpunit/cli/util.php" ]; then + PHPUNIT_UTIL="$GITHUB_WORKSPACE/moodle/admin/tool/phpunit/cli/util.php" + else + echo "Could not locate phpunit util.php" + exit 1 + fi + cd moodle php "$PHPUNIT_UTIL" --snapshot=ci-phpunit - PHPUNIT_DATAROOT=$(php -r "define('CLI_SCRIPT', true); require 'config.php'; echo \$CFG->phpunit_dataroot;") + PHPUNIT_DATAROOT=$(php -r "define('ABORT_AFTER_CONFIG', true); require 'config.php'; echo \$CFG->phpunit_dataroot;") PHPUNIT_SNAPSHOT=$(find "$PHPUNIT_DATAROOT" -maxdepth 1 -type f -name '*-ci-phpunit-*.zip' | head -n1) if [ -z "$PHPUNIT_SNAPSHOT" ]; then From f7202fceca4eec54dd51075cbae327471b7ce1b1 Mon Sep 17 00:00:00 2001 From: Abhinav Gandham Date: Tue, 26 May 2026 17:13:46 +1000 Subject: [PATCH 28/43] Updated 401-403 and 404-500 patch files. --- .github/patches/401-403-phpunit-restore.patch | 104 +++++++----------- .github/patches/404-500-phpunit-restore.patch | 104 ++++++------------ 2 files changed, 76 insertions(+), 132 deletions(-) diff --git a/.github/patches/401-403-phpunit-restore.patch b/.github/patches/401-403-phpunit-restore.patch index 95a079ca..d08ee70e 100644 --- a/.github/patches/401-403-phpunit-restore.patch +++ b/.github/patches/401-403-phpunit-restore.patch @@ -1,18 +1,16 @@ -From 6825a9454816483b474a436dcd3bd9536f6ddff4 Mon Sep 17 00:00:00 2001 +From 806bbf2aa9140dad2a060bd098d991aa989378eb Mon Sep 17 00:00:00 2001 From: Abhinav Gandham -Date: Wed, 13 May 2026 15:33:48 +1000 -Subject: [PATCH] MDL-88495 unit tests: Implmented test site upgrade, snapshot, - and restore functionality." +Date: Fri, 22 May 2026 15:05:47 +1000 +Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. --- - admin/tool/phpunit/cli/util.php | 30 ++- - lib/phpunit/bootstraplib.php | 10 +- - lib/phpunit/classes/util.php | 353 ++++++++++++++++++++++++++++++++ - lib/testing/classes/util.php | 11 + - 4 files changed, 397 insertions(+), 7 deletions(-) + admin/tool/phpunit/cli/util.php | 28 ++- + lib/phpunit/classes/util.php | 371 +++++++++++++++++++++++++++++++- + lib/testing/classes/util.php | 7 + + 3 files changed, 402 insertions(+), 4 deletions(-) diff --git a/admin/tool/phpunit/cli/util.php b/admin/tool/phpunit/cli/util.php -index 1d176900fdf..2fd70820601 100644 +index 1d176900fdf..39cd67fb20f 100644 --- a/admin/tool/phpunit/cli/util.php +++ b/admin/tool/phpunit/cli/util.php @@ -47,6 +47,9 @@ list($options, $unrecognized) = cli_get_params( @@ -25,15 +23,6 @@ index 1d176900fdf..2fd70820601 100644 'help' => false, ), array( -@@ -61,7 +64,7 @@ if (!file_exists(__DIR__.'/../../../../vendor/phpunit/phpunit/composer.json') || - phpunit_bootstrap_error(PHPUNIT_EXITCODE_PHPUNITMISSING); - } - --if ($options['install'] or $options['drop']) { -+if ($options['install'] || $options['drop'] || $options['upgrade']) { - define('CACHE_DISABLE_ALL', true); - } - @@ -102,8 +105,16 @@ $drop = $options['drop']; $install = $options['install']; $buildconfig = $options['buildconfig']; @@ -78,38 +67,30 @@ index 1d176900fdf..2fd70820601 100644 + phpunit_util::restore_site(is_string($restore) ? $restore : ''); + exit(0); } -diff --git a/lib/phpunit/bootstraplib.php b/lib/phpunit/bootstraplib.php -index a9640994bfd..d91bee4dd92 100644 ---- a/lib/phpunit/bootstraplib.php -+++ b/lib/phpunit/bootstraplib.php -@@ -65,12 +65,14 @@ function phpunit_bootstrap_error($errorcode, $text = '') { - $text = "Moodle PHPUnit environment configuration warning:\n".$text; - break; - case PHPUNIT_EXITCODE_INSTALL: -- $path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php'); -- $text = "Moodle PHPUnit environment is not initialised, please use:\n php $path"; -+ $initpath = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php'); -+ $text = "Moodle PHPUnit environment is not initialised, please use:\n php $initpath"; - break; - case PHPUNIT_EXITCODE_REINSTALL: -- $path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php'); -- $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php $path"; -+ $initpath = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php'); -+ $utilpath = testing_cli_argument_path('/admin/tool/phpunit/cli/util.php'); -+ $text = "Moodle PHPUnit environment was initialised for different version, please use:\n" -+ . " php $initpath\n or php $utilpath --upgrade"; - break; - default: - $text = empty($text) ? '' : ': '.$text; diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php -index 9d5acc4bae0..5193830ac57 100644 +index 9d5acc4bae0..298fa0dffde 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php -@@ -495,6 +495,359 @@ class phpunit_util extends testing_util { - self::store_database_state(); +@@ -427,6 +427,13 @@ class phpunit_util extends testing_util { + echo "Purging dataroot:\n"; + } + ++ // Preserve any snapshot zips so they survive the drop. ++ foreach (scandir(self::get_dataroot()) as $item) { ++ if (str_ends_with($item, '.zip')) { ++ static::$datarootskiponreset[] = $item; ++ } ++ } ++ + self::reset_dataroot(); + testing_initdataroot($CFG->dataroot, 'phpunit'); + +@@ -496,8 +503,368 @@ class phpunit_util extends testing_util { } -+ /** + /** +- * Builds dirroot/phpunit.xml file using defaults from /phpunit.xml.dist +- * @static + * Create a snapshot of the current test site database and site data. + * + * @param string $snapshotname The name of the snapshot. @@ -262,6 +243,11 @@ index 9d5acc4bae0..5193830ac57 100644 + + echo "Restoring from $snapshot\n"; + ++ $phpunitdir = $dataroot . DIRECTORY_SEPARATOR . 'phpunit'; ++ if (!is_dir($phpunitdir)) { ++ mkdir($phpunitdir, 0777, true); ++ } ++ + // Copy snapshot DB control files to their active locations. + foreach ($datafiles as $src => $dest) { + copy($src, $dest); @@ -276,10 +262,11 @@ index 9d5acc4bae0..5193830ac57 100644 + self::reset_database(); + + echo "Data imported. Resetting datarooot...\n"; ++ + // Clear current dataroot before restoring filedir from snapshot. ++ static::$datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess', $snapshot, "$snapshot.zip"]; + self::reset_dataroot(); + self::reset_original_data(); -+ static::$datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess']; + + $snapshotfiledir = $snapshotdir . DIRECTORY_SEPARATOR . 'filedir'; + $filedir = $dataroot . DIRECTORY_SEPARATOR . 'filedir'; @@ -301,6 +288,7 @@ index 9d5acc4bae0..5193830ac57 100644 + self::$lastdbwrites = $DB->perf_get_writes(); + + remove_dir($snapshotdir); ++ unlink($zipfile); + + echo "Snapshot successfully restored.\n"; + } @@ -462,11 +450,14 @@ index 9d5acc4bae0..5193830ac57 100644 + echo "\nTables successfully created from snapshot.\n"; + } + - /** - * Builds dirroot/phpunit.xml file using defaults from /phpunit.xml.dist - * @static ++ /** ++ * Builds root/phpunit.xml file using defaults from /phpunit.xml.dist ++ * + * @return bool true means main config file created, false means only dataroot file created + */ + public static function build_config_file() { diff --git a/lib/testing/classes/util.php b/lib/testing/classes/util.php -index 0c241e6d2b9..f3f4d2000c1 100644 +index 0c241e6d2b9..fec29319bb1 100644 --- a/lib/testing/classes/util.php +++ b/lib/testing/classes/util.php @@ -136,6 +136,13 @@ abstract class testing_util { @@ -483,17 +474,6 @@ index 0c241e6d2b9..f3f4d2000c1 100644 /** * Get data generator * @static -@@ -781,6 +788,10 @@ abstract class testing_util { - // Clean up the dataroot folder. - $handle = opendir(self::get_dataroot()); - while (false !== ($item = readdir($handle))) { -+ // Explicitly check for a snapshot zip and add it to the skip on reset array if there is one. -+ if (str_contains($item, 'snapshot-')) { -+ $childclassname::$datarootskiponreset[] = $item; -+ } - if (in_array($item, $childclassname::$datarootskiponreset)) { - continue; - } -- 2.43.0 diff --git a/.github/patches/404-500-phpunit-restore.patch b/.github/patches/404-500-phpunit-restore.patch index 174c5af8..3e15c149 100644 --- a/.github/patches/404-500-phpunit-restore.patch +++ b/.github/patches/404-500-phpunit-restore.patch @@ -1,18 +1,16 @@ -From d3f3855897edf3c5411ff3633c8e860884a1894d Mon Sep 17 00:00:00 2001 +From e3e643ce27ade04e96514e1ccbc1792bd212b4b7 Mon Sep 17 00:00:00 2001 From: Abhinav Gandham -Date: Wed, 13 May 2026 15:33:48 +1000 -Subject: [PATCH] MDL-88495 unit tests: Implmented test site upgrade, snapshot, - and restore functionality." +Date: Fri, 22 May 2026 15:05:47 +1000 +Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. --- - admin/tool/phpunit/cli/util.php | 30 ++- - lib/phpunit/bootstraplib.php | 10 +- - lib/phpunit/classes/util.php | 357 +++++++++++++++++++++++++++++++- - lib/testing/classes/util.php | 20 +- - 4 files changed, 407 insertions(+), 10 deletions(-) + admin/tool/phpunit/cli/util.php | 28 ++- + lib/phpunit/classes/util.php | 371 +++++++++++++++++++++++++++++++- + lib/testing/classes/util.php | 7 + + 3 files changed, 402 insertions(+), 4 deletions(-) diff --git a/admin/tool/phpunit/cli/util.php b/admin/tool/phpunit/cli/util.php -index 3cc928a9e5a..8af8dc64531 100644 +index 3cc928a9e5a..988e45f150d 100644 --- a/admin/tool/phpunit/cli/util.php +++ b/admin/tool/phpunit/cli/util.php @@ -47,6 +47,9 @@ list($options, $unrecognized) = cli_get_params( @@ -25,15 +23,6 @@ index 3cc928a9e5a..8af8dc64531 100644 'help' => false, ], [ -@@ -61,7 +64,7 @@ if (!file_exists(__DIR__.'/../../../../vendor/phpunit/phpunit/composer.json') || - phpunit_bootstrap_error(PHPUNIT_EXITCODE_PHPUNITMISSING); - } - --if ($options['install'] || $options['drop']) { -+if ($options['install'] || $options['drop'] || $options['upgrade']) { - define('CACHE_DISABLE_ALL', true); - } - @@ -102,8 +105,16 @@ $drop = $options['drop']; $install = $options['install']; $buildconfig = $options['buildconfig']; @@ -78,34 +67,25 @@ index 3cc928a9e5a..8af8dc64531 100644 + phpunit_util::restore_site(is_string($restore) ? $restore : ''); + exit(0); } -diff --git a/lib/phpunit/bootstraplib.php b/lib/phpunit/bootstraplib.php -index a9640994bfd..bf3112cf1f5 100644 ---- a/lib/phpunit/bootstraplib.php -+++ b/lib/phpunit/bootstraplib.php -@@ -65,12 +65,14 @@ function phpunit_bootstrap_error($errorcode, $text = '') { - $text = "Moodle PHPUnit environment configuration warning:\n".$text; - break; - case PHPUNIT_EXITCODE_INSTALL: -- $path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php'); -- $text = "Moodle PHPUnit environment is not initialised, please use:\n php $path"; -+ $initpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); -+ $text = "Moodle PHPUnit environment is not initialised, please use:\n php $initpath"; - break; - case PHPUNIT_EXITCODE_REINSTALL: -- $path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php'); -- $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php $path"; -+ $initpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); -+ $utilpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/util.php'); -+ $text = "Moodle PHPUnit environment was initialised for different version, please use:\n" -+ . " php $initpath\n or php $utilpath --upgrade"; - break; - default: - $text = empty($text) ? '' : ': '.$text; diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php -index 86b52503aa3..6406c98a2fb 100644 +index 86b52503aa3..9063b7e5717 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php -@@ -520,8 +520,361 @@ class phpunit_util extends testing_util { +@@ -451,6 +451,13 @@ class phpunit_util extends testing_util { + echo "Purging dataroot:\n"; + } + ++ // Preserve any snapshot zips so they survive the drop. ++ foreach (scandir(self::get_dataroot()) as $item) { ++ if (str_ends_with($item, '.zip')) { ++ static::$datarootskiponreset[] = $item; ++ } ++ } ++ + self::reset_dataroot(); + testing_initdataroot($CFG->dataroot, 'phpunit'); + +@@ -520,8 +527,368 @@ class phpunit_util extends testing_util { } /** @@ -263,6 +243,11 @@ index 86b52503aa3..6406c98a2fb 100644 + + echo "Restoring from $snapshot\n"; + ++ $phpunitdir = $dataroot . DIRECTORY_SEPARATOR . 'phpunit'; ++ if (!is_dir($phpunitdir)) { ++ mkdir($phpunitdir, 0777, true); ++ } ++ + // Copy snapshot DB control files to their active locations. + foreach ($datafiles as $src => $dest) { + copy($src, $dest); @@ -277,10 +262,11 @@ index 86b52503aa3..6406c98a2fb 100644 + self::reset_database(); + + echo "Data imported. Resetting datarooot...\n"; ++ + // Clear current dataroot before restoring filedir from snapshot. ++ static::$datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess', $snapshot, "$snapshot.zip"]; + self::reset_dataroot(); + self::reset_original_data(); -+ static::$datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess']; + + $snapshotfiledir = $snapshotdir . DIRECTORY_SEPARATOR . 'filedir'; + $filedir = $dataroot . DIRECTORY_SEPARATOR . 'filedir'; @@ -302,6 +288,7 @@ index 86b52503aa3..6406c98a2fb 100644 + self::$lastdbwrites = $DB->perf_get_writes(); + + remove_dir($snapshotdir); ++ unlink($zipfile); + + echo "Snapshot successfully restored.\n"; + } @@ -470,20 +457,10 @@ index 86b52503aa3..6406c98a2fb 100644 */ public static function build_config_file() { diff --git a/lib/testing/classes/util.php b/lib/testing/classes/util.php -index 5315c77c94b..4550e4d4a2d 100644 +index 5315c77c94b..dea37ca6a56 100644 --- a/lib/testing/classes/util.php +++ b/lib/testing/classes/util.php -@@ -122,9 +122,23 @@ abstract class testing_util { - */ - final protected static function get_framework() { - $classname = get_called_class(); -+ $reflectedclass = new \ReflectionClass($classname); -+ -+ if ($reflectedclass->inNamespace()) { -+ $namespaces = explode('\\', $reflectedclass->getNamespaceName()); -+ return array_shift($namespaces); -+ } -+ +@@ -125,6 +125,13 @@ abstract class testing_util { return substr($classname, 0, strpos($classname, '_')); } @@ -497,19 +474,6 @@ index 5315c77c94b..4550e4d4a2d 100644 /** * Get data generator * @static -@@ -712,7 +726,11 @@ abstract class testing_util { - // Clean up the dataroot folder. - $handle = opendir(self::get_dataroot()); - while (false !== ($item = readdir($handle))) { -- if (in_array($item, $childclassname::$datarootskiponreset)) { -+ // Explicitly check for a snapshot zip and add it to the skip on reset array if there is one. -+ if (str_contains($item, 'snapshot-')) { -+ static::$datarootskiponreset[] = $item; -+ } -+ if (in_array($item, static::$datarootskiponreset)) { - continue; - } - if (is_dir(self::get_dataroot() . "/$item")) { -- 2.43.0 From a0d8e59c7b1910da059ab42d85de9ce80a78bcde Mon Sep 17 00:00:00 2001 From: Abhinav Gandham Date: Tue, 26 May 2026 17:42:06 +1000 Subject: [PATCH 29/43] Updated 501 and 502 patches. --- .github/patches/501-501-phpunit-restore.patch | 102 ++++++------------ .github/patches/502-999-phpunit-restore.patch | 90 ++++++---------- 2 files changed, 67 insertions(+), 125 deletions(-) diff --git a/.github/patches/501-501-phpunit-restore.patch b/.github/patches/501-501-phpunit-restore.patch index 561881a8..5a8aac9b 100644 --- a/.github/patches/501-501-phpunit-restore.patch +++ b/.github/patches/501-501-phpunit-restore.patch @@ -1,17 +1,16 @@ -From e724df9b1296c1d2daf6ecf5da617aff951a9b79 Mon Sep 17 00:00:00 2001 +From b04b0c870a7e997becd9e44c2e0035a8f548565e Mon Sep 17 00:00:00 2001 From: Abhinav Gandham -Date: Thu, 21 May 2026 22:04:10 +1000 -Subject: [PATCH] phpunit patch 501 +Date: Tue, 26 May 2026 17:26:32 +1000 +Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. --- - public/admin/tool/phpunit/cli/util.php | 30 ++- - public/lib/phpunit/bootstraplib.php | 10 +- - public/lib/phpunit/classes/util.php | 353 +++++++++++++++++++++++++ - public/lib/testing/classes/util.php | 15 +- - 4 files changed, 399 insertions(+), 9 deletions(-) + public/admin/tool/phpunit/cli/util.php | 28 +- + public/lib/phpunit/classes/util.php | 367 +++++++++++++++++++++++++ + public/lib/testing/classes/util.php | 7 + + 3 files changed, 400 insertions(+), 2 deletions(-) diff --git a/public/admin/tool/phpunit/cli/util.php b/public/admin/tool/phpunit/cli/util.php -index ff5eb956ddd..658263ab3b6 100644 +index ff5eb956ddd..2ac4af73b74 100644 --- a/public/admin/tool/phpunit/cli/util.php +++ b/public/admin/tool/phpunit/cli/util.php @@ -47,6 +47,9 @@ list($options, $unrecognized) = cli_get_params( @@ -24,15 +23,6 @@ index ff5eb956ddd..658263ab3b6 100644 'help' => false, ], [ -@@ -61,7 +64,7 @@ if (!file_exists(__DIR__.'/../../../../../vendor/phpunit/phpunit/composer.json') - phpunit_bootstrap_error(PHPUNIT_EXITCODE_PHPUNITMISSING); - } - --if ($options['install'] || $options['drop']) { -+if ($options['install'] || $options['drop'] || $options['upgrade']) { - define('CACHE_DISABLE_ALL', true); - } - @@ -102,8 +105,16 @@ $drop = $options['drop']; $install = $options['install']; $buildconfig = $options['buildconfig']; @@ -77,38 +67,29 @@ index ff5eb956ddd..658263ab3b6 100644 + phpunit_util::restore_site(is_string($restore) ? $restore : ''); + exit(0); } -diff --git a/public/lib/phpunit/bootstraplib.php b/public/lib/phpunit/bootstraplib.php -index 48c75dfa1dc..bf3112cf1f5 100644 ---- a/public/lib/phpunit/bootstraplib.php -+++ b/public/lib/phpunit/bootstraplib.php -@@ -65,12 +65,14 @@ function phpunit_bootstrap_error($errorcode, $text = '') { - $text = "Moodle PHPUnit environment configuration warning:\n".$text; - break; - case PHPUNIT_EXITCODE_INSTALL: -- $path = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); -- $text = "Moodle PHPUnit environment is not initialised, please use:\n php $path"; -+ $initpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); -+ $text = "Moodle PHPUnit environment is not initialised, please use:\n php $initpath"; - break; - case PHPUNIT_EXITCODE_REINSTALL: -- $path = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); -- $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php $path"; -+ $initpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); -+ $utilpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/util.php'); -+ $text = "Moodle PHPUnit environment was initialised for different version, please use:\n" -+ . " php $initpath\n or php $utilpath --upgrade"; - break; - default: - $text = empty($text) ? '' : ': '.$text; diff --git a/public/lib/phpunit/classes/util.php b/public/lib/phpunit/classes/util.php -index fcc574117ae..55a31f505ac 100644 +index fcc574117ae..a8764ba0e98 100644 --- a/public/lib/phpunit/classes/util.php +++ b/public/lib/phpunit/classes/util.php -@@ -524,6 +524,359 @@ class phpunit_util extends testing_util { +@@ -454,6 +454,13 @@ class phpunit_util extends testing_util { + echo "Purging dataroot:\n"; + } + ++ // Preserve any snapshot zips so they survive the drop. ++ foreach (scandir(self::get_dataroot()) as $item) { ++ if (str_ends_with($item, '.zip')) { ++ static::$datarootskiponreset[] = $item; ++ } ++ } ++ + self::reset_dataroot(); + testing_initdataroot($CFG->dataroot, 'phpunit'); + +@@ -524,6 +531,366 @@ class phpunit_util extends testing_util { self::store_database_state(); } -+ /** ++ /** + * Create a snapshot of the current test site database and site data. + * + * @param string $snapshotname The name of the snapshot. @@ -261,6 +242,11 @@ index fcc574117ae..55a31f505ac 100644 + + echo "Restoring from $snapshot\n"; + ++ $phpunitdir = $dataroot . DIRECTORY_SEPARATOR . 'phpunit'; ++ if (!is_dir($phpunitdir)) { ++ mkdir($phpunitdir, 0777, true); ++ } ++ + // Copy snapshot DB control files to their active locations. + foreach ($datafiles as $src => $dest) { + copy($src, $dest); @@ -275,10 +261,11 @@ index fcc574117ae..55a31f505ac 100644 + self::reset_database(); + + echo "Data imported. Resetting datarooot...\n"; ++ + // Clear current dataroot before restoring filedir from snapshot. ++ static::$datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess', $snapshot, "$snapshot.zip"]; + self::reset_dataroot(); + self::reset_original_data(); -+ static::$datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess']; + + $snapshotfiledir = $snapshotdir . DIRECTORY_SEPARATOR . 'filedir'; + $filedir = $dataroot . DIRECTORY_SEPARATOR . 'filedir'; @@ -300,6 +287,7 @@ index fcc574117ae..55a31f505ac 100644 + self::$lastdbwrites = $DB->perf_get_writes(); + + remove_dir($snapshotdir); ++ unlink($zipfile); + + echo "Snapshot successfully restored.\n"; + } @@ -465,18 +453,9 @@ index fcc574117ae..55a31f505ac 100644 * Builds root/phpunit.xml file using defaults from /phpunit.xml.dist * diff --git a/public/lib/testing/classes/util.php b/public/lib/testing/classes/util.php -index 3073cde3bf7..0e4c36d7d29 100644 +index 3073cde3bf7..070adfa78ce 100644 --- a/public/lib/testing/classes/util.php +++ b/public/lib/testing/classes/util.php -@@ -115,7 +115,7 @@ abstract class testing_util { - self::$dataroot = $dataroot; - } - -- /** -+ /** - * Returns the testing framework name - * @static - * @return string @@ -125,6 +125,13 @@ abstract class testing_util { return substr($classname, 0, strpos($classname, '_')); } @@ -491,19 +470,6 @@ index 3073cde3bf7..0e4c36d7d29 100644 /** * Get data generator * @static -@@ -656,7 +663,11 @@ abstract class testing_util { - // Clean up the dataroot folder. - $handle = opendir(self::get_dataroot()); - while (false !== ($item = readdir($handle))) { -- if (in_array($item, $childclassname::$datarootskiponreset)) { -+ // Explicitly check for a snapshot zip and add it to the skip on reset array if there is one. -+ if (str_contains($item, 'snapshot-')) { -+ static::$datarootskiponreset[] = $item; -+ } -+ if (in_array($item, static::$datarootskiponreset)) { - continue; - } - if (is_dir(self::get_dataroot() . "/$item")) { -- 2.43.0 diff --git a/.github/patches/502-999-phpunit-restore.patch b/.github/patches/502-999-phpunit-restore.patch index e2984cfe..c006faeb 100644 --- a/.github/patches/502-999-phpunit-restore.patch +++ b/.github/patches/502-999-phpunit-restore.patch @@ -1,18 +1,16 @@ -From e9e25c9c214ce86aed65a6ddf1d4a9b179197551 Mon Sep 17 00:00:00 2001 +From 7d361620f30e380cd585aac4cfa1758082249e9c Mon Sep 17 00:00:00 2001 From: Abhinav Gandham -Date: Wed, 13 May 2026 15:33:48 +1000 -Subject: [PATCH] MDL-88495 unit tests: Implmented test site upgrade, snapshot, - and restore functionality." +Date: Fri, 22 May 2026 15:05:47 +1000 +Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. --- - public/admin/tool/phpunit/cli/util.php | 30 +- - .../lib/classes/test/phpunit/phpunit_util.php | 353 ++++++++++++++++++ - public/lib/classes/test/testing_util.php | 11 + - public/lib/phpunit/bootstraplib.php | 10 +- - 4 files changed, 397 insertions(+), 7 deletions(-) + public/admin/tool/phpunit/cli/util.php | 28 +- + .../lib/classes/test/phpunit/phpunit_util.php | 367 ++++++++++++++++++ + public/lib/classes/test/testing_util.php | 7 + + 3 files changed, 400 insertions(+), 2 deletions(-) diff --git a/public/admin/tool/phpunit/cli/util.php b/public/admin/tool/phpunit/cli/util.php -index 0137344ba03..2bc7c5dd290 100644 +index 0137344ba03..f1aa8a15290 100644 --- a/public/admin/tool/phpunit/cli/util.php +++ b/public/admin/tool/phpunit/cli/util.php @@ -49,6 +49,9 @@ list($options, $unrecognized) = cli_get_params( @@ -25,15 +23,6 @@ index 0137344ba03..2bc7c5dd290 100644 'help' => false, ], [ -@@ -74,7 +77,7 @@ foreach ($requiredclasses as $requiredclass) { - } - } - --if ($options['install'] || $options['drop']) { -+if ($options['install'] || $options['drop'] || $options['upgrade']) { - define('CACHE_DISABLE_ALL', true); - } - @@ -115,8 +118,16 @@ $drop = $options['drop']; $install = $options['install']; $buildconfig = $options['buildconfig']; @@ -79,10 +68,24 @@ index 0137344ba03..2bc7c5dd290 100644 + exit(0); } diff --git a/public/lib/classes/test/phpunit/phpunit_util.php b/public/lib/classes/test/phpunit/phpunit_util.php -index adfd5a61c2c..46fedd65872 100644 +index adfd5a61c2c..2da3d15458a 100644 --- a/public/lib/classes/test/phpunit/phpunit_util.php +++ b/public/lib/classes/test/phpunit/phpunit_util.php -@@ -506,6 +506,359 @@ class phpunit_util extends \core\test\testing_util { +@@ -436,6 +436,13 @@ class phpunit_util extends \core\test\testing_util { + echo "Purging dataroot:\n"; + } + ++ // Preserve any snapshot zips so they survive the drop. ++ foreach (scandir(self::get_dataroot()) as $item) { ++ if (str_ends_with($item, '.zip')) { ++ static::$datarootskiponreset[] = $item; ++ } ++ } ++ + self::reset_dataroot(); + testing_initdataroot($CFG->dataroot, 'phpunit'); + +@@ -506,6 +513,366 @@ class phpunit_util extends \core\test\testing_util { self::store_database_state(); } @@ -239,6 +242,11 @@ index adfd5a61c2c..46fedd65872 100644 + + echo "Restoring from $snapshot\n"; + ++ $phpunitdir = $dataroot . DIRECTORY_SEPARATOR . 'phpunit'; ++ if (!is_dir($phpunitdir)) { ++ mkdir($phpunitdir, 0777, true); ++ } ++ + // Copy snapshot DB control files to their active locations. + foreach ($datafiles as $src => $dest) { + copy($src, $dest); @@ -253,10 +261,11 @@ index adfd5a61c2c..46fedd65872 100644 + self::reset_database(); + + echo "Data imported. Resetting datarooot...\n"; ++ + // Clear current dataroot before restoring filedir from snapshot. ++ static::$datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess', $snapshot, "$snapshot.zip"]; + self::reset_dataroot(); + self::reset_original_data(); -+ static::$datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess']; + + $snapshotfiledir = $snapshotdir . DIRECTORY_SEPARATOR . 'filedir'; + $filedir = $dataroot . DIRECTORY_SEPARATOR . 'filedir'; @@ -278,6 +287,7 @@ index adfd5a61c2c..46fedd65872 100644 + self::$lastdbwrites = $DB->perf_get_writes(); + + remove_dir($snapshotdir); ++ unlink($zipfile); + + echo "Snapshot successfully restored.\n"; + } @@ -443,7 +453,7 @@ index adfd5a61c2c..46fedd65872 100644 * Builds root/phpunit.xml file using defaults from /phpunit.xml.dist * diff --git a/public/lib/classes/test/testing_util.php b/public/lib/classes/test/testing_util.php -index c3aa2a8f3cb..d19aa0ad446 100644 +index c3aa2a8f3cb..4ed9f7fc021 100644 --- a/public/lib/classes/test/testing_util.php +++ b/public/lib/classes/test/testing_util.php @@ -140,6 +140,13 @@ abstract class testing_util { @@ -460,40 +470,6 @@ index c3aa2a8f3cb..d19aa0ad446 100644 /** * Get data generator * -@@ -665,6 +672,10 @@ abstract class testing_util { - // Clean up the dataroot folder. - $handle = opendir(self::get_dataroot()); - while (false !== ($item = readdir($handle))) { -+ // Explicitly check for a snapshot zip and add it to the skip on reset array if there is one. -+ if (str_contains($item, 'snapshot-')) { -+ static::$datarootskiponreset[] = $item; -+ } - if (in_array($item, static::$datarootskiponreset)) { - continue; - } -diff --git a/public/lib/phpunit/bootstraplib.php b/public/lib/phpunit/bootstraplib.php -index 48c75dfa1dc..bf3112cf1f5 100644 ---- a/public/lib/phpunit/bootstraplib.php -+++ b/public/lib/phpunit/bootstraplib.php -@@ -65,12 +65,14 @@ function phpunit_bootstrap_error($errorcode, $text = '') { - $text = "Moodle PHPUnit environment configuration warning:\n".$text; - break; - case PHPUNIT_EXITCODE_INSTALL: -- $path = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); -- $text = "Moodle PHPUnit environment is not initialised, please use:\n php $path"; -+ $initpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); -+ $text = "Moodle PHPUnit environment is not initialised, please use:\n php $initpath"; - break; - case PHPUNIT_EXITCODE_REINSTALL: -- $path = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); -- $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php $path"; -+ $initpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/init.php'); -+ $utilpath = testing_cli_argument_path('/public/admin/tool/phpunit/cli/util.php'); -+ $text = "Moodle PHPUnit environment was initialised for different version, please use:\n" -+ . " php $initpath\n or php $utilpath --upgrade"; - break; - default: - $text = empty($text) ? '' : ': '.$text; -- 2.43.0 From ea64a1cd5c503d9c37ae61d4fd420e44ca56a60f Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Tue, 26 May 2026 17:49:05 +1000 Subject: [PATCH 30/43] fix phpunit snapshot --- .github/workflows/update-snapshots.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-snapshots.yml b/.github/workflows/update-snapshots.yml index 9d599eae..497bc6d7 100644 --- a/.github/workflows/update-snapshots.yml +++ b/.github/workflows/update-snapshots.yml @@ -251,7 +251,8 @@ jobs: cd moodle php "$PHPUNIT_UTIL" --snapshot=ci-phpunit - PHPUNIT_DATAROOT=$(php -r "define('ABORT_AFTER_CONFIG', true); require 'config.php'; echo \$CFG->phpunit_dataroot;") + # phpunit_dataroot is always $GITHUB_WORKSPACE/moodledata/phpunit per the config template. + PHPUNIT_DATAROOT="$GITHUB_WORKSPACE/moodledata/phpunit" PHPUNIT_SNAPSHOT=$(find "$PHPUNIT_DATAROOT" -maxdepth 1 -type f -name '*-ci-phpunit-*.zip' | head -n1) if [ -z "$PHPUNIT_SNAPSHOT" ]; then From 48482a1953d8bbd3c25e3bc7000b527441ec521b Mon Sep 17 00:00:00 2001 From: Abhinav Gandham Date: Tue, 26 May 2026 18:53:04 +1000 Subject: [PATCH 31/43] Updated patches. --- .github/patches/401-403-phpunit-restore.patch | 18 ++++++++++++++++-- .github/patches/404-500-phpunit-restore.patch | 18 ++++++++++++++++-- .github/patches/501-501-phpunit-restore.patch | 18 ++++++++++++++++-- .github/patches/502-999-phpunit-restore.patch | 18 ++++++++++++++++-- 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/.github/patches/401-403-phpunit-restore.patch b/.github/patches/401-403-phpunit-restore.patch index d08ee70e..2713001b 100644 --- a/.github/patches/401-403-phpunit-restore.patch +++ b/.github/patches/401-403-phpunit-restore.patch @@ -1,13 +1,14 @@ -From 806bbf2aa9140dad2a060bd098d991aa989378eb Mon Sep 17 00:00:00 2001 +From a370c7a7f21e815fa43b492cfc802c72413534f8 Mon Sep 17 00:00:00 2001 From: Abhinav Gandham Date: Fri, 22 May 2026 15:05:47 +1000 Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. --- admin/tool/phpunit/cli/util.php | 28 ++- + lib/phpunit/bootstrap.php | 2 +- lib/phpunit/classes/util.php | 371 +++++++++++++++++++++++++++++++- lib/testing/classes/util.php | 7 + - 3 files changed, 402 insertions(+), 4 deletions(-) + 4 files changed, 403 insertions(+), 5 deletions(-) diff --git a/admin/tool/phpunit/cli/util.php b/admin/tool/phpunit/cli/util.php index 1d176900fdf..39cd67fb20f 100644 @@ -67,6 +68,19 @@ index 1d176900fdf..39cd67fb20f 100644 + phpunit_util::restore_site(is_string($restore) ? $restore : ''); + exit(0); } +diff --git a/lib/phpunit/bootstrap.php b/lib/phpunit/bootstrap.php +index e8be0752c2d..d1306ff2c54 100644 +--- a/lib/phpunit/bootstrap.php ++++ b/lib/phpunit/bootstrap.php +@@ -135,7 +135,7 @@ if (!is_writable($CFG->phpunit_dataroot)) { + if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) { + if ($dh = opendir($CFG->phpunit_dataroot)) { + while (($file = readdir($dh)) !== false) { +- if ($file === 'phpunit' or $file === '.' or $file === '..' or $file === '.DS_Store') { ++ if ($file === 'phpunit' || $file === '.' || $file === '..' || $file === '.DS_Store' || str_ends_with($file, '.zip')) { + continue; + } + phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, '$CFG->phpunit_dataroot directory is not empty, can not run tests! Is it used for anything else?'); diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index 9d5acc4bae0..298fa0dffde 100644 --- a/lib/phpunit/classes/util.php diff --git a/.github/patches/404-500-phpunit-restore.patch b/.github/patches/404-500-phpunit-restore.patch index 3e15c149..0b58b9c3 100644 --- a/.github/patches/404-500-phpunit-restore.patch +++ b/.github/patches/404-500-phpunit-restore.patch @@ -1,13 +1,14 @@ -From e3e643ce27ade04e96514e1ccbc1792bd212b4b7 Mon Sep 17 00:00:00 2001 +From 40bed02dd1204c7ef275fd6a6c672d96a73758cb Mon Sep 17 00:00:00 2001 From: Abhinav Gandham Date: Fri, 22 May 2026 15:05:47 +1000 Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. --- admin/tool/phpunit/cli/util.php | 28 ++- + lib/phpunit/bootstrap.php | 2 +- lib/phpunit/classes/util.php | 371 +++++++++++++++++++++++++++++++- lib/testing/classes/util.php | 7 + - 3 files changed, 402 insertions(+), 4 deletions(-) + 4 files changed, 403 insertions(+), 5 deletions(-) diff --git a/admin/tool/phpunit/cli/util.php b/admin/tool/phpunit/cli/util.php index 3cc928a9e5a..988e45f150d 100644 @@ -67,6 +68,19 @@ index 3cc928a9e5a..988e45f150d 100644 + phpunit_util::restore_site(is_string($restore) ? $restore : ''); + exit(0); } +diff --git a/lib/phpunit/bootstrap.php b/lib/phpunit/bootstrap.php +index 9aedea38c3e..19ebfdec034 100644 +--- a/lib/phpunit/bootstrap.php ++++ b/lib/phpunit/bootstrap.php +@@ -151,7 +151,7 @@ if (!is_writable($CFG->phpunit_dataroot)) { + if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) { + if ($dh = opendir($CFG->phpunit_dataroot)) { + while (($file = readdir($dh)) !== false) { +- if ($file === 'phpunit' || $file === '.' || $file === '..' || $file === '.DS_Store') { ++ if ($file === 'phpunit' || $file === '.' || $file === '..' || $file === '.DS_Store' || str_ends_with($file, '.zip')) { + continue; + } + phpunit_bootstrap_error( diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index 86b52503aa3..9063b7e5717 100644 --- a/lib/phpunit/classes/util.php diff --git a/.github/patches/501-501-phpunit-restore.patch b/.github/patches/501-501-phpunit-restore.patch index 5a8aac9b..dea00c63 100644 --- a/.github/patches/501-501-phpunit-restore.patch +++ b/.github/patches/501-501-phpunit-restore.patch @@ -1,13 +1,14 @@ -From b04b0c870a7e997becd9e44c2e0035a8f548565e Mon Sep 17 00:00:00 2001 +From c23dbbf7bb4dc5b2105d827672e4acab695f8e1e Mon Sep 17 00:00:00 2001 From: Abhinav Gandham Date: Tue, 26 May 2026 17:26:32 +1000 Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. --- public/admin/tool/phpunit/cli/util.php | 28 +- + public/lib/phpunit/bootstrap.php | 2 +- public/lib/phpunit/classes/util.php | 367 +++++++++++++++++++++++++ public/lib/testing/classes/util.php | 7 + - 3 files changed, 400 insertions(+), 2 deletions(-) + 4 files changed, 401 insertions(+), 3 deletions(-) diff --git a/public/admin/tool/phpunit/cli/util.php b/public/admin/tool/phpunit/cli/util.php index ff5eb956ddd..2ac4af73b74 100644 @@ -67,6 +68,19 @@ index ff5eb956ddd..2ac4af73b74 100644 + phpunit_util::restore_site(is_string($restore) ? $restore : ''); + exit(0); } +diff --git a/public/lib/phpunit/bootstrap.php b/public/lib/phpunit/bootstrap.php +index 8916ad69765..60da5faee50 100644 +--- a/public/lib/phpunit/bootstrap.php ++++ b/public/lib/phpunit/bootstrap.php +@@ -151,7 +151,7 @@ if (!is_writable($CFG->phpunit_dataroot)) { + if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) { + if ($dh = opendir($CFG->phpunit_dataroot)) { + while (($file = readdir($dh)) !== false) { +- if ($file === 'phpunit' || $file === '.' || $file === '..' || $file === '.DS_Store') { ++ if ($file === 'phpunit' || $file === '.' || $file === '..' || $file === '.DS_Store' || str_ends_with($file, '.zip')) { + continue; + } + phpunit_bootstrap_error( diff --git a/public/lib/phpunit/classes/util.php b/public/lib/phpunit/classes/util.php index fcc574117ae..a8764ba0e98 100644 --- a/public/lib/phpunit/classes/util.php diff --git a/.github/patches/502-999-phpunit-restore.patch b/.github/patches/502-999-phpunit-restore.patch index c006faeb..80fb7731 100644 --- a/.github/patches/502-999-phpunit-restore.patch +++ b/.github/patches/502-999-phpunit-restore.patch @@ -1,4 +1,4 @@ -From 7d361620f30e380cd585aac4cfa1758082249e9c Mon Sep 17 00:00:00 2001 +From 03392413753019ef5ef486590610d63ad0ca5213 Mon Sep 17 00:00:00 2001 From: Abhinav Gandham Date: Fri, 22 May 2026 15:05:47 +1000 Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. @@ -7,7 +7,8 @@ Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. public/admin/tool/phpunit/cli/util.php | 28 +- .../lib/classes/test/phpunit/phpunit_util.php | 367 ++++++++++++++++++ public/lib/classes/test/testing_util.php | 7 + - 3 files changed, 400 insertions(+), 2 deletions(-) + public/lib/phpunit/bootstrap.php | 2 +- + 4 files changed, 401 insertions(+), 3 deletions(-) diff --git a/public/admin/tool/phpunit/cli/util.php b/public/admin/tool/phpunit/cli/util.php index 0137344ba03..f1aa8a15290 100644 @@ -470,6 +471,19 @@ index c3aa2a8f3cb..4ed9f7fc021 100644 /** * Get data generator * +diff --git a/public/lib/phpunit/bootstrap.php b/public/lib/phpunit/bootstrap.php +index f49b8e614e7..48181e28342 100644 +--- a/public/lib/phpunit/bootstrap.php ++++ b/public/lib/phpunit/bootstrap.php +@@ -151,7 +151,7 @@ if (!is_writable($CFG->phpunit_dataroot)) { + if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) { + if ($dh = opendir($CFG->phpunit_dataroot)) { + while (($file = readdir($dh)) !== false) { +- if ($file === 'phpunit' || $file === '.' || $file === '..' || $file === '.DS_Store') { ++ if ($file === 'phpunit' || $file === '.' || $file === '..' || $file === '.DS_Store' || str_ends_with($file, '.zip')) { + continue; + } + phpunit_bootstrap_error( -- 2.43.0 From 40dcaa93fe8ffeea51883821780677a4d7056d65 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Tue, 26 May 2026 23:38:06 +1000 Subject: [PATCH 32/43] Fixed phpunit buildconfig after adding the plugin --- .github/plugin/setup/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 8147a034..9aae7631 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -205,6 +205,7 @@ runs: php "$PHPUNIT_UTIL" --restore=ci-phpunit php "$PHPUNIT_UTIL" --upgrade + php "$PHPUNIT_UTIL" --buildconfig END_TS=$(date +%s) echo "Create database and restore PHPUnit snapshot took $((END_TS - START_TS))s" From 35509d98b0be8be7696b1a0d4df99a7ba4db2792 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Tue, 26 May 2026 23:47:24 +1000 Subject: [PATCH 33/43] Fix privacy related unit test calls --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1216e52c..5b33143e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -561,8 +561,8 @@ jobs: run: | cd moodle vendor/bin/phpunit --fail-on-risky --disallow-test-output --testsuite ${{ needs.prepare_matrix.outputs.component }}_testsuite - vendor/bin/phpunit --fail-on-risky --disallow-test-output --filter tool_dataprivacy_metadata_registry_testcase - vendor/bin/phpunit --fail-on-risky --disallow-test-output --testsuite core_privacy_testsuite --filter provider_testcase + vendor/bin/phpunit --fail-on-risky --disallow-test-output --filter metadata_registry + vendor/bin/phpunit --fail-on-risky --disallow-test-output --testsuite core_privacy_testsuite --filter provider_test shell: bash behat: name: ${{ matrix.moodle-branch-short }} - behat - php${{ matrix.php }} - ${{ matrix.database }} From 82fc36cdfede55570d7c3c478e98e585021a802d Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Wed, 27 May 2026 00:08:48 +1000 Subject: [PATCH 34/43] Filter tests to just critical privacy detection tests --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b33143e..8fcff6a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -561,8 +561,9 @@ jobs: run: | cd moodle vendor/bin/phpunit --fail-on-risky --disallow-test-output --testsuite ${{ needs.prepare_matrix.outputs.component }}_testsuite - vendor/bin/phpunit --fail-on-risky --disallow-test-output --filter metadata_registry - vendor/bin/phpunit --fail-on-risky --disallow-test-output --testsuite core_privacy_testsuite --filter provider_test + if [ -f "$GITHUB_WORKSPACE/plugin/classes/privacy/provider.php" ]; then + vendor/bin/phpunit --fail-on-risky --disallow-test-output --testsuite core_privacy_testsuite --filter test_all_providers_compliant + fi shell: bash behat: name: ${{ matrix.moodle-branch-short }} - behat - php${{ matrix.php }} - ${{ matrix.database }} From 5cff6530330b3fd1b016c55bff8a0849b38a5820 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Wed, 27 May 2026 01:42:53 +1000 Subject: [PATCH 35/43] Tune postgres for speed --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fcff6a6..e9fc76c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -549,6 +549,15 @@ jobs: repository: catalyst/catalyst-moodle-workflows ref: ${{ inputs.internal_workflow_branch }} token: ${{ github.token }} + - name: Tune PostgreSQL for CI speed + if: matrix.database == 'pgsql' + run: | + psql -h 127.0.0.1 -U postgres -c "ALTER SYSTEM SET fsync = off" + psql -h 127.0.0.1 -U postgres -c "ALTER SYSTEM SET synchronous_commit = off" + psql -h 127.0.0.1 -U postgres -c "ALTER SYSTEM SET full_page_writes = off" + psql -h 127.0.0.1 -U postgres -c "ALTER SYSTEM SET checkpoint_completion_target = 0.9" + psql -h 127.0.0.1 -U postgres -c "ALTER SYSTEM SET max_wal_size = '1GB'" + psql -h 127.0.0.1 -U postgres -c "SELECT pg_reload_conf()" - name: Run plugin setup uses: ./ci/.github/plugin/setup with: From 6954573ad90a52be2fa1db6740429eb9e56190c3 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Wed, 27 May 2026 02:15:03 +1000 Subject: [PATCH 36/43] plit out DB and restore to its own step --- .github/plugin/setup/action.yml | 46 --------------------------------- .github/workflows/ci.yml | 39 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 46 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 9aae7631..5d2a0799 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -166,49 +166,3 @@ runs: echo "MOODLE_DIR=$GITHUB_WORKSPACE/moodle" >> "$GITHUB_ENV" echo "PLUGIN_DIR=$PLUGIN_PATH" >> "$GITHUB_ENV" - - name: Create database and restore PHPUnit snapshot - if: ${{ inputs.database != '' }} - shell: bash - env: - DB: ${{ inputs.database }} - run: | - START_TS=$(date +%s) - echo "::group::Create database and restore PHPUnit snapshot" - - # Extract phpunit snapshot zip from the image — fail if missing. - CID=$(docker create "$SNAPSHOT_IMAGE") - docker cp "$CID:/phpunit-snapshot.zip" ./phpunit-snapshot.zip - docker rm "$CID" - - # Create the Moodle database. - if [ "$DB" = "pgsql" ]; then - psql -h 127.0.0.1 -U postgres -d postgres -c "DROP DATABASE IF EXISTS moodle;" - psql -h 127.0.0.1 -U postgres -d postgres -c "CREATE DATABASE moodle;" - else - mysql -h 127.0.0.1 -u root -e "DROP DATABASE IF EXISTS moodle; CREATE DATABASE moodle DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" - fi - - # phpunit_dataroot is always $GITHUB_WORKSPACE/moodledata/phpunit per the config template. - PHPUNIT_DATAROOT="$GITHUB_WORKSPACE/moodledata/phpunit" - mkdir -p "$PHPUNIT_DATAROOT" - cp "$GITHUB_WORKSPACE/phpunit-snapshot.zip" "$PHPUNIT_DATAROOT/ci-phpunit.zip" - - # Detect phpunit util location (differs between classic and public/ layouts). - if [ -f "$GITHUB_WORKSPACE/moodle/public/admin/tool/phpunit/cli/util.php" ]; then - PHPUNIT_UTIL="$GITHUB_WORKSPACE/moodle/public/admin/tool/phpunit/cli/util.php" - elif [ -f "$GITHUB_WORKSPACE/moodle/admin/tool/phpunit/cli/util.php" ]; then - PHPUNIT_UTIL="$GITHUB_WORKSPACE/moodle/admin/tool/phpunit/cli/util.php" - else - echo "Could not locate phpunit util.php" - exit 1 - fi - - php "$PHPUNIT_UTIL" --restore=ci-phpunit - php "$PHPUNIT_UTIL" --upgrade - php "$PHPUNIT_UTIL" --buildconfig - - END_TS=$(date +%s) - echo "Create database and restore PHPUnit snapshot took $((END_TS - START_TS))s" - echo "::endgroup::" - - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9fc76c2..553de358 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -566,6 +566,45 @@ jobs: extra_plugin_runners: ${{ inputs.extra_plugin_runners }} moodle_branch: ${{ matrix.moodle-branch }} database: ${{ matrix.database }} + - name: Create database and restore PHPUnit snapshot + run: | + START_TS=$(date +%s) + + # Extract phpunit snapshot zip from the image — fail if missing. + CID=$(docker create "$SNAPSHOT_IMAGE") + docker cp "$CID:/phpunit-snapshot.zip" ./phpunit-snapshot.zip + docker rm "$CID" + + # Create the Moodle database. + if [ "${{ matrix.database }}" = "pgsql" ]; then + psql -h 127.0.0.1 -U postgres -d postgres -c "DROP DATABASE IF EXISTS moodle;" + psql -h 127.0.0.1 -U postgres -d postgres -c "CREATE DATABASE moodle;" + else + mysql -h 127.0.0.1 -u root -e "DROP DATABASE IF EXISTS moodle; CREATE DATABASE moodle DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + fi + + # phpunit_dataroot is always $GITHUB_WORKSPACE/moodledata/phpunit per the config template. + PHPUNIT_DATAROOT="$GITHUB_WORKSPACE/moodledata/phpunit" + mkdir -p "$PHPUNIT_DATAROOT" + cp "$GITHUB_WORKSPACE/phpunit-snapshot.zip" "$PHPUNIT_DATAROOT/ci-phpunit.zip" + + # Detect phpunit util location (differs between classic and public/ layouts). + if [ -f "$GITHUB_WORKSPACE/moodle/public/admin/tool/phpunit/cli/util.php" ]; then + PHPUNIT_UTIL="$GITHUB_WORKSPACE/moodle/public/admin/tool/phpunit/cli/util.php" + elif [ -f "$GITHUB_WORKSPACE/moodle/admin/tool/phpunit/cli/util.php" ]; then + PHPUNIT_UTIL="$GITHUB_WORKSPACE/moodle/admin/tool/phpunit/cli/util.php" + else + echo "Could not locate phpunit util.php" + exit 1 + fi + + php "$PHPUNIT_UTIL" --restore=ci-phpunit + php "$PHPUNIT_UTIL" --upgrade + php "$PHPUNIT_UTIL" --buildconfig + + END_TS=$(date +%s) + echo "Create database and restore PHPUnit snapshot took $((END_TS - START_TS))s" + shell: bash - name: Run phpunit run: | cd moodle From 4afef9455651f5ccb46f729999edd2d0219ad2c5 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Wed, 27 May 2026 02:32:34 +1000 Subject: [PATCH 37/43] Always run privacy tests --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 553de358..cb357183 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -609,9 +609,7 @@ jobs: run: | cd moodle vendor/bin/phpunit --fail-on-risky --disallow-test-output --testsuite ${{ needs.prepare_matrix.outputs.component }}_testsuite - if [ -f "$GITHUB_WORKSPACE/plugin/classes/privacy/provider.php" ]; then - vendor/bin/phpunit --fail-on-risky --disallow-test-output --testsuite core_privacy_testsuite --filter test_all_providers_compliant - fi + vendor/bin/phpunit --fail-on-risky --disallow-test-output --testsuite core_privacy_testsuite --filter test_all_providers_compliant shell: bash behat: name: ${{ matrix.moodle-branch-short }} - behat - php${{ matrix.php }} - ${{ matrix.database }} From 8d2674edd3ba3c898a33308e0908ace57a9e4ac0 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Wed, 27 May 2026 02:40:04 +1000 Subject: [PATCH 38/43] Nicer grouping of unit tests --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb357183..8722df9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -606,10 +606,14 @@ jobs: echo "Create database and restore PHPUnit snapshot took $((END_TS - START_TS))s" shell: bash - name: Run phpunit + working-directory: moodle run: | - cd moodle + echo "::group::Plugin unit tests" vendor/bin/phpunit --fail-on-risky --disallow-test-output --testsuite ${{ needs.prepare_matrix.outputs.component }}_testsuite + echo "::endgroup::" + echo "::group::Privacy unit tests" vendor/bin/phpunit --fail-on-risky --disallow-test-output --testsuite core_privacy_testsuite --filter test_all_providers_compliant + echo "::endgroup::" shell: bash behat: name: ${{ matrix.moodle-branch-short }} - behat - php${{ matrix.php }} - ${{ matrix.database }} From d5f2399bef54661900e4095c54ae8cb252e51596 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Wed, 27 May 2026 02:49:49 +1000 Subject: [PATCH 39/43] Move timing outside of groups --- .github/plugin/setup/action.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 5d2a0799..436c0d31 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -78,9 +78,9 @@ runs: echo $(cd m-ci/bin; pwd) >> $GITHUB_PATH echo $(cd m-ci/vendor/bin; pwd) >> $GITHUB_PATH + echo "::endgroup::" END_TS=$(date +%s) echo "Add moodle-plugin-ci binaries to PATH took $((END_TS - START_TS))s" - echo "::endgroup::" shell: bash - name: Generate en_AU locale @@ -91,23 +91,22 @@ runs: # PHPUnit depends on en_AU.UTF-8 locale sudo locale-gen en_AU.UTF-8 + echo "::endgroup::" END_TS=$(date +%s) echo "Generate en_AU locale took $((END_TS - START_TS))s" - echo "::endgroup::" shell: bash - name: Install dependencies if: ${{ inputs.extra_plugin_runners }} run: | START_TS=$(date +%s) - # Install dependencies echo "::group::Install dependencies" ${{ inputs.extra_plugin_runners }} + echo "::endgroup::" END_TS=$(date +%s) echo "Install dependencies took $((END_TS - START_TS))s" - echo "::endgroup::" shell: bash - name: Apply plugin branch patch @@ -128,9 +127,9 @@ runs: echo "No plugin branch patch found for ${{ inputs.moodle_branch }}" fi + echo "::endgroup::" END_TS=$(date +%s) echo "Apply plugin branch patch took $((END_TS - START_TS))s" - echo "::endgroup::" shell: bash - name: Install plugin into Moodle From af67c404ff9a5b90fdcac9c866f96a7b734b9e0c Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Wed, 27 May 2026 02:56:10 +1000 Subject: [PATCH 40/43] Cleanup timing group titles --- .github/plugin/setup/action.yml | 8 ++++---- .github/workflows/ci.yml | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 436c0d31..08339e6b 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -71,8 +71,8 @@ runs: - name: Add moodle-plugin-ci binaries to PATH run: | - START_TS=$(date +%s) echo "::group::Add moodle-plugin-ci binaries to PATH" + START_TS=$(date +%s) # Add dirs to $PATH echo $(cd m-ci/bin; pwd) >> $GITHUB_PATH @@ -85,8 +85,8 @@ runs: - name: Generate en_AU locale run: | - START_TS=$(date +%s) echo "::group::Generate en_AU locale" + START_TS=$(date +%s) # PHPUnit depends on en_AU.UTF-8 locale sudo locale-gen en_AU.UTF-8 @@ -99,8 +99,8 @@ runs: - name: Install dependencies if: ${{ inputs.extra_plugin_runners }} run: | - START_TS=$(date +%s) echo "::group::Install dependencies" + START_TS=$(date +%s) ${{ inputs.extra_plugin_runners }} @@ -111,8 +111,8 @@ runs: - name: Apply plugin branch patch run: | - START_TS=$(date +%s) echo "::group::Apply plugin branch patch" + START_TS=$(date +%s) TARGET_REPO="$GITHUB_WORKSPACE/moodle" PATCH="$GITHUB_WORKSPACE/plugin/patch/${{ inputs.moodle_branch }}.diff" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8722df9d..45c451f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -568,6 +568,7 @@ jobs: database: ${{ matrix.database }} - name: Create database and restore PHPUnit snapshot run: | + echo "::group::Create database and restore PHPUnit snapshot" START_TS=$(date +%s) # Extract phpunit snapshot zip from the image — fail if missing. @@ -602,6 +603,7 @@ jobs: php "$PHPUNIT_UTIL" --upgrade php "$PHPUNIT_UTIL" --buildconfig + echo "::endgroup::" END_TS=$(date +%s) echo "Create database and restore PHPUnit snapshot took $((END_TS - START_TS))s" shell: bash From 72f15816065c127ec5bb84ab28b5d1a08d8f494e Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Wed, 27 May 2026 03:01:11 +1000 Subject: [PATCH 41/43] Fix group titles --- .github/plugin/setup/action.yml | 16 ---------------- .github/workflows/ci.yml | 2 -- 2 files changed, 18 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index 08339e6b..f50eeecc 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -71,27 +71,18 @@ runs: - name: Add moodle-plugin-ci binaries to PATH run: | - echo "::group::Add moodle-plugin-ci binaries to PATH" START_TS=$(date +%s) - - # Add dirs to $PATH echo $(cd m-ci/bin; pwd) >> $GITHUB_PATH echo $(cd m-ci/vendor/bin; pwd) >> $GITHUB_PATH - - echo "::endgroup::" END_TS=$(date +%s) echo "Add moodle-plugin-ci binaries to PATH took $((END_TS - START_TS))s" shell: bash - name: Generate en_AU locale run: | - echo "::group::Generate en_AU locale" START_TS=$(date +%s) - # PHPUnit depends on en_AU.UTF-8 locale sudo locale-gen en_AU.UTF-8 - - echo "::endgroup::" END_TS=$(date +%s) echo "Generate en_AU locale took $((END_TS - START_TS))s" shell: bash @@ -99,21 +90,15 @@ runs: - name: Install dependencies if: ${{ inputs.extra_plugin_runners }} run: | - echo "::group::Install dependencies" START_TS=$(date +%s) - ${{ inputs.extra_plugin_runners }} - - echo "::endgroup::" END_TS=$(date +%s) echo "Install dependencies took $((END_TS - START_TS))s" shell: bash - name: Apply plugin branch patch run: | - echo "::group::Apply plugin branch patch" START_TS=$(date +%s) - TARGET_REPO="$GITHUB_WORKSPACE/moodle" PATCH="$GITHUB_WORKSPACE/plugin/patch/${{ inputs.moodle_branch }}.diff" @@ -127,7 +112,6 @@ runs: echo "No plugin branch patch found for ${{ inputs.moodle_branch }}" fi - echo "::endgroup::" END_TS=$(date +%s) echo "Apply plugin branch patch took $((END_TS - START_TS))s" shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45c451f4..8722df9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -568,7 +568,6 @@ jobs: database: ${{ matrix.database }} - name: Create database and restore PHPUnit snapshot run: | - echo "::group::Create database and restore PHPUnit snapshot" START_TS=$(date +%s) # Extract phpunit snapshot zip from the image — fail if missing. @@ -603,7 +602,6 @@ jobs: php "$PHPUNIT_UTIL" --upgrade php "$PHPUNIT_UTIL" --buildconfig - echo "::endgroup::" END_TS=$(date +%s) echo "Create database and restore PHPUnit snapshot took $((END_TS - START_TS))s" shell: bash From dbf1b25fb1b29733e4201239c5bdaf6b7285ac57 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Wed, 27 May 2026 03:02:58 +1000 Subject: [PATCH 42/43] Add groups for docker commands --- .github/plugin/setup/action.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/plugin/setup/action.yml b/.github/plugin/setup/action.yml index f50eeecc..33837a6d 100644 --- a/.github/plugin/setup/action.yml +++ b/.github/plugin/setup/action.yml @@ -57,10 +57,17 @@ runs: IMAGE="ghcr.io/catalyst/snapshot-${BRANCH}-${DB}" IMAGE="${IMAGE//_/-}" IMAGE="${IMAGE,,}:latest" - echo "Pulling snapshot image $IMAGE..." - docker pull "$IMAGE" echo "SNAPSHOT_IMAGE=$IMAGE" >> "$GITHUB_ENV" + START_TS=$(date +%s) + echo "::group::Pull snapshot Docker image" + docker pull "$IMAGE" + echo "::endgroup::" + END_TS=$(date +%s) + echo "Pull snapshot Docker image took $((END_TS - START_TS))s" + + START_TS=$(date +%s) + echo "::group::Extract artifacts" CID=$(docker create "$IMAGE") docker cp "$CID:/m-ci.tar.gz" ./m-ci.tar.gz docker cp "$CID:/moodle.tar.gz" ./moodle.tar.gz @@ -68,6 +75,9 @@ runs: tar -C "$GITHUB_WORKSPACE" -xzf "$GITHUB_WORKSPACE/m-ci.tar.gz" tar -C "$GITHUB_WORKSPACE" -xzf "$GITHUB_WORKSPACE/moodle.tar.gz" mkdir -p "$GITHUB_WORKSPACE/moodledata" + echo "::endgroup::" + END_TS=$(date +%s) + echo "Extract artifacts took $((END_TS - START_TS))s" - name: Add moodle-plugin-ci binaries to PATH run: | From ea9eed5b10e3310f3ea6dd4f86a8adea5a9c8c9b Mon Sep 17 00:00:00 2001 From: Abhinav Gandham Date: Wed, 27 May 2026 12:54:06 +1000 Subject: [PATCH 43/43] Updated patches with mariadb fix. --- .github/patches/401-403-phpunit-restore.patch | 21 ++++++++++++------- .github/patches/404-500-phpunit-restore.patch | 21 ++++++++++++------- .github/patches/501-501-phpunit-restore.patch | 21 ++++++++++++------- .github/patches/502-999-phpunit-restore.patch | 21 ++++++++++++------- 4 files changed, 52 insertions(+), 32 deletions(-) diff --git a/.github/patches/401-403-phpunit-restore.patch b/.github/patches/401-403-phpunit-restore.patch index 2713001b..6d5b14b7 100644 --- a/.github/patches/401-403-phpunit-restore.patch +++ b/.github/patches/401-403-phpunit-restore.patch @@ -1,4 +1,4 @@ -From a370c7a7f21e815fa43b492cfc802c72413534f8 Mon Sep 17 00:00:00 2001 +From 922f91b1cfd353c00f5dcd41d7c52bced26917b4 Mon Sep 17 00:00:00 2001 From: Abhinav Gandham Date: Fri, 22 May 2026 15:05:47 +1000 Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. @@ -6,9 +6,9 @@ Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. --- admin/tool/phpunit/cli/util.php | 28 ++- lib/phpunit/bootstrap.php | 2 +- - lib/phpunit/classes/util.php | 371 +++++++++++++++++++++++++++++++- + lib/phpunit/classes/util.php | 376 +++++++++++++++++++++++++++++++- lib/testing/classes/util.php | 7 + - 4 files changed, 403 insertions(+), 5 deletions(-) + 4 files changed, 408 insertions(+), 5 deletions(-) diff --git a/admin/tool/phpunit/cli/util.php b/admin/tool/phpunit/cli/util.php index 1d176900fdf..39cd67fb20f 100644 @@ -82,7 +82,7 @@ index e8be0752c2d..d1306ff2c54 100644 } phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, '$CFG->phpunit_dataroot directory is not empty, can not run tests! Is it used for anything else?'); diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php -index 9d5acc4bae0..298fa0dffde 100644 +index 9d5acc4bae0..44a3215e1f5 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -427,6 +427,13 @@ class phpunit_util extends testing_util { @@ -99,7 +99,7 @@ index 9d5acc4bae0..298fa0dffde 100644 self::reset_dataroot(); testing_initdataroot($CFG->dataroot, 'phpunit'); -@@ -496,8 +503,368 @@ class phpunit_util extends testing_util { +@@ -496,8 +503,373 @@ class phpunit_util extends testing_util { } /** @@ -411,9 +411,14 @@ index 9d5acc4bae0..298fa0dffde 100644 + $field->setLength($col->max_length ?: 10); + break; + case 'N': -+ $field->setType(XMLDB_TYPE_NUMBER); -+ $field->setLength($col->max_length ?: 10); -+ $field->setDecimals($col->scale ?: 0); ++ $floattypes = ['float', 'double', 'float4', 'float8', 'double precision']; ++ if (in_array(strtolower($col->type ?? ''), $floattypes)) { ++ $field->setType(XMLDB_TYPE_FLOAT); ++ } else { ++ $field->setType(XMLDB_TYPE_NUMBER); ++ $field->setLength($col->max_length ?: 10); ++ $field->setDecimals($col->scale ?: 0); ++ } + break; + case 'C': + $field->setType(XMLDB_TYPE_CHAR); diff --git a/.github/patches/404-500-phpunit-restore.patch b/.github/patches/404-500-phpunit-restore.patch index 0b58b9c3..a490ca33 100644 --- a/.github/patches/404-500-phpunit-restore.patch +++ b/.github/patches/404-500-phpunit-restore.patch @@ -1,4 +1,4 @@ -From 40bed02dd1204c7ef275fd6a6c672d96a73758cb Mon Sep 17 00:00:00 2001 +From 192dc1a2146891462759923aaf6e6f934999c99a Mon Sep 17 00:00:00 2001 From: Abhinav Gandham Date: Fri, 22 May 2026 15:05:47 +1000 Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. @@ -6,9 +6,9 @@ Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. --- admin/tool/phpunit/cli/util.php | 28 ++- lib/phpunit/bootstrap.php | 2 +- - lib/phpunit/classes/util.php | 371 +++++++++++++++++++++++++++++++- + lib/phpunit/classes/util.php | 376 +++++++++++++++++++++++++++++++- lib/testing/classes/util.php | 7 + - 4 files changed, 403 insertions(+), 5 deletions(-) + 4 files changed, 408 insertions(+), 5 deletions(-) diff --git a/admin/tool/phpunit/cli/util.php b/admin/tool/phpunit/cli/util.php index 3cc928a9e5a..988e45f150d 100644 @@ -82,7 +82,7 @@ index 9aedea38c3e..19ebfdec034 100644 } phpunit_bootstrap_error( diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php -index 86b52503aa3..9063b7e5717 100644 +index 86b52503aa3..efca15659f3 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -451,6 +451,13 @@ class phpunit_util extends testing_util { @@ -99,7 +99,7 @@ index 86b52503aa3..9063b7e5717 100644 self::reset_dataroot(); testing_initdataroot($CFG->dataroot, 'phpunit'); -@@ -520,8 +527,368 @@ class phpunit_util extends testing_util { +@@ -520,8 +527,373 @@ class phpunit_util extends testing_util { } /** @@ -411,9 +411,14 @@ index 86b52503aa3..9063b7e5717 100644 + $field->setLength($col->max_length ?: 10); + break; + case 'N': -+ $field->setType(XMLDB_TYPE_NUMBER); -+ $field->setLength($col->max_length ?: 10); -+ $field->setDecimals($col->scale ?: 0); ++ $floattypes = ['float', 'double', 'float4', 'float8', 'double precision']; ++ if (in_array(strtolower($col->type ?? ''), $floattypes)) { ++ $field->setType(XMLDB_TYPE_FLOAT); ++ } else { ++ $field->setType(XMLDB_TYPE_NUMBER); ++ $field->setLength($col->max_length ?: 10); ++ $field->setDecimals($col->scale ?: 0); ++ } + break; + case 'C': + $field->setType(XMLDB_TYPE_CHAR); diff --git a/.github/patches/501-501-phpunit-restore.patch b/.github/patches/501-501-phpunit-restore.patch index dea00c63..d781c3f9 100644 --- a/.github/patches/501-501-phpunit-restore.patch +++ b/.github/patches/501-501-phpunit-restore.patch @@ -1,4 +1,4 @@ -From c23dbbf7bb4dc5b2105d827672e4acab695f8e1e Mon Sep 17 00:00:00 2001 +From 6bdbe047e859f83718c56333449f1ce2ca4e602a Mon Sep 17 00:00:00 2001 From: Abhinav Gandham Date: Tue, 26 May 2026 17:26:32 +1000 Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. @@ -6,9 +6,9 @@ Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. --- public/admin/tool/phpunit/cli/util.php | 28 +- public/lib/phpunit/bootstrap.php | 2 +- - public/lib/phpunit/classes/util.php | 367 +++++++++++++++++++++++++ + public/lib/phpunit/classes/util.php | 372 +++++++++++++++++++++++++ public/lib/testing/classes/util.php | 7 + - 4 files changed, 401 insertions(+), 3 deletions(-) + 4 files changed, 406 insertions(+), 3 deletions(-) diff --git a/public/admin/tool/phpunit/cli/util.php b/public/admin/tool/phpunit/cli/util.php index ff5eb956ddd..2ac4af73b74 100644 @@ -82,7 +82,7 @@ index 8916ad69765..60da5faee50 100644 } phpunit_bootstrap_error( diff --git a/public/lib/phpunit/classes/util.php b/public/lib/phpunit/classes/util.php -index fcc574117ae..a8764ba0e98 100644 +index fcc574117ae..7436abae5f6 100644 --- a/public/lib/phpunit/classes/util.php +++ b/public/lib/phpunit/classes/util.php @@ -454,6 +454,13 @@ class phpunit_util extends testing_util { @@ -99,7 +99,7 @@ index fcc574117ae..a8764ba0e98 100644 self::reset_dataroot(); testing_initdataroot($CFG->dataroot, 'phpunit'); -@@ -524,6 +531,366 @@ class phpunit_util extends testing_util { +@@ -524,6 +531,371 @@ class phpunit_util extends testing_util { self::store_database_state(); } @@ -410,9 +410,14 @@ index fcc574117ae..a8764ba0e98 100644 + $field->setLength($col->max_length ?: 10); + break; + case 'N': -+ $field->setType(XMLDB_TYPE_NUMBER); -+ $field->setLength($col->max_length ?: 10); -+ $field->setDecimals($col->scale ?: 0); ++ $floattypes = ['float', 'double', 'float4', 'float8', 'double precision']; ++ if (in_array(strtolower($col->type ?? ''), $floattypes)) { ++ $field->setType(XMLDB_TYPE_FLOAT); ++ } else { ++ $field->setType(XMLDB_TYPE_NUMBER); ++ $field->setLength($col->max_length ?: 10); ++ $field->setDecimals($col->scale ?: 0); ++ } + break; + case 'C': + $field->setType(XMLDB_TYPE_CHAR); diff --git a/.github/patches/502-999-phpunit-restore.patch b/.github/patches/502-999-phpunit-restore.patch index 80fb7731..3f3e5ddc 100644 --- a/.github/patches/502-999-phpunit-restore.patch +++ b/.github/patches/502-999-phpunit-restore.patch @@ -1,14 +1,14 @@ -From 03392413753019ef5ef486590610d63ad0ca5213 Mon Sep 17 00:00:00 2001 +From 3f43ee764e2582fb0d32f6e710fccea61c1e7e5a Mon Sep 17 00:00:00 2001 From: Abhinav Gandham Date: Fri, 22 May 2026 15:05:47 +1000 Subject: [PATCH] MDL-88495 Unit tests: Snapshot and restore for test site. --- public/admin/tool/phpunit/cli/util.php | 28 +- - .../lib/classes/test/phpunit/phpunit_util.php | 367 ++++++++++++++++++ + .../lib/classes/test/phpunit/phpunit_util.php | 372 ++++++++++++++++++ public/lib/classes/test/testing_util.php | 7 + public/lib/phpunit/bootstrap.php | 2 +- - 4 files changed, 401 insertions(+), 3 deletions(-) + 4 files changed, 406 insertions(+), 3 deletions(-) diff --git a/public/admin/tool/phpunit/cli/util.php b/public/admin/tool/phpunit/cli/util.php index 0137344ba03..f1aa8a15290 100644 @@ -69,7 +69,7 @@ index 0137344ba03..f1aa8a15290 100644 + exit(0); } diff --git a/public/lib/classes/test/phpunit/phpunit_util.php b/public/lib/classes/test/phpunit/phpunit_util.php -index adfd5a61c2c..2da3d15458a 100644 +index adfd5a61c2c..d9675291181 100644 --- a/public/lib/classes/test/phpunit/phpunit_util.php +++ b/public/lib/classes/test/phpunit/phpunit_util.php @@ -436,6 +436,13 @@ class phpunit_util extends \core\test\testing_util { @@ -86,7 +86,7 @@ index adfd5a61c2c..2da3d15458a 100644 self::reset_dataroot(); testing_initdataroot($CFG->dataroot, 'phpunit'); -@@ -506,6 +513,366 @@ class phpunit_util extends \core\test\testing_util { +@@ -506,6 +513,371 @@ class phpunit_util extends \core\test\testing_util { self::store_database_state(); } @@ -397,9 +397,14 @@ index adfd5a61c2c..2da3d15458a 100644 + $field->setLength($col->max_length ?: 10); + break; + case 'N': -+ $field->setType(XMLDB_TYPE_NUMBER); -+ $field->setLength($col->max_length ?: 10); -+ $field->setDecimals($col->scale ?: 0); ++ $floattypes = ['float', 'double', 'float4', 'float8', 'double precision']; ++ if (in_array(strtolower($col->type ?? ''), $floattypes)) { ++ $field->setType(XMLDB_TYPE_FLOAT); ++ } else { ++ $field->setType(XMLDB_TYPE_NUMBER); ++ $field->setLength($col->max_length ?: 10); ++ $field->setDecimals($col->scale ?: 0); ++ } + break; + case 'C': + $field->setType(XMLDB_TYPE_CHAR);