drush_testcase.inc

  1. 6.x tests/drush_testcase.inc
  2. 4.x tests/drush_testcase.inc
  3. 5.x tests/drush_testcase.inc

Functions

Namesort descending Description
parse_backend_output A slightly less functional copy of drush_backend_parse_output().
unish_file_delete_recursive Same code as drush_delete_dir().
unish_init

Classes

Namesort descending Description
Drush_CommandTestCase
Drush_TestCase
Drush_UnitTestCase Base class for Drush unit tests

File

tests/drush_testcase.inc
View source
  1. <?php
  2. /*
  3. * @file
  4. * Initialize a sandboxed environment. Starts with call unish_init() at bottom.
  5. */
  6. abstract class Drush_TestCase extends PHPUnit_Framework_TestCase {
  7. function __construct() {
  8. $this->_output = false;
  9. }
  10. /**
  11. * Assure that each class starts with an empty sandbox directory and
  12. * a clean environment - http://drupal.org/node/1103568.
  13. */
  14. public static function setUpBeforeClass() {
  15. self::setUpFreshSandBox();
  16. }
  17. /**
  18. * Remove any pre-existing sandbox, then create a new one.
  19. */
  20. public static function setUpFreshSandBox() {
  21. $sandbox = UNISH_SANDBOX;
  22. if (file_exists($sandbox)) {
  23. unish_file_delete_recursive($sandbox);
  24. }
  25. $ret = mkdir($sandbox, 0777, TRUE);
  26. chdir(UNISH_SANDBOX);
  27. mkdir(getenv('HOME') . '/.drush', 0777, TRUE);
  28. mkdir($sandbox . '/etc/drush', 0777, TRUE);
  29. mkdir($sandbox . '/share/drush/commands', 0777, TRUE);
  30. if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
  31. // Hack to make git use unix line endings on windows
  32. // We need it to make hashes of files pulled from git match ones hardcoded in tests
  33. if (!file_exists($sandbox . '\home')) {
  34. mkdir($sandbox . '\home');
  35. }
  36. exec("git config --file $sandbox\\home\\.gitconfig core.autocrlf false", $output, $return);
  37. }
  38. }
  39. /**
  40. * Runs after all tests in a class are run. Remove sandbox directory.
  41. */
  42. public static function tearDownAfterClass() {
  43. if (file_exists(UNISH_SANDBOX)) {
  44. unish_file_delete_recursive(UNISH_SANDBOX);
  45. }
  46. }
  47. /**
  48. * Print a log message to the console.
  49. *
  50. * @param string $message
  51. * @param string $type
  52. * Supported types are:
  53. * - notice
  54. * - verbose
  55. * - debug
  56. */
  57. function log($message, $type = 'notice') {
  58. $line = "\nLog: $message\n";
  59. switch ($this->log_level()) {
  60. case 'verbose':
  61. if (in_array($type, array('notice', 'verbose'))) print $line;
  62. break;
  63. case 'debug':
  64. print $line;
  65. break;
  66. default:
  67. if ($type == 'notice') print $line;
  68. break;
  69. }
  70. }
  71. function log_level() {
  72. if (in_array('--debug', $_SERVER['argv'])) {
  73. return 'debug';
  74. }
  75. elseif (in_array('--verbose', $_SERVER['argv'])) {
  76. return 'verbose';
  77. }
  78. }
  79. public static function is_windows() {
  80. return (strtoupper(substr(PHP_OS, 0, 3)) == "WIN");
  81. }
  82. public static function get_tar_executable() {
  83. return self::is_windows() ? "bsdtar.exe" : "tar";
  84. }
  85. /**
  86. * Converts a Windows path (dir1\dir2\dir3) into a Unix path (dir1/dir2/dir3).
  87. * Also converts a cygwin "drive emulation" path (/cygdrive/c/dir1) into a
  88. * proper drive path, still with Unix slashes (c:/dir1).
  89. *
  90. * @copied from Drush's environment.inc
  91. */
  92. function convert_path($path) {
  93. $path = str_replace('\\','/', $path);
  94. $path = preg_replace('/^\/cygdrive\/([A-Za-z])(.*)$/', '\1:\2', $path);
  95. return $path;
  96. }
  97. /**
  98. * Borrowed from Drush.
  99. * Checks operating system and returns
  100. * supported bit bucket folder.
  101. */
  102. function bit_bucket() {
  103. if (!$this->is_windows()) {
  104. return '/dev/null';
  105. }
  106. else {
  107. return 'nul';
  108. }
  109. }
  110. public static function escapeshellarg($arg) {
  111. // Short-circuit escaping for simple params (keep stuff readable)
  112. if (preg_match('|^[a-zA-Z0-9.:/_-]*$|', $arg)) {
  113. return $arg;
  114. }
  115. elseif (self::is_windows()) {
  116. return self::_escapeshellarg_windows($arg);
  117. }
  118. else {
  119. return escapeshellarg($arg);
  120. }
  121. }
  122. public static function _escapeshellarg_windows($arg) {
  123. // Double up existing backslashes
  124. $arg = preg_replace('/\\\/', '\\\\\\\\', $arg);
  125. // Double up double quotes
  126. $arg = preg_replace('/"/', '""', $arg);
  127. // Double up percents.
  128. // $arg = preg_replace('/%/', '%%', $arg);
  129. // Add surrounding quotes.
  130. $arg = '"' . $arg . '"';
  131. return $arg;
  132. }
  133. /**
  134. * Helper function to generate a random string of arbitrary length.
  135. *
  136. * Copied from drush_generate_password(), which is otherwise not available here.
  137. *
  138. * @param $length
  139. * Number of characters the generated string should contain.
  140. * @return
  141. * The generated string.
  142. */
  143. public function randomString($length = 10) {
  144. // This variable contains the list of allowable characters for the
  145. // password. Note that the number 0 and the letter 'O' have been
  146. // removed to avoid confusion between the two. The same is true
  147. // of 'I', 1, and 'l'.
  148. $allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
  149. // Zero-based count of characters in the allowable list:
  150. $len = strlen($allowable_characters) - 1;
  151. // Declare the password as a blank string.
  152. $pass = '';
  153. // Loop the number of times specified by $length.
  154. for ($i = 0; $i < $length; $i++) {
  155. // Each iteration, pick a random character from the
  156. // allowable string and append it to the password:
  157. $pass .= $allowable_characters[mt_rand(0, $len)];
  158. }
  159. return $pass;
  160. }
  161. /**
  162. * Accessor for the last output.
  163. * @return string Output as text.
  164. * @access public
  165. */
  166. function getOutput() {
  167. return implode("\n", $this->_output);
  168. }
  169. /**
  170. * Accessor for the last output.
  171. * @return array Output as array of lines.
  172. * @access public
  173. */
  174. function getOutputAsList() {
  175. return $this->_output;
  176. }
  177. function webroot() {
  178. return UNISH_SANDBOX . '/web';
  179. }
  180. function directory_cache($subdir = '') {
  181. return getenv('CACHE_PREFIX') . '/' . $subdir;
  182. }
  183. function db_url($env) {
  184. return substr(UNISH_DB_URL, 0, 6) == 'sqlite' ? "sqlite://sites/$env/files/unish.sqlite" : UNISH_DB_URL . '/unish_' . $env;
  185. }
  186. function setUpDrupal($num_sites = 1, $install = FALSE, $version_string = UNISH_DRUPAL_MAJOR_VERSION, $profile = NULL) {
  187. $sites_subdirs_all = array('dev', 'stage', 'prod', 'retired', 'elderly', 'dead', 'dust');
  188. $sites_subdirs = array_slice($sites_subdirs_all, 0, $num_sites);
  189. $root = $this->webroot();
  190. if (is_null($profile)) {
  191. $profile = substr($version_string, 0, 1) >= 7 ? 'testing' : 'default';
  192. }
  193. $db_driver = parse_url(UNISH_DB_URL, PHP_URL_SCHEME);
  194. $cache_keys = array($num_sites, $install ? 'install' : 'noinstall', $version_string, $profile, $db_driver);
  195. $source = $this->directory_cache('environments') . '/' . implode('-', $cache_keys) . '.tar.gz';
  196. if (file_exists($source)) {
  197. $this->log('Cache HIT. Environment: ' . $source, 'verbose');
  198. $this->drush('archive-restore', array($source), array('destination' => $root, 'overwrite' => NULL));
  199. }
  200. else {
  201. $this->log('Cache MISS. Environment: ' . $source, 'verbose');
  202. // Build the site(s), install (if needed), then cache.
  203. foreach ($sites_subdirs as $subdir) {
  204. $this->fetchInstallDrupal($subdir, $install, $version_string, $profile);
  205. }
  206. $options = array(
  207. 'destination' => $source,
  208. 'root' => $root,
  209. 'uri' => reset($sites_subdirs),
  210. 'overwrite' => NULL,
  211. );
  212. $this->drush('archive-dump', array('@sites'), $options);
  213. }
  214. // Stash details about each site.
  215. foreach ($sites_subdirs as $subdir) {
  216. $this->sites[$subdir] = array(
  217. 'db_url' => $this->db_url($subdir),
  218. );
  219. // Make an alias for the site
  220. $alias_definition = array($subdir => array('root' => $root, 'uri' => $subdir));
  221. file_put_contents(UNISH_SANDBOX . '/etc/drush/' . $subdir . '.alias.drushrc.php', $this->file_aliases($alias_definition));
  222. }
  223. return $this->sites;
  224. }
  225. function fetchInstallDrupal($env = 'dev', $install = FALSE, $version_string = UNISH_DRUPAL_MAJOR_VERSION, $profile = NULL) {
  226. $root = $this->webroot();
  227. $site = "$root/sites/$env";
  228. // Download Drupal if not already present.
  229. if (!file_exists($root)) {
  230. $options = array(
  231. 'destination' => UNISH_SANDBOX,
  232. 'drupal-project-rename' => 'web',
  233. 'yes' => NULL,
  234. 'quiet' => NULL,
  235. 'cache' => NULL,
  236. );
  237. $this->drush('pm-download', array("drupal-$version_string"), $options);
  238. mkdir(UNISH_SANDBOX . '/web/sites/all/drush');
  239. }
  240. // If specified, install Drupal as a multi-site.
  241. if ($install) {
  242. $options = array(
  243. 'root' => $root,
  244. 'db-url' => $this->db_url($env),
  245. 'sites-subdir' => $env,
  246. 'yes' => NULL,
  247. 'quiet' => NULL,
  248. );
  249. $this->drush('site-install', array($profile), $options);
  250. // Give us our write perms back.
  251. chmod($site, 0777);
  252. }
  253. else {
  254. mkdir($site);
  255. touch("$site/settings.php");
  256. }
  257. }
  258. }
  259. abstract class Drush_CommandTestCase extends Drush_TestCase {
  260. // Unix exit codes.
  261. const EXIT_SUCCESS = 0;
  262. const EXIT_ERROR = 1;
  263. /*
  264. * An array of Drupal sites that are setup in the drush-sandbox.
  265. */
  266. var $sites;
  267. /**
  268. * Actually runs the command. Does not trap the error stream output as this
  269. * need PHP 4.3+.
  270. *
  271. * @param string $command
  272. * The actual command line to run.
  273. * @param integer $expected_return
  274. * The return code to expect
  275. * @return integer
  276. * Exit code. Usually self::EXIT_ERROR or self::EXIT_SUCCESS.
  277. */
  278. function execute($command, $expected_return = self::EXIT_SUCCESS, $env = array()) {
  279. $this->_output = FALSE;
  280. $return = 1;
  281. $this->log("Executing: $command", 'notice');
  282. // Apply the environment variables we need for our test
  283. // to the head of the command
  284. $prefix = '';
  285. foreach ($env as $env_name => $env_value) {
  286. $prefix .= $env_name . '=' . self::escapeshellarg($env_value) . ' ';
  287. }
  288. exec($prefix . $command, $this->_output, $return);
  289. $this->assertEquals($expected_return, $return, 'Unexpected exit code: ' . $command);
  290. return $return;
  291. }
  292. /**
  293. * Invoke drush in via execute().
  294. *
  295. * @param command
  296. * A defined drush command such as 'cron', 'status' or any of the available ones such as 'drush pm'.
  297. * @param args
  298. * Command arguments.
  299. * @param $options
  300. * An associative array containing options.
  301. * @param $site_specification
  302. * A site alias or site specification. Include the '@' at start of a site alias.
  303. * @param $cd
  304. * A directory to change into before executing.
  305. * @param $expected_return
  306. * The expected exit code. Usually self::EXIT_ERROR or self::EXIT_SUCCESS.
  307. * @param $suffix
  308. * Any code to append to the command. For example, redirection like 2>&1.
  309. * @return integer
  310. * An exit code.
  311. */
  312. function drush($command, array $args = array(), array $options = array(), $site_specification = NULL, $cd = NULL, $expected_return = self::EXIT_SUCCESS, $suffix = NULL, $env = array()) {
  313. $global_option_list = array('simulate', 'root', 'uri', 'include', 'config', 'alias-path', 'ssh-options');
  314. // insert "cd ... ; drush"
  315. $cmd[] = $cd ? sprintf('cd %s &&', self::escapeshellarg($cd)) : NULL;
  316. $cmd[] = UNISH_DRUSH;
  317. // insert global options
  318. foreach ($options as $key => $value) {
  319. if (in_array($key, $global_option_list)) {
  320. unset($options[$key]);
  321. if (is_null($value)) {
  322. $cmd[] = "--$key";
  323. }
  324. else {
  325. $cmd[] = "--$key=" . self::escapeshellarg($value);
  326. }
  327. }
  328. }
  329. if ($level = $this->log_level()) {
  330. $cmd[] = '--' . $level;
  331. }
  332. $cmd[] = "--nocolor";
  333. // insert site specification and drush command
  334. $cmd[] = empty($site_specification) ? NULL : self::escapeshellarg($site_specification);
  335. $cmd[] = $command;
  336. // insert drush command arguments
  337. foreach ($args as $arg) {
  338. $cmd[] = self::escapeshellarg($arg);
  339. }
  340. // insert drush command options
  341. foreach ($options as $key => $value) {
  342. if (is_null($value)) {
  343. $cmd[] = "--$key";
  344. }
  345. else {
  346. $cmd[] = "--$key=" . self::escapeshellarg($value);
  347. }
  348. }
  349. $cmd[] = $suffix;
  350. $exec = array_filter($cmd, 'strlen'); // Remove NULLs
  351. return $this->execute(implode(' ', $exec), $expected_return, $env);
  352. }
  353. function drush_major_version() {
  354. static $major;
  355. if (!isset($major)) {
  356. $this->drush('version', array('drush_version'), array('pipe' => NULL));
  357. $version = $this->getOutput();
  358. list($major) = explode('.', $version);
  359. }
  360. return $major;
  361. }
  362. /*
  363. * Prepare the contents of an aliases file.
  364. */
  365. function file_aliases($aliases) {
  366. foreach ($aliases as $name => $alias) {
  367. $records[] = sprintf('$aliases[\'%s\'] = %s;', $name, var_export($alias, TRUE));
  368. }
  369. $contents = "<?php\n\n" . implode("\n\n", $records);
  370. return $contents;
  371. }
  372. }
  373. /**
  374. * Base class for Drush unit tests
  375. *
  376. * Those tests will run in a bootstrapped Drush environment
  377. *
  378. * This should be ran in separate processes, which the following
  379. * annotation should do in 3.6 and above:
  380. *
  381. * @runTestsInSeparateProcesses
  382. */
  383. abstract class Drush_UnitTestCase extends Drush_TestCase {
  384. /**
  385. * Minimally bootstrap drush
  386. *
  387. * This is equivalent to the level DRUSH_BOOTSTRAP_NONE, as we
  388. * haven't run drush_bootstrap() yet. To do anything, you'll need to
  389. * bootstrap to some level using drush_bootstrap().
  390. *
  391. * @see drush_bootstrap()
  392. */
  393. public static function setUpBeforeClass() {
  394. parent::setUpBeforeClass();
  395. require_once(dirname(__FILE__) . '/../includes/bootstrap.inc');
  396. drush_bootstrap_prepare();
  397. }
  398. public static function tearDownAfterClass() {
  399. parent::tearDownAfterClass();
  400. drush_bootstrap_finish();
  401. }
  402. function drush_major_version() {
  403. return DRUSH_MAJOR_VERSION;
  404. }
  405. }
  406. /*
  407. * Initialize our environment at the start of each run (i.e. suite).
  408. */
  409. function unish_init() {
  410. // Default drupal major version to run tests over.
  411. $unish_drupal_major = '7';
  412. if (getenv('UNISH_DRUPAL_MAJOR_VERSION')) {
  413. $unish_drupal_major = getenv('UNISH_DRUPAL_MAJOR_VERSION');
  414. }
  415. elseif (isset($GLOBALS['UNISH_DRUPAL_MAJOR_VERSION'])) {
  416. $unish_drupal_major = $GLOBALS['UNISH_DRUPAL_MAJOR_VERSION'];
  417. }
  418. define('UNISH_DRUPAL_MAJOR_VERSION', $unish_drupal_major);
  419. // We read from env then globals then default to mysql.
  420. $unish_db_url = 'mysql://root:@127.0.0.1';
  421. if (getenv('UNISH_DB_URL')) {
  422. $unish_db_url = getenv('UNISH_DB_URL');
  423. }
  424. elseif (isset($GLOBALS['UNISH_DB_URL'])) {
  425. $unish_db_url = $GLOBALS['UNISH_DB_URL'];
  426. }
  427. define('UNISH_DB_URL', $unish_db_url);
  428. // UNISH_DRUSH value can come from phpunit.xml or `which drush`.
  429. if (!defined('UNISH_DRUSH')) {
  430. // Let the UNISH_DRUSH environment variable override if set.
  431. $unish_drush = isset($_SERVER['UNISH_DRUSH']) ? $_SERVER['UNISH_DRUSH'] : NULL;
  432. $unish_drush = isset($GLOBALS['UNISH_DRUSH']) ? $GLOBALS['UNISH_DRUSH'] : $unish_drush;
  433. if (empty($unish_drush)) {
  434. $unish_drush = Drush_TestCase::is_windows() ? exec('for %i in (drush) do @echo. %~$PATH:i') : trim(`which drush`);
  435. }
  436. define('UNISH_DRUSH', $unish_drush);
  437. }
  438. define('UNISH_TMP', getenv('UNISH_TMP') ? getenv('UNISH_TMP') : (isset($GLOBALS['UNISH_TMP']) ? $GLOBALS['UNISH_TMP'] : sys_get_temp_dir()));
  439. define('UNISH_SANDBOX', UNISH_TMP . DIRECTORY_SEPARATOR . 'drush-sandbox');
  440. // Cache dir lives outside the sandbox so that we get persistence across classes.
  441. define('UNISH_CACHE', UNISH_TMP . DIRECTORY_SEPARATOR . 'drush-cache');
  442. putenv("CACHE_PREFIX=" . UNISH_CACHE);
  443. // Wipe at beginning of run.
  444. if (file_exists(UNISH_CACHE)) {
  445. // TODO: We no longer clean up cache dir between runs. Much faster, but we
  446. // we should watch for subtle problems. To manually clean up, delete the
  447. // UNISH_TMP/drush-cache directory.
  448. // unish_file_delete_recursive($cache);
  449. }
  450. else {
  451. $ret = mkdir(UNISH_CACHE, 0777, TRUE);
  452. }
  453. $home = UNISH_SANDBOX . '/home';
  454. putenv("HOME=$home");
  455. putenv("HOMEDRIVE=$home");
  456. putenv('ETC_PREFIX=' . UNISH_SANDBOX);
  457. putenv('SHARE_PREFIX=' . UNISH_SANDBOX);
  458. define('UNISH_USERGROUP', isset($GLOBALS['UNISH_USERGROUP']) ? $GLOBALS['UNISH_USERGROUP'] : NULL);
  459. define('UNISH_BACKEND_OUTPUT_DELIMITER', 'DRUSH_BACKEND_OUTPUT_START>>>%s<<<DRUSH_BACKEND_OUTPUT_END');
  460. }
  461. /**
  462. * Same code as drush_delete_dir().
  463. * @see drush_delete_dir()
  464. *
  465. * @param string $dir
  466. * @return boolean
  467. */
  468. function unish_file_delete_recursive($dir) {
  469. if (!file_exists($dir)) {
  470. return TRUE;
  471. }
  472. @chmod($dir, 0777); // Make file/dir writeable
  473. if (!is_dir($dir)) {
  474. return unlink($dir);
  475. }
  476. foreach (scandir($dir) as $item) {
  477. if ($item == '.' || $item == '..') {
  478. continue;
  479. }
  480. if (!unish_file_delete_recursive($dir.'/'.$item)) {
  481. return FALSE;
  482. }
  483. }
  484. return rmdir($dir);
  485. }
  486. /**
  487. * A slightly less functional copy of drush_backend_parse_output().
  488. */
  489. function parse_backend_output($string) {
  490. $regex = sprintf(UNISH_BACKEND_OUTPUT_DELIMITER, '(.*)');
  491. preg_match("/$regex/s", $string, $match);
  492. if ($match[1]) {
  493. // we have our JSON encoded string
  494. $output = $match[1];
  495. // remove the match we just made and any non printing characters
  496. $string = trim(str_replace(sprintf(UNISH_BACKEND_OUTPUT_DELIMITER, $match[1]), '', $string));
  497. }
  498. if ($output) {
  499. $data = json_decode($output, TRUE);
  500. if (is_array($data)) {
  501. return $data;
  502. }
  503. }
  504. return $string;
  505. }
  506. // This code is in global scope.
  507. // TODO: I would rather this code at top of file, but I get Fatal error: Class 'Drush_TestCase' not found
  508. unish_init();