diff --git a/project.test b/project.test index d0027c8..1596279 100644 --- a/project.test +++ b/project.test @@ -7,10 +7,8 @@ class ProjectWebTestCase extends DrupalWebTestCase { function setUp() { // Setup the required modules for all tests. $modules = func_get_args(); - $modules = array_merge(array('project'), $modules); - // We can't call parent::setUp() with a single array argument, so we need - // this ugly call_user_func_array(). - call_user_func_array(array('ProjectWebTestCase', 'parent::setUp'), $modules); + $modules = array_merge(array('views', 'project'), $modules); + parent::setUp($modules); $perms = array('create full projects', 'access user profiles', 'access projects'); @@ -87,8 +85,11 @@ class ProjectWebTestCase extends DrupalWebTestCase { * * @param $edit array * An array of form values to be passed to DrupalWebTestCase::drupalPost(). + * @return + * A node object. + * @see DrupalWebTestCase::drupalGetNodeByTitle() */ - function createProject($edit = array()) { + protected function createProject($edit = array()) { $edit += array( 'title' => $this->randomName(), 'project[uri]' => $this->randomName(8), @@ -106,6 +107,43 @@ class ProjectWebTestCase extends DrupalWebTestCase { return $this->drupalGetNodeByTitle($edit['title']); } + + /** + * Helper function for creating a new project release. + * + * @param $project + * Project node object to create release for. + * @param $edit array + * An array of form values to be passed to DrupalWebTestCase::drupalPost(). + * @return + * A node object. + * @see DrupalWebTestCase::drupalGetNodeByTitle() + */ + protected function createRelease($project, $edit = array()) { + $edit += array( + 'project_release[version_major]' => 1, + 'project_release[version_minor]' => '', + 'project_release[version_patch]' => 'x', + 'project_release[version_extra]' => 'dev', + 'body' => $this->randomString(128), + ); + $this->drupalGet('node/add/project-release/' . $project->nid); + if (!$this->xpath('//input[@name="project_release[version_minor]"]')) { + unset($edit['project_release[version_minor]']); + } + $this->drupalPost('node/add/project-release/' . $project->nid, $edit, t('Save')); + + $version = new stdClass(); + foreach (array('major', 'minor', 'patch', 'extra') as $field) { + $field = 'version_' . $field; + $version->$field = isset($edit['project_release[' . $field . ']']) ? $edit['project_release[' . $field . ']'] : NULL; + } + $version = project_release_get_version($version, $project); + $title = $project->project['uri'] . ' ' . $version; + $this->assertRaw(t('!post %title has been created.', array('!post' => 'Project release', '%title' => $title)), t('Project release created.')); + + return $this->drupalGetNodeByTitle($title); + } } @@ -357,12 +395,11 @@ class ProjectDrupalOrgWebTestCase extends ProjectWebTestCase { 'project_type' => $core ? 1 : 2, 'title' => ucfirst($project), 'project[uri]' => $project, - 'body' => $this->randomString(64), ); $this->projects[$project] = $this->createProject($edit); } - foreach ($this->projects as $project) { + foreach ($this->projects as $name => $project) { $core = ($project->title == 'Drupal') ? TRUE : FALSE; // Drupal core requires a special version format string. if ($core) { @@ -376,13 +413,8 @@ class ProjectDrupalOrgWebTestCase extends ProjectWebTestCase { $edit = array( 'taxonomy[3][]' => 8, // 7.x. 'project_release[version_major]' => $core ? '7' : '1', - 'project_release[version_patch]' => 'x', - 'project_release[version_extra]' => 'dev', - 'body' => $this->randomString(64), ); - $this->drupalPost('node/add/project-release/' . $project->nid, $edit, t('Save')); - - $this->releases[$project->title] = $this->drupalGetNodeByTitle($project->title . ' 7.x' . ($core ? '' : ' -1.x') . '-dev'); + $this->releases[$name] = $this->createRelease($project, $edit); } } } diff --git a/release/README.release_dependencies.md b/release/README.release_dependencies.md new file mode 100644 index 0000000..f2ac7e8 --- /dev/null +++ b/release/README.release_dependencies.md @@ -0,0 +1,63 @@ +# Project Release Dependency Generation Information + +## Terms +* *Component*: A module in a package. Many packages contain more than one module. +* *Release*: A row in the project_release_nodes table that signifies a release package. + +## Processing technique + +Before we can do recursive dependency checking, we have to have a complete +database of related components. + +1. Set the directory where git repositories should be stored. This is a semi-permanent directory, because it saves us the trouble of checking things out anytime we're processing dependencies. For example: + +drush vset PROJECT_RELEASE_DIRECTORY /home/rfay/tmp/git_repos + +or set it at admin/project/project-release-settings +2. Pre-seed a set of git repositories. Although the system can check out each project (and will do so) it's one more level of pain, so it's better to use the techniques in http://drupal.org/node/1057386 to pre-build all the repositories. +3. Create all first-level dependencies. This walks the info files for each release node and finds its explicit dependencies and creates rows in the component and dependency tables. Uses drush command drush prpa or the related drush commands described below. +4. Create all recursive dependencies. Here we have to have #2 already in place, and we check each project for external dependencies, and where they exist, create new dependency rows for the top-level component. + +## Key functions + +* *project_release_get_external_component_dependencies($release_nid)* searches for dependency information at the for a given release. +* *project_release_process_release($shortname, $version)* is called to process a new release node. +* *project_release_get_external_release_dependencies($depending_release_nid, $depending_components = array())* returns an array of release dependency information, given a release nid and optional component names. This will probably be the most common function to be called. + + +## How we choose dependent packages + +* Get all release packages that contain a given component with a given major version tid. +* The first one will be a dev release, if there is a dev release. +* If there is a dev release and there are stable releases, take the first stable, otherwise use the dev. + +## Drush commands + +* drush project-release-process-project (prpp): Process and store all dependency information for a project or projects, as in drush prpp views ctools commerce +* drush project-release-process-version (prpv): Process and store all dependency information for a single release node, as in drush prpv views 7.x-3.0-beta1 +* drush project-release-process-all (prpa): Process and store all dependency information for all projects. This one takes several hours to run, but provides completion information as it runs. drush prpa +* drush project-release-show-dependencies (prsd): Given a project and version, shows the external packages (releases) that depend on this project and version. drush prsd views 7.x-3.0-beta1. + +## Database Tables and Modifications + +There are two tables introduced with this patch. + +* project_release_component: One row for every component of every release. +* project_release_dependency: One row for every component which is a dependency of another. + +(Note that these obsolete the project_info_* tables that are currently deployed on drupal.org.) + +In addition, a timestamp is added to the project_release_nodes table so that all dependencies older than a given date may be rebuilt. + +## Changes from earlier approaches + +* There is now only one round of dependency generation (where each project's direct dependencies are calculated). Everything else can be determined from that. +* There is (almost) no web UI for this at this time. Since project dependencies need only be calculated on a project node creation, there's really no need. Initial work (and fixups, or paranoid debugging) is available with the drush commands. + +## Open Issues + +* The info file feature 'test_dependencies' is not currently being respected. We can either test this now, or go with what we have at this point and add that later. It is a very attractive feature. + +## TODO +* Tests for the new features. +* project_release_get_external_component_dependencies() may need to be cached. \ No newline at end of file diff --git a/release/includes/admin.settings.inc b/release/includes/admin.settings.inc index b808771..21e856c 100644 --- a/release/includes/admin.settings.inc +++ b/release/includes/admin.settings.inc @@ -9,16 +9,14 @@ * Build the form for the project_release settings administration page. */ function project_release_settings_form() { - if ($rel_dir = variable_get('project_release_directory', '')) { - $form['project_release_directory'] = array( - '#type' => 'textfield', - '#title' => t('Release directory'), - '#default_value' => $rel_dir, - '#size' => 50, - '#maxlength' => 255, - '#description' => t('This setting has been temporarily deprecated. If your site depends on scanning for releases generated by an external tool, you should wait to upgrade until a future version of the project_release.module is available that restores this functionality. Set the value blank to acknowlege that you do not need this behavior, and this error will disappear.'), + $form['project_release_directory'] = array( + '#type' => 'textfield', + '#title' => t('Release directory'), + '#default_value' => variable_get('project_release_directory', '/tmp/project_release_directory'), + '#size' => 50, + '#maxlength' => 255, + '#description' => t('The absolute path to the directory used to check out and scan releases. This directory should be writable by the webserver user and should not be periodically deleted.'), ); - } $form['project_release_default_version_format'] = array( '#type' => 'textfield', '#title' => t('Default version format string'), diff --git a/release/package-release-nodes.php b/release/package-release-nodes.php index 875366d..e9b1d4f 100755 --- a/release/package-release-nodes.php +++ b/release/package-release-nodes.php @@ -151,6 +151,8 @@ function package_releases($type, $project_id = 0) { global $drupal_root, $dest_root, $dest_rel, $tmp_dir, $wd_err_msg; global $php, $project_release_create_history; + module_load_include('drupal.inc', 'project_release'); + if (!empty($project_id)) { if (is_numeric($project_id)) { $project_nid = $project_id; @@ -226,6 +228,10 @@ function package_releases($type, $project_id = 0) { continue; } + // Process the module .info files. + // @todo: Fix me. This requires us to *gather* the required $info_files. + //project_release_info_process_all($release_node->nid, $info_files, $project_short_name, $version); + $packager = project_release_get_packager_plugin($release_node, $dest_root, $dest_rel, $tmp_dir); if (empty($packager)) { wd_err("ERROR: Can't find packager plugin to use for %release", array('%release' => $release_node->title)); diff --git a/release/project_release.drupal.inc b/release/project_release.drupal.inc new file mode 100644 index 0000000..2d5e796 --- /dev/null +++ b/release/project_release.drupal.inc @@ -0,0 +1,509 @@ + 'Unknown', + 'description' => '', + 'dependencies' => array(), + 'test_dependencies' => array(), + ); + + $info = array(); + foreach ($info_files as $file) { + $component = basename($file, '.info'); + $info[$component] = drupal_parse_info_file($file) + $defaults; + + // Change info keys to suite project_release_info. + $info[$component]['title'] = $info[$component]['name']; + $info[$component]['name'] = $component; + } + return $info; +} + +/** + * Determine and store the list of dependencies for a release. + * + * @param $release_nid + * The project release NID. + * @param $records + * List of saved component information from + * project_release_info_package_list_store(). + * @param $info + * Associative array of information from component .info files keyed by component + * name and merged with a set of defaults from project_release_info_parse(). + */ +function project_release_info_process($release_nid, array $records, array $info) { + foreach ($info as $component => $component_info) { + if ($component_info['dependencies']) { + project_release_info_process_dependencies($release_nid, $records[$component], PROJECT_RELEASE_DEPENDENCY_REQUIRED, $component_info['dependencies']); + } + if ($component_info['test_dependencies']) { + project_release_info_process_dependencies($release_nid, $records[$component], PROJECT_RELEASE_DEPENDENCY_RECOMMENDED, $component_info['test_dependencies']); + } + } +} + + +/** + * Get the releases that contain a component and are compatible with an API. + * @todo: This doesn't yet take into account *recommended* releases, so + * could pick up garbage. + * + * @param $component + * Component name. + * @param $api_tid + * Core API compatibility tid. + * + * @return + * Array of release arrays, each of which has keys + * - 'uri' Project URI (shortname) + * - 'component_id' Numeric key + * - 'release_nid' + * - 'version' Release name + * - 'tag' VCS tag + * . Each indicates that the given component + * name is found in the given release. These are ordered by version string + * descending. + */ +function project_release_info_releases_get($component, $api_tid) { + $result = db_query("SELECT DISTINCT pp.uri, c.component_id, c.release_nid, prn.version, prn.tag + FROM {project_release_component} c + INNER JOIN {project_release_nodes} prn ON c.release_nid = prn.nid + LEFT JOIN {project_projects} pp ON prn.pid = pp.nid + INNER JOIN {term_node} t ON c.release_nid = t.nid + WHERE t.tid = %d AND c.name = '%s' + ORDER BY prn.version DESC, t.vid DESC + ", $api_tid, $component); + + $releases = array(); + while ($release = db_fetch_array($result)) { + $releases[] = $release; + } + return $releases; +} + +/** + * Attempt to determine the Drupal core API term. + * + * @param $node + * Node object. + * + * @return + * Core API term to which the node belongs, otherwise FALSE. + */ +function project_release_info_core_api($node) { + static $api_terms = array(); + + if (empty($node->nid)) { + return NULL; + } + if (empty($api_terms[$node->nid])) { + $api_terms[$node->nid] = FALSE; + + if (!empty($node->taxonomy)) { + // Relase API version vocabulary. + $api_vid = _project_release_get_api_vid(); + + foreach ($node->taxonomy as $tid => $term) { + if ($term->vid == $api_vid) { + $api_terms[$node->nid] = $term; + } + } + } + } + return $api_terms[$node->nid]; +} + +/** + * Given a core API taxonomy term name like '6.x', return the numeric core api + * associated with it. + * + * @param $core_api_term_name + */ +function project_release_core_api_from_term($core_api_term_name) { + return preg_replace('/\.x.*$/', '', $core_api_term_name); +} + +/** + * Parse a dependency for comparison by project_release_info_check_incompatibility(). + * + * @param $dependency + * A dependency string, for example 'foo (>=7.x-4.5-beta5, 3.x)'. + * @param $core_api + * The term name for the core version of Drupal. Like '6.x' + * + * @return + * An associative array with three keys: + * - 'name' includes the component name of the thing to depend on + * (e.g. 'page_manager'). + * - 'original_version' contains the original version string (which can be + * used in the UI for reporting incompatibilities). + * - 'versions' is a list of associative arrays, each containing the keys + * 'op' and 'version'. 'op' can be one of: '=', '==', '!=', '<>', '<', + * '<=', '>', or '>='. 'version' is one piece like '4.5-beta3'. + * Callers should pass this structure to project_release_info_check_incompatibility(). + * + * @see project_release_info_check_incompatibility() + */ +function project_release_info_parse_dependency($dependency, $core_api) { + // We use named subpatterns and support every op that version_compare + // supports. Also, op is optional and defaults to equals. + $p_op = '(?P!=|==|=|<|<=|>|>=|<>)?'; + // Core version is always optional: 7.x-2.x and 2.x is treated the same. + $p_core = '(?:' . preg_quote($core_api . '.x') . '-)?'; + $p_major = '(?P\d+)'; + // By setting the minor version to x, branches can be matched. + $p_minor = '(?P(?:\d+|x)(?:-[A-Za-z]+\d+)?)'; + $value = array(); + $parts = explode('(', $dependency, 2); + $value['name'] = trim($parts[0]); + if (isset($parts[1])) { + $value['original_version'] = ' (' . $parts[1]; + foreach (explode(',', $parts[1]) as $version) { + if (preg_match("/^\s*$p_op\s*$p_core$p_major\.$p_minor/", $version, $matches)) { + $op = !empty($matches['operation']) ? $matches['operation'] : '='; + if ($matches['minor'] == 'x') { + // Drupal considers "2.x" to mean any version that begins with + // "2" (e.g. 2.0, 2.9 are all "2.x"). PHP's version_compare(), + // on the other hand, treats "x" as a string; so to + // version_compare(), "2.x" is considered less than 2.0. This + // means that >=2.x and <2.x are handled by version_compare() + // as we need, but > and <= are not. + if ($op == '>' || $op == '<=') { + $matches['major']++; + } + // Equivalence can be checked by adding two restrictions. + if ($op == '=' || $op == '==') { + $value['versions'][] = array('op' => '<', 'version' => ($matches['major'] + 1) . '.x'); + $op = '>='; + } + } + $value['versions'][] = array('op' => $op, 'version' => $matches['major'] . '.' . $matches['minor']); + } + } + } + return $value; +} + +/** + * Check out a release and determine relevant information. + * + * Locate all .info files contained within the project, determine the names of + * the module(s) contained within the project and store the dependencies for + * later processing. + * + * @param $release + * A keyed array of release information, containing + * - 'tag' => The tag of the release (like 7.x-1.0) + * - 'uri' => The project shortname (like 'views') + * - 'nid' => The nid of the project release node + * + * @return + * An array of dependencies, or FALSE on failure. + */ +function project_release_info_batch_process_release(array $release) { + static $fetched = array(); // We don't need to fetch every time. + + // Use batch directory. + if (!is_dir(PROJECT_RELEASE_DIRECTORY)) { + if (!mkdir(PROJECT_RELEASE_DIRECTORY)) { + watchdog('project', 'Failed to create PROJECT_RELEASE_DIRECTORY %directory', array('%directory' => PROJECT_RELEASE_DIRECTORY)); + return FALSE; + } + } + + // Checkout release. + $release['tag'] = escapeshellcmd($release['tag']); + $release['uri'] = escapeshellcmd($release['uri']); + + $url = 'git://git.drupal.org/project/' . $release['uri'] . '.git'; + $tag = $release['tag']; + $module = $release['uri']; + $directory = $release['uri']; + + $work_tree = PROJECT_RELEASE_DIRECTORY . "/$directory"; + $git_dir = "$work_tree/.git"; + // If the git directory doesn't exist, then clone into it. + if (!is_dir("$git_dir")) { + $command = "git clone $url $work_tree 2>&1"; + exec($command, $output, $status); + if ($status) { + watchdog('project', 'Failed to execute git command %cmd; output=%output', array('%cmd' => $command, '%output' => $output)); + return FALSE; + } + } + // But if it did already exist + else if (empty($fetched[$release['uri']])) { + $command = "git --work-tree $work_tree --git-dir $git_dir fetch 2>&1"; + exec($command, $output, $status); + if ($status) { + watchdog('project', 'Failed to execute git command %cmd; output=%output', array('%cmd' => $command, '%output' => $output)); + return FALSE; + } + } + $fetched[$release['uri']] = TRUE; + + exec("git --work-tree $work_tree --git-dir $git_dir reset --hard 2>&1", $output, $status); + exec("git --work-tree $work_tree --git-dir $git_dir clean -fd 2>&1", $output, $status); + $command = "git --work-tree $work_tree --git-dir $git_dir checkout $tag 2>&1"; + exec($command, $output, $status); + if ($status) { + watchdog('project', 'Failed to execute git command %cmd; output=%output', array('%cmd' => $command, '%output' => $output)); + return FALSE; + } + + // Scan checkout for .info files and create a list in the same format that + // project release uses, so that standard API functions can be used. + + // Note that DRUPAL_PHP_FUNCTION_PATTERN croaks in file_scan_directory() + // "WD php: Warning: ereg(): REG_ERANGE in file_scan_directory()" + // $function_pattern = DRUPAL_PHP_FUNCTION_PATTERN; + $function_pattern = '[a-zA-Z][a-zA-Z0-9_]*'; + $files = file_scan_directory(PROJECT_RELEASE_DIRECTORY . '/' . $directory, '^' . $function_pattern . '\.info$'); + $info_files = array(); + foreach ($files as $file) { + $info_files[] = $file->filename; + } + $info = project_release_info_parse($info_files); + + $dependencies = array(); + foreach ($info as $module => $module_info) { + $dependencies[$module] = $module_info['dependencies']; + } + + // Clear previous records for the release. + project_release_info_package_clear($release['nid']); + + $components_in_release = array_keys($info); + // Store the list of components contained by the project. + $component_info = project_release_info_package_list_store($release['nid'], $info); + foreach ($component_info as $component => $item) { + $new_dependencies = array(); + if (is_array($info[$component]['dependencies'])) { + foreach ($info[$component]['dependencies'] as $new_dependency) { + $new_dependencies[$new_dependency] = array('external' => !in_array($new_dependency, $components_in_release)); + } + } + project_release_info_package_dependencies_store($item['release_nid'], $item['component_id'], $new_dependencies); + } + + return $dependencies; +} + + +/** + * Build dependency information for a single release node if it meets the + * minimum Drupal major version API requirement. + * + * @param $shortname + * The shortname of the project. + * @param $version + * The version of the project + * + * @return + * An array of dependency information or FALSE on failure. + */ +function project_release_process_release($shortname, $version) { + // Load the project. + $pid = project_get_nid_from_uri($shortname); + $query = 'SELECT nid,tag FROM {project_release_nodes} WHERE pid = %d AND version = "%s"'; + $release = db_fetch_array(db_query($query, $pid, $version)); + $release['uri'] = $shortname; + + $node = node_load($release['nid']); + if (empty($node)) { + return FALSE; + } + $core_api_term = project_release_info_core_api($node); + if (empty($core_api_term)) { + return FALSE; + } + + $core_api = project_release_core_api_from_term($core_api_term->name); + if ($core_api < PROJECT_RELEASE_INFO_MINIMUM_API) { + return FALSE; + } + + $dependencies = project_release_info_batch_process_release($release); + return $dependencies; +} + +/** + * Find all dependencies (as release nids) of a given release nid and optional + * components. If components are given, only their dependencies will be + * returned, rather than the dependencies of every component in the release. + * + * @param $release_nid + * nid of the release node. + * @param $depending_components + * an array of component names, like + * array('views_ui', 'token', 'page_manager') + * @param $recursion_level + * Recursion level of this call to prevent infinite recursion on some + * database problem. + * @return + * an array of release node nids which are dependencies. + * + * Same as project_release_info_process_dependencies($rid, $modules[$module], $dependencies); + */ +function project_release_get_external_component_dependencies($release_nid, $depending_components = array(), $recursion_level = 0) { + // Limit this to 4 levels for now, as we probably don't know of anything + // that needs recursion_level > 2. + if ($recursion_level > 4) { + return array(); + } + // Get the core api tid. + $release_node = node_load($release_nid); + + if (!($api_term = project_release_info_core_api($release_node))) { + watchdog('project', 'ERROR: No core release API term found for release nid=%nid', array('%nid' => $release_nid)); + return; + } + + $dependent_releases = array(); + + // If no list of depending components was passed in, create a list of all + // components in this release. + if (empty($depending_components)) { + $query = 'SELECT name FROM {project_release_component} WHERE release_nid = %d'; + $result = db_query($query, $release_nid); + while ($row = db_fetch_array($result)) { + $depending_components[] = $row['name']; + } + } + // Now we have a list of component names for which we need to find + // external dependencies. + foreach ($depending_components as $depending_component) { + $external_dependencies = project_release_find_external_dependencies($release_nid, $depending_component); + + if (empty($external_dependencies)) { + continue; + } + foreach ($external_dependencies as $component_id => $component_info) { + $dependency_component = $component_info['dependency']; + + + // Find what releases that component is shipped with. + // We could hope that it would only be in one package/rid, + // but there are probably cases (refactoring) where that is not + // the case. + $available_releases = project_release_info_releases_get($dependency_component, $api_term->tid); + // If the first one returned is a dev release, and there are + // others, remove it and use the next. + if (!empty($available_releases)) { + $version = $available_releases[0]['version']; + $is_dev_release = (substr($version, strlen($version) - 3, 3)) == 'dev'; + if ($is_dev_release && count($available_releases) > 1) { + $release = $available_releases[1]; + } + else { + $release = $available_releases[0]; + } + $release['component'] = $dependency_component; + // Now we know the release information for the package that + // contains this component. + $dependent_releases[$release['component']] = $release; + } + } + + $new_dependencies = array(); + foreach($dependent_releases as $component => $component_info) { + // Recurse. + $new_dependencies += project_release_get_external_component_dependencies($component_info['release_nid'], array($component), $recursion_level + 1); + } + $dependent_releases += $new_dependencies; + } + return $dependent_releases; +} + + +/** + * Get an array of release dependencies of a given release nid and optional + * components in that release nid. + * + * @param unknown_type $depending_release_nid + * The release nid for which we're looking for dependencies. + * @param unknown_type $depending_components + * An array of component names (modules within the release nid). + * + * @return + * An array keyed by release nids of the dependencies. So: + * array($nid => array('version' => $version, 'tag' => $tag), ...) + */ +function project_release_get_external_release_dependencies($depending_release_nid, $depending_components = array()) { + $component_dependencies = project_release_get_external_component_dependencies($depending_release_nid, $depending_components); + + $release_dependencies = array(); + foreach ($component_dependencies as $component => $component_info) { + $release_dependencies[$component_info['release_nid']] = array( + 'uri' => $component_info['uri'], + 'version' => $component_info['version'], + 'tag' => $component_info['tag'], + ); + } + return $release_dependencies; +} + + +/** + * Given a release nid and component name, find all external dependencies for + * that component. + * @param $release_nid + * Release nid. + * @param $component_name + * Component name. + * + * @return + * An array of component information. + */ +function project_release_find_external_dependencies($release_nid, $component_name) { + $rows = array(); + $query = 'SELECT DISTINCT(prc.component_id), name AS depender, dependency, release_nid FROM {project_release_dependency} prd INNER JOIN {project_release_component} prc + ON prd.component_id = prc.component_id WHERE prc.name = "%s" AND prc.release_nid = %d AND prd.external = 1'; + $result = db_query($query, $component_name, $release_nid); + while($row = db_fetch_array($result)) { + $rows[$row['component_id']] = $row; + } + return $rows; +} diff --git a/release/project_release.drush.inc b/release/project_release.drush.inc new file mode 100644 index 0000000..3c9e81d --- /dev/null +++ b/release/project_release.drush.inc @@ -0,0 +1,149 @@ + 'Build dependencies for a given project shortname.', + 'callback' => 'drush_project_release_process_project', + 'arguments' => array( + 'shortname' => 'Project shortname/uri', + ), + 'aliases' => array('prpp'), + ); + $items['project-release-process-version'] = array( + 'description' => 'Build dependencies for a given project version (primarily for debugging).', + 'callback' => 'drush_project_release_process_version', + 'arguments' => array( + 'shortname' => 'Project shortname/uri', + 'version' => 'Version (like 7.x-3.x-dev)', + ), + 'aliases' => array('prpv'), + ); + $items['project-release-process-all-projects'] = array( + 'description' => 'Build dependency records for all projects. This will take a long time', + 'callback' => 'drush_project_release_process_all_projects', + 'aliases' => array('prap'), + ); + $items['project-release-show-dependencies'] = array( + 'description' => 'Show the dependencies of the given release node', + 'callback' => 'drush_project_show_dependencies', + 'arguments' => array( + 'shortname' => 'Shortname of the project (uri)', + 'version' => 'Version of the project (like "7.x-1.x-dev"', + ), + 'aliases' => array('prsd'), + ); + + return $items; +} + + + +/** + * Update and save all dependencies for the project named. + * + * @param $shortname + * Any number of projct shortnames to be processed + */ +function drush_project_release_process_project() { + module_load_include('inc', 'project_release', 'project_release.drupal'); + + $args = pm_parse_arguments(func_get_args()); + + foreach ($args as $shortname) { + drush_print("Processing dependencies for $shortname"); + + // Get all versions for a project and build dependencies for them. + $pid = project_get_nid_from_uri($shortname); + + // Find all releases for a project. + $query = 'SELECT nid,tag,version FROM {project_release_nodes} WHERE pid = %d ORDER BY nid DESC'; + $result = db_query($query, $pid); + while ($release_info = db_fetch_array($result)) { + $dependencies = project_release_process_release($shortname, $release_info['version']); + if ($dependencies === FALSE) { + drush_print("Skipped $shortname: {$release_info['version']}"); + continue; + } + drush_print_r("Processed $shortname: {$release_info['version']}"); + } + } +} + + +/** + * Process a single release node (version) + * @param $shortname + * The project shortname (uri) + * @param $version + * The version string (like 7.x-3.x-dev) + */ +function drush_project_release_process_version($shortname, $version) { + module_load_include('inc', 'project_release', 'project_release.drupal'); + drush_print("Processing dependencies for $shortname: $version"); + + $dependencies = project_release_process_release($shortname, $version); + if ($dependencies === FALSE) { + drush_print("Skipped $shortname: $version"); + return; + } + drush_print_r($dependencies); +} + +/** + * Drush handler for processing all projects. + * + * Harvests and stores dependency information for all projects. + * Just let this one run. + */ +function drush_project_release_process_all_projects() { + module_load_include('inc', 'project_release', 'project_release.drupal'); + + $total = $to_do = db_result(db_query('SELECT COUNT(*) FROM {project_release_nodes')); + $done = 0; + + $query = 'SELECT prn.nid, prn.version, p.uri AS shortname FROM {project_release_nodes} prn INNER JOIN {project_projects} p ON p.nid = prn.pid WHERE prn.project_release_dependencies_timestamp < UNIX_TIMESTAMP() ORDER BY shortname, version'; + $result = db_query($query); + $count = $skipped = 0; + while ($release_info = db_fetch_array($result)) { + $dependencies = project_release_process_release($release_info['shortname'], $release_info['version']); + if ($dependencies === FALSE) { + $skipped++; + } + $pct = round($done * 100/$total, 2); + drush_print("$pct% done. Done: $done; Skipped: $skipped; Remaining: $to_do ({$release_info['shortname']}:{$release_info['version']} nid={$release_info['nid']})"); + $done++; + $to_do--; + } +} + +/** + * Show dependencies for a given project + version + * + * @param $shortname + * Project shortname (uri) like 'views' + * @param unknown_type $version + * Project version, like '7.x-1.1' + */ +function drush_project_show_dependencies($shortname, $version) { + module_load_include('inc', 'project_release', 'project_release.drupal'); + + // Find the release nid for the given shortname + version + $pid = project_get_nid_from_uri($shortname); + $nid = db_result(db_query('SELECT nid FROM {project_release_nodes} WHERE pid = %d AND version = "%s"', $pid, $version)); + if (empty($nid)) { + drush_print_r("Failed to load a release for $shortname:$version"); + return; + } + $dependent_releases = project_release_get_external_release_dependencies($nid); + + drush_print_r($dependent_releases); +} diff --git a/release/project_release.info b/release/project_release.info index 0ea1e2d..bf8718a 100644 --- a/release/project_release.info +++ b/release/project_release.info @@ -6,3 +6,5 @@ dependencies[] = taxonomy dependencies[] = upload dependencies[] = views core = 6.x + +test_dependencies[] = pift diff --git a/release/project_release.install b/release/project_release.install index a8331d8..a73867c 100644 --- a/release/project_release.install +++ b/release/project_release.install @@ -144,6 +144,12 @@ function project_release_schema() { 'not null' => TRUE, 'default' => 0, ), + 'project_release_dependencies_timestamp' => array( + 'description' => 'Timestamp of last processing for release and dependency information', + 'type' => 'int', + 'unsigned' => TRUE, + 'default' => 0, + ), ), 'primary key' => array('nid'), 'indexes' => array( @@ -392,6 +398,95 @@ function project_release_schema() { 'primary key' => array('nid', 'uid'), ); + $schema['project_release_component'] = array( + 'description' => 'The component(s) contained by a project release.', + 'fields' => array( + 'component_id' => array( + 'description' => 'Unique component ID.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'release_nid' => array( + 'description' => 'The {node}.nid of the project_release node that includes a component.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'name' => array( + 'description' => 'The machine readable name of a component.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'title' => array( + 'description' => 'The human readable name of a component', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'description' => array( + 'description' => 'A description of a component.', + 'type' => 'text', + 'size' => 'medium' + ), + ), + 'primary key' => array('component_id'), + 'indexes' => array( + 'release_nid' => array('release_nid'), + 'name' => array('name'), + ), + ); + + $schema['project_release_dependency'] = array( + 'description' => 'The dependencies of a component.', + 'fields' => array( + 'dependency_id' => array( + 'description' => 'Dependency ID', + 'type' => 'serial', + ), + 'component_id' => array( + 'description' => 'ID of a component.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'dependency' => array( + 'description' => 'Name (shortname) of a component that the component is dependent upon.', + 'type' => 'varchar', + 'length' => 50, + 'not null' => TRUE, + ), + 'core_api' => array( + 'type' => 'varchar', + 'length' => 8, + 'description' => t('The core api version of this component, as in "6.x" or "7.x".'), + 'not null' => TRUE, + ), + 'external' => array( + 'description' => 'TRUE if the dependency is not in the same package as this component.', + 'type' => 'int', + 'unsigned' => TRUE, + 'default' => 0, + ), + 'external_release_nid' => array( + 'description' => 'Release nid for this dependency', + 'type' => 'int', + 'unsigned' => TRUE, + 'default' => 0, + ), + 'dependency_type' => array( + 'description' => 'Dependency type, PROJECT_RELEASE_DEPENDENCY_*.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + ), + 'primary key' => array('dependency_id'), + 'indexes' => array('component_dependency' => array('component_id', 'dependency')), + ); + return $schema; } @@ -827,3 +922,126 @@ function project_release_update_6012() { db_add_field($ret, 'project_release_file', 'downloads', $spec); return $ret; } + +/** + * Create project dependency info tables. + */ +function project_release_update_6013() { + $ret = array(); + if (!db_table_exists('project_release_component')) { + db_create_table($ret, 'project_release_component', array( + 'description' => 'The component(s) contained by a project release.', + 'fields' => array( + 'component_id' => array( + 'description' => 'Unique component ID.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'release_nid' => array( + 'description' => 'The {node}.nid of the project_release node that includes a component.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'name' => array( + 'description' => 'The machine readable name of a component.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'title' => array( + 'description' => 'The human readable name of a component', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'description' => array( + 'description' => 'A description of a component.', + 'type' => 'text', + 'size' => 'medium' + ), + ), + 'primary key' => array('component_id'), + 'indexes' => array( + 'release_nid' => array('release_nid'), + 'name' => array('name'), + ), + )); + } + if (!db_table_exists('project_release_dependency')) { + db_create_table($ret, 'project_release_dependency', array( + 'description' => 'The dependencies of a component.', + 'fields' => array( + 'dependency_id' => array( + 'description' => 'Dependency ID', + 'type' => 'serial', + ), + 'component_id' => array( + 'description' => 'ID of a component.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'dependency' => array( + 'description' => 'Name (shortname) of a component that the component is dependent upon.', + 'type' => 'varchar', + 'length' => 50, + 'not null' => TRUE, + ), + 'core_api' => array( + 'type' => 'varchar', + 'length' => 8, + 'description' => t('The core api version of this component, as in "6.x" or "7.x".'), + 'not null' => TRUE, + ), + 'external' => array( + 'description' => 'TRUE if the dependency is not in the same package as this component.', + 'type' => 'int', + 'unsigned' => TRUE, + 'default' => 0, + ), + 'external_release_nid' => array( + 'description' => 'Release nid for this dependency', + 'type' => 'int', + 'unsigned' => TRUE, + 'default' => 0, + ), + 'dependency_type' => array( + 'description' => 'Dependency type, PROJECT_RELEASE_DEPENDENCY_*.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + ), + 'primary key' => array('dependency_id'), + 'indexes' => array('component_dependency' => array('component_id', 'dependency')), + )); + } + return $ret; +} + +/** + * Add the 'project_release_dependencies_timestamp' column to + * {project_release_nodes} so we can prioritize processing of + * dependencies based on whether they've been processed or not. + * + * Drop and restart the project_release_dependency table, as it + * needed to be refactored. + */ +function project_release_update_6014() { + $ret = array(); + $spec = array( + 'description' => 'Timestamp of last processing for release and dependency information', + 'type' => 'int', + 'unsigned' => TRUE, + 'default' => 0, + ); + + db_add_field($ret, 'project_release_nodes', 'project_release_dependencies_timestamp', $spec); + + return $ret; +} + + diff --git a/release/project_release.module b/release/project_release.module index f94d8af..31507ad 100644 --- a/release/project_release.module +++ b/release/project_release.module @@ -10,6 +10,12 @@ define('PROJECT_RELEASE_UPDATE_STATUS_CURRENT', 0); define('PROJECT_RELEASE_UPDATE_STATUS_NOT_CURRENT', 1); define('PROJECT_RELEASE_UPDATE_STATUS_NOT_SECURE', 2); +/* + * Constants for the possible values of {project_release_dependency}.dependency_type. + */ +define('PROJECT_RELEASE_DEPENDENCY_REQUIRED', 0); +define('PROJECT_RELEASE_DEPENDENCY_RECOMMENDED', 1); + /** * @defgroup project_release_core Core Drupal hooks */ @@ -29,6 +35,15 @@ function project_release_init() { } /** + * Implementation of hook_perm(). + */ +function project_release_perm() { + return array( + 'parse project_release_info batch', + ); +} + +/** * Implementation of hook_menu() * @ingroup project_release_core */ @@ -2297,3 +2312,125 @@ function project_release_ctools_plugin_directory($module, $plugin) { } } } + +/** + * @defgroup project_release_info Project release information + * @{ + */ + +/** + * Loads a component. + * + * @param $component_id + * Component ID to load. + * + * @return + * Array of component information. + */ +function project_release_component_load($component_id) { + return db_fetch_array(db_query('SELECT * FROM {project_release_component} WHERE component_id = %d', $component_id)); +} + +/** + * Loads the list of components contained by a project release. + * + * @param $release_nid + * Project release ID. + * + * @return + * Associative array of components keyed by component name and containing + * array keys: 'name', 'title', 'description'. + */ +function project_release_get_components($release_nid) { + $result = db_query('SELECT * FROM {project_release_component} WHERE release_nid = %d', $release_nid); + $components = array(); + while ($component = db_fetch_array($result)) { + $components[$component['name']] = $component; + } + return $components; +} + +/** + * Gets a list of dependency components for a component. + * + * @param $component_id + * Component ID to get dependencies for. + * @param $dependency_type + * (Optional) Minimum dependency type, PROJECT_RELEASE_DEPENDENCY_*. All + * dependencies of equal or lesser stringency will be included. + * + * @return + * Associative array of dependencies keyed by component name. + */ +function project_release_component_dependencies_load_all($component_id, $dependency_type = PROJECT_RELEASE_DEPENDENCY_REQUIRED) { + $result = db_query('SELECT c.* + FROM {project_release_dependency} d + JOIN {project_release_component} c + ON c.component_id = d.target_id + WHERE source_id = %d + AND dependency_type <= %d', $component_id, $dependency_type); + $dependencies = array(); + while ($dependency = db_fetch_array($result)) { + $dependencies[$dependency['name']] = $dependency; + } + return $dependencies; +} + +/** + * Recursively determines the list of dependencies of a release. + * + * @param $release_nid + * Project release ID. + * @param $dependency_type + * (Optional) Minimum dependency type, PROJECT_RELEASE_DEPENDENCY_*. All + * dependencies of equal or lesser stringency will be included. + * + * @return + * List of dependencies of the release. + */ +function project_release_get_dependencies($release_nid, $dependency_type = PROJECT_RELEASE_DEPENDENCY_REQUIRED) { + $components = project_release_get_components($release_nid); + return _project_release_get_component_dependencies($components, $dependency_type); +} + +/** + * Recursively determines the list of dependencies for a list of components. + * + * @param array $components + * List of components to determine the dependencies for. + * @param $dependency_type + * (Optional) Minimum dependency type, PROJECT_RELEASE_DEPENDENCY_*. All + * dependencies of equal or lesser stringency will be included. + * @param $dependencies + * (Internal) Array of already determined dependencies. + * + * @return + * Associative array of dependencies keyed by component name and containing + * all component info keys: 'component_id', 'rid', 'name', 'title', and + * 'description'. + */ +function _project_release_get_component_dependencies(array $components, $dependency_type, array $dependencies = array()) { + foreach ($components as $component) { + // Get all dependencies of the component. + $dependencies_new = project_release_component_dependencies_load_all($component['component_id'], $dependency_type); + + // Add the new dependencies to the list of dependencies and remove the + // dependencies that have already been added. + foreach ($dependencies_new as $name => $dependency_new) { + if (!isset($dependencies[$name])) { + $dependencies[$name] = $dependency_new; + } + else { + unset($dependencies_new[$name]); + } + } + + // Process the dependencies of all the new dependency components. + $dependencies = _project_release_get_component_dependencies($dependencies_new, $dependency_type, $dependencies); + } + return $dependencies; +} + +/** + * @} End of "defgroup project_release_info". + */ diff --git a/release/project_release.package.inc b/release/project_release.package.inc new file mode 100644 index 0000000..ad42da1 --- /dev/null +++ b/release/project_release.package.inc @@ -0,0 +1,82 @@ + $component_info) { + // These will always be insertions, because we've already deleted the old + // ones. + $project_release_component_info = array( + 'release_nid' => $release_nid, + 'name' => $component_info['name'], + 'title' => $component_info['title'], + 'description' => $component_info['description'], + ); + $result = drupal_write_record('project_release_component', $project_release_component_info); + $records[$component] = $project_release_component_info; + } + return $records; +} + + +/** + * Store a dependency row into the project_release_nodes table. + * + * @param $release_nid + * The nid for the release node which contains the component that depends + * on the dependencies. + * @param $component_id + * The component_id for the component which depends on the dependencies. + * @param array $dependencies + * Array of component_name => array('external' => TRUE|FALSE') + * @param $dependency_type + * The type of the dependency (PROJECT_RELEASE_DEPENDENCY_*) + */ +function project_release_info_package_dependencies_store($release_nid, $component_id, array $dependencies, $dependency_type = PROJECT_RELEASE_DEPENDENCY_REQUIRED) { + $release = node_load($release_nid); + $api_term = project_release_info_core_api($release); + + foreach($dependencies as $dependency => $attributes) { + $record = array( + 'component_id' => $component_id, + 'dependency' => $dependency, + 'core_api' => $api_term->name, + 'external' => !empty($attributes['external']), + 'dependency_type' => $dependency_type, + ); + drupal_write_record('project_release_dependency', $record); + } + db_query('UPDATE {project_release_nodes} SET project_release_dependencies_timestamp = UNIX_TIMESTAMP() WHERE nid=%d', $release_nid); +} diff --git a/release/project_release.test b/release/project_release.test new file mode 100644 index 0000000..55b1917 --- /dev/null +++ b/release/project_release.test @@ -0,0 +1,251 @@ + 'Component API', + 'description' => 'Ensure that the component storage and retrieval API works correctly.', + 'group' => 'Project release', + ); + } + + protected function testComponentAPI() { + module_load_include('package.inc', 'project_release'); + + // Create a project and release. + $project = $this->createProject(); + $release = $this->createRelease($project); + + // Store two components for the release. + $components = array( + 'foo' => array( + 'name' => 'foo', + 'title' => 'Foo', + 'description' => 'Foo is not bar.', + ), + 'bar' => array( + 'name' => 'bar', + 'title' => 'Bar', + 'description' => 'Bar is not foo.', + ), + ); + $components = project_release_info_package_list_store($release->nid, $components); + project_release_info_package_dependencies_store($release->nid, $components['foo']['component_id'], !in_array($dependency, array_keys($components)), array($components['bar']['component_id'])); + + // Retrieve stored components and compared with components array. + $stored = project_release_get_components($release->nid); + $this->assertEqual($stored, $components, 'Components stored and retrieved successfully'); + + // Retrieve stored component dependency and compare with components array. + $dependencies = project_release_component_dependencies_load_all($components['foo']['component_id']); + $this->assertEqual(count($dependencies), 1, 'One dependency retrieved for foo'); + $this->assertEqual(current($dependencies), $components['bar'], 'Component dependency stored and retrieved successfully'); + + // Retrieve stored release dependency and compare with components array. + $release_dependencies = project_release_get_dependencies($release->nid); + $this->assertEqual($release_dependencies, $dependencies, 'Release dependency stored and retrieved successfully'); + + // Load an individual component and ensure that it works properly. + $component = project_release_component_load($components['foo']['component_id']); + $this->assertEqual($component, $components['foo'], 'Component stored and retrieved successfully'); + + // Create a dependency tree and ensure that it is resolved properly. + // Foo -> Bar -> Baz. + $project2 = $this->createProject(); + $release2 = $this->createRelease($project2); + $component = array( + 'name' => 'baz', + 'title' => 'Baz', + 'description' => 'Baz is not bar.', + ); + $component = current(project_release_info_package_list_store($release2->nid, array($component))); + project_release_info_package_dependencies_store($release->nid, $components['bar']['component_id'], !in_array($dependency, array_keys($info)), array($component['component_id'])); + + // Ensure that the dependency is handled properly. + $dependencies = project_release_component_dependencies_load_all($components['bar']['component_id']); + $this->assertFalse($dependencies, 'Bar has no required dependencies'); + + // Retrieve stored component dependency and compare with component array. + $dependencies = project_release_component_dependencies_load_all($components['bar']['component_id'], PROJECT_RELEASE_DEPENDENCY_RECOMMENDED); + $this->assertEqual(count($dependencies), 1, 'One dependency retrieved for bar'); + $this->assertEqual(current($dependencies), $component, 'Component dependency stored and retrieved successfully'); + + // Ensure that retrieval of the dependency tree respects type. + $dependencies = project_release_get_dependencies($release->nid); + $this->assertEqual(array_keys($dependencies), array('bar'), 'Foo requires bar'); + + // Ensure that retreval of the dependency tree is correct. + $dependencies = project_release_get_dependencies($release->nid, PROJECT_RELEASE_DEPENDENCY_RECOMMENDED); + $this->assertEqual(array_keys($dependencies), array('bar', 'baz'), 'Foo recommends bar and baz'); + } +} + +/** + * Ensure that the Drupal component parsing functions work properly. + */ +class ProjectReleaseComponentDrupalTestCase extends ProjectDrupalOrgWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Drupal component parsing', + 'description' => 'Ensure that the Drupal component parsing functions work properly.', + 'group' => 'Project release', + ); + } + + protected function testDrupalComponentParsing() { + $this->process(); + + // Ensure that the component information is correct. + $components = project_release_get_components($this->releases['foo']->nid); + $this->assertEqual(array_keys($components), array('project_release_foo'), 'Project foo contains foo'); + $this->assertEqual($components, array( + 'project_release_foo' => array( + 'component_id' => 1, + 'release_nid' => $this->releases['foo']->nid, + 'name' => 'project_release_foo', + 'title' => 'Project release foo', + 'description' => 'Test module for project_release. (do not enable)', + ), + ), 'Project foo components contain proper information'); + + $components = project_release_get_components($this->releases['bar']->nid); + $this->assertEqual(array_keys($components), array('project_release_bar', 'project_release_baz'), 'Project bar contains bar and baz'); + $this->assertEqual($components, array( + 'project_release_bar' => array( + 'component_id' => 2, + 'release_nid' => $this->releases['bar']->nid, + 'name' => 'project_release_bar', + 'title' => 'Project release bar', + 'description' => 'Test module for project_release. (do not enable)', + ), + 'project_release_baz' => array( + 'component_id' => 3, + 'release_nid' => $this->releases['bar']->nid, + 'name' => 'project_release_baz', + 'title' => 'Project release baz', + 'description' => 'Test module for project_release. (do not enable)', + ), + ), 'Project bar components contain proper information'); + + // Ensure that dependencies were parsed properly. + $dependencies = project_release_get_dependencies($this->releases['foo']->nid); + $this->assertFalse($dependencies, 'No dependencies found for project foo'); + + $dependencies = project_release_get_dependencies($this->releases['bar']->nid); // @TODO Should pick up soft dependency. + $this->assertEqual(array_keys($dependencies), array('project_release_foo'), 'Project bar depends on component foo'); + + // Ensure that parsing creates an error due to the lack of a stable release. + $this->assertPackageError('ERROR: No development release with a corresponding stable release was found that matches the requirements for dependency [project_release_foo] of [project_release_bar].'); + + // @TODO Figure out what CVS/Version control API stuff needs to be in setup. +// $this->createTag('bar', 'DRUPAL-7--1-0'); +// $this->drupalPost('node/add/project-release/' . $this->projects['bar']->nid, array(), t('Next')); +// $edit = array( +// 'body' => $this->randomString(32), +// ); +// $this->drupalPost(NULL, $edit, t('Save')); +// +// $this->process(); +// +// $this->assertNoPackageErrors(); + } + + /** + * Process the test modules. + */ + protected function process() { + // Remove all previously existing project_release_info data. + db_query('DELETE FROM {project_release_dependency}'); + db_query('DELETE FROM {project_release_component}'); + + // Reset packaging errors. + wd_err(FALSE, TRUE); + + // Load Drupal parsing implementation. + module_load_include('drupal.inc', 'project_release'); + + // Parse test modules related to projects foo and bar. + $info_files = array( + drupal_get_path('module', 'project_release_foo') . '/project_release_foo.info', + ); + project_release_info_process_all($this->releases['foo']->nid, $info_files, 'foo', '7.x-1.0'); + + $info_files = array( + drupal_get_path('module', 'project_release_bar') . '/project_release_bar.info', + drupal_get_path('module', 'project_release_baz') . '/project_release_baz.info', + ); + project_release_info_process_all($this->releases['bar']->nid, $info_files, 'bar', '7.x-1.2'); + } + + protected function createTag($project, $tag, $branch = FALSE) { + db_query("INSERT INTO {cvs_tags} VALUES (%d, '%s', %d, %d)", $this->projects[$project]->nid, $tag, $branch ? 1 : 0, time()); + } + + /** + * Assert that the package error were created. + * + * @param $message + * The packaging error message. + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertPackageError($message) { + $found = FALSE; + foreach (wd_err() as $error) { + if ($error == $message) { + $found = TRUE; + break; + } + } + return $this->assertTrue($found, 'Found package error [' . $message . ']'); + } + + /** + * Assert that no package errors were created. + * + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertNoPackageErrors() { + if (!$this->assertFalse(wd_err(), 'No packaging errors')) { + foreach (wd_err() as $error) { + $this->fail($error); + } + } + } +} + +function wd_err($message = FALSE, $reset = FALSE) { + static $messages = array(); + + if ($reset) { + $messages = array(); + } + + if ($message) { + $messages[] = $message; + } + return $messages; +} diff --git a/release/tests/bar/baz/project_release_baz.info b/release/tests/bar/baz/project_release_baz.info new file mode 100644 index 0000000..dcd4fbd --- /dev/null +++ b/release/tests/bar/baz/project_release_baz.info @@ -0,0 +1,8 @@ +name = Project release baz +description = Test module for project_release. (do not enable) +package = Project +version = 1.2 +core = 7.x +dependencies[] = project_release_bar + +test_dependencies[] = project_release_foo (1.x) diff --git a/release/tests/bar/baz/project_release_baz.module b/release/tests/bar/baz/project_release_baz.module new file mode 100644 index 0000000..ec803ae --- /dev/null +++ b/release/tests/bar/baz/project_release_baz.module @@ -0,0 +1,8 @@ +