Added the Installer module to Horde_Element. It allows to install a PEAR based Horde...
authorGunnar Wrobel <p@rdus.de>
Mon, 23 Aug 2010 07:42:38 +0000 (09:42 +0200)
committerGunnar Wrobel <p@rdus.de>
Mon, 23 Aug 2010 07:51:52 +0000 (09:51 +0200)
This should now allow a continuous integration (CI) setup that tests
the PEAR based Horde framework packages including their
dependencies. While we will support PEAR based releases for Horde4 I
assume from past experiences that the focus will remain on the
applications. As those rely on a fully installed framework it will be
quite easy to overlook PEAR dependency problems. So I feel it is
necessary to include the full installation method via PEAR into the CI
in order to spot problems early.

The general idea of the Horde_Element package is to provide tools for
managing Horde "Elements" which I consider to be PEAR based packages
from the framework (and maybe later also the applications). While I'm
currently focusing on the basics I think it would make sense to later
add methods that allow publishing the elements better than via
pear.horde.org.

framework/Element/lib/Horde/Element/Module/Installer.php [new file with mode: 0644]
framework/Element/lib/Horde/Element/Module/PearPackageXml.php
framework/Element/lib/Horde/Element/Modules.php
framework/Element/package.xml
framework/Element/test/Horde/Element/Integration/ElementTest.php
framework/Element/test/Horde/Element/StoryTestCase.php
framework/Element/test/Horde/Element/fixture/empty/package.xml [new file with mode: 0644]
framework/Element/test/Horde/Element/fixture/package.xml [deleted file]
framework/Element/test/Horde/Element/fixture/simple/lib/New.php [new file with mode: 0644]
framework/Element/test/Horde/Element/fixture/simple/lib/Second.php [new file with mode: 0644]
framework/Element/test/Horde/Element/fixture/simple/package.xml [new file with mode: 0644]

diff --git a/framework/Element/lib/Horde/Element/Module/Installer.php b/framework/Element/lib/Horde/Element/Module/Installer.php
new file mode 100644 (file)
index 0000000..6478c7b
--- /dev/null
@@ -0,0 +1,154 @@
+<?php
+/**
+ * Horde_Element_Module_Installer:: installs a Horde element including
+ * its dependencies.
+ *
+ * PHP version 5
+ *
+ * @category Horde
+ * @package  Element
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Element
+ */
+
+/**
+ * Horde_Element_Module_Installer:: installs a Horde element including
+ * its dependencies.
+ *
+ * Copyright 2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Horde
+ * @package  Element
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Element
+ */
+class Horde_Element_Module_Installer
+implements Horde_Element_Module
+{
+    public function getOptionGroupTitle()
+    {
+        return 'Installer';
+    }
+
+    public function getOptionGroupDescription()
+    {
+        return 'This module installs a Horde element including its dependencies.';
+    }
+
+    public function getOptionGroupOptions()
+    {
+        return array(
+            new Horde_Argv_Option(
+                '-i',
+                '--install',
+                array(
+                    'action' => 'store',
+                    'help'   => 'install the element into the specified absolute INSTALL location'
+                )
+            ),
+        );
+    }
+
+    public function handle(Horde_Element_Config $config)
+    {
+        $options = $config->getOptions();
+        if (!empty($options['install'])) {
+            $this->run($config);
+        }
+    }
+
+    public function run(Horde_Element_Config $config)
+    {
+        $options = $config->getOptions();
+
+        $pear = new PEAR();
+        $pear->setErrorHandling(PEAR_ERROR_DIE);
+
+        $pearrc = $options['install'] . DIRECTORY_SEPARATOR . '.pearrc';
+        $command_config = new PEAR_Command_Config(new PEAR_Frontend_CLI(), new stdClass);
+        $command_config->doConfigCreate(
+            'config-create', array(), array($options['install'], $pearrc)
+        );
+
+        $pear_config = new PEAR_Config($pearrc);
+        $GLOBALS['_PEAR_Config_instance'] = $pear_config;
+
+        $channel = new PEAR_Command_Channels(
+            new PEAR_Frontend_CLI(),
+            $pear_config
+        );
+        $channel->doDiscover('channel-discover', array(), array('pear.horde.org'));
+        $channel->doDiscover('channel-discover', array(), array('pear.phpunit.de'));
+
+        $installer = new PEAR_Command_Install(
+            new PEAR_Frontend_CLI(),
+            $pear_config
+        );
+
+        $arguments = $config->getArguments();
+        $element = basename(realpath($arguments[0]));
+        $root_path = dirname(realpath($arguments[0]));
+
+        $this->_installHordeDependency(
+            $installer,
+            $pear_config,
+            $root_path,
+            $element
+        );
+    }
+
+    /**
+     * Install a Horde dependency from the current tree (the framework).
+     *
+     * @param PEAR_Command_Install $installer   Installs the dependency.
+     * @param PEAR_Config          $pear_config The configuration of the PEAR
+     *                                          environment in which the
+     *                                          dependency will be installed.
+     * @param string               $root_path   Root path to the Horde framework.
+     * @param string               $dependency  Package name of the dependency.
+     */
+    private function _installHordeDependency(
+        PEAR_Command_Install $installer,
+        PEAR_Config $pear_config,
+        $root_path,
+        $dependency
+    ) {
+        $package_file = $root_path . DIRECTORY_SEPARATOR
+            . $dependency . DIRECTORY_SEPARATOR . 'package.xml';
+
+        $parser = new PEAR_PackageFile_Parser_v2();
+        $parser->setConfig($pear_config);
+        $pkg = $parser->parse(file_get_contents($package_file), $package_file);
+
+        $dependencies = $pkg->getDeps();
+        foreach ($dependencies as $dependency) {
+            if (isset($dependency['channel']) && $dependency['channel'] != 'pear.horde.org') {
+                $installer->doInstall(
+                    'install',
+                    array(
+                        'force' => true,
+                        'channel' => $dependency['channel'],
+                    ),
+                    array($dependency['name'])
+                );
+            } else if (isset($dependency['channel'])) {
+                $this->_installHordeDependency(
+                    $installer,
+                    $pear_config,
+                    $root_path,
+                    $dependency['name']
+                );
+            }
+        }
+        $installer->doInstall(
+            'install',
+            array('nodeps' => true),
+            array($package_file)
+        );
+    }
+}
index e59b0af..04b95b3 100644 (file)
@@ -102,6 +102,9 @@ implements Horde_Element_Module
             )
         );
 
+        if ($package instanceOf PEAR_Error) {
+            throw new Horde_Element_Exception($package->getMessage());
+        }
         $package->generateContents();
 
         /**
index 5633b73..9c188f4 100644 (file)
@@ -57,9 +57,6 @@ implements Iterator, Countable
         foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($module_directory)) as $file) {
             if ($file->isFile() && preg_match('/.php$/', $file->getFilename())) {
                 $class = $base . preg_replace("/^(.*)\.php/", '\\1', $file->getFilename());
-                if (!class_exists($class)) {
-                    require $file->getPathname();
-                }
                 $this->_modules[$class] = new $class();
             }
         }
index aecbe7a..22aaea7 100644 (file)
@@ -24,8 +24,8 @@
   <email>jan@horde.org</email>
   <active>yes</active>
  </lead>
- <date>2010-07-05</date>
- <time>05:44:10</time>
+ <date>2010-08-22</date>
+ <time>21:12:03</time>
  <version>
   <release>0.0.1</release>
   <api>0.0.1</api>
@@ -47,6 +47,7 @@
        <file name="Cli.php" role="php" />
       </dir> <!-- /lib/Horde/Element/Config -->
       <dir name="Module">
+       <file name="Installer.php" role="php" />
        <file name="PearPackageXml.php" role="php" />
       </dir> <!-- /lib/Horde/Element/Module -->
       <file name="Autoloader.php" role="php" />
  <dependencies>
   <required>
    <php>
-    <min>5.0.0</min>
+    <min>5.2.1</min>
    </php>
    <pearinstaller>
     <min>1.7.0</min>
    </pearinstaller>
    <package>
+    <name>PEAR_PackageFileManager2</name>
+    <channel>pear.php.net</channel>
+   </package>
+   <package>
     <name>Autoloader</name>
     <channel>pear.horde.org</channel>
    </package>
    <install as="Horde/Element/Module.php" name="lib/Horde/Element/Module.php" />
    <install as="Horde/Element/Modules.php" name="lib/Horde/Element/Modules.php" />
    <install as="Horde/Element/Config/Cli.php" name="lib/Horde/Element/Config/Cli.php" />
+   <install as="Horde/Element/Module/Installer.php" name="lib/Horde/Element/Module/Installer.php" />
    <install as="Horde/Element/Module/PearPackageXml.php" name="lib/Horde/Element/Module/PearPackageXml.php" />
    <install as="horde-element" name="script/horde-element.php" />
    <install as="Horde/Element/AllTests.php" name="test/Horde/Element/AllTests.php" />
     <release>alpha</release>
     <api>alpha</api>
    </stability>
-   <date>2010-07-05</date>
+   <date>2010-08-22</date>
    <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
    <notes>
 * Initial release
index b9e8382..8c667df 100644 (file)
@@ -64,4 +64,48 @@ extends Horde_Element_StoryTestCase
             ->when('calling the package with the help option')
             ->then('the help will contain the "u" option.');
     }
+
+    /**
+     * @scenario
+     */
+    public function theThePOptionProvidesAnUpdatedPackageXml()
+    {
+        $this->given('the default Element setup')
+            ->when('calling the package with the packagexml option and a Horde element')
+            ->then('the new package.xml of the Horde element will be printed.');
+    }
+
+    /**
+     * @todo Test (and fix) the reactions to three more scenarios:
+     *  - invalid XML in the package.xml (e.g. tag missing)
+     *  - empty file list
+     *  - file list with just one entry.
+     *
+     * All three scenarios yield errors which are still hard to
+     * understand.
+     */
+
+    /**
+     * @scenario
+     */
+    public function theInstallerModuleAddsTheIOptionInTheHelpOutput()
+    {
+        $this->given('the default Element setup')
+            ->when('calling the package with the help option')
+            ->then('the help will contain the "i" option.');
+    }
+
+    /**
+     * @scenario
+     */
+    public function theTheIOptionInstallsThePackageFromTheCurrentTree()
+    {
+        $this->given('the default Element setup')
+            ->when('calling the package with the install option and a Horde element')
+            ->then('a new PEAR configuration file will be installed')
+            ->and('the PEAR package will be installed')
+            ->and('the non-Horde dependencies of the Horde element will get installed from the network.')
+            ->and('the Horde dependencies of the Horde element will get installed from the current tree.')
+            ->and('the Horde element will be installed');
+    }
 }
\ No newline at end of file
index 7b41e0a..9059466 100644 (file)
 class Horde_Element_StoryTestCase
 extends PHPUnit_Extensions_Story_TestCase
 {
+    public function tearDown()
+    {
+        if (!empty($this->_temp_dir)) {
+            $this->_rrmdir($this->_temp_dir);
+        }
+    }
+
     /**
      * Handle a "given" step.
      *
@@ -65,12 +72,42 @@ extends PHPUnit_Extensions_Story_TestCase
             $_SERVER['argv'] = array(
                 'horde-element',
                 '--help',
-                dirname(__FILE__) . '/fixture'
+                dirname(__FILE__) . '/fixture/empty'
+            );
+            ob_start();
+            $parameters = array();
+            $parameters['cli']['parser']['class'] = 'Horde_Element_Stub_Parser';
+            Horde_Element::main($parameters);
+            $world['output'] = ob_get_contents();
+            ob_end_clean();
+            break;
+        case 'calling the package with the packagexml option and a Horde element':
+            $_SERVER['argv'] = array(
+                'horde-element',
+                '--packagexml',
+                dirname(__FILE__) . '/fixture/simple'
+            );
+            ob_start();
+            $parameters = array();
+            $parameters['cli']['parser']['class'] = 'Horde_Element_Stub_Parser';
+            $old_errorreporting = error_reporting(E_ALL & ~E_STRICT);
+            Horde_Element::main($parameters);
+            error_reporting($old_errorreporting);
+            $world['output'] = ob_get_contents();
+            ob_end_clean();
+            break;
+        case 'calling the package with the install option and a Horde element':
+            $_SERVER['argv'] = array(
+                'horde-element',
+                '--install=' . $this->_getTemporaryDirectory(),
+                dirname(__FILE__) . '/../../../'
             );
             ob_start();
             $parameters = array();
             $parameters['cli']['parser']['class'] = 'Horde_Element_Stub_Parser';
+            $old_errorreporting = error_reporting(E_ALL & ~E_STRICT);
             Horde_Element::main($parameters);
+            error_reporting($old_errorreporting);
             $world['output'] = ob_get_contents();
             ob_end_clean();
             break;
@@ -109,9 +146,95 @@ extends PHPUnit_Extensions_Story_TestCase
                 $world['output']
             );
             break;
+        case 'the help will contain the "i" option.':
+            $this->assertRegExp(
+                '/-i\s*INSTALL,\s*--install=INSTALL/',
+                $world['output']
+            );
+            break;
+        case 'the new package.xml of the Horde element will be printed.':
+            $this->assertRegExp(
+                '/<file name="New.php" role="php" \/>/',
+                $world['output']
+            );
+            break;
+        case 'a new PEAR configuration file will be installed':
+            $this->assertTrue(
+                file_exists($this->_temp_dir . DIRECTORY_SEPARATOR . '.pearrc')
+            );
+            break;
+        case 'the PEAR package will be installed':
+            $this->assertTrue(
+                file_exists(
+                    $this->_temp_dir . DIRECTORY_SEPARATOR
+                    . 'pear' . DIRECTORY_SEPARATOR 
+                    . 'php' . DIRECTORY_SEPARATOR
+                    . 'PEAR.php'
+                )
+            );
+            break;
+        case 'the non-Horde dependencies of the Horde element will get installed from the network.':
+            var_dump($world['output']);
+            $this->assertTrue(
+                file_exists(
+                    $this->_temp_dir . DIRECTORY_SEPARATOR
+                    . 'pear' . DIRECTORY_SEPARATOR 
+                    . 'php' . DIRECTORY_SEPARATOR
+                    . 'PEAR' . DIRECTORY_SEPARATOR
+                    . 'PackageFileManager2.php'
+                )
+            );
+            break;
+        case 'the Horde dependencies of the Horde element will get installed from the current tree.':
+            $this->assertTrue(
+                file_exists(
+                    $this->_temp_dir . DIRECTORY_SEPARATOR
+                    . 'pear' . DIRECTORY_SEPARATOR 
+                    . 'php' . DIRECTORY_SEPARATOR
+                    . 'Horde' . DIRECTORY_SEPARATOR
+                    . 'Autoloader.php'
+                )
+            );
+            break;
+        case 'the Horde element will be installed':
+            $this->assertTrue(
+                file_exists(
+                    $this->_temp_dir . DIRECTORY_SEPARATOR
+                    . 'pear' . DIRECTORY_SEPARATOR 
+                    . 'php' . DIRECTORY_SEPARATOR
+                    . 'Horde' . DIRECTORY_SEPARATOR
+                    . 'Element.php'
+                )
+            );
+            break;
         default:
             return $this->notImplemented($action);
         }
     }
 
+    private function _getTemporaryDirectory()
+    {
+        $this->_temp_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR
+            . 'Horde_Element_' . mt_rand();
+        mkdir($this->_temp_dir);
+        return $this->_temp_dir;
+    }
+
+    private function _rrmdir($dir)
+    {
+        if (is_dir($dir)) {
+            $objects = scandir($dir);
+            foreach ($objects as $object) {
+                if ($object != '.' && $object != '..') {
+                    if (filetype($dir . DIRECTORY_SEPARATOR . $object) == 'dir') {
+                        $this->_rrmdir($dir . DIRECTORY_SEPARATOR . $object);
+                    } else {
+                        unlink($dir . DIRECTORY_SEPARATOR . $object);
+                    }
+                }
+            }
+            reset($objects);
+            rmdir($dir);
+        }
+    } 
 }
\ No newline at end of file
diff --git a/framework/Element/test/Horde/Element/fixture/empty/package.xml b/framework/Element/test/Horde/Element/fixture/empty/package.xml
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/framework/Element/test/Horde/Element/fixture/package.xml b/framework/Element/test/Horde/Element/fixture/package.xml
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/framework/Element/test/Horde/Element/fixture/simple/lib/New.php b/framework/Element/test/Horde/Element/fixture/simple/lib/New.php
new file mode 100644 (file)
index 0000000..a814366
--- /dev/null
@@ -0,0 +1 @@
+<?php
\ No newline at end of file
diff --git a/framework/Element/test/Horde/Element/fixture/simple/lib/Second.php b/framework/Element/test/Horde/Element/fixture/simple/lib/Second.php
new file mode 100644 (file)
index 0000000..b3d9bbc
--- /dev/null
@@ -0,0 +1 @@
+<?php
diff --git a/framework/Element/test/Horde/Element/fixture/simple/package.xml b/framework/Element/test/Horde/Element/fixture/simple/package.xml
new file mode 100644 (file)
index 0000000..1e9db87
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.9.0" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Fixture</name>
+ <channel>pear.horde.org</channel>
+ <summary>Test fixture.</summary>
+ <description>A dummy package.xml used for testing the Horde_Element package.</description>
+ <lead>
+  <name>Gunnar Wrobel</name>
+  <user>wrobel</user>
+  <email>p@rdus.de</email>
+  <active>yes</active>
+ </lead>
+ <date>2010-08-22</date>
+ <time>21:12:03</time>
+ <version>
+  <release>0.0.1</release>
+  <api>0.0.1</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+ <notes>
+* Initial release
+ </notes>
+ <contents>
+  <dir baseinstalldir="/" name="/">
+   <dir name="lib">
+    <file name="Old.php" role="php" />
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.0.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.7.0</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install as="Old.php" name="lib/Old.php" />
+  </filelist>
+ </phprelease>
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2010-08-22</date>
+   <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+   <notes>
+* Initial release
+   </notes>
+  </release>
+ </changelog>
+</package>