Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

move Pyrus_Developer to trunk/branches

  • Loading branch information...
commit dc470fd6d30baaf24d9ac5a8c7600c0b34177737 0 parents
@cellog cellog authored
Showing with 5,590 additions and 0 deletions.
  1. +2 −0  CREDITS
  2. +5 −0 README
  3. +1 −0  RELEASE-0.1.0
  4. +299 −0 customcommand/commands.xml
  5. +12 −0 data/phartemplate.php
  6. +103 −0 package.xml
  7. +109 −0 package_compatible.xml
  8. +10 −0 src/Pyrus/Developer/CoverageAnalyzer/AbstractSourceDecorator.php
  9. +175 −0 src/Pyrus/Developer/CoverageAnalyzer/Aggregator.php
  10. +381 −0 src/Pyrus/Developer/CoverageAnalyzer/DefaultSourceDecorator.php
  11. +5 −0 src/Pyrus/Developer/CoverageAnalyzer/Exception.php
  12. +101 −0 src/Pyrus/Developer/CoverageAnalyzer/SourceFile.php
  13. +45 −0 src/Pyrus/Developer/CoverageAnalyzer/SourceFile/PerTest.php
  14. +836 −0 src/Pyrus/Developer/CoverageAnalyzer/Sqlite.php
  15. +74 −0 src/Pyrus/Developer/CoverageAnalyzer/Web/Aggregator.php
  16. +90 −0 src/Pyrus/Developer/CoverageAnalyzer/Web/Controller.php
  17. +5 −0 src/Pyrus/Developer/CoverageAnalyzer/Web/Exception.php
  18. +407 −0 src/Pyrus/Developer/CoverageAnalyzer/Web/View.php
  19. +43 −0 src/Pyrus/Developer/CoverageAnalyzer/make-coverage-phar.php
  20. BIN  src/Pyrus/Developer/CoverageAnalyzer/pear2coverage.phar.php
  21. +91 −0 src/Pyrus/Developer/CoverageAnalyzer/test-modified.php
  22. +26 −0 src/Pyrus/Developer/CoverageAnalyzer/test.php
  23. +3 −0  src/Pyrus/Developer/Creator/Exception.php
  24. +218 −0 src/Pyrus/Developer/Creator/Phar.php
  25. +57 −0 src/Pyrus/Developer/Creator/Phar/PHPArchive.php
  26. +171 −0 src/Pyrus/Developer/Creator/Tar.php
  27. +75 −0 src/Pyrus/Developer/Creator/Xml.php
  28. +79 −0 src/Pyrus/Developer/Creator/Zip.php
  29. +393 −0 src/Pyrus/Developer/PackageFile/Commands.php
  30. +409 −0 src/Pyrus/Developer/PackageFile/PEAR2SVN.php
  31. +49 −0 src/Pyrus/Developer/PackageFile/PEAR2SVN/Filter.php
  32. +148 −0 src/Pyrus/Developer/PackageFile/PECL.php
  33. +87 −0 src/Pyrus/Developer/PackageFile/PECL/Filter.php
  34. +118 −0 src/Pyrus/Developer/PackageFile/v2.php
  35. +3 −0  src/Pyrus/Developer/Runphpt/Exception.php
  36. +960 −0 src/Pyrus/Developer/Runphpt/Runner.php
2  CREDITS
@@ -0,0 +1,2 @@
+;; maintainers of Pyrus
+Gregory Beaver [cellog] <cellog@php.net> (lead)
5 README
@@ -0,0 +1,5 @@
+Developer tools for the Pyrus installer
+
+This package contains several custom commands for Pyrus,
+including command to automatically generate a package.xml,
+and commands to create a package from an existing package.xml
1  RELEASE-0.1.0
@@ -0,0 +1 @@
+Initial development release
299 customcommand/commands.xml
@@ -0,0 +1,299 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<commands version="2.0" xmlns="http://pear2.php.net/dtd/customcommand-2.0">
+ <command>
+ <name>make</name>
+ <class>pear2\Pyrus\Developer\PackageFile\Commands</class>
+ <function>makePackageXml</function>
+ <autoloadpath></autoloadpath>
+ <summary>Create or update a package.xml from a standard PEAR2 directory layout</summary>
+ <shortcut>mk</shortcut>
+ <options>
+ <option>
+ <name>nocompatible</name>
+ <shortopt>n</shortopt>
+ <type><bool/></type>
+ <doc>Do not generate package_compatible.xml</doc>
+ </option>
+ <option>
+ <name>packagexmlsetup</name>
+ <shortopt>s</shortopt>
+ <type><bool/></type>
+ <doc>php file to execute that does customization of the package.xml</doc>
+ </option>
+ <option>
+ <name>package</name>
+ <shortopt>p</shortopt>
+ <type><string/></type>
+ <doc>also create a package release. value should be a comma-delimited list of formats desired</doc>
+ </option>
+ <option>
+ <name>stub</name>
+ <shortopt>u</shortopt>
+ <type><string/></type>
+ <doc>Path to stub for phar archive (ignored if --package not passed)</doc>
+ </option>
+ <option>
+ <name>extrasetup</name>
+ <shortopt>e</shortopt>
+ <type><string/></type>
+ <doc>Path to script with extra needed setup (ignored if --package not passed)</doc>
+ </option>
+ </options>
+ <arguments>
+ <argument>
+ <name>packagename</name>
+ <multiple>0</multiple>
+ <optional>1</optional>
+ <doc>Name of the package to create, default is grabbed from existing package.xml.</doc>
+ </argument>
+ <argument>
+ <name>channel</name>
+ <multiple>0</multiple>
+ <optional>1</optional>
+ <doc>Name of the channel of the package to create, default is grabbed from existing package.xml or pear2.php.net.</doc>
+ </argument>
+ <argument>
+ <name>dir</name>
+ <multiple>0</multiple>
+ <optional>1</optional>
+ <doc>path to directory to make the packagefile for.</doc>
+ </argument>
+ </arguments>
+ <doc>Make a package.xml file from a standard PEAR2 directory layout.
+
+This command looks for these standard files:
+
+CREDITS, README, RELEASE-X.Y.Z (where X.Y.Z is the release version)
+API-X.Y.Z (where X.Y.Z is the API version)
+
+and for a standard directory layout of
+
+src/ PHP files
+data/ Data files
+tests/ Test files
+doc/ Documentation files
+examples/ Example files (documentation)
+scripts/ Executable files, scripts
+www/ Web files
+customrole/ Custom installer role xml definition files
+customtask/ Custom installer task xml definition files
+customcommand/ Custom command xml definition files
+
+The CREDITS file must have this format:
+
+;; comments ignored
+Maintainer One [handle1] &lt;email@example.com&gt; (role)
+Maintainer Two [handle2] &lt;email@example.com&gt; (role)
+
+Where role is one of lead, developer, contributor, helper.
+
+The first line of README is used as the summary of the package.
+RELEASE-X.Y.Z is used as the release notes.
+
+if --packagexmlsetup is specified, the script should work with variable
+$package for modifying the package.xml, and $compatible for the compatible
+one (if present). If --packagexmlsetup is not specified, and packagexmlsetup.php
+exists in the same directory where the package.xml will go, it will be used.
+
+if --package is specified, it should be a comma-delimited list of package formats,
+such as --package=phar,tar,zip
+ </doc>
+ </command>
+ <command>
+ <name>pickle</name>
+ <class>pear2\Pyrus\Developer\PackageFile\Commands</class>
+ <function>makePECLPackage</function>
+ <autoloadpath></autoloadpath>
+ <summary>Create or update a package.xml and then package a PECL extension release</summary>
+ <shortcut>pi</shortcut>
+ <options>
+ <option>
+ <name>donotpackage</name>
+ <shortopt>n</shortopt>
+ <type><bool/></type>
+ <doc>Generate package.xml only, do not package a release</doc>
+ </option>
+ </options>
+ <arguments>
+ <argument>
+ <name>packagename</name>
+ <multiple>0</multiple>
+ <optional>1</optional>
+ <doc>Name of the package to create. If blank, the name is drawn from existng package.xml</doc>
+ </argument>
+ <argument>
+ <name>channel</name>
+ <multiple>0</multiple>
+ <optional>1</optional>
+ <doc>Name of the channel of the package to create, default is pecl.php.net or whatever is in package.xml.</doc>
+ </argument>
+ <argument>
+ <name>dir</name>
+ <multiple>0</multiple>
+ <optional>1</optional>
+ <doc>path to Package directory, default is the current directory.</doc>
+ </argument>
+ <argument>
+ <name>extension</name>
+ <multiple>1</multiple>
+ <optional>1</optional>
+ <doc>source file extension (default is c, cc, h, m4, w32, re, y, l, frag).</doc>
+ </argument>
+ </arguments>
+ <doc>Create or update a package.xml and then package a PECL extension release.
+
+This command looks for these standard files:
+
+CREDITS, README, RELEASE-X.Y.Z (where X.Y.Z is the release version),
+API-X.Y.Z (where X.Y.Z is the API version)
+
+and for a standard directory layout of
+
+/ Extension source files
+data/ Data files
+tests/ Test files
+doc/ Documentation files
+examples/ Example files (documentation)
+
+The CREDITS file must have this format:
+
+;; comments ignored
+Maintainer One [handle1] &lt;email@example.com&gt; (role)
+Maintainer Two [handle2] &lt;email@example.com&gt; (role)
+
+Where role is one of lead, developer, contributor, helper.
+
+The first line of README is used as the summary of the package.
+RELEASE-X.Y.Z is used as the release notes.
+ </doc>
+ </command>
+ <command>
+ <name>package</name>
+ <class>pear2\Pyrus\Developer\PackageFile\Commands</class>
+ <function>package</function>
+ <autoloadpath></autoloadpath>
+ <summary>Create a release from an existing package.xml</summary>
+ <shortcut>p</shortcut>
+ <options>
+ <option>
+ <name>phar</name>
+ <shortopt>p</shortopt>
+ <type><bool/></type>
+ <doc>Create a phar archive</doc>
+ </option>
+ <option>
+ <name>tgz</name>
+ <shortopt>g</shortopt>
+ <type><bool/></type>
+ <doc>Create a tgz archive (default choice)</doc>
+ </option>
+ <option>
+ <name>tar</name>
+ <shortopt>t</shortopt>
+ <type><bool/></type>
+ <doc>Create a tar archive (default choice if zlib ext not present)</doc>
+ </option>
+ <option>
+ <name>zip</name>
+ <shortopt>z</shortopt>
+ <type><bool/></type>
+ <doc>Create a zip archive</doc>
+ </option>
+ <option>
+ <name>stub</name>
+ <shortopt>s</shortopt>
+ <type><string/></type>
+ <doc>Path to stub for phar archive</doc>
+ </option>
+ <option>
+ <name>extrasetup</name>
+ <shortopt>e</shortopt>
+ <type><string/></type>
+ <doc>Path to script with extra needed setup</doc>
+ </option>
+ <option>
+ <name>outputfile</name>
+ <shortopt>o</shortopt>
+ <type><string/></type>
+ <doc>Path file to output the result to</doc>
+ </option>
+ </options>
+ <arguments>
+ <argument>
+ <name>packagexml</name>
+ <multiple>0</multiple>
+ <optional>1</optional>
+ <doc>path to the package.xml for this package.</doc>
+ </argument>
+ </arguments>
+ <doc>Create a release from an existing package.xml
+
+If the package.xml is not specified, here is the search order
+
+ - ./package.xml
+ - if package.xml is version 1.0, ./package2.xml
+
+Use PEAR to validate package.xml version 1.0, Pyrus will not validate it.
+
+Packages with a PEAR2 package.xml are packaged using Pyrus, packages with
+a PEAR package.xml (pearinstaller dep is not >= 2.0.0a1) are packaged straight
+into a tar archive.
+
+By default, an attempt is made to make a .phar release for a Pyrus package,
+falling back to a .tgz or .tar. PEAR packages are only ever packaged as .tgz
+or .tar.
+
+If the --stub option is not explicit and stub.php exists in the same directory as
+package.xml, it will be used as the stub for phar archives.
+
+If the --extrasetup option is not explicit and extrasetup.php exists in the same
+directory as package.xml, it will be used for extra setup for all archives.
+ </doc>
+ </command>
+ <command>
+ <name>run-phpt</name>
+ <class>pear2\Pyrus\Developer\PackageFile\Commands</class>
+ <function>runTests</function>
+ <autoloadpath></autoloadpath>
+ <summary>Run PHPT tests</summary>
+ <shortcut>rp</shortcut>
+ <options>
+ <option>
+ <name>coverage</name>
+ <shortopt>x</shortopt>
+ <type><bool/></type>
+ <doc>Generate xdebug code coverage</doc>
+ </option>
+ <option>
+ <name>recursive</name>
+ <shortopt>r</shortopt>
+ <type><bool/></type>
+ <doc>Scan test directories recursively for tests to run</doc>
+ </option>
+ <option>
+ <name>modified</name>
+ <shortopt>m</shortopt>
+ <type><bool/></type>
+ <doc>Use code coverage database to only run modified tests</doc>
+ </option>
+ </options>
+ <arguments>
+ <argument>
+ <name>path</name>
+ <multiple>1</multiple>
+ <optional>1</optional>
+ <doc>path to tests, or to the tests directory followed by source directory if --modified (default is ./tests ./src).</doc>
+ </argument>
+ </arguments>
+ <doc>Run PHPT tests
+
+This command can be used to execute tests for PECL extensions, or for packages
+written using phpt tests.
+
+The --modified option allows running only tests that have been modified, added,
+or are affected by modified source files.
+
+Coverage can be viewed in a web browser by opening pear2coverage.phar.php
+ </doc>
+ </command>
+</commands>
12 data/phartemplate.php
@@ -0,0 +1,12 @@
+<?php
+if (!class_exists('Phar')) {
+ if (!class_exists('PHP_Archive')) {
+@PHPARCHIVE@
+ }
+ if (!in_array('phar', stream_get_wrappers(), true)) {
+ stream_wrapper_register('phar', 'PHP_Archive');
+ }
+}
+define('PYRUS_PHAR_FILE', __FILE__);
+include 'phar://' . __FILE__ . '/__index.php';
+__HALT_COMPILER();
103 package.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package version="2.1" xmlns="http://pear.php.net/dtd/package-2.1" 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.1 http://pear.php.net/dtd/package-2.1.xsd">
+ <name>PEAR2_Pyrus_Developer</name>
+ <channel>pear2.php.net</channel>
+ <summary>Developer tools for the Pyrus installer
+</summary>
+ <description>
+This package contains several custom commands for Pyrus,
+including command to automatically generate a package.xml,
+and commands to create a package from an existing package.xml</description>
+ <lead>
+ <name>Gregory Beaver</name>
+ <user>cellog</user>
+ <email>cellog@php.net</email>
+ <active>yes</active>
+ </lead>
+ <date>2009-07-06</date>
+ <time>12:29:24</time>
+ <version>
+ <release>0.1.0</release>
+ <api>0.1.0</api>
+ </version>
+ <stability>
+ <release>alpha</release>
+ <api>alpha</api>
+ </stability>
+ <license uri="http://www.opensource.org/licenses/bsd-license.php">New BSD License</license>
+ <notes>Initial development release</notes>
+ <contents>
+ <dir name="/">
+ <dir name="customcommand" baseinstalldir="/">
+ <file role="customcommand" name="commands.xml"/>
+ </dir>
+ <dir name="data" baseinstalldir="/">
+ <file role="data" name="phartemplate.php"/>
+ </dir>
+ <dir name="src" baseinstalldir="pear2">
+ <dir name="Pyrus">
+ <dir name="Developer">
+ <dir name="CoverageAnalyzer">
+ <dir name="SourceFile">
+ <file role="php" name="PerTest.php"/>
+ </dir>
+ <dir name="Web">
+ <file role="php" name="Aggregator.php"/>
+ <file role="php" name="Controller.php"/>
+ <file role="php" name="Exception.php"/>
+ <file role="php" name="View.php"/>
+ </dir>
+ <file role="php" name="AbstractSourceDecorator.php"/>
+ <file role="php" name="Aggregator.php"/>
+ <file role="php" name="DefaultSourceDecorator.php"/>
+ <file role="php" name="Exception.php"/>
+ <file role="php" name="make-coverage-phar.php"/>
+ <file role="php" name="pear2coverage.phar.php"/>
+ <file role="php" name="SourceFile.php"/>
+ <file role="php" name="Sqlite.php"/>
+ <file role="php" name="test-modified.php"/>
+ <file role="php" name="test.php"/>
+ </dir>
+ <dir name="Creator">
+ <dir name="Phar">
+ <file role="php" name="PHPArchive.php"/>
+ </dir>
+ <file role="php" name="Exception.php"/>
+ <file role="php" name="Phar.php"/>
+ <file role="php" name="Tar.php"/>
+ <file role="php" name="Xml.php"/>
+ <file role="php" name="Zip.php"/>
+ </dir>
+ <dir name="PackageFile">
+ <dir name="PEAR2SVN">
+ <file role="php" name="Filter.php"/>
+ </dir>
+ <dir name="PECL">
+ <file role="php" name="Filter.php"/>
+ </dir>
+ <file role="php" name="Commands.php"/>
+ <file role="php" name="PEAR2SVN.php"/>
+ <file role="php" name="PECL.php"/>
+ <file role="php" name="v2.php"/>
+ </dir>
+ <dir name="Runphpt">
+ <file role="php" name="Exception.php"/>
+ <file role="php" name="Runner.php"/>
+ </dir>
+ </dir>
+ </dir>
+ </dir>
+ </dir>
+ </contents>
+ <dependencies>
+ <required>
+ <php>
+ <min>5.2.0</min>
+ </php>
+ <pearinstaller>
+ <min>2.0.0a1</min>
+ </pearinstaller>
+ </required>
+ </dependencies>
+ <phprelease/>
+</package>
109 package_compatible.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package version="2.1" xmlns="http://pear.php.net/dtd/package-2.1" 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.1 http://pear.php.net/dtd/package-2.1.xsd">
+ <name>PEAR2_Pyrus_Developer</name>
+ <channel>pear2.php.net</channel>
+ <summary>Developer tools for the Pyrus installer
+</summary>
+ <description>
+This package contains several custom commands for Pyrus,
+including command to automatically generate a package.xml,
+and commands to create a package from an existing package.xml</description>
+ <lead>
+ <name>Gregory Beaver</name>
+ <user>cellog</user>
+ <email>cellog@php.net</email>
+ <active>yes</active>
+ </lead>
+ <date>2009-07-06</date>
+ <time>12:29:24</time>
+ <version>
+ <release>0.1.0</release>
+ <api>0.1.0</api>
+ </version>
+ <stability>
+ <release>alpha</release>
+ <api>alpha</api>
+ </stability>
+ <license uri="http://www.opensource.org/licenses/bsd-license.php">New BSD License</license>
+ <notes>Initial development release</notes>
+ <contents>
+ <dir name="/">
+ <file role="php" name="php/pear2/Pyrus/Developer/Runphpt/Runner.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/Runphpt/Exception.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/PackageFile/v2.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/PackageFile/PECL/Filter.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/PackageFile/PECL.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/PackageFile/PEAR2SVN/Filter.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/PackageFile/PEAR2SVN.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/PackageFile/Commands.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/Creator/Zip.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/Creator/Xml.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/Creator/Tar.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/Creator/Phar/PHPArchive.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/Creator/Phar.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/Creator/Exception.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Web/View.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Web/Exception.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Web/Controller.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Web/Aggregator.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/test.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/test-modified.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Sqlite.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/SourceFile/PerTest.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/SourceFile.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/pear2coverage.phar.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/make-coverage-phar.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Exception.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/DefaultSourceDecorator.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Aggregator.php"/>
+ <file role="php" name="php/pear2/Pyrus/Developer/CoverageAnalyzer/AbstractSourceDecorator.php"/>
+ <file role="data" name="data/PEAR2_Pyrus_Developer/pear2.php.net/phartemplate.php"/>
+ <file role="data" name="customcommand/PEAR2_Pyrus_Developer/pear2.php.net/commands.xml"/>
+ </dir>
+ </contents>
+ <dependencies>
+ <required>
+ <php>
+ <min>5.2.0</min>
+ </php>
+ <pearinstaller>
+ <min>1.4.8</min>
+ </pearinstaller>
+ </required>
+ </dependencies>
+ <phprelease>
+ <filelist>
+ <install name="customcommand/PEAR2_Pyrus_Developer/pear2.php.net/commands.xml" as="pear2.php.net/commands.xml"/>
+ <install name="data/PEAR2_Pyrus_Developer/pear2.php.net/phartemplate.php" as="pear2.php.net/phartemplate.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/AbstractSourceDecorator.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/AbstractSourceDecorator.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Aggregator.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/Aggregator.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/DefaultSourceDecorator.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/DefaultSourceDecorator.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Exception.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/Exception.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/make-coverage-phar.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/make-coverage-phar.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/pear2coverage.phar.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/pear2coverage.phar.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/SourceFile.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/SourceFile.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/SourceFile/PerTest.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/SourceFile/PerTest.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Sqlite.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/Sqlite.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/test-modified.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/test-modified.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/test.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/test.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Web/Aggregator.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/Web/Aggregator.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Web/Controller.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/Web/Controller.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Web/Exception.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/Web/Exception.php"/>
+ <install name="php/pear2/Pyrus/Developer/CoverageAnalyzer/Web/View.php" as="pear2/Pyrus/Developer/CoverageAnalyzer/Web/View.php"/>
+ <install name="php/pear2/Pyrus/Developer/Creator/Exception.php" as="pear2/Pyrus/Developer/Creator/Exception.php"/>
+ <install name="php/pear2/Pyrus/Developer/Creator/Phar.php" as="pear2/Pyrus/Developer/Creator/Phar.php"/>
+ <install name="php/pear2/Pyrus/Developer/Creator/Phar/PHPArchive.php" as="pear2/Pyrus/Developer/Creator/Phar/PHPArchive.php"/>
+ <install name="php/pear2/Pyrus/Developer/Creator/Tar.php" as="pear2/Pyrus/Developer/Creator/Tar.php"/>
+ <install name="php/pear2/Pyrus/Developer/Creator/Xml.php" as="pear2/Pyrus/Developer/Creator/Xml.php"/>
+ <install name="php/pear2/Pyrus/Developer/Creator/Zip.php" as="pear2/Pyrus/Developer/Creator/Zip.php"/>
+ <install name="php/pear2/Pyrus/Developer/PackageFile/Commands.php" as="pear2/Pyrus/Developer/PackageFile/Commands.php"/>
+ <install name="php/pear2/Pyrus/Developer/PackageFile/PEAR2SVN.php" as="pear2/Pyrus/Developer/PackageFile/PEAR2SVN.php"/>
+ <install name="php/pear2/Pyrus/Developer/PackageFile/PEAR2SVN/Filter.php" as="pear2/Pyrus/Developer/PackageFile/PEAR2SVN/Filter.php"/>
+ <install name="php/pear2/Pyrus/Developer/PackageFile/PECL.php" as="pear2/Pyrus/Developer/PackageFile/PECL.php"/>
+ <install name="php/pear2/Pyrus/Developer/PackageFile/PECL/Filter.php" as="pear2/Pyrus/Developer/PackageFile/PECL/Filter.php"/>
+ <install name="php/pear2/Pyrus/Developer/PackageFile/v2.php" as="pear2/Pyrus/Developer/PackageFile/v2.php"/>
+ <install name="php/pear2/Pyrus/Developer/Runphpt/Exception.php" as="pear2/Pyrus/Developer/Runphpt/Exception.php"/>
+ <install name="php/pear2/Pyrus/Developer/Runphpt/Runner.php" as="pear2/Pyrus/Developer/Runphpt/Runner.php"/>
+ </filelist>
+ </phprelease>
+</package>
10 src/Pyrus/Developer/CoverageAnalyzer/AbstractSourceDecorator.php
@@ -0,0 +1,10 @@
+<?php
+namespace pear2\Pyrus\Developer\CoverageAnalyzer {
+abstract class AbstractSourceDecorator
+{
+ abstract function render(SourceFile $source);
+ abstract function renderSummary(Aggregator $agg, array $results, $basePath, $istest = false, $total = 1, $covered = 1);
+ abstract function renderTestCoverage(Aggregator $agg, $testpath, $basePath);
+}
+}
+?>
175 src/Pyrus/Developer/CoverageAnalyzer/Aggregator.php
@@ -0,0 +1,175 @@
+<?php
+namespace pear2\Pyrus\Developer\CoverageAnalyzer {
+class Aggregator
+{
+ protected $codepath;
+ protected $testpath;
+ protected $sqlite;
+ public $totallines = 0;
+ public $totalcoveredlines = 0;
+
+ /**
+ * @var string $testpath Location of .phpt files
+ * @var string $codepath Location of code whose coverage we are testing
+ */
+ function __construct($testpath, $codepath, $db = ':memory:')
+ {
+ $newcodepath = realpath($codepath);
+ if (!$newcodepath) {
+ if (!strpos($codepath, '://') || !file_exists($codepath)) {
+ // stream wrapper not found
+ throw new Exception('Can not find code path ' . $codepath);
+ }
+ } else {
+ $codepath = $newcodepath;
+ }
+ $this->sqlite = new Sqlite($db, $codepath, $testpath);
+ $this->codepath = $codepath;
+ $this->sqlite->begin();
+ echo "Scanning for xdebug coverage files...";
+ $files = $this->scan($testpath);
+ echo "done\n";
+ $infostring = '';
+ echo "Parsing xdebug results\n";
+ if (count($files)) {
+ foreach ($files as $testid => $xdebugfile) {
+ if (!file_exists(str_replace('.xdebug', '.phpt', $xdebugfile))) {
+ echo "\nWARNING: outdated .xdebug file $xdebugfile, delete this relic\n";
+ continue;
+ }
+ $id = $this->sqlite->addTest(str_replace('.xdebug', '.phpt', $xdebugfile));
+ echo '(' . $testid . ' of ' . count($files) . ') ' . $xdebugfile;
+ $this->retrieveXdebug($xdebugfile, $id);
+ echo "done\n";
+ }
+ echo "done\n";
+ $this->sqlite->updateTotalCoverage();
+ $this->sqlite->commit();
+ } else {
+ echo "done (no modified xdebug files)\n";
+ }
+ }
+
+ function retrieveLineLinks($file)
+ {
+ return $this->sqlite->retrieveLineLinks($file);
+ }
+
+ function retrievePaths()
+ {
+ return $this->sqlite->retrievePaths();
+ }
+
+ function retrievePathsForTest($test)
+ {
+ return $this->sqlite->retrievePathsForTest($test);
+ }
+
+ function retrieveTestPaths()
+ {
+ return $this->sqlite->retrieveTestPaths();
+ }
+
+ function coveragePercentage($sourcefile, $testfile = null)
+ {
+ return $this->sqlite->coveragePercentage($sourcefile, $testfile);
+ }
+
+ function coverageInfo($path)
+ {
+ return $this->sqlite->retrievePathCoverage($path);
+ }
+
+ function coverageInfoByTest($path, $test)
+ {
+ return $this->sqlite->retrievePathCoverageByTest($path, $test);
+ }
+
+ function retrieveCoverage($path)
+ {
+ return $this->sqlite->retrieveCoverage($path);
+ }
+
+ function retrieveCoverageByTest($path, $test)
+ {
+ return $this->sqlite->retrieveCoverageByTest($path, $test);
+ }
+
+ function retrieveProjectCoverage()
+ {
+ return $this->sqlite->retrieveProjectCoverage();
+ }
+
+ function retrieveXdebug($path, $testid)
+ {
+ $source = '$xdebug = ' . file_get_contents($path) . ";\n";
+ eval($source);
+ $this->sqlite->addCoverage(str_replace('.xdebug', '.phpt', $path), $testid, $xdebug);
+ }
+
+ function scan($testpath)
+ {
+ $a = $testpath;
+ $testpath = realpath($testpath);
+ if (!$testpath) {
+ throw new Exception('Unable to process path' . $a);
+ }
+ $testpath = str_replace('\\', '/', $testpath);
+ $this->testpath = $testpath;
+
+ // get a list of all xdebug files
+ foreach (new \RegexIterator(
+ new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($testpath,
+ 0|\RecursiveDirectoryIterator::SKIP_DOTS)),
+ '/\.xdebug$/') as $file) {
+ if (strpos((string) $file, '.svn')) {
+ continue;
+ }
+ $xdebugs[] = realpath((string) $file);
+ }
+ echo count($xdebugs), ' total...';
+
+ $modified = array();
+ $unmodified = array();
+ foreach ($xdebugs as $path) {
+ if ($this->sqlite->unChangedXdebug($path)) {
+ $unmodified[$path] = true;
+ continue;
+ }
+ $modified[] = $path;
+ }
+ $xdebugs = $modified;
+ sort($xdebugs);
+ // index from 1
+ array_unshift($xdebugs, '');
+ unset($xdebugs[0]);
+ $test = array_flip($xdebugs);
+ foreach ($this->sqlite->retrieveTestPaths() as $path) {
+ $xdebugpath = str_replace('.phpt', '.xdebug', $path);
+ if (isset($test[$xdebugpath]) || isset($unmodified[$xdebugpath])) {
+ continue;
+ }
+ // remove outdated tests
+ echo "Removing results from $xdebugpath\n";
+ $this->sqlite->removeOldTest($path);
+ }
+ return $xdebugs;
+ }
+
+ function render($toPath)
+ {
+ $decorator = new DefaultSourceDecorator($toPath, $this->testpath, $this->codepath);
+ echo "Generating project coverage data...";
+ $coverage = $this->sqlite->retrieveProjectCoverage();
+ echo "done\n";
+ $decorator->renderSummary($this, $this->retrievePaths(), $this->codepath, false, $coverage[1],
+ $coverage[0]);
+ $a = $this->codepath;
+ echo "[Step 2 of 2] Rendering per-test coverage...";
+ $decorator->renderTestCoverage($this, $this->testpath, $a);
+ echo "done\n";
+ }
+}
+}
+?>
381 src/Pyrus/Developer/CoverageAnalyzer/DefaultSourceDecorator.php
@@ -0,0 +1,381 @@
+<?php
+namespace pear2\Pyrus\Developer\CoverageAnalyzer {
+/**
+ * Takes a source file and outputs HTML source highlighting showing the
+ * number of hits on each line, highlights un-executed lines in red
+ */
+class DefaultSourceDecorator extends AbstractSourceDecorator
+{
+ protected $savePath;
+ protected $testPath;
+ protected $sourcePath;
+ protected $source;
+
+ function __construct($savePath, $testPath, $sourcePath)
+ {
+ if (!$savePath || !file_exists($savePath) || !is_dir($savePath)) {
+ throw new Exception('Invalid save path for default renderer');
+ }
+ $this->testPath = $testPath;
+ $this->sourcePath = $sourcePath;
+ $this->savePath = realpath($savePath);
+ file_put_contents($this->savePath . '/cover.css', '.ln {background-color:yellow;}
+.cv {background-color:#CAD7FE;}
+.nc {background-color:red;}
+.bad {background-color:red;white-space:pre;font-family:courier;}
+.ok {background-color:yellow;white-space:pre;font-family:courier;}
+.good {background-color:green;white-space:pre;font-family:courier;}');
+ }
+
+ function manglePath($path, $istest = false)
+ {
+ return $this->savePath . '/' . $this->mangleFile($path, $istest);
+ }
+
+ function mangleFile($path, $istest = false)
+ {
+ $path = substr($path, strlen($this->sourcePath) + 1);
+ if ($istest) {
+ $istest = str_replace($this->testPath . DIRECTORY_SEPARATOR, '', $istest);
+ return 'cov-test-' . str_replace(array('/', '\\'), array('@','@'), $istest) . '-' .
+ str_replace(array('/', '\\'), array('@','@'), $path) . '.html';
+ }
+ return 'cov-' . str_replace(array('/', '\\'), array('@','@'), $path) . '.html';
+ }
+
+ function getLinePath($name, $line)
+ {
+ return $this->savePath . '/' . $this->getLineLink($name, $line);
+ }
+
+ function mangleTestFile($path)
+ {
+ $path = substr($path, strlen($this->testPath));
+ return 'test-' . str_replace(array('/', '\\'), array('@','@'), $path) . '.html';
+ }
+
+ function mangleTestPath($path)
+ {
+ return $this->savePath . '/' . $this->mangleTestFile($path);
+ }
+
+ function getLineLink($name, $line)
+ {
+ return 'line-' . $line . '-' . $this->mangleFile($name);
+ }
+
+ function renderLineSummary($name, $line, $testpath, $tests)
+ {
+ $output = new \XMLWriter;
+ if (!$output->openUri($this->getLinePath($name, $line))) {
+ throw new Exception('Cannot render ' . $name . ' line ' . $line . ', opening XML failed');
+ }
+ $output->setIndentString(' ');
+ $output->setIndent(true);
+ $output->startElement('html');
+ $output->startElement('head');
+ $output->writeElement('title', 'Tests covering line ' . $line . ' of ' . $name);
+ $output->startElement('link');
+ $output->writeAttribute('href', 'cover.css');
+ $output->writeAttribute('rel', 'stylesheet');
+ $output->writeAttribute('type', 'text/css');
+ $output->endElement();
+ $output->endElement();
+ $output->startElement('body');
+ $output->writeElement('h2', 'Tests covering line ' . $line . ' of ' . $name);
+ $output->startElement('p');
+ $output->startElement('a');
+ $output->writeAttribute('href', 'index.html');
+ $output->text('Aggregate Code Coverage for all tests');
+ $output->endElement();
+ $output->endElement();
+ $output->startElement('p');
+ $output->startElement('a');
+ $output->writeAttribute('href', $this->mangleFile($name));
+ $output->text('File ' . $name . ' code coverage');
+ $output->endElement();
+ $output->endElement();
+ $output->startElement('ul');
+ foreach ($tests as $testfile) {
+ $output->startElement('li');
+ $output->startElement('a');
+ $output->writeAttribute('href', $this->mangleTestFile($testfile));
+ $output->text(str_replace($testpath . '/', '', $testfile));
+ $output->endElement();
+ $output->endElement();
+ }
+ $output->endElement();
+ $output->endElement();
+ $output->endDocument();
+ }
+
+ /**
+ * @param pear2\Pyrus\Developer\CodeCoverage\SourceFile $source
+ * @param string $istest path to test file this is covering, or false for aggregate
+ */
+ function render(SourceFile $source, $istest = false)
+ {
+ $output = new \XMLWriter;
+ if (!$output->openUri($this->manglePath($source->name(), $istest))) {
+ throw new Exception('Cannot render ' . $source->name() . ', opening XML failed');
+ }
+ $output->setIndent(false);
+ $output->startElement('html');
+ $output->text("\n ");
+ $output->startElement('head');
+ $output->text("\n ");
+ if ($istest) {
+ $output->writeElement('title', 'Code Coverage for ' . $source->shortName() . ' in ' . $istest);
+ } else {
+ $output->writeElement('title', 'Code Coverage for ' . $source->shortName());
+ }
+ $output->text("\n ");
+ $output->startElement('link');
+ $output->writeAttribute('href', 'cover.css');
+ $output->writeAttribute('rel', 'stylesheet');
+ $output->writeAttribute('type', 'text/css');
+ $output->endElement();
+ $output->text("\n ");
+ $output->endElement();
+ $output->text("\n ");
+ $output->startElement('body');
+ $output->text("\n ");
+ if ($istest) {
+ $output->writeElement('h2', 'Code Coverage for ' . $source->shortName() . ' in ' . $istest);
+ } else {
+ $output->writeElement('h2', 'Code Coverage for ' . $source->shortName());
+ }
+ $output->text("\n ");
+ $output->writeElement('h3', 'Coverage: ' . $source->coveragePercentage() . '%');
+ $output->text("\n ");
+ $output->startElement('p');
+ $output->startElement('a');
+ $output->writeAttribute('href', 'index.html');
+ $output->text('Aggregate Code Coverage for all tests');
+ $output->endElement();
+ $output->endElement();
+ $output->startElement('pre');
+ foreach ($source->source() as $num => $line) {
+ $coverage = $source->coverage($num);
+
+ $output->startElement('span');
+ $output->writeAttribute('class', 'ln');
+ $output->text(str_pad($num, 8, ' ', STR_PAD_LEFT));
+ $output->endElement();
+
+ if ($coverage === false) {
+ $output->text(str_pad(': ', 13, ' ', STR_PAD_LEFT) . $line);
+ continue;
+ }
+
+ $output->startElement('span');
+ if ($coverage < 1) {
+ $output->writeAttribute('class', 'nc');
+ $output->text(' ');
+ } else {
+ $output->writeAttribute('class', 'cv');
+ if (!$istest) {
+ $output->startElement('a');
+ $output->writeAttribute('href', $this->getLineLink($source->name(), $num));
+ }
+ $output->text(str_pad($coverage, 10, ' ', STR_PAD_LEFT) . ' ');
+ if (!$istest) {
+ $output->endElement();
+ $this->renderLineSummary($source->name(), $num, $source->testpath(),
+ $source->getLineLinks($num));
+ }
+ }
+
+ $output->text(': ' . $line);
+ $output->endElement();
+ }
+ $output->endElement();
+ $output->text("\n ");
+ $output->endElement();
+ $output->text("\n ");
+ $output->endElement();
+ $output->endDocument();
+ }
+
+ function renderSummary(Aggregator $agg, array $results, $basePath, $istest = false, $total = 1, $covered = 1)
+ {
+ $output = new \XMLWriter;
+ if ($istest) {
+ if (!$output->openUri($this->savePath . '/index-' . str_replace($istest, '/', '@') . '.html')) {
+ throw new Exception('Cannot render test ' . $istest . ' summary, opening XML failed');
+ }
+ } else {
+ if (!$output->openUri($this->savePath . '/index.html')) {
+ throw new Exception('Cannot render test summary, opening XML failed');
+ }
+ }
+ $output->setIndentString(' ');
+ $output->setIndent(true);
+ $output->startElement('html');
+ $output->startElement('head');
+ if ($istest) {
+ $output->writeElement('title', 'Code Coverage Summary [' . $istest . ']');
+ } else {
+ $output->writeElement('title', 'Code Coverage Summary');
+ }
+ $output->startElement('link');
+ $output->writeAttribute('href', 'cover.css');
+ $output->writeAttribute('rel', 'stylesheet');
+ $output->writeAttribute('type', 'text/css');
+ $output->endElement();
+ $output->endElement();
+ $output->startElement('body');
+ if ($istest) {
+ $output->writeElement('h2', 'Code Coverage Files for test ' . $istest);
+ } else {
+ $output->writeElement('h2', 'Code Coverage Files for ' . $basePath);
+ $output->writeElement('h3', 'Total lines: ' . $total . ', covered lines: ' . $covered);
+ $percent = 0;
+ if ($total > 0) {
+ $percent = round(($covered / $total) * 100);
+ }
+ $output->startElement('p');
+ if ($percent < 50) {
+ $output->writeAttribute('class', 'bad');
+ } elseif ($percent < 75) {
+ $output->writeAttribute('class', 'ok');
+ } else {
+ $output->writeAttribute('class', 'good');
+ }
+ $output->text($percent . '% code coverage');
+ $output->endElement();
+ }
+ $output->startElement('p');
+ $output->startElement('a');
+ $output->writeAttribute('href', 'index-test.html');
+ $output->text('Code Coverage per PHPT test');
+ $output->endElement();
+ $output->endElement();
+ $output->startElement('ul');
+ echo "[Step 1 of 2] Rendering files\n";
+ foreach ($results as $i => $name) {
+ echo '(' . ($i+1) . ' of ' . count($results) . ') ' . $name . "\n";
+ $source = new SourceFile($name, $agg, $this->testPath, $this->sourcePath);
+ $output->startElement('li');
+ $percent = $source->coveragePercentage();
+ $output->startElement('span');
+ if ($percent < 50) {
+ $output->writeAttribute('class', 'bad');
+ } elseif ($percent < 75) {
+ $output->writeAttribute('class', 'ok');
+ } else {
+ $output->writeAttribute('class', 'good');
+ }
+ $output->text(' Coverage: ' . str_pad($percent . '%', 4, ' ', STR_PAD_LEFT));
+ $output->endElement();
+ $output->startElement('a');
+ $output->writeAttribute('href', $this->mangleFile($name, $istest));
+ $output->text($source->shortName());
+ $output->endElement();
+ $output->endElement();
+ $source->render($this);
+ }
+ echo "done\n";
+ $output->endElement();
+ $output->endElement();
+ $output->endDocument();
+ }
+
+ function renderTestSummary(Aggregator $agg, $testpath)
+ {
+ $output = new \XMLWriter;
+ if (!$output->openUri($this->savePath . '/index-test.html')) {
+ throw new Exception('Cannot render tests summary, opening XML failed');
+ }
+ $output->setIndentString(' ');
+ $output->setIndent(true);
+ $output->startElement('html');
+ $output->startElement('head');
+ $output->writeElement('title', 'Test Summary');
+ $output->startElement('link');
+ $output->writeAttribute('href', 'cover.css');
+ $output->writeAttribute('rel', 'stylesheet');
+ $output->writeAttribute('type', 'text/css');
+ $output->endElement();
+ $output->endElement();
+ $output->startElement('body');
+ $output->writeElement('h2', 'Tests Executed, click for code coverage summary');
+ $output->startElement('p');
+ $output->startElement('a');
+ $output->writeAttribute('href', 'index.html');
+ $output->text('Aggregate Code Coverage for all tests');
+ $output->endElement();
+ $output->endElement();
+ $output->startElement('ul');
+ foreach ($agg->retrieveTestPaths() as $test) {
+ $output->startElement('li');
+ $output->startElement('a');
+ $output->writeAttribute('href', $this->mangleTestFile($test));
+ $output->text(str_replace($testpath . '/', '', $test));
+ $output->endElement();
+ $output->endElement();
+ }
+ $output->endElement();
+ $output->endElement();
+ $output->endDocument();
+ }
+
+ function renderTestCoverage(Aggregator $agg, $testpath, $basePath)
+ {
+ $this->renderTestSummary($agg, $testpath);
+ $testpaths = $agg->retrieveTestPaths();
+ echo "Rendering test files\n";
+ foreach ($testpaths as $i => $test) {
+ echo '(', $i+1, ' of ', count($testpaths) . ') ', $test;
+ $reltest = str_replace($testpath . '/', '', $test);
+ $output = new \XMLWriter;
+ if (!$output->openUri($this->mangleTestPath($test))) {
+ throw new Exception('Cannot render test ' . $reltest . ' coverage, opening XML failed');
+ }
+ $output->setIndentString(' ');
+ $output->setIndent(true);
+ $output->startElement('html');
+ $output->startElement('head');
+ $output->writeElement('title', 'Code Coverage Summary for test ' . $reltest);
+ $output->startElement('link');
+ $output->writeAttribute('href', 'cover.css');
+ $output->writeAttribute('rel', 'stylesheet');
+ $output->writeAttribute('type', 'text/css');
+ $output->endElement();
+ $output->endElement();
+ $output->startElement('body');
+ $output->writeElement('h2', 'Code Coverage Files for test ' . $reltest);
+ $output->startElement('ul');
+ $paths = $agg->retrievePathsForTest($test);
+ foreach ($paths as $name) {
+ echo '.';
+ $source = new SourceFile\PerTest($name, $agg, $testpath, $basePath, $test);
+ $this->render($source, $reltest);
+ $output->startElement('li');
+ $percent = $source->coveragePercentage();
+ $output->startElement('span');
+ if ($percent < 50) {
+ $output->writeAttribute('class', 'bad');
+ } elseif ($percent < 75) {
+ $output->writeAttribute('class', 'ok');
+ } else {
+ $output->writeAttribute('class', 'good');
+ }
+ $output->text(' Coverage: ' . str_pad($source->coveragePercentage() . '%', 4, ' ', STR_PAD_LEFT));
+ $output->endElement();
+ $output->startElement('a');
+ $output->writeAttribute('href', $this->mangleFile($name, $reltest));
+ $output->text($source->shortName());
+ $output->endElement();
+ $output->endElement();
+ }
+ echo "done\n";
+ $output->endElement();
+ $output->endElement();
+ $output->endDocument();
+ }
+ echo "done\n";
+ }
+}
+}
+?>
5 src/Pyrus/Developer/CoverageAnalyzer/Exception.php
@@ -0,0 +1,5 @@
+<?php
+namespace pear2\Pyrus\Developer\CoverageAnalyzer {
+class Exception extends \Exception {}
+}
+?>
101 src/Pyrus/Developer/CoverageAnalyzer/SourceFile.php
@@ -0,0 +1,101 @@
+<?php
+namespace pear2\Pyrus\Developer\CoverageAnalyzer {
+class SourceFile
+{
+ protected $source;
+ protected $path;
+ protected $sourcepath;
+ protected $coverage;
+ protected $aggregator;
+ protected $testpath;
+ protected $linelinks;
+
+ function __construct($path, Aggregator $agg, $testpath, $sourcepath)
+ {
+ $this->source = file($path);
+ $this->path = $path;
+ $this->sourcepath = $sourcepath;
+
+ array_unshift($this->source, '');
+ unset($this->source[0]); // make source array indexed by line number
+
+ $this->aggregator = $agg;
+ $this->testpath = $testpath;
+ $this->setCoverage();
+ }
+
+ function setCoverage()
+ {
+ $this->coverage = $this->aggregator->retrieveCoverage($this->path);
+ }
+
+ function aggregator()
+ {
+ return $this->aggregator;
+ }
+
+ function testpath()
+ {
+ return $this->testpath;
+ }
+
+ function render(AbstractSourceDecorator $decorator = null)
+ {
+ if ($decorator === null) {
+ $decorator = new DefaultSourceDecorator('.');
+ }
+ return $decorator->render($this);
+ }
+
+ function coverage($line)
+ {
+ if (!isset($this->coverage[$line])) {
+ return false;
+ }
+ return $this->coverage[$line];
+ }
+
+ function coveragePercentage()
+ {
+ return $this->aggregator->coveragePercentage($this->path);
+ }
+
+ function coverageInfo()
+ {
+ return $this->aggregator->coverageInfo($this->path);
+ }
+
+ function name()
+ {
+ return $this->path;
+ }
+
+ function shortName()
+ {
+ return str_replace($this->sourcepath . DIRECTORY_SEPARATOR, '', $this->path);
+ }
+
+ function source()
+ {
+ return $this->source;
+ }
+
+ function coveredLines()
+ {
+ $info = $this->aggregator->coverageInfo($this->path);
+ return $info[0];
+ }
+
+ function getLineLinks($line)
+ {
+ if (!isset($this->linelinks)) {
+ $this->linelinks = $this->aggregator->retrieveLineLinks($this->path);
+ }
+ if (isset($this->linelinks[$line])) {
+ return $this->linelinks[$line];
+ }
+ return false;
+ }
+}
+}
+?>
45 src/Pyrus/Developer/CoverageAnalyzer/SourceFile/PerTest.php
@@ -0,0 +1,45 @@
+<?php
+namespace pear2\Pyrus\Developer\CoverageAnalyzer\SourceFile {
+use pear2\Pyrus\Developer\CoverageAnalyzer\Aggregator,
+ pear2\Pyrus\Developer\CoverageAnalyzer\AbstractSourceDecorator;
+class PerTest extends \pear2\Pyrus\Developer\CoverageAnalyzer\SourceFile
+{
+ protected $testname;
+
+ function __construct($path, Aggregator $agg, $testpath, $sourcepath, $testname)
+ {
+ $this->testname = $testname;
+ parent::__construct($path, $agg, $testpath, $sourcepath);
+ }
+
+ function setCoverage()
+ {
+ $this->coverage = $this->aggregator->retrieveCoverageByTest($this->path, $this->testname);
+ }
+
+ function coveredLines()
+ {
+ $info = $this->aggregator->coverageInfoByTest($this->path, $this->testname);
+ return $info[0];
+ }
+
+ function render(AbstractSourceDecorator $decorator = null)
+ {
+ if ($decorator === null) {
+ $decorator = new DefaultSourceDecorator('.');
+ }
+ return $decorator->render($this, $this->testname);
+ }
+
+ function coveragePercentage()
+ {
+ return $this->aggregator->coveragePercentage($this->path, $this->testname);
+ }
+
+ function coverageInfo()
+ {
+ return $this->aggregator->coverageInfoByTest($this->path, $this->testname);
+ }
+}
+}
+?>
836 src/Pyrus/Developer/CoverageAnalyzer/Sqlite.php
@@ -0,0 +1,836 @@
+<?php
+namespace pear2\Pyrus\Developer\CoverageAnalyzer {
+class Sqlite
+{
+ protected $db;
+ protected $totallines = 0;
+ protected $coveredlines = 0;
+ protected $pathCovered = array();
+ protected $pathTotal = array();
+ public $codepath;
+ public $testpath;
+
+ function __construct($path = ':memory:', $codepath = null, $testpath = null)
+ {
+ $this->db = new \Sqlite3($path);
+
+ $sql = 'SELECT version FROM analyzerversion';
+ if (@$this->db->querySingle($sql) == '5.0.0') {
+ $this->codepath = $this->db->querySingle('SELECT codepath FROM paths');
+ $this->testpath = $this->db->querySingle('SELECT testpath FROM paths');
+ return;
+ }
+ // restart the database
+ echo "Upgrading database to version 5.0.0";
+ if (!$codepath || !$testpath) {
+ throw new Exception('Both codepath and testpath must be set in ' .
+ 'order to initialize a coverage database');
+ }
+ $this->codepath = $codepath;
+ $this->testpath = $testpath;
+ $this->db->exec('DROP TABLE IF EXISTS coverage;');
+ echo ".";
+ $this->db->exec('DROP TABLE IF EXISTS coverage_nonsource;');
+ echo ".";
+ $this->db->exec('DROP TABLE IF EXISTS not_covered;');
+ echo ".";
+ $this->db->exec('DROP TABLE IF EXISTS files;');
+ echo ".";
+ $this->db->exec('DROP TABLE IF EXISTS tests;');
+ echo ".";
+ $this->db->exec('DROP TABLE IF EXISTS paths;');
+ echo ".";
+ $this->db->exec('DROP TABLE IF EXISTS coverage_per_file;');
+ echo ".";
+ $this->db->exec('DROP TABLE IF EXISTS line_info;');
+ echo ".";
+ $this->db->exec('DROP TABLE IF EXISTS all_lines;');
+ echo ".";
+ $this->db->exec('DROP TABLE IF EXISTS xdebugs;');
+ echo ".";
+ $this->db->exec('DROP TABLE IF EXISTS analyzerversion;');
+ echo ".";
+ $this->db->exec('VACUUM;');
+
+ echo ".";
+ $this->db->exec('BEGIN');
+
+ $query = '
+ CREATE TABLE coverage (
+ files_id integer NOT NULL,
+ tests_id integer NOT NULL,
+ linenumber INTEGER NOT NULL,
+ PRIMARY KEY (files_id, linenumber, tests_id)
+ );
+
+ CREATE TABLE all_lines (
+ files_id integer NOT NULL,
+ linenumber INTEGER NOT NULL,
+ PRIMARY KEY (files_id, linenumber)
+ );
+
+ CREATE TABLE line_info (
+ files_id integer NOT NULL,
+ covered INTEGER NOT NULL,
+ total INTEGER NOT NULL,
+ PRIMARY KEY (files_id)
+ );
+ ';
+ $worked = $this->db->exec($query);
+ if (!$worked) {
+ @$this->db->exec('ROLLBACK');
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Unable to create Code Coverage SQLite3 database: ' . $error);
+ }
+
+ echo ".";
+ $query = '
+ CREATE TABLE coverage_nonsource (
+ files_id integer NOT NULL,
+ tests_id integer NOT NULL,
+ PRIMARY KEY (files_id, tests_id)
+ );
+ ';
+ $worked = $this->db->exec($query);
+ if (!$worked) {
+ @$this->db->exec('ROLLBACK');
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Unable to create Code Coverage SQLite3 database: ' . $error);
+ }
+
+ echo ".";
+ $query = '
+ CREATE TABLE files (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ filepath TEXT(500) NOT NULL,
+ filepathmd5 TEXT(32) NOT NULL,
+ issource BOOL NOT NULL,
+ UNIQUE (filepath)
+ );
+ CREATE INDEX files_issource on files (issource);
+ ';
+ $worked = $this->db->exec($query);
+ if (!$worked) {
+ @$this->db->exec('ROLLBACK');
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Unable to create Code Coverage SQLite3 database: ' . $error);
+ }
+
+ echo ".";
+ $query = '
+ CREATE TABLE xdebugs (
+ xdebugpath TEXT(500) NOT NULL,
+ xdebugpathmd5 TEXT(32) NOT NULL,
+ PRIMARY KEY (xdebugpath)
+ );';
+ $worked = $this->db->exec($query);
+ if (!$worked) {
+ @$this->db->exec('ROLLBACK');
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Unable to create Code Coverage SQLite3 database: ' . $error);
+ }
+
+ echo ".";
+ $query = '
+ CREATE TABLE tests (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ testpath TEXT(500) NOT NULL,
+ testpathmd5 TEXT(32) NOT NULL,
+ UNIQUE (testpath)
+ );';
+ $worked = $this->db->exec($query);
+ if (!$worked) {
+ @$this->db->exec('ROLLBACK');
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Unable to create Code Coverage SQLite3 database: ' . $error);
+ }
+
+ echo ".";
+ $query = '
+ CREATE TABLE analyzerversion (
+ version TEXT(5) NOT NULL
+ );
+
+ INSERT INTO analyzerversion VALUES("5.0.0");
+
+ CREATE TABLE paths (
+ codepath TEXT NOT NULL,
+ testpath TEXT NOT NULL
+ );';
+ $worked = $this->db->exec($query);
+ if (!$worked) {
+ @$this->db->exec('ROLLBACK');
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Unable to create Code Coverage SQLite3 database: ' . $error);
+ }
+
+ echo ".";
+ $query = '
+ INSERT INTO paths VALUES(
+ "' . $this->db->escapeString($codepath) . '",
+ "' . $this->db->escapeString($testpath). '");';
+ $worked = $this->db->exec($query);
+ if (!$worked) {
+ @$this->db->exec('ROLLBACK');
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Unable to create Code Coverage SQLite3 database: ' . $error);
+ }
+ $this->db->exec('COMMIT');
+ echo "done\n";
+ }
+
+ function retrieveLineLinks($file)
+ {
+ $id = $this->getFileId($file);
+ $query = 'SELECT t.testpath, c.linenumber
+ FROM
+ coverage c, tests t
+ WHERE
+ c.files_id=' . $id . ' AND t.id=c.tests_id';
+ $result = $this->db->query($query);
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve line links for ' . $file .
+ ' line #' . $line . ': ' . $error);
+ }
+
+ $ret = array();
+ while ($res = $result->fetchArray(SQLITE3_ASSOC)) {
+ $ret[$res['linenumber']][] = $res['testpath'];
+ }
+ return $ret;
+ }
+
+ function retrieveTestPaths()
+ {
+ $query = 'SELECT testpath from tests ORDER BY testpath';
+ $result = $this->db->query($query);
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve test paths :' . $error);
+ }
+ $ret = array();
+ while ($res = $result->fetchArray(SQLITE3_NUM)) {
+ $ret[] = $res[0];
+ }
+ return $ret;
+ }
+
+ function retrievePathsForTest($test, $all = 0)
+ {
+ $id = $this->getTestId($test);
+ $ret = array();
+ if ($all) {
+ $query = 'SELECT DISTINCT filepath
+ FROM coverage_nonsource c, files
+ WHERE c.tests_id=' . $id . '
+ AND files.id=c.files_id
+ GROUP BY c.files_id
+ ORDER BY filepath';
+ $result = $this->db->query($query);
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve file paths for test ' . $test . ':' . $error);
+ }
+ while ($res = $result->fetchArray(SQLITE3_NUM)) {
+ $ret[] = $res[0];
+ }
+ }
+ $query = 'SELECT DISTINCT filepath
+ FROM coverage c, files
+ WHERE c.tests_id=' . $id . '
+ AND files.id=c.files_id
+ GROUP BY c.files_id
+ ORDER BY filepath';
+ $result = $this->db->query($query);
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve file paths for test ' . $test . ':' . $error);
+ }
+ while ($res = $result->fetchArray(SQLITE3_NUM)) {
+ $ret[] = $res[0];
+ }
+ return $ret;
+ }
+
+ function retrievePaths($all = 0)
+ {
+ if ($all) {
+ $query = 'SELECT filepath from files ORDER BY filepath';
+ } else {
+ $query = 'SELECT filepath from files WHERE issource=1 ORDER BY filepath';
+ }
+ $result = $this->db->query($query);
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve file paths :' . $error);
+ }
+ $ret = array();
+ while ($res = $result->fetchArray(SQLITE3_NUM)) {
+ $ret[] = $res[0];
+ }
+ return $ret;
+ }
+
+ function coveragePercentage($sourcefile, $testfile = null)
+ {
+ if ($testfile) {
+ $coverage = $this->retrievePathCoverageByTest($sourcefile, $testfile);
+ } else {
+ $coverage = $this->retrievePathCoverage($sourcefile);
+ }
+ if ($coverage[1]) {
+ return round(($coverage[0] / $coverage[1]) * 100);
+ }
+ return 0;
+ }
+
+ function retrieveProjectCoverage()
+ {
+ if ($this->totallines) {
+ return array($this->coveredlines, $this->totallines);
+ }
+ $query = 'SELECT covered, total, filepath FROM line_info, files
+ WHERE files.id=line_info.files_id';
+ $result = $this->db->query($query);
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve coverage for ' . $path. ': ' . $error);
+ }
+ while ($res = $result->fetchArray(SQLITE3_ASSOC)) {
+ $this->pathTotal[$res['filepath']] = $res['total'];
+ $this->totallines += $res['total'];
+ $this->pathCovered[$res['filepath']] = $res['covered'];
+ $this->coveredlines += $res['covered'];
+ }
+
+ return array($this->coveredlines, $this->totallines);
+ }
+
+ function retrievePathCoverage($path)
+ {
+ if (!$this->totallines) {
+ // set up the cache
+ $this->retrieveProjectCoverage();
+ }
+ if (!isset($this->pathCovered[$path])) {
+ return array(0, 0);
+ }
+ return array($this->pathCovered[$path], $this->pathTotal[$path]);
+ }
+
+ function retrievePathCoverageByTest($path, $test)
+ {
+ $id = $this->getFileId($path);
+ $testid = $this->getTestId($test);
+
+ $query = 'SELECT COUNT(linenumber)
+ FROM all_lines
+ WHERE files_id=' . $id;
+ $result = $this->db->query($query);
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve path coverage for ' . $path .
+ ' in test ' . $test . ': ' . $error);
+ }
+
+ $ret = array();
+ while ($res = $result->fetchArray(SQLITE3_NUM)) {
+ $total = $res[0];
+ }
+
+ $query = 'SELECT COUNT(linenumber)
+ FROM coverage
+ WHERE files_id=' . $id. ' AND tests_id=' . $testid;
+ $result = $this->db->query($query);
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve path coverage for ' . $path .
+ ' in test ' . $test . ': ' . $error);
+ }
+
+ $ret = array();
+ while ($res = $result->fetchArray(SQLITE3_NUM)) {
+ $covered = $res[0];
+ }
+ return array($covered, $total);
+ }
+
+ function retrieveCoverageByTest($path, $test)
+ {
+ $id = $this->getFileId($path);
+ $testid = $this->getTestId($test);
+
+ $query = 'SELECT 1 as coverage, linenumber FROM coverage
+ WHERE files_id=' . $id . ' AND tests_id=' . $testid;
+ $result = $this->db->query($query);
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve test ' . $test .
+ ' coverage for ' . $path. ': ' . $error);
+ }
+
+ $ret = array();
+ while ($res = $result->fetchArray(SQLITE3_ASSOC)) {
+ $ret[$res['linenumber']] = $res['coverage'];
+ }
+ $query = 'SELECT linenumber
+ FROM all_lines
+ WHERE files_id=' . $id;
+ $result = $this->db->query($query);
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve path coverage for ' . $path .
+ ' in test ' . $test . ': ' . $error);
+ }
+
+ while ($res = $result->fetchArray(SQLITE3_ASSOC)) {
+ if (isset($ret[$res['linenumber']])) {
+ continue;
+ }
+ $ret[$res['linenumber']] = 0;
+ }
+ return $ret;
+ }
+
+ function getFileId($path)
+ {
+ $query = 'SELECT id FROM files WHERE filepath=:filepath';
+ $stmt = $this->db->prepare($query);
+ $stmt->bindValue(':filepath', $path);
+ if (!($result = $stmt->execute())) {
+ throw new Exception('Unable to retrieve file ' . $path . ' id from database');
+ }
+ while ($id = $result->fetchArray(SQLITE3_NUM)) {
+ return $id[0];
+ }
+ throw new Exception('Unable to retrieve file ' . $path . ' id from database');
+ }
+
+ function getTestId($path)
+ {
+ $query = 'SELECT id FROM tests WHERE testpath=:filepath';
+ $stmt = $this->db->prepare($query);
+ $stmt->bindValue(':filepath', $path);
+ if (!($result = $stmt->execute())) {
+ throw new Exception('Unable to retrieve test file ' . $path . ' id from database');
+ }
+ while ($id = $result->fetchArray(SQLITE3_NUM)) {
+ return $id[0];
+ }
+ throw new Exception('Unable to retrieve test file ' . $path . ' id from database');
+ }
+
+ function removeOldTest($testpath, $id = null)
+ {
+ if ($id === null) {
+ $id = $this->getTestId($testpath);
+ }
+ echo "deleting old test ", $testpath,'.';
+ $this->db->exec('DELETE FROM tests WHERE id=' . $id);
+ echo '.';
+ $this->db->exec('DELETE FROM coverage WHERE tests_id=' . $id);
+ echo '.';
+ $this->db->exec('DELETE FROM coverage_nonsource WHERE tests_id=' . $id);
+ echo '.';
+ $this->db->exec('DELETE FROM xdebugs WHERE xdebugpath="' .
+ $this->db->escapeString(str_replace('.phpt', '.xdebug', $testpath)) . '"');
+ echo "done\n";
+ }
+
+ function addTest($testpath, $id = null)
+ {
+ try {
+ $id = $this->getTestId($testpath);
+ $this->db->exec('UPDATE tests SET testpathmd5="' . md5_file($testpath) . '" WHERE id=' . $id);
+ } catch (Exception $e) {
+ echo "Adding new test $testpath\n";
+ $query = 'REPLACE INTO tests
+ (testpath, testpathmd5)
+ VALUES(:testpath, :md5)';
+ $stmt = $this->db->prepare($query);
+ $stmt->bindValue(':testpath', $testpath);
+ $md5 = md5_file($testpath);
+ $stmt->bindValue(':md5', $md5);
+ $stmt->execute();
+ $id = $this->db->lastInsertRowID();
+ }
+
+ $query = 'REPLACE INTO xdebugs
+ (xdebugpath, xdebugpathmd5)
+ VALUES(:testpath, :md5)';
+ $stmt = $this->db->prepare($query);
+ $stmt->bindValue(':testpath', str_replace('.phpt', '.xdebug', $testpath));
+ $md5 = md5_file(str_replace('.phpt', '.xdebug', $testpath));
+ $stmt->bindValue(':md5', $md5);
+ $stmt->execute();
+ return $id;
+ }
+
+ function unChangedXdebug($path)
+ {
+ $query = 'SELECT xdebugpathmd5 FROM xdebugs
+ WHERE xdebugpath=:path';
+ $stmt = $this->db->prepare($query);
+ $stmt->bindValue(':path', $path);
+ $result = $stmt->execute();
+ if (!$result) {
+ return false;
+ }
+ $md5 = 0;
+ while ($res = $result->fetchArray(SQLITE3_NUM)) {
+ $md5 = $res[0];
+ }
+ if (!$md5) {
+ return false;
+ }
+ if ($md5 == md5_file($path)) {
+ return true;
+ }
+ return false;
+ }
+
+ function getTotalCoverage($file, $linenumber)
+ {
+ $query = 'SELECT COUNT(linenumber) FROM coverage
+ WHERE files_id=' . $this->getFileId($file) . ' AND linenumber=' . $linenumber;
+ $result = $this->db->query($query);
+ if (!$result) {
+ return false;
+ }
+ $coverage = 0;
+ while ($res = $result->fetchArray(SQLITE3_NUM)) {
+ $coverage = $res[0];
+ }
+ return $coverage;
+ }
+
+ function retrieveCoverage($path)
+ {
+ $id = $this->getFileId($path);
+ $links = $this->retrieveLineLinks($path);
+ $links = array_map(function ($arr) {return count($arr);}, $links);
+
+ $query = 'SELECT linenumber FROM all_lines
+ WHERE files_id=' . $this->getFileId($path);
+ $result = $this->db->query($query);
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve coverage for ' . $path. ': ' . $error);
+ }
+ $coverage = 0;
+ while ($res = $result->fetchArray(SQLITE3_NUM)) {
+ if (!isset($links[$res[0]])) {
+ $links[$res[0]] = 0;
+ }
+ }
+ return $links;
+ }
+
+ /**
+ * This is used to get the coverage which is then inserted into our
+ * intermediate coverage_per_file table to speed things up at rendering
+ */
+ function retrieveSlowCoverage($id)
+ {
+ $query = 'SELECT COUNT(*) as coverage, linenumber FROM coverage WHERE files_id=' . $id . '
+ GROUP BY linenumber UNION
+ SELECT 0 as coverage, linenumber FROM all_lines WHERE files_id=' . $id . '
+ GROUP BY linenumber
+ ORDER BY linenumber';
+ $result = $this->db->query($query);
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve coverage for ' . $path. ': ' . $error);
+ }
+
+ $ret = array();
+ while ($res = $result->fetchArray(SQLITE3_ASSOC)) {
+ $ret[$res['linenumber']] = $res['coverage'];
+ }
+ return $ret;
+ }
+
+ function updateTotalCoverage()
+ {
+ echo "Updating coverage per-file intermediate table\n";
+ $query = 'SELECT COUNT(DISTINCT linenumber), files_id FROM coverage GROUP BY files_id';
+ $result = $this->db->query($query);
+ echo ".";
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve coverage for ' . $path. ': ' . $error);
+ }
+
+ $ret = array();
+ while ($res = $result->fetchArray(SQLITE3_NUM)) {
+ $ret[$res[1]]['covered'] = $res[0];
+ }
+
+ $query = 'SELECT COUNT(linenumber), files_id FROM all_lines GROUP BY files_id';
+ $result = $this->db->query($query);
+ echo ".";
+ if (!$result) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot retrieve coverage for ' . $path. ': ' . $error);
+ }
+
+ while ($res = $result->fetchArray(SQLITE3_NUM)) {
+ $ret[$res[1]]['total'] = $res[0];
+ }
+ echo ".";
+
+ foreach ($ret as $id => $lineinfo) {
+ if (!isset($lineinfo['covered'])) {
+ // this file has no coverage any more (was deleted), remove it
+ $this->db->exec('DELETE FROM all_lines WHERE files_id=' . $id);
+ $this->db->exec('DELETE FROM files WHERE id=' . $id);
+ continue;
+ }
+ $this->db->exec('REPLACE INTO line_info (files_id, covered, total)
+ VALUES(' . $id . ',' . $lineinfo['covered'] . ',' . $lineinfo['total'] . ')');
+ echo ".";
+ }
+
+ echo "done\n";
+ }
+
+ function updateAllLines($id, $results)
+ {
+ $query = 'SELECT linenumber FROM all_lines WHERE files_id=' . $id . ' ORDER BY linenumber ASC';
+ $result = $this->db->query($query);
+ $lines = array();
+ while ($res = $result->fetchArray(SQLITE3_NUM)) {
+ $lines[] = $res[0];
+ }
+ $new = array_diff($results, $lines);
+ $old = array_diff($lines, $results);
+ if (count($new) || count($old)) {
+ foreach ($new as $line) {
+ if (!$line) {
+ continue; // line 0 does not exist, skip this (xdebug quirk)
+ }
+ $query = 'INSERT INTO all_lines (files_id, linenumber) VALUES (' . $id . ',' . $line . ')';
+ $this->db->exec($query);
+ }
+ if (count($old)) {
+ $query = 'DELETE FROM all_lines WHERE files_id=' . $id .
+ ' AND linenumber IN (' . implode(',', $old) . ')';
+ $this->db->exec($query);
+ $query = 'DELETE FROM coverage WHERE files_id=' . $id .
+ ' AND linenumber IN (' . implode(',', $old) . ')';
+ $this->db->exec($query);
+ }
+ }
+ }
+
+ function addFile($filepath, $issource = 0, $results = array())
+ {
+ $query = 'SELECT id FROM files WHERE filepath=:filepath';
+ $stmt = $this->db->prepare($query);
+ $stmt->bindParam(':filepath', $filepath);
+ if (!($result = $stmt->execute())) {
+ throw new Exception('Unable to add file ' . $filepath . ' to database');
+ }
+ while ($id = $result->fetchArray(SQLITE3_NUM)) {
+ if ($issource) {
+ $this->updateAllLines($id[0], $results);
+ }
+ $query = 'UPDATE files SET filepathmd5=:md5 WHERE filepath=:filepath';
+ $stmt = $this->db->prepare($query);
+ $stmt->bindParam(':filepath', $filepath);
+ $md5 = md5_file($filepath);
+ $stmt->bindParam(':md5', $md5);
+ if (!$stmt->execute()) {
+ throw new Exception('Unable to update file ' . $filepath . ' md5 in database');
+ }
+ return $id[0];
+ }
+ $query = 'INSERT INTO files
+ (filepath, filepathmd5, issource)
+ VALUES(:testpath, :md5, :issource)';
+ $stmt = $this->db->prepare($query);
+ $stmt->bindValue(':testpath', $filepath);
+ $md5 = md5_file($filepath);
+ $stmt->bindValue(':md5', $md5);
+ $stmt->bindValue(':issource', $issource);
+ if (!$stmt->execute()) {
+ throw new Exception('Unable to add file ' . $filepath . ' to database');
+ }
+ $id = $this->db->lastInsertRowID();
+ if ($issource) {
+ $this->updateAllLines($id, $results);
+ }
+ return $id;
+ }
+
+ function addCoverage($testpath, $testid, $xdebug)
+ {
+ $query = 'DELETE FROM coverage WHERE tests_id=' . $testid . ';
+ DELETE FROM coverage_nonsource WHERE tests_id=' . $testid;
+ $worked = $this->db->exec($query);
+ foreach ($xdebug as $path => $results) {
+ if (!file_exists($path)) {
+ continue;
+ }
+ if (strpos($path, $this->codepath) !== 0) {
+ $issource = 0;
+ } else {
+ if (strpos($path, $this->testpath) === 0) {
+ $issource = 0;
+ } else {
+ $issource = 1;
+ }
+ }
+ echo ".";
+ $id = $this->addFile($path, $issource, array_keys($results));
+ if (!$issource) {
+ $query = 'REPLACE INTO coverage_nonsource
+ (files_id, tests_id)
+ VALUES(' . $id . ', ' . $testid . ')';
+ $worked = $this->db->exec($query);
+ if (!$worked) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot add coverage for test ' . $testpath .
+ ', covered file ' . $path . ': ' . $error);
+ }
+ continue;
+ }
+ foreach ($results as $line => $info) {
+ if (!$line) {
+ continue; // line 0 does not exist, skip this (xdebug quirk)
+ }
+ if ($info < 0) {
+ continue;
+ }
+ $query = 'REPLACE INTO coverage
+ (files_id, linenumber, tests_id)
+ VALUES(' . $id . ', ' . $line . ', ' . $testid . ')';
+
+ $worked = $this->db->exec($query);
+ if (!$worked) {
+ $error = $this->db->lastErrorMsg();
+ throw new Exception('Cannot add coverage for test ' . $testpath .
+ ', covered file ' . $path . ': ' . $error);
+ }
+ }
+ }
+ }
+
+ function begin()
+ {
+ $this->db->exec('PRAGMA synchronous=OFF'); // make inserts super fast
+ $this->db->exec('BEGIN');
+ }
+
+ function commit()
+ {
+ $this->db->exec('COMMIT');
+ $this->db->exec('PRAGMA synchronous=NORMAL'); // make inserts super fast
+ $this->db->exec('VACUUM');
+ }
+
+ /**
+ * Retrieve a list of .phpt tests that either have been modified,
+ * or the files they access have been modified
+ * @return array
+ */
+ function getModifiedTests()
+ {
+ // first scan for new .phpt files
+ foreach (new \RegexIterator(
+ new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($this->testpath,
+ 0|\RecursiveDirectoryIterator::SKIP_DOTS)),
+ '/\.phpt$/') as $file) {
+ if (strpos((string) $file, '.svn')) {
+ continue;
+ }
+ $tests[] = realpath((string) $file);
+ }
+ $newtests = array();
+ foreach ($tests as $path) {
+ if ($path == $this->db->querySingle('SELECT testpath FROM tests WHERE testpath="' .
+ $this->db->escapeString($path) . '"')) {
+ continue;
+ }
+ $newtests[] = $path;
+ }
+
+ $modifiedPaths = array();
+ $modifiedTests = array();
+ $paths = $this->retrievePaths(1);
+ echo "Scanning ", count($paths), " source files";
+ foreach ($paths as $path) {
+ echo '.';
+ $query = '
+ SELECT id, filepathmd5, issource FROM files where filepath="' .
+ $this->db->escapeString($path) . '"';
+ $result = $this->db->query($query);
+ while ($res = $result->fetchArray(SQLITE3_ASSOC)) {
+ if (!file_exists($path) || md5_file($path) == $res['filepathmd5']) {
+ if ($res['issource'] && !file_exists($path)) {
+ $this->db->exec('
+ DELETE FROM files WHERE id='. $res['id'] .';
+ DELETE FROM coverage WHERE files_id='. $res['id'] . ';
+ DELETE FROM all_lines WHERE files_id='. $res['id'] . ';
+ DELETE FROM line_info WHERE files_id='. $res['id'] . ';');
+ }
+ break;
+ }
+ $modifiedPaths[] = $path;
+ // file is modified, get a list of tests that execute this file
+ if ($res['issource']) {
+ $query = '
+ SELECT t.testpath
+ FROM coverage c, tests t
+ WHERE
+ c.files_id=' . $res['id'] . '
+ AND t.id=c.tests_id';
+ $result2 = $this->db->query($query);
+ while ($res = $result2->fetchArray(SQLITE3_NUM)) {
+ $modifiedTests[$res[0]] = true;
+ }
+ } else {
+ $query = '
+ SELECT t.testpath
+ FROM coverage_nonsource c, tests t
+ WHERE
+ c.files_id=' . $res['id'] . '
+ AND t.id=c.tests_id';
+ $result2 = $this->db->query($query);
+ while ($res = $result2->fetchArray(SQLITE3_NUM)) {
+ $modifiedTests[$res[0]] = true;
+ }
+ }
+ break;
+ }
+ }
+ echo "done\n";
+ echo count($modifiedPaths), ' modified files resulting in ',
+ count($modifiedTests), " modified tests\n";
+ $paths = $this->retrieveTestPaths();
+ echo "Scanning ", count($paths), " test paths";
+ foreach ($paths as $path) {
+ echo '.';
+ $query = '
+ SELECT id, testpathmd5 FROM tests where testpath="' .
+ $this->db->escapeString($path) . '"';
+ $result = $this->db->query($query);
+ while ($res = $result->fetchArray(SQLITE3_ASSOC)) {
+ if (!file_exists($path)) {
+ $this->removeOldTest($path, $res['id']);
+ continue;
+ }
+ if (md5_file($path) != $res['testpathmd5']) {
+ $modifiedTests[$path] = true;
+ }
+ }
+ }
+ echo "done\n";
+ echo count($newtests), ' new tests and ', count($modifiedTests), " modified tests should be re-run\n";
+ return array_merge($newtests, array_keys($modifiedTests));
+ }
+}
+}
+?>
74 src/Pyrus/Developer/CoverageAnalyzer/Web/Aggregator.php
@@ -0,0 +1,74 @@
+<?php
+namespace pear2\Pyrus\Developer\CoverageAnalyzer\Web {
+use pear2\Pyrus\Developer\CoverageAnalyzer;
+class Aggregator extends CoverageAnalyzer\Aggregator
+{
+ public $codepath;
+ public $testpath;
+ protected $sqlite;
+ public $totallines = 0;
+ public $totalcoveredlines = 0;
+
+ /**
+ * @var string $testpath Location of .phpt files
+ * @var string $codepath Location of code whose coverage we are testing
+ */
+ function __construct($db = ':memory:')
+ {
+ $this->sqlite = new CoverageAnalyzer\Sqlite($db);
+ $this->codepath = $this->sqlite->codepath;
+ $this->testpath = $this->sqlite->testpath;
+ }
+
+ function retrieveLineLinks($file)
+ {
+ return $this->sqlite->retrieveLineLinks($file);
+ }
+
+ function retrievePaths()
+ {
+ return $this->sqlite->retrievePaths();