Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / vendor / drush / drush / tests / UnishTestCase.php
1 <?php
2
3 namespace Unish;
4
5 use Symfony\Component\Yaml\Yaml;
6 use Webmozart\PathUtil\Path;
7
8 abstract class UnishTestCase extends \PHPUnit_Framework_TestCase
9 {
10
11     /**
12      * A list of Drupal sites that have been recently installed. They key is the
13      * site name and values are details about each site.
14      *
15      * @var array
16      */
17     private static $sites = [];
18
19     private static $sandbox;
20
21     private static $drush;
22
23     private static $tmp;
24
25     private static $db_url;
26
27     private static $usergroup = null;
28
29     private static $backendOutputDelimiter = 'DRUSH_BACKEND_OUTPUT_START>>>%s<<<DRUSH_BACKEND_OUTPUT_END';
30
31     /**
32      * @return array
33      */
34     public static function getSites()
35     {
36         return self::$sites;
37     }
38
39     /**
40      * @return array
41      */
42     public static function getAliases()
43     {
44         // Prefix @sut. onto each site.
45         foreach (self::$sites as $key => $site) {
46             $aliases[$key] = '@sut.' . $key;
47         }
48         return $aliases;
49     }
50
51     public static function getUri($site = 'dev')
52     {
53         return self::$sites[$site]['uri'];
54     }
55
56     /**
57      * @return string
58      */
59     public static function getDrush()
60     {
61         return self::$drush;
62     }
63
64     /**
65      * @return string
66      */
67     public static function getTmp()
68     {
69         return self::$tmp;
70     }
71
72     /**
73      * @return string
74      */
75     public static function getSandbox()
76     {
77         return self::$sandbox;
78     }
79
80     /**
81      * @return string
82      */
83     public static function getSut()
84     {
85         return Path::join(self::getTmp(), 'drush-sut');
86     }
87
88     /**
89      * - Remove sandbox directory.
90      * - Empty /modules, /profiles, /themes in SUT.
91      */
92     public static function cleanDirs()
93     {
94         if (empty(getenv('UNISH_DIRTY'))) {
95             $sandbox = self::getSandbox();
96             if (file_exists($sandbox)) {
97                 self::recursiveDelete($sandbox);
98             }
99             foreach (['modules', 'themes', 'profiles', 'drush'] as $dir) {
100                 $target = Path::join(self::getSut(), 'web', $dir, 'contrib');
101                 if (file_exists($target)) {
102                     self::recursiveDeleteDirContents($target);
103                 }
104             }
105             foreach (['sites/dev', 'sites/stage', 'sites/prod'] as $dir) {
106                 $target = Path::join(self::getSut(), 'web', $dir);
107                 if (file_exists($target)) {
108                     self::recursiveDelete($target);
109                 }
110             }
111         }
112     }
113
114     /**
115      * @return string
116      */
117     public static function getDbUrl()
118     {
119         return self::$db_url;
120     }
121
122     /**
123      * @return string
124      */
125     public static function getUserGroup()
126     {
127         return self::$usergroup;
128     }
129
130     /**
131      * @return string
132      */
133     public static function getBackendOutputDelimiter()
134     {
135         return self::$backendOutputDelimiter;
136     }
137
138     public function __construct($name = null, array $data = [], $dataName = '')
139     {
140         parent::__construct($name, $data, $dataName);
141
142         // Default drupal major version to run tests over.
143         // @todo Remove this.
144         if (!defined('UNISH_DRUPAL_MAJOR_VERSION')) {
145             define('UNISH_DRUPAL_MAJOR_VERSION', '8');
146         }
147
148         // We read from env then globals then default to mysql.
149         self::$db_url = getenv('UNISH_DB_URL') ?: (isset($GLOBALS['UNISH_DB_URL']) ? $GLOBALS['UNISH_DB_URL'] : 'mysql://root:@127.0.0.1');
150
151         require_once __DIR__ . '/unish.inc';
152         list($unish_tmp, $unish_sandbox, $unish_drush_dir) = \unishGetPaths();
153         $unish_cache = Path::join($unish_sandbox, 'cache');
154
155         self::$drush = $unish_drush_dir . '/drush';
156         self::$tmp = $unish_tmp;
157         self::$sandbox = $unish_sandbox;
158         self::$usergroup = isset($GLOBALS['UNISH_USERGROUP']) ? $GLOBALS['UNISH_USERGROUP'] : null;
159
160         self::setEnv(['CACHE_PREFIX' => $unish_cache]);
161         $home = $unish_sandbox . '/home';
162         self::setEnv(['HOME' => $home]);
163         self::setEnv(['HOMEDRIVE' => $home]);
164         $composer_home = $unish_cache . '/.composer';
165         self::setEnv(['COMPOSER_HOME' => $composer_home]);
166         self::setEnv(['ETC_PREFIX' => $unish_sandbox]);
167         self::setEnv(['SHARE_PREFIX' => $unish_sandbox]);
168         self::setEnv(['TEMP' => Path::join($unish_sandbox, 'tmp')]);
169         self::setEnv(['DRUSH_AUTOLOAD_PHP' => PHPUNIT_COMPOSER_INSTALL]);
170     }
171
172     /**
173      * We used to assure that each class starts with an empty sandbox directory and
174      * a clean environment except for the SUT. History: http://drupal.org/node/1103568.
175      */
176     public static function setUpBeforeClass()
177     {
178         self::cleanDirs();
179
180         // Create all the dirs.
181         $sandbox = self::getSandbox();
182         $dirs = [getenv('HOME') . '/.drush', $sandbox . '/etc/drush', $sandbox . '/share/drush/commands', "$sandbox/cache", getenv('TEMP')];
183         foreach ($dirs as $dir) {
184             self::mkdir($dir);
185         }
186
187         if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
188             // Hack to make git use unix line endings on windows
189             exec("git config --file $sandbox\\home\\.gitconfig core.autocrlf false", $output, $return);
190         }
191         parent::setUpBeforeClass();
192     }
193
194     /**
195      * Runs after all tests in a class are run.
196      */
197     public static function tearDownAfterClass()
198     {
199         self::cleanDirs();
200
201         self::$sites = [];
202         parent::tearDownAfterClass();
203     }
204
205     /**
206      * Print a log message to the console.
207      *
208      * @param string $message
209      * @param string $type
210      *   Supported types are:
211      *     - notice
212      *     - verbose
213      *     - debug
214      */
215     public function log($message, $type = 'notice')
216     {
217         $line = "\nLog: $message\n";
218         switch ($this->logLevel()) {
219             case 'verbose':
220                 if (in_array($type, ['notice', 'verbose'])) {
221                     fwrite(STDERR, $line);
222                 }
223                 break;
224             case 'debug':
225                 fwrite(STDERR, $line);
226                 break;
227             default:
228                 if ($type == 'notice') {
229                     fwrite(STDERR, $line);
230                 }
231                 break;
232         }
233     }
234
235     public function logLevel()
236     {
237         // -d is reserved by `phpunit`
238         if (in_array('--debug', $_SERVER['argv'])) {
239             return 'debug';
240         } elseif (in_array('--verbose', $_SERVER['argv']) || in_array('-v', $_SERVER['argv'])) {
241             return 'verbose';
242         }
243     }
244
245     public static function isWindows()
246     {
247         return strtoupper(substr(PHP_OS, 0, 3)) == "WIN";
248     }
249
250     public static function getTarExecutable()
251     {
252         return self::isWindows() ? "bsdtar.exe" : "tar";
253     }
254
255     /**
256      * Print out a tick mark.
257      *
258      * Useful for longer running tests to indicate they're working.
259      */
260     public function tick()
261     {
262         static $chars = ['/', '-', '\\', '|'];
263         static $counter = 0;
264         // ANSI support is flaky on Win32, so don't try to do ticks there.
265         if (!$this->isWindows()) {
266             print $chars[($counter++ % 4)] . "\033[1D";
267         }
268     }
269
270     /**
271      * Borrowed from Drush.
272      * Checks operating system and returns
273      * supported bit bucket folder.
274      */
275     public function bitBucket()
276     {
277         if (!$this->isWindows()) {
278             return '/dev/null';
279         } else {
280             return 'nul';
281         }
282     }
283
284     public static function escapeshellarg($arg)
285     {
286         // Short-circuit escaping for simple params (keep stuff readable)
287         if (preg_match('|^[a-zA-Z0-9.:/_-]*$|', $arg)) {
288             return $arg;
289         } elseif (self::isWindows()) {
290             return self::_escapeshellargWindows($arg);
291         } else {
292             return escapeshellarg($arg);
293         }
294     }
295
296     public static function _escapeshellargWindows($arg)
297     {
298         // Double up existing backslashes
299         $arg = preg_replace('/\\\/', '\\\\\\\\', $arg);
300
301         // Double up double quotes
302         $arg = preg_replace('/"/', '""', $arg);
303
304         // Double up percents.
305         $arg = preg_replace('/%/', '%%', $arg);
306
307         // Add surrounding quotes.
308         $arg = '"' . $arg . '"';
309
310         return $arg;
311     }
312
313     /**
314      * Helper function to generate a random string of arbitrary length.
315      *
316      * Copied from drush_generate_password(), which is otherwise not available here.
317      *
318      * @param $length
319      *   Number of characters the generated string should contain.
320      * @return
321      *   The generated string.
322      */
323     public function randomString($length = 10)
324     {
325         // This variable contains the list of allowable characters for the
326         // password. Note that the number 0 and the letter 'O' have been
327         // removed to avoid confusion between the two. The same is true
328         // of 'I', 1, and 'l'.
329         $allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
330
331         // Zero-based count of characters in the allowable list:
332         $len = strlen($allowable_characters) - 1;
333
334         // Declare the password as a blank string.
335         $pass = '';
336
337         // Loop the number of times specified by $length.
338         for ($i = 0; $i < $length; $i++) {
339             // Each iteration, pick a random character from the
340             // allowable string and append it to the password:
341             $pass .= $allowable_characters[mt_rand(0, $len)];
342         }
343
344         return $pass;
345     }
346
347     public static function mkdir($path)
348     {
349         if (!is_dir($path)) {
350             if (self::mkdir(dirname($path))) {
351                 if (@mkdir($path)) {
352                     return true;
353                 }
354             }
355             return false;
356         }
357         return true;
358     }
359
360     public static function recursiveCopy($src, $dst)
361     {
362         $dir = opendir($src);
363         self::mkdir($dst);
364         while (false !== ( $file = readdir($dir))) {
365             if (( $file != '.' ) && ( $file != '..' )) {
366                 if (is_dir($src . '/' . $file)) {
367                     self::recursiveCopy($src . '/' . $file, $dst . '/' . $file);
368                 } else {
369                     copy($src . '/' . $file, $dst . '/' . $file);
370                 }
371             }
372         }
373         closedir($dir);
374     }
375
376
377     /**
378      * Deletes the specified file or directory and everything inside it.
379      *
380      * Usually respects read-only files and folders. To do a forced delete use
381      * drush_delete_tmp_dir() or set the parameter $forced.
382      *
383      * To avoid permission denied error on Windows, make sure your CWD is not
384      * inside the directory being deleted.
385      *
386      * This is essentially a copy of drush_delete_dir().
387      *
388      * @todo This sort of duplication isn't very DRY. This is bound to get out of
389      *   sync with drush_delete_dir(), as in fact it already has before.
390      *
391      * @param string $dir
392      *   The file or directory to delete.
393      * @param bool $force
394      *   Whether or not to try everything possible to delete the directory, even if
395      *   it's read-only. Defaults to FALSE.
396      * @param bool $follow_symlinks
397      *   Whether or not to delete symlinked files. Defaults to FALSE--simply
398      *   unlinking symbolic links.
399      *
400      * @return bool
401      *   FALSE on failure, TRUE if everything was deleted.
402      *
403      * @see drush_delete_dir()
404      */
405     public static function recursiveDelete($dir, $force = true, $follow_symlinks = false)
406     {
407         // Do not delete symlinked files, only unlink symbolic links
408         if (is_link($dir) && !$follow_symlinks) {
409             return unlink($dir);
410         }
411         // Allow to delete symlinks even if the target doesn't exist.
412         if (!is_link($dir) && !file_exists($dir)) {
413             return true;
414         }
415         if (!is_dir($dir)) {
416             if ($force) {
417                 // Force deletion of items with readonly flag.
418                 @chmod($dir, 0777);
419             }
420             return unlink($dir);
421         }
422         if (self::recursiveDeleteDirContents($dir, $force) === false) {
423             return false;
424         }
425         if ($force) {
426             // Force deletion of items with readonly flag.
427             @chmod($dir, 0777);
428         }
429         return rmdir($dir);
430     }
431
432     /**
433      * Deletes the contents of a directory.
434      *
435      * This is essentially a copy of drush_delete_dir_contents().
436      *
437      * @param string $dir
438      *   The directory to delete.
439      * @param bool $force
440      *   Whether or not to try everything possible to delete the contents, even if
441      *   they're read-only. Defaults to FALSE.
442      *
443      * @return bool
444      *   FALSE on failure, TRUE if everything was deleted.
445      *
446      * @see drush_delete_dir_contents()
447      */
448     public static function recursiveDeleteDirContents($dir, $force = false)
449     {
450         $scandir = @scandir($dir);
451         if (!is_array($scandir)) {
452             return false;
453         }
454
455         foreach ($scandir as $item) {
456             if ($item == '.' || $item == '..') {
457                 continue;
458             }
459             if ($force) {
460                 @chmod($dir, 0777);
461             }
462             if (!self::recursiveDelete($dir . '/' . $item, $force)) {
463                 return false;
464             }
465         }
466         return true;
467     }
468
469     public function webroot()
470     {
471         return Path::join(self::getSut(), 'web');
472     }
473
474     public function directoryCache($subdir = '')
475     {
476         return getenv('CACHE_PREFIX') . '/' . $subdir;
477     }
478
479     /**
480      * @param $env
481      * @return string
482      */
483     public function dbUrl($env)
484     {
485         return substr(self::getDbUrl(), 0, 6) == 'sqlite'  ?  "sqlite://sites/$env/files/unish.sqlite" : self::getDbUrl() . '/unish_' . $env;
486     }
487
488     public function dbDriver($db_url = null)
489     {
490         return parse_url($db_url ?: self::getDbUrl(), PHP_URL_SCHEME);
491     }
492
493     /**
494      * Create some fixture sites that only have a 'settings.php' file
495      * with a database record.
496      *
497      * @param array $sites key=site_subder value=array of extra alias data
498      * @param string $aliasGroup Write aliases into a file named group.alias.yml
499      */
500     public function setUpSettings(array $sites, $aliasGroup = 'fixture')
501     {
502         foreach ($sites as $subdir => $extra) {
503             $this->createSettings($subdir);
504         }
505         // Create basic site alias data with root and uri
506         $siteAliasData = $this->createAliasFileData(array_keys($sites), $aliasGroup);
507         // Add in caller-provided site alias data
508         $siteAliasData = array_merge_recursive($siteAliasData, $sites);
509         $this->writeSiteAliases($siteAliasData, $aliasGroup);
510     }
511
512     public function createSettings($subdir)
513     {
514         $settingsContents = <<<EOT
515 <?php
516
517 \$databases['default']['default'] = array (
518   'database' => 'unish_$subdir',
519   'username' => 'root',
520   'password' => '',
521   'prefix' => '',
522   'host' => '127.0.0.1',
523   'port' => '',
524   'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
525   'driver' => 'mysql',
526 );
527 \$settings['install_profile'] = 'testing';
528 EOT;
529
530         $root = $this->webroot();
531         $settingsPath = "$root/sites/$subdir/settings.php";
532         self::mkdir(dirname($settingsPath));
533         file_put_contents($settingsPath, $settingsContents);
534     }
535     /**
536      * Assemble (and optionally install) one or more Drupal sites using a single codebase.
537      *
538      * It is no longer supported to pass alternative versions of Drupal or an alternative install_profile.
539      */
540     public function setUpDrupal($num_sites = 1, $install = false)
541     {
542         $sites_subdirs_all = ['dev', 'stage', 'prod', 'retired', 'elderly', 'dead', 'dust'];
543         $sites_subdirs = array_slice($sites_subdirs_all, 0, $num_sites);
544         $root = $this->webroot();
545
546         // Install (if needed).
547         foreach ($sites_subdirs as $subdir) {
548             $this->installDrupal($subdir, $install);
549         }
550
551         // Write an empty sites.php. Needed for multi-site on D8+.
552         if (!file_exists($root . '/sites/sites.php')) {
553             copy($root . '/sites/example.sites.php', $root . '/sites/sites.php');
554         }
555
556         $siteData = $this->createAliasFile($sites_subdirs, 'unish');
557         self::$sites = [];
558         foreach ($siteData as $key => $data) {
559             self::$sites[$key] = $data;
560         }
561         return self::$sites;
562     }
563
564     public function createAliasFileData($sites_subdirs, $aliasGroup = 'unish')
565     {
566         $root = $this->webroot();
567         // Stash details about each site.
568         $sites = [];
569         foreach ($sites_subdirs as $subdir) {
570             $sites[$subdir] = [
571             'root' => $root,
572             'uri' => $subdir,
573             'dbUrl' => $this->dbUrl($subdir),
574             ];
575         }
576         return $sites;
577     }
578
579     public function createAliasFile($sites_subdirs, $aliasGroup = 'unish')
580     {
581         // Make an alias group for the sites.
582         $sites = $this->createAliasFileData($sites_subdirs, $aliasGroup);
583         $this->writeSiteAliases($sites, $aliasGroup);
584
585         return $sites;
586     }
587
588     /**
589      * Install a Drupal site.
590      *
591      * It is no longer supported to pass alternative versions of Drupal or an alternative install_profile.
592      */
593     public function installDrupal($env = 'dev', $install = false)
594     {
595         $root = $this->webroot();
596         $uri = $env;
597         $site = "$root/sites/$uri";
598
599         // If specified, install Drupal as a multi-site.
600         if ($install) {
601             $options = [
602             'root' => $root,
603             'db-url' => $this->dbUrl($env),
604             'sites-subdir' => $uri,
605             'yes' => null,
606             'quiet' => null,
607             ];
608             $this->drush('site-install', ['testing', 'install_configure_form.enable_update_status_emails=NULL'], $options);
609             // Give us our write perms back.
610             chmod($site, 0777);
611         } else {
612             $this->mkdir($site);
613             touch("$site/settings.php");
614         }
615     }
616
617     /**
618      * Write an alias group file and a config file which points to same dir.
619      *
620      * @param $sites
621      */
622     public function writeSiteAliases($sites, $aliasGroup = 'unish')
623     {
624         $this->writeUnishConfig($sites, [], $aliasGroup);
625     }
626
627     public function writeUnishConfig($unishAliases, $config = [], $aliasGroup = 'unish')
628     {
629         $etc = self::getSandbox() . '/etc/drush';
630         $aliases_dir = Path::join($etc, 'sites');
631         @mkdir($aliases_dir);
632         file_put_contents(Path::join($aliases_dir, $aliasGroup . '.site.yml'), Yaml::dump($unishAliases, PHP_INT_MAX, 2));
633         $config['drush']['paths']['alias-path'][] = $aliases_dir;
634         file_put_contents(Path::join($etc, 'drush.yml'), Yaml::dump($config, PHP_INT_MAX, 2));
635     }
636
637     /**
638      * The sitewide directory for Drupal extensions.
639      */
640     public function drupalSitewideDirectory()
641     {
642         return '/sites/all';
643     }
644
645     /**
646      * Set environment variables that should be passed to child processes.
647      *
648      * @param array $vars
649      *   The variables to set.
650      *
651      *   We will change implementation to take advantage of https://github.com/symfony/symfony/pull/19053/files once we drop Symfony 2 compat.
652      */
653     public static function setEnv(array $vars)
654     {
655         foreach ($vars as $k => $v) {
656             putenv($k . '=' . $v);
657             // Value must be a string. See \Symfony\Component\Process\Process::getDefaultEnv.
658             $_SERVER[$k]= (string) $v;
659         }
660     }
661 }