<?php declare(strict_types=1);

namespace GitWrapper;

use GitWrapper\Event\GitEvent;
use GitWrapper\Event\GitEvents;
use RuntimeException;
use Symfony\Component\Process\Process;

final class GitProcess extends Process
{
    /**
     * @var GitWrapper
     */
    protected $gitWrapper;

    /**
     * @var GitCommand
     */
    protected $gitCommand;

    public function __construct(GitWrapper $gitWrapper, GitCommand $gitCommand, ?string $cwd = null)
    {
        $this->gitWrapper = $gitWrapper;
        $this->gitCommand = $gitCommand;

        // Build the command line options, flags, and arguments.
        $commandLine = $gitCommand->getCommandLine();
        $gitBinary = $gitWrapper->getGitBinary();
        if (is_string($commandLine)) {
            // Support for executing an arbitrary git command.
            $commandLine = [$gitBinary, $commandLine];
        } else {
            array_unshift($commandLine, $gitBinary);
        }

        // Resolve the working directory of the Git process. Use the directory
        // in the command object if it exists.
        $cwd = $this->resolveWorkingDirectory($cwd, $gitCommand);

        // Finalize the environment variables, an empty array is converted
        // to null which enherits the environment of the PHP process.
        $env = $gitWrapper->getEnvVars();
        if (! $env) {
            $env = null;
        }

        parent::__construct($commandLine, $cwd, $env, null, (float) $gitWrapper->getTimeout());
    }

    /**
     * {@inheritdoc}
     */
    public function start(?callable $callback = null, array $env = []): void
    {
        $this->dispatchGitEvent(GitEvents::GIT_PREPARE);

        if ($this->gitCommand->notBypassed()) {
            parent::start($callback, $env);
        } else {
            $this->dispatchGitEvent(GitEvents::GIT_BYPASS);
        }
    }

    public function wait(?callable $callback = null): int
    {
        if (! $this->gitCommand->notBypassed()) {
            return -1;
        }

        try {
            $exitCode = parent::wait($callback);

            if ($this->isSuccessful()) {
                $this->dispatchGitEvent(GitEvents::GIT_SUCCESS);
            } else {
                $output = $this->getErrorOutput();

                if (trim($output) === '') {
                    $output = $this->getOutput();
                }

                throw new GitException($output);
            }
        } catch (RuntimeException $runtimeException) {
            $this->dispatchGitEvent(GitEvents::GIT_ERROR);
            throw new GitException($runtimeException->getMessage(), $runtimeException->getCode(), $runtimeException);
        }

        return $exitCode;
    }

    private function resolveWorkingDirectory(?string $cwd, GitCommand $gitCommand): ?string
    {
        if ($cwd !== null) {
            return $cwd;
        }

        $directory = $gitCommand->getDirectory();
        if ($directory === null) {
            return $cwd;
        }

        if (! $cwd = realpath($directory)) {
            throw new GitException('Path to working directory could not be resolved: ' . $directory);
        }

        return $cwd;
    }

    private function dispatchGitEvent(string $eventName): void
    {
        $this->gitWrapper->getDispatcher()->dispatch(
            $eventName,
            new GitEvent($this->gitWrapper, $this, $this->gitCommand)
        );
    }
}


<?php declare(strict_types=1);

namespace GitWrapper;

/**
 * Interacts with a working copy.
 *
 * All commands executed via an instance of this class act on the working copy
 * that is set through the constructor.
 */
final class GitWorkingCopy
{
    /**
     * A boolean flagging whether the repository is cloned.
     *
     * If the variable is null, the a rudimentary check will be performed to see
     * if the directory looks like it is a working copy.
     *
     * @var bool|null
     */
    private $cloned;

    /**
     * Path to the directory containing the working copy.
     *
     * @var string
     */
    private $directory;

    /**
     * The GitWrapper object that likely instantiated this class.
     *
     * @var GitWrapper
     */
    private $gitWrapper;

    public function __construct(GitWrapper $gitWrapper, string $directory)
    {
        $this->gitWrapper = $gitWrapper;
        $this->directory = $directory;
    }

    public function getWrapper(): GitWrapper
    {
        return $this->gitWrapper;
    }

    public function getDirectory(): string
    {
        return $this->directory;
    }

    public function setCloned(bool $cloned): void
    {
        $this->cloned = $cloned;
    }

    /**
     * Checks whether a repository has already been cloned to this directory.
     *
     * If the flag is not set, test if it looks like we're at a git directory.
     */
    public function isCloned(): bool
    {
        if ($this->cloned === null) {
            $gitDir = $this->directory;
            if (is_dir($gitDir . '/.git')) {
                $gitDir .= '/.git';
            }

            $this->cloned = is_dir($gitDir . '/objects') && is_dir($gitDir . '/refs') && is_file($gitDir . '/HEAD');
        }

        return $this->cloned;
    }

    /**
     * Runs a Git command and returns the output.
     *
     * @param mixed[] $argsAndOptions
     */
    public function run(string $command, array $argsAndOptions = [], bool $setDirectory = true): string
    {
        $command = new GitCommand($command, ...$argsAndOptions);
        if ($setDirectory) {
            $command->setDirectory($this->directory);
        }

        return $this->gitWrapper->run($command);
    }

    /**
     * Returns the output of a `git status -s` command.
     */
    public function getStatus(): string
    {
        return $this->run('status', ['-s']);
    }

    /**
     * Returns true if there are changes to commit.
     */
    public function hasChanges(): bool
    {
        $output = $this->getStatus();
        return ! empty($output);
    }

    /**
     * Returns whether HEAD has a remote tracking branch.
     */
    public function isTracking(): bool
    {
        try {
            $this->run('rev-parse', ['@{u}']);
        } catch (GitException $gitException) {
            return false;
        }

        return true;
    }

    /**
     * Returns whether HEAD is up-to-date with its remote tracking branch.
     */
    public function isUpToDate(): bool
    {
        if (! $this->isTracking()) {
            throw new GitException(
                'Error: HEAD does not have a remote tracking branch. Cannot check if it is up-to-date.'
            );
        }

        $mergeBase = $this->run('merge-base', ['@', '@{u}']);
        $remoteSha = $this->run('rev-parse', ['@{u}']);
        return $mergeBase === $remoteSha;
    }

    /**
     * Returns whether HEAD is ahead of its remote tracking branch.
     *
     * If this returns true it means that commits are present locally which have
     * not yet been pushed to the remote.
     */
    public function isAhead(): bool
    {
        if (! $this->isTracking()) {
            throw new GitException('Error: HEAD does not have a remote tracking branch. Cannot check if it is ahead.');
        }

        $mergeBase = $this->run('merge-base', ['@', '@{u}']);
        $localSha = $this->run('rev-parse', ['@']);
        $remoteSha = $this->run('rev-parse', ['@{u}']);
        return $mergeBase === $remoteSha && $localSha !== $remoteSha;
    }

    /**
     * Returns whether HEAD is behind its remote tracking branch.
     *
     * If this returns true it means that a pull is needed to bring the branch
     * up-to-date with the remote.
     */
    public function isBehind(): bool
    {
        if (! $this->isTracking()) {
            throw new GitException('Error: HEAD does not have a remote tracking branch. Cannot check if it is behind.');
        }

        $mergeBase = $this->run('merge-base', ['@', '@{u}']);
        $localSha = $this->run('rev-parse', ['@']);
        $remoteSha = $this->run('rev-parse', ['@{u}']);
        return $mergeBase === $localSha && $localSha !== $remoteSha;
    }

    /**
     * Returns whether HEAD needs to be merged with its remote tracking branch.
     *
     * If this returns true it means that HEAD has diverged from its remote
     * tracking branch; new commits are present locally as well as on the
     * remote.
     */
    public function needsMerge(): bool
    {
        if (! $this->isTracking()) {
            throw new GitException('Error: HEAD does not have a remote tracking branch. Cannot check if it is behind.');
        }

        $mergeBase = $this->run('merge-base', ['@', '@{u}']);
        $localSha = $this->run('rev-parse', ['@']);
        $remoteSha = $this->run('rev-parse', ['@{u}']);
        return $mergeBase !== $localSha && $mergeBase !== $remoteSha;
    }

    /**
     * Returns a GitBranches object containing information on the repository's
     * branches.
     */
    public function getBranches(): GitBranches
    {
        return new GitBranches($this);
    }

    /**
     * This is synonymous with `git push origin tag v1.2.3`.
     *
     * @param string $repository The destination of the push operation, which is either a URL or name of
     *   the remote. Defaults to "origin".
     * @param mixed[] $options
     */
    public function pushTag(string $tag, string $repository = 'origin', array $options = []): string
    {
        return $this->push($repository, 'tag', $tag, $options);
    }

    /**
     * This is synonymous with `git push --tags origin`.
     *
     * @param string $repository The destination of the push operation, which is either a URL or name of the remote.
     * @param mixed[] $options
     */
    public function pushTags(string $repository = 'origin', array $options = []): string
    {
        $options['tags'] = true;
        return $this->push($repository, $options);
    }

    /**
     * Fetches all remotes.
     *
     * This is synonymous with `git fetch --all`.
     *
     * @param mixed[] $options
     */
    public function fetchAll(array $options = []): string
    {
        $options['all'] = true;
        return $this->fetch($options);
    }

    /**
     * Create a new branch and check it out.
     *
     * This is synonymous with `git checkout -b`.
     *
     * @param mixed[] $options
     */
    public function checkoutNewBranch(string $branch, array $options = []): string
    {
        $options['b'] = true;
        return $this->checkout($branch, $options);
    }

    /**
     * Adds a remote to the repository.
     *
     * @param mixed[] $options An associative array of options, with the following keys:
     *   - -f: Boolean, set to true to run git fetch immediately after the
     *     remote is set up. Defaults to false.
     *   - --tags: Boolean. By default only the tags from the fetched branches
     *     are imported when git fetch is run. Set this to true to import every
     *     tag from the remote repository. Defaults to false.
     *   - --no-tags: Boolean, when set to true, git fetch does not import tags
     *     from the remote repository. Defaults to false.
     *   - -t: Optional array of branch names to track. If left empty, all
     *     branches will be tracked.
     *   - -m: Optional name of the master branch to track. This will set up a
     *     symbolic ref 'refs/remotes/<name>/HEAD which points at the specified
     *     master branch on the remote. When omitted, no symbolic ref will be
     *     created.
     */
    public function addRemote(string $name, string $url, array $options = []): string
    {
        $this->ensureAddRemoveArgsAreValid($name, $url);

        $args = ['add'];

        // Add boolean options.
        foreach (['-f', '--tags', '--no-tags'] as $option) {
            if (! empty($options[$option])) {
                $args[] = $option;
            }
        }

        // Add tracking branches.
        if (! empty($options['-t'])) {
            foreach ($options['-t'] as $branch) {
                array_push($args, '-t', $branch);
            }
        }

        // Add master branch.
        if (! empty($options['-m'])) {
            array_push($args, '-m', $options['-m']);
        }

        // Add remote name and URL.
        array_push($args, $name, $url);

        return $this->remote(...$args);
    }

    public function removeRemote(string $name): string
    {
        return $this->remote('rm', $name);
    }

    public function hasRemote(string $name): bool
    {
        return array_key_exists($name, $this->getRemotes());
    }

    /**
     * @return string[] An associative array with the following keys:
     *  - fetch: the fetch URL.
     *  - push: the push URL.
     */
    public function getRemote(string $name): array
    {
        if (! $this->hasRemote($name)) {
            throw new GitException(sprintf('The remote "%s" does not exist.', $name));
        }

        return $this->getRemotes()[$name];
    }

    /**
     * @return mixed[] An associative array, keyed by remote name, containing an associative array with keys:
     *  - fetch: the fetch URL.
     *  - push: the push URL.
     */
    public function getRemotes(): array
    {
        $result = rtrim($this->remote());
        if (empty($result)) {
            return [];
        }

        $remotes = [];
        foreach (explode(PHP_EOL, $result) as $remote) {
            $remotes[$remote]['fetch'] = $this->getRemoteUrl($remote);
            $remotes[$remote]['push'] = $this->getRemoteUrl($remote, 'push');
        }

        return $remotes;
    }

    /**
     * Returns the fetch or push URL of a given remote.
     *
     * @param string $operation The operation for which to return the remote. Can be either 'fetch' or 'push'.
     */
    public function getRemoteUrl(string $remote, string $operation = 'fetch'): string
    {
        $argsAndOptions = ['get-url', $remote];

        if ($operation === 'push') {
            $argsAndOptions[] = '--push';
        }

        return rtrim($this->remote(...$argsAndOptions));
    }

    /**
     * @code $git->add('some/file.txt');
     *
     * @param mixed[] $options
     */
    public function add(string $filepattern, array $options = []): string
    {
        return $this->run('add', [$filepattern, $options]);
    }

    /**
     * @code $git->apply('the/file/to/read/the/patch/from');
     *
     * @param mixed ...$argsAndOptions
     */
    public function apply(...$argsAndOptions): string
    {
        return $this->run('apply', $argsAndOptions);
    }

    /**
     * Find by binary search the change that introduced a bug.
     *
     * @code $git->bisect('good', '2.6.13-rc2');
     * $git->bisect('view', ['stat' => true]);
     *
     * @param mixed ...$argsAndOptions
     */
    public function bisect(...$argsAndOptions): string
    {
        return $this->run('bisect', $argsAndOptions);
    }

    /**
     * @code $git->branch('my2.6.14', 'v2.6.14');
     * $git->branch('origin/html', 'origin/man', ['d' => true, 'r' => 'origin/todo']);
     *
     * @param mixed ...$argsAndOptions
     */
    public function branch(...$argsAndOptions): string
    {
        return $this->run('branch', $argsAndOptions);
    }

    /**
     * @code $git->checkout('new-branch', ['b' => true]);
     *
     * @param mixed ...$argsAndOptions
     */
    public function checkout(...$argsAndOptions): string
    {
        return $this->run('checkout', $argsAndOptions);
    }

    /**
     * Executes a `git clone` command.
     *
     * @code $git->cloneRepository('git://github.com/cpliakas/git-wrapper.git');
     *
     * @param mixed[] $options
     */
    public function cloneRepository(string $repository, array $options = []): string
    {
        $argsAndOptions = [$repository, $this->directory, $options];
        return $this->run('clone', $argsAndOptions, false);
    }

    /**
     * Record changes to the repository. If only one argument is passed, it is  assumed to be the commit message.
     * Therefore `$git->commit('Message');` yields a `git commit -am "Message"` command.
     *
     * @code $git->commit('My commit message');
     * $git->commit('Makefile', ['m' => 'My commit message']);
     *
     * @param mixed ...$argsAndOptions
     */
    public function commit(...$argsAndOptions): string
    {
        if (isset($argsAndOptions[0]) && is_string($argsAndOptions[0]) && ! isset($argsAndOptions[1])) {
            $argsAndOptions[0] = [
                'm' => $argsAndOptions[0],
                'a' => true,
            ];
        }

        return $this->run('commit', $argsAndOptions);
    }

    /**
     * @code $git->config('user.email', 'opensource@chrispliakas.com');
     * $git->config('user.name', 'Chris Pliakas');
     *
     * @param mixed ...$argsAndOptions
     */
    public function config(...$argsAndOptions): string
    {
        return $this->run('config', $argsAndOptions);
    }

    /**
     * @code $git->diff();
     * $git->diff('topic', 'master');
     *
     * @param mixed ...$argsAndOptions
     */
    public function diff(...$argsAndOptions): string
    {
        return $this->run('diff', $argsAndOptions);
    }

    /**
     * @code $git->fetch('origin');
     * $git->fetch(['all' => true]);
     *
     * @param mixed ...$argsAndOptions
     */
    public function fetch(...$argsAndOptions): string
    {
        return $this->run('fetch', $argsAndOptions);
    }

    /**
     * Print lines matching a pattern.
     *
     * @code $git->grep('time_t', '--', '*.[ch]');
     *
     * @param mixed ...$argsAndOptions
     */
    public function grep(...$argsAndOptions): string
    {
        return $this->run('grep', $argsAndOptions);
    }

    /**
     * Create an empty git repository or reinitialize an existing one.
     *
     * @code $git->init(['bare' => true]);
     *
     * @param mixed[] $options
     */
    public function init(array $options = []): string
    {
        $argsAndOptions = [$this->directory, $options];
        return $this->run('init', $argsAndOptions, false);
    }

    /**
     * @code $git->log(['no-merges' => true]);
     * $git->log('v2.6.12..', 'include/scsi', 'drivers/scsi');
     *
     * @param mixed ...$argsAndOptions
     */
    public function log(...$argsAndOptions): string
    {
        return $this->run('log', $argsAndOptions);
    }

    /**
     * @code $git->merge('fixes', 'enhancements');
     *
     * @param mixed ...$argsAndOptions
     */
    public function merge(...$argsAndOptions): string
    {
        return $this->run('merge', $argsAndOptions);
    }

    /**
     * @code $git->mv('orig.txt', 'dest.txt');
     *
     * @param mixed[] $options
     */
    public function mv(string $source, string $destination, array $options = []): string
    {
        $argsAndOptions = [$source, $destination, $options];
        return $this->run('mv', $argsAndOptions);
    }

    /**
     * @code $git->pull('upstream', 'master');
     *
     * @param mixed ...$argsAndOptions
     */
    public function pull(...$argsAndOptions): string
    {
        return $this->run('pull', $argsAndOptions);
    }

    /**
     * @code $git->push('upstream', 'master');
     *
     * @param mixed ...$argsAndOptions
     */
    public function push(...$argsAndOptions): string
    {
        return $this->run('push', $argsAndOptions);
    }

    /**
     * @code $git->rebase('subsystem@{1}', ['onto' => 'subsystem']);
     *
     * @param mixed ...$argsAndOptions
     */
    public function rebase(...$argsAndOptions): string
    {
        return $this->run('rebase', $argsAndOptions);
    }

    /**
     * @code $git->remote('add', 'upstream', 'git://github.com/cpliakas/git-wrapper.git');
     *
     * @param mixed ...$argsAndOptions
     */
    public function remote(...$argsAndOptions): string
    {
        return $this->run('remote', $argsAndOptions);
    }

    /**
     * @code $git->reset(['hard' => true]);
     *
     * @param mixed ...$argsAndOptions
     */
    public function reset(...$argsAndOptions): string
    {
        return $this->run('reset', $argsAndOptions);
    }

    /**
     * @code $git->rm('oldfile.txt');
     *
     * @param mixed[] $options
     */
    public function rm(string $filepattern, array $options = []): string
    {
        $args = [$filepattern, $options];
        return $this->run('rm', $args);
    }

    /**
     * @code $git->show('v1.0.0');
     *
     * @param mixed[] $options
     */
    public function show(string $object, array $options = []): string
    {
        $args = [$object, $options];
        return $this->run('show', $args);
    }

    /**
     * @code $git->status(['s' => true]);
     *
     * @param mixed ...$argsAndOptions
     */
    public function status(...$argsAndOptions): string
    {
        return $this->run('status', $argsAndOptions);
    }

    /**
     * @code $git->tag('v1.0.0');
     *
     * @param mixed ...$argsAndOptions
     */
    public function tag(...$argsAndOptions): string
    {
        return $this->run('tag', $argsAndOptions);
    }

    /**
     * @code $git->clean('-d', '-f');
     *
     * @param mixed ...$argsAndOptions
     */
    public function clean(...$argsAndOptions): string
    {
        return $this->run('clean', $argsAndOptions);
    }

    /**
     * @code $git->archive('HEAD', ['o' => '/path/to/archive']);
     *
     * @param mixed ...$argsAndOptions
     */
    public function archive(...$argsAndOptions): string
    {
        return $this->run('archive', $argsAndOptions);
    }

    /**
     * Returns a GitTags object containing  information on the repository's tags.
     */
    public function tags(): GitTags
    {
        return new GitTags($this);
    }

    private function ensureAddRemoveArgsAreValid(string $name, string $url): void
    {
        if (empty($name)) {
            throw new GitException('Cannot add remote without a name.');
        }

        if (empty($url)) {
            throw new GitException('Cannot add remote without a URL.');
        }
    }
}


<?php declare(strict_types=1);

namespace GitWrapper;

use ArrayIterator;
use IteratorAggregate;

/**
 * Class that parses and returnes an array of Tags.
 */
final class GitTags implements IteratorAggregate
{
    /**
     * @var GitWorkingCopy
     */
    private $gitWorkingCopy;

    public function __construct(GitWorkingCopy $gitWorkingCopy)
    {
        $this->gitWorkingCopy = clone $gitWorkingCopy;
    }

    /**
     * Fetches the Tags via the `git branch` command.
     *
     * @return mixed[]
     */
    public function fetchTags(): array
    {
        $output = $this->gitWorkingCopy->tag(['l' => true]);
        $tags = (array) preg_split("/\r\n|\n|\r/", rtrim($output));
        return array_map([$this, 'trimTags'], $tags);
    }

    /**
     * Strips unwanted characters from the branch
     */
    public function trimTags(string $branch): string
    {
        return ltrim($branch, ' *');
    }

    public function getIterator(): ArrayIterator
    {
        $tags = $this->all();
        return new ArrayIterator($tags);
    }

    /**
     * @return mixed[]
     */
    public function all(): array
    {
        return $this->fetchTags();
    }
}


<?php declare(strict_types=1);

namespace GitWrapper;

use GitWrapper\Event\GitEvents;
use GitWrapper\Event\GitLoggerEventSubscriber;
use GitWrapper\Event\GitOutputEvent;
use GitWrapper\Event\GitOutputListenerInterface;
use GitWrapper\Event\GitOutputStreamListener;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Process\ExecutableFinder;

/**
 * A wrapper class around the Git binary.
 *
 * A GitWrapper object contains the necessary context to run Git commands such
 * as the path to the Git binary and environment variables. It also provides
 * helper methods to run Git commands as set up the connection to the GIT_SSH
 * wrapper script.
 */
final class GitWrapper
{
    /**
     * Path to the Git binary.
     *
     * @var string
     */
    private $gitBinary;

    /**
     * The timeout of the Git command in seconds.
     *
     * @var int
     */
    private $timeout = 60;

    /**
     * Environment variables defined in the scope of the Git command.
     *
     * @var string[]
     */
    private $env = [];

    /**
     * @var GitOutputListenerInterface
     */
    private $gitOutputListener;

    /**
     * @var EventDispatcherInterface
     */
    private $eventDispatcher;

    public function __construct(?string $gitBinary = null)
    {
        if ($gitBinary === null) {
            $finder = new ExecutableFinder();
            $gitBinary = $finder->find('git');
            if (! $gitBinary) {
                throw new GitException('Unable to find the Git executable.');
            }
        }

        $this->setGitBinary($gitBinary);
    }

    public function getDispatcher(): EventDispatcherInterface
    {
        if ($this->eventDispatcher === null) {
            $this->eventDispatcher = new EventDispatcher();
        }

        return $this->eventDispatcher;
    }

    public function setDispatcher(EventDispatcherInterface $eventDispatcher): void
    {
        $this->eventDispatcher = $eventDispatcher;
    }

    public function setGitBinary(string $gitBinary): void
    {
        $this->gitBinary = $gitBinary;
    }

    public function getGitBinary(): string
    {
        return $this->gitBinary;
    }

    /**
     * @param mixed $value
     */
    public function setEnvVar(string $var, $value): void
    {
        $this->env[$var] = $value;
    }

    public function unsetEnvVar(string $var): void
    {
        unset($this->env[$var]);
    }

    /**
     * Returns an environment variable that is defined only in the scope of the
     * Git command.
     *
     * @param string $var The name of the environment variable, e.g. "HOME", "GIT_SSH".
     * @param mixed $default The value returned if the environment variable is not set, defaults to
     *   null.
     *
     * @return mixed
     */
    public function getEnvVar(string $var, $default = null)
    {
        return $this->env[$var] ?? $default;
    }

    /**
     * @return mixed[]
     */
    public function getEnvVars(): array
    {
        return $this->env;
    }

    public function setTimeout(int $timeout): void
    {
        $this->timeout = $timeout;
    }

    public function getTimeout(): int
    {
        return $this->timeout;
    }

    /**
     * Set an alternate private key used to connect to the repository.
     *
     * This method sets the GIT_SSH environment variable to use the wrapper
     * script included with this library. It also sets the custom GIT_SSH_KEY
     * and GIT_SSH_PORT environment variables that are used by the script.
     *
     * @param string|null $wrapper Path the the GIT_SSH wrapper script, defaults to null which uses the
     *   script included with this library.
     */
    public function setPrivateKey(string $privateKey, int $port = 22, ?string $wrapper = null): void
    {
        if ($wrapper === null) {
            $wrapper = __DIR__ . '/../bin/git-ssh-wrapper.sh';
        }

        if (! $wrapperPath = realpath($wrapper)) {
            throw new GitException('Path to GIT_SSH wrapper script could not be resolved: ' . $wrapper);
        }

        if (! $privateKeyPath = realpath($privateKey)) {
            throw new GitException('Path private key could not be resolved: ' . $privateKey);
        }

        $this->setEnvVar('GIT_SSH', $wrapperPath);
        $this->setEnvVar('GIT_SSH_KEY', $privateKeyPath);
        $this->setEnvVar('GIT_SSH_PORT', $port);
    }

    /**
     * Unsets the private key by removing the appropriate environment variables.
     */
    public function unsetPrivateKey(): void
    {
        $this->unsetEnvVar('GIT_SSH');
        $this->unsetEnvVar('GIT_SSH_KEY');
        $this->unsetEnvVar('GIT_SSH_PORT');
    }

    public function addOutputListener(GitOutputListenerInterface $gitOutputListener): void
    {
        $this->getDispatcher()
            ->addListener(GitEvents::GIT_OUTPUT, [$gitOutputListener, 'handleOutput']);
    }

    public function addLoggerEventSubscriber(GitLoggerEventSubscriber $gitLoggerEventSubscriber): void
    {
        $this->getDispatcher()
            ->addSubscriber($gitLoggerEventSubscriber);
    }

    public function removeOutputListener(GitOutputListenerInterface $gitOutputListener): void
    {
        $this->getDispatcher()
            ->removeListener(GitEvents::GIT_OUTPUT, [$gitOutputListener, 'handleOutput']);
    }

    /**
     * Set whether or not to stream real-time output to STDOUT and STDERR.
     */
    public function streamOutput(bool $streamOutput = true): void
    {
        if ($streamOutput && ! isset($this->gitOutputListener)) {
            $this->gitOutputListener = new GitOutputStreamListener();
            $this->addOutputListener($this->gitOutputListener);
        }

        if (! $streamOutput && isset($this->gitOutputListener)) {
            $this->removeOutputListener($this->gitOutputListener);
            unset($this->gitOutputListener);
        }
    }

    /**
     * Returns an object that interacts with a working copy.
     *
     * @param string $directory Path to the directory containing the working copy.
     */
    public function workingCopy(string $directory): GitWorkingCopy
    {
        return new GitWorkingCopy($this, $directory);
    }

    /**
     * Returns the version of the installed Git client.
     */
    public function version(): string
    {
        return $this->git('--version');
    }

    /**
     * For example, passing the "git@github.com:cpliakas/git-wrapper.git"
     * repository would return "git-wrapper".
     */
    public static function parseRepositoryName(string $repositoryUrl): string
    {
        $scheme = parse_url($repositoryUrl, PHP_URL_SCHEME);

        if ($scheme === null) {
            $parts = explode('/', $repositoryUrl);
            $path = end($parts);
        } else {
            $strpos = strpos($repositoryUrl, ':');
            $path = substr($repositoryUrl, $strpos + 1);
        }

        /** @var string $path */
        return basename($path, '.git');
    }

    /**
     * Executes a `git init` command.
     *
     * Create an empty git repository or reinitialize an existing one.
     *
     * @param mixed[] $options An associative array of command line options.
     */
    public function init(string $directory, array $options = []): GitWorkingCopy
    {
        $git = $this->workingCopy($directory);
        $git->init($options);
        $git->setCloned(true);

        return $git;
    }

    /**
     * Executes a `git clone` command and returns a working copy object.
     *
     * Clone a repository into a new directory. Use @see GitWorkingCopy::cloneRepository()
     * instead for more readable code.
     *
     * @param string $repository The Git URL of the repository being cloned.
     * @param string $directory The directory that the repository will be cloned into. If null is
     *   passed, the directory will automatically be generated from the URL via
     *   the GitWrapper::parseRepositoryName() method.
     * @param mixed[] $options An associative array of command line options.
     */
    public function cloneRepository(string $repository, ?string $directory = null, array $options = []): GitWorkingCopy
    {
        if ($directory === null) {
            $directory = self::parseRepositoryName($repository);
        }

        $git = $this->workingCopy($directory);
        $git->cloneRepository($repository, $options);
        $git->setCloned(true);
        return $git;
    }

    /**
     * The command is simply a raw command line entry for everything after the Git binary.
     * For example, a `git config -l` command would be passed as `config -l` via the first argument of this method.
     *
     * @return string The STDOUT returned by the Git command.
     */
    public function git(string $commandLine, ?string $cwd = null): string
    {
        $command = new GitCommand($commandLine);
        $command->executeRaw(is_string($commandLine));
        $command->setDirectory($cwd);
        return $this->run($command);
    }

    /**
     * @return string The STDOUT returned by the Git command.
     */
    public function run(GitCommand $gitCommand, ?string $cwd = null): string
    {
        $process = new GitProcess($this, $gitCommand, $cwd);
        $process->run(function ($type, $buffer) use ($process, $gitCommand): void {
            $event = new GitOutputEvent($this, $process, $gitCommand, $type, $buffer);
            $this->getDispatcher()->dispatch(GitEvents::GIT_OUTPUT, $event);
        });

        return $gitCommand->notBypassed() ? $process->getOutput() : '';
    }
}


<?php declare(strict_types=1);

namespace GitWrapper;

final class GitCommand
{
    /**
     * Path to the directory containing the working copy. If this variable is
     * set, then the process will change into this directory while the Git
     * command is being run.
     *
     * @var string|null
     */
    private $directory;

    /**
     * The command being run, e.g. "clone", "commit", etc.
     *
     * @var string
     */
    private $command = '';

    /**
     * Whether command execution should be bypassed.
     *
     * @var bool
     */
    private $bypass = false;

    /**
     * Whether to execute the raw command without escaping it. This is useful
     * for executing arbitrary commands, e.g. "status -s". If this is true,
     * any options and arguments are ignored.
     *
     * @var bool
     */
    private $executeRaw = false;

    /**
     * @var mixed[]
     */
    private $options = [];

    /**
     * @var mixed[]
     */
    private $args = [];

    /**
     * @param mixed ...$argsAndOptions
     */
    public function __construct(string $command = '', ...$argsAndOptions)
    {
        $this->command = $command;

        foreach ($argsAndOptions as $argOrOption) {
            if (is_array($argOrOption)) {
                // If item is array, set it as the options
                $this->setOptions($argOrOption);
            } else {
                // Pass all other as the Git command arguments
                $this->addArgument($argOrOption);
            }
        }
    }

    /**
     * Returns Git command being run, e.g. "clone", "commit", etc.
     */
    public function getCommand(): string
    {
        return $this->command;
    }

    public function setDirectory(?string $directory): void
    {
        $this->directory = $directory;
    }

    public function getDirectory(): ?string
    {
        return $this->directory;
    }

    /**
     * A bool flagging whether to skip running the command.
     */
    public function bypass(bool $bypass = true): void
    {
        $this->bypass = $bypass;
    }

    /**
     * Set whether to execute the command as-is without escaping it.
     */
    public function executeRaw(bool $executeRaw = true): void
    {
        $this->executeRaw = $executeRaw;
    }

    /**
     * Returns true if the Git command should be run.
     */
    public function notBypassed(): bool
    {
        return ! $this->bypass;
    }

    /**
     * Builds the command line options for use in the Git command.
     *
     * @return mixed[]
     */
    public function buildOptions(): array
    {
        $options = [];
        foreach ($this->options as $option => $values) {
            foreach ((array) $values as $value) {
                // Render the option.
                $prefix = strlen($option) !== 1 ? '--' : '-';
                $options[] = $prefix . $option;

                // Render apend the value if the option isn't a flag.
                if ($value !== true) {
                    $options[] = $value;
                }
            }
        }

        return $options;
    }

    /**
     * @param mixed[]|string|true $value The option's value, pass true if the options is a flag.
     */
    public function setOption(string $option, $value): void
    {
        $this->options[$option] = $value;
    }

    /**
     * @param mixed[] $options
     */
    public function setOptions(array $options): void
    {
        foreach ($options as $option => $value) {
            $this->setOption($option, $value);
        }
    }

    public function setFlag(string $option): void
    {
        $this->setOption($option, true);
    }

    /**
     * @param mixed $default
     * @return mixed
     */
    public function getOption(string $option, $default = null)
    {
        return $this->options[$option] ?? $default;
    }

    public function unsetOption(string $option): void
    {
        unset($this->options[$option]);
    }

    public function addArgument(string $arg): void
    {
        $this->args[] = $arg;
    }

    /**
     * Renders the arguments and options for the Git command.
     *
     * @return string|mixed[]
     */
    public function getCommandLine()
    {
        if ($this->executeRaw) {
            return $this->getCommand();
        }

        $command = array_merge([$this->getCommand()], $this->buildOptions(), $this->args);

        return array_filter($command, 'strlen');
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 */

namespace Humbug;

use Symfony\Component\Finder\Finder;

class Generator
{
    /**
     * The collection of possible mutations stored as sets of mutation
     * instructions (allowing us to apply and reverse mutations on the fly)
     *
     * @var \Humbug\Mutable[]
     */
    protected $mutables = [];

    /**
     * Given a file iterator pass each to a \Humbug\Mutable
     * instance which is used to generate mutations and store
     * the instructions for applying and reversing them as
     * a set of mutables (instances of \Humbug\Mutation).
     *
     * @return void
     */
    public function generate(Finder $finder)
    {
        foreach ($finder as $file) {
            $mutable = new Mutable($file->getRealpath());

            $this->mutables[] = $mutable;
        }
    }

    /**
     * Return an array of mutable files.
     *
     * @return \Humbug\Mutable[]
     */
    public function getMutables()
    {
        return $this->mutables;
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 *
 * @author     rafal.wartalski@gmail.com
 */

namespace Humbug\Adapter\Phpunit;

use Humbug\Exception\RuntimeException;

class ConfigurationLocator
{
    /**
     * @param $configurationDir
     * @return string
     *
     */
    public function locate($configurationDir)
    {
        $conf = $configurationDir . '/phpunit.xml';

        if (file_exists($conf)) {
            return realpath($conf);
        }

        if (file_exists($conf . '.dist')) {
            return realpath($conf . '.dist');
        }

        throw new RuntimeException('Unable to locate phpunit.xml(.dist) file. This is required by Humbug.');
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 */

namespace Humbug\Renderer;

use Humbug\TestSuite\Mutant\Collector;
use Humbug\TestSuite\Mutant\Result as MutantResult;
use Humbug\TestSuite\Unit\Result as TestSuiteResult;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\FormatterHelper;

class Text
{
    protected $output;

    protected $progressCount = 0;

    protected $type = OutputInterface::OUTPUT_NORMAL;

    protected $buffer = '';

    protected $useBuffer = false;

    public function __construct(OutputInterface $output, $buffer = false, $colors = true)
    {
        $this->output = $output;
        if (!$colors) {
            $this->type = OutputInterface::OUTPUT_PLAIN;
        }
        $this->useBuffer = $buffer;
    }

    /**
     * @param string $string
     */
    public function write($string, $eol = true)
    {
        if ($eol) {
            $this->output->writeln($string, $this->type);
        } else {
            $this->output->write($string, false, $this->type);
        }
        if ($this->useBuffer === true) {
            $this->buffer .= strip_tags($string) . ($eol? PHP_EOL : '');
        }
    }

    public function getBuffer()
    {
        $return = $this->buffer;
        $this->buffer = '';
        return $return;
    }

    /**
     * Render message preceding the initial test run
     */
    public function renderPreTestIntroduction()
    {
        $this->write('Humbug running test suite to generate logs and code coverage data...');
    }

    /**
     * Render message where the initial test run did not pass (excl. incomplete/skipped/risky tests)
     *
     * @param TestSuiteResult $result
     */
    public function renderInitialRunFail(TestSuiteResult $result)
    {
        $error = [];
        $error[] = 'Tests must be in a fully passing state before Humbug is run.';
        $error[] = 'Incomplete, skipped or risky tests are allowed.';
        if ($result->getExitCode() !== 0) {
            $error[] = 'The testing framework reported an exit code of ' . $result->getExitCode() . '.';
        }
        if ($result->hasFailure()) {
            $error[] = 'The testing framework ran into a failure or error. Refer to output below.';
        }
        if ($result->hasStdOut()) {
            $error[] = 'Stdout:';
            $error = array_merge($error, $this->indent($this->extractFail($result->getStdOut()), true));
        }
        if ($result->hasStdErr()) {
            $error[] = 'Stderr:';
            $error = array_merge($error, $this->indent($result->getStdErr(), true));
        }
        foreach ($error as $err) {
            $this->write('<fg=red>' . $err . '</fg=red>');
        }
    }

    /**
     * Render message where the initial test run didn't pass (excl. incomplete/skipped/risky tests)
     *
     * @param TestSuiteResult $result
     * @param int $testCount
     */
    public function renderInitialRunPass(TestSuiteResult $result, $testCount = null)
    {
        $this->write('Humbug has completed the initial test run successfully.');
        if (!is_null($testCount)) {
            $this->write(
                'Tests: <fg=cyan>' . $testCount . '</fg=cyan> '
                . 'Line Coverage: <fg=cyan>' . sprintf('%3.2f%%', $result->getLineCoverage()) . '</fg=cyan>'
            );
        }
    }

    /**
     * Render message that Humbug is analysing files.
     */
    public function renderStaticAnalysisStart()
    {
        $this->write('Humbug is analysing source files...');
    }

    /**
     * Render message that mutation testing loop is starting
     *
     * @param int $count
     */
    public function renderMutationTestingStart($count)
    {
        $this->write('Mutation Testing is commencing on ' . $count . ' files...');
        $this->write(
            '(<options=bold>.</options=bold>: killed, '
            . '<fg=red;options=bold>M</fg=red;options=bold>: escaped, '
            . '<fg=blue;options=bold>S</fg=blue;options=bold>: uncovered, '
            . '<fg=yellow;options=bold>E</fg=yellow;options=bold>: fatal error, '
            . '<fg=cyan;options=bold>T</fg=cyan;options=bold>: timed out)'
        );
    }

    /**
     * Render a progress marker:
     *  T: The test run timed out, possibly due to an infinite loop or underestimated timeout
     *  E: The test run hit a fatal error, either kicked out from a test or due to a Humbug issue
     *  M: The test run was successful. The mutation went undetected by the unit tests.
     *  .: The test run included a fail condition. The mutation was detected!!!
     *
     * @param MutantResult $result
     * @param int $count
     * @param int $current
     * @param int $eolInterval
     */
    public function renderProgressMark(MutantResult $result, $count, $current, $eolInterval = 60)
    {
        $this->progressCount++;

        switch ($result->getResult()) {
            case MutantResult::TIMEOUT:
                $this->write('<fg=cyan;options=bold>T</fg=cyan;options=bold>', false);
                break;
            case MutantResult::ERROR:
                $this->write('<fg=yellow;options=bold>E</fg=yellow;options=bold>', false);
                break;
            case MutantResult::ESCAPE:
                $this->write('<fg=red;options=bold>M</fg=red;options=bold>', false);
                break;
            case MutantResult::KILL:
            default:
                $this->write('<options=bold>.</options=bold>', false);
                break;
        }

        if (($this->progressCount % $eolInterval) == 0) {
            $counter = str_pad($this->progressCount, 5, ' ', STR_PAD_LEFT);
            $this->write(
                ' |' . $counter . ' ('
                . str_pad($current, strlen($count), ' ', STR_PAD_LEFT)
                . '/' . $count . ')' . PHP_EOL,
                false
            );
        }
    }

    /**
     * Render a shadow marker, i.e. a mutation whose source code line is
     * not covered by any test based on current code coverage data.
     *
     * @param int $count
     * @param int $current
     * @param int $eolInterval
     */
    public function renderShadowMark($count, $current, $eolInterval = 60)
    {
        $this->progressCount++;
        $this->write('<fg=blue;options=bold>S</fg=blue;options=bold>', false);
        if (($this->progressCount % $eolInterval) == 0) {
            $counter = str_pad($this->progressCount, 5, ' ', STR_PAD_LEFT);
            $this->write(
                ' |' . $counter . ' ('
                . str_pad($current, strlen($count), ' ', STR_PAD_LEFT)
                . '/' . $count . ')' . PHP_EOL,
                false
            );
        }
    }

    /**
     * Render performance data for the mutation testing run
     *
     * @param string $time
     * @param string $memory
     */
    public function renderPerformanceData($time, $memory)
    {
        $this->write(
            'Time: <fg=cyan>' . $time . '</fg=cyan> '
            . 'Memory: <fg=cyan>' . $memory . '</fg=cyan>'
        );
    }

    /**
     * Render message that mutation testing loop is starting
     *
     * @param Collector $collector
     */
    public function renderSummaryReport(Collector $collector)
    {
        $pkills = str_pad($collector->getKilledCount(), 8, ' ', STR_PAD_LEFT);
        $pescapes = str_pad($collector->getEscapeCount(), 8, ' ', STR_PAD_LEFT);
        $perrors = str_pad($collector->getErrorCount(), 8, ' ', STR_PAD_LEFT);
        $ptimeouts = str_pad($collector->getTimeoutCount(), 8, ' ', STR_PAD_LEFT);
        $pshadows = str_pad($collector->getShadowCount(), 8, ' ', STR_PAD_LEFT);
        $this->write(PHP_EOL, false);
        $this->write($collector->getTotalCount() . ' mutations were generated:');
        $this->write($pkills . ' mutants were killed');
        $this->write($pshadows . ' mutants were not covered by tests');
        $this->write($pescapes . ' covered mutants were not detected');
        $this->write($perrors . ' fatal errors were encountered');
        $this->write($ptimeouts . ' time outs were encountered');
        $this->write(PHP_EOL, false);
        $vanquishedTotal = $collector->getVanquishedTotal();
        $measurableTotal = $collector->getMeasurableTotal();
        if ($measurableTotal !== 0) {
            $detectionRateTested  = round(100 * ($vanquishedTotal / $measurableTotal));
        } else {
            $detectionRateTested  = 0;
        }
        if ($collector->getTotalCount() !== 0) {
            $coveredRate = round(100 * (($measurableTotal) / $collector->getTotalCount()));
            $detectionRateAll = round(100 * ($vanquishedTotal / $collector->getTotalCount()));
        } else {
            $coveredRate = 0;
            $detectionRateAll = 0;
        }
        $this->write('Metrics:');
        $this->write('    Mutation Score Indicator (MSI): <options=bold>' . $detectionRateAll . '%</options=bold>');
        $this->write('    Mutation Code Coverage: <options=bold>' . $coveredRate . '%</options=bold>');
        $this->write('    Covered Code MSI: <options=bold>' . $detectionRateTested . '%</options=bold>');
        $this->write(PHP_EOL, false);
        $this->write('Remember that some mutants will inevitably be harmless (i.e. false positives).');
    }

    /**
     * Render JSON logging message
     *
     * @param string $log
     */
    public function renderLogToJson($log)
    {
        $this->write('Humbug results are being logged as JSON to: <options=bold>' . $log . '</options=bold>');
    }

    /**
     * Render JSON logging message
     *
     * @param string $log
     */
    public function renderLogToText($log)
    {
        $this->write('Humbug results are being logged as TEXT to: <options=bold>' . $log . '</options=bold>');
    }

    /**
     * Utility function to prefix output lines with an indent
     *
     * @param string $output
     * @param bool $asArray
     * @return string
     */
    private function indent($output, $asArray = false)
    {
        $lines = explode("\n", $output);
        $out = [];
        foreach ($lines as $line) {
            $out[] = '   > ' . $line;
        }
        if ($asArray) {
            return $out;
        }
        $return = implode("\n", $out);
        return $return;
    }

    /**
     * Extract failure details and reformat into human readable form.
     *
     * @param  string $output
     * @return string
     */
    private function extractFail($output)
    {
        if (preg_match('%##teamcity\[testFailed.*\]%', $output, $matches)) {
            preg_match(
                "/##teamcity\\[testFailed.*name='(.*)' message='(.*)' details='\\s*(.*)' flowId=.*/",
                $output,
                $matches
            );
            $matches = $this->replaceEscapedChars($matches);
            $fail = sprintf(
                'Test Name: %s' . PHP_EOL . 'Failure Message: %s' . PHP_EOL . 'Trace:' . PHP_EOL . '%s',
                $matches[1],
                $matches[2],
                $matches[3]
            );
            return $fail;
        }
        return 'No failure output was detected by Humbug, but a failure was reported by PHPUnit.';
    }

    private function replaceEscapedChars(array $chars)
    {
        return str_replace(
            ["|n ", "|n", "|'", "|\""],
            ["\n", "\n", "'", "\""],
            $chars
        );
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 */

namespace Humbug\Mutator\Arithmetic;

use Humbug\Mutator\MutatorAbstract;

class Addition extends MutatorAbstract
{

    /**
     * Replace plus sign (+) with minus sign (-)
     *
     * @param array $tokens
     * @param int $index
     * @return array
     */
    public static function getMutation(array &$tokens, $index)
    {
        $tokens[$index] = '-';
    }

    /**
     * Not all additions can be mutated.
     *
     * The PHP language allows union of arrays : $var = ['foo' => true] + ['bar' => true]
     * see http://php.net/manual/en/language.operators.array.php for details.
     *
     * So for this case, we can't create a mutation.
     *
     * @param array $tokens
     * @param $index
     * @return bool
     */
    public static function mutates(array &$tokens, $index)
    {
        $t = $tokens[$index];
        if (!is_array($t) && $t == '+') {
            $tokenCount = count($tokens);
            for ($i = $index + 1; $i < $tokenCount; $i++) {
                // check for short array syntax
                if (!is_array($tokens[$i]) && $tokens[$i][0] == '[') {
                    return false;
                }

                // check for long array syntax
                if (is_array($tokens[$i]) && $tokens[$i][0] == T_ARRAY && $tokens[$i][1] == 'array') {
                    return false;
                }

                // if we're at the end of the array
                // and we didn't see any array, we
                // can probably mutate this addition
                if (!is_array($tokens[$i]) && $tokens[$i] == ';') {
                    return true;
                }
            }
            return true;
        }

        return false;
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 */

namespace Humbug\Command;

use Humbug\Config;
use Humbug\Container;
use Humbug\Config\JsonParser;
use Humbug\Exception\InvalidArgumentException;
use Humbug\File\Collector as FileCollector;
use Humbug\File\Collection as FileCollection;
use Humbug\MutableIterator;
use Humbug\Renderer\Text;
use Humbug\TestSuite\Mutant\Builder as MutantBuilder;
use Humbug\TestSuite\Mutant\IncrementalCache;
use Humbug\TestSuite\Unit\Observers\LoggingObserver;
use Humbug\TestSuite\Unit\Observers\ProgressBarObserver;
use Humbug\TestSuite\Unit\Runner as UnitTestRunner;
use Humbug\Utility\Performance;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;

class Humbug extends Command
{
    protected $container;

    /**
     * @var MutantBuilder
     */
    protected $builder;

    /**
     * @var MutableIterator
     */
    protected $mutableIterator;

    /**
     * @var string
     */
    private $jsonLogFile;

    /**
     * @var string
     */
    private $textLogFile;

    /**
     * Execute the command.
     *
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return void|int
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        if (PHP_SAPI !== 'phpdbg' && !defined('HHVM_VERSION') && !extension_loaded('xdebug')) {
            $output->writeln(
                '<error>You need to install and enable xdebug, or use phpdbg, '
                . 'in order to allow for code coverage generation.</error>'
            );
            return 1;
        }

        Performance::upMemProfiler();

        $this->validate($input);
        $container = $this->container = new Container($input->getOptions());

        $this->doConfiguration($input);

        if ($this->isLoggingEnabled()) {
            $this->removeOldLogFiles();
        } else {
            $output->writeln('<error>No log file is specified. Detailed results '
                . 'will not be available.</error>');
            $output->write(PHP_EOL);
        }

        if ($input->getOption('incremental')) {
            $output->writeln('<error>Incremental Analysis is an experimental feature and will very likely</error>');
            $output->writeln('<error>yield inaccurate results at this time.</error>');
            $output->write(PHP_EOL);
        }

        if ($this->textLogFile) {
            $renderer = new Text($output, true);
        } else {
            $renderer = new Text($output);
        }

        /**
         * Make initial test run to ensure tests are in a starting passing state
         * and also log the results so test runs during the mutation phase can
         * be optimised.
         */
        $testSuiteRunner = new UnitTestRunner(
            $container->getAdapter(),
            $container->getAdapter()->getProcess($container, true),
            $container->getTempDirectory() . '/coverage.humbug.txt'
        );

        $testSuiteRunner->addObserver(
            new LoggingObserver(
                $renderer,
                $output,
                new ProgressBarObserver($input, $output)
            )
        );

        $result = $testSuiteRunner->run($container);

        /**
         * Check if the initial test run ended with a fatal error
         */
        if (! $result->isSuccess()) {
            return 1;
        }

        $output->write(PHP_EOL);

        /**
         * Message re Static Analysis
         */
        $renderer->renderStaticAnalysisStart();
        $output->write(PHP_EOL);

        $incrementalCache = null;
        if ($input->getOption('incremental')) {
            $incrementalCache = new IncrementalCache($container);
        }

        $mutationTestingRunner = $this->builder->build($container, $renderer, $input, $output);
        $mutationTestingRunner->run($result->getCoverage(), $this->mutableIterator, $incrementalCache);

        if ($this->isLoggingEnabled()) {
            $output->write(PHP_EOL);
        }

        if ($input->getOption('incremental')) {
            $incrementalCache->write();
        }
    }

    protected function prepareFinder($directories, $excludes, array $names = null, $paths = null)
    {
        $finder = new Finder;
        $finder->files();
        if (!is_null($names) && count($names) > 0) {
            foreach ($names as $name) {
                $finder->name($name);
            }
        } else {
            $finder->name('*.php');
        }
        if ($directories) {
            foreach ($directories as $directory) {
                $finder->in($directory);
            }
        } else {
            $finder->in('.');
        }
        if (isset($excludes)) {
            foreach ($excludes as $exclude) {
                $finder->exclude($exclude);
            }
        }

        if (!is_null($paths) && count($paths) > 0) {
            $finder->filter(function (SplFileInfo $file) use ($paths) {
                return in_array($file, $paths);
            });
        }
        return $finder;
    }

    protected function doConfiguration(InputInterface $input)
    {
        $this->container->setBaseDirectory(getcwd());
        $config = (new JsonParser())->parseFile();
        $newConfig = new Config($config);
        $source = $newConfig->getSource();

        $this->container->setSourceList($source);
        $timeout = $newConfig->getTimeout();
        if ($timeout !== null) {
            $this->container->setTimeout((int) $timeout);
        }
        $chDir = $newConfig->getChDir();
        if ($chDir !== null) {
            $this->container->setTestRunDirectory($chDir);
        }
        $this->jsonLogFile = $input->getOption('log-json') ?: $newConfig->getLogsJson();
        $this->textLogFile = $input->getOption('log-text') ?: $newConfig->getLogsText();

        $this->builder = new MutantBuilder();
        $this->builder->setLogFiles($this->textLogFile, $this->jsonLogFile);

        $finder = $this->prepareFinder(
            isset($source->directories)? $source->directories : null,
            isset($source->excludes)? $source->excludes : null,
            $input->getOption('file'),
            $input->getOption('path')
        );

        $this->mutableIterator = new MutableIterator($this->container, $finder);
    }

    protected function configure()
    {
        $this
            ->setName('run')
            ->setDescription('Run Humbug for target tests')
            ->addOption(
                'adapter',
                'a',
                InputOption::VALUE_REQUIRED,
                'Set name of the test adapter to use.',
                'phpunit'
            )
            ->addOption(
                'options',
                'o',
                InputOption::VALUE_REQUIRED,
                'Set command line options string to pass to test adapter. '
                    . 'Default is dictated dynamically by '.'Humbug'.'.'
            )
            ->addOption(
                'file',
                'f',
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
                'Pattern representing file(s) to mutate. Can set more than once.'
            )
            ->addOption(
                'path',
                'p',
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
                'Full path file(s) to mutate. If present, an intersection between this and config files will be made.'
            )
            ->addOption(
                'constraints',
                'c',
                InputOption::VALUE_REQUIRED,
                'Options set on adapter to constrain which tests are run. '
                    . 'Applies only to the very first test run.'
            )
            ->addOption(
                'timeout',
                't',
                InputOption::VALUE_REQUIRED,
                'Sets a timeout applied for each test run to combat infinite loop mutations.',
                10
            )
            ->addOption(
                'no-progress-bar',
                'b',
                InputOption::VALUE_NONE,
                'Removes dynamic output like the progress bar and performance data from output.'
            )
            ->addOption(
                'incremental',
                'i',
                InputOption::VALUE_NONE,
                'Enable incremental mutation testing by relying on cached results.'
            )
            ->addOption(
                'log-json',
                null,
                InputOption::VALUE_REQUIRED,
                'Generate log file in JSON format.'
            )
            ->addOption(
                'log-text',
                null,
                InputOption::VALUE_REQUIRED,
                'Generate log file in text format.'
            )
        ;
    }

    private function validate(InputInterface $input)
    {
        /**
         * Adapter
         */
        if ($input->getOption('adapter') !== 'phpunit') {
            throw new InvalidArgumentException(
                'Only a PHPUnit adapter is supported at this time. Sorry!'
            );
        }
        /**
         * Timeout
         */
        if (!is_numeric($input->getOption('timeout')) || $input->getOption('timeout') <= 0) {
            throw new InvalidArgumentException(
                'The timeout must be an integer specifying a number of seconds. '
                . 'A number greater than zero is expected, and greater than maximum '
                . 'test suite execution time under any given constraint option is '
                . 'highly recommended.'
            );
        }
    }

    private function removeOldLogFiles()
    {
        if (file_exists($this->jsonLogFile)) {
            unlink($this->jsonLogFile);
        }

        if (file_exists($this->textLogFile)) {
            unlink($this->textLogFile);
        }
    }

    private function isLoggingEnabled()
    {
        return $this->jsonLogFile !== null || $this->textLogFile !== null;
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 */

namespace Humbug\Mutator\ReturnValue;

use Humbug\Mutator\MutatorAbstract;

class IntegerNegation extends MutatorAbstract
{

    /**
     * Replace any integer with its sign inverted (i.e. multiply by -1)
     *
     * @param array $tokens
     * @param int $index
     * @return array
     */
    public static function getMutation(array &$tokens, $index)
    {
        $tokenCount = count($tokens);
        for ($i=$index+1; $i < $tokenCount; $i++) {
            if (is_array($tokens[$i]) && $tokens[$i][0] == T_WHITESPACE) {
                continue;
            } elseif (is_array($tokens[$i]) && $tokens[$i][0] == T_LNUMBER) {
                // Dump the negation - string for negative ints
                if (self::getPreviousToken($tokens, $i) == '-') {
                    $tokens[$i-1] = [
                        T_WHITESPACE,
                        ''
                    ];
                    break;
                }
                // otherwise multiply by -1 to make negative for positive ints
                $num = (integer) $tokens[$i][1];
                $replace = -1 * $num;
                $tokens[$i] = [
                    T_LNUMBER,
                    (string) $replace
                ];
                break;
            }
        }
    }

    public static function mutates(array &$tokens, $index)
    {
        $t = $tokens[$index];
        if (is_array($t) && $t[0] == T_RETURN) {
            $has = false;
            $tokenCount = count($tokens);
            for ($i=$index+1; $i < $tokenCount; $i++) {
                if (is_array($tokens[$i]) && $tokens[$i][0] == T_WHITESPACE) {
                    continue;
                } elseif (is_array($tokens[$i]) && $tokens[$i][0] == T_LNUMBER && $tokens[$i][1] != 0) {
                    $has = true;
                    continue;
                } elseif (!is_array($tokens[$i]) && $tokens[$i] == ';') {
                    // return statement terminated
                    if ($has === true) {
                        return true;
                    }
                    return false;
                } else {
                    $has = false;
                }
            }
        }
        return false;
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 *
 * @author     rafal.wartalski@gmail.com
 */

namespace Humbug\Command;

use Humbug\Adapter\Phpunit\ConfigurationLocator;
use Humbug\Exception\RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;

class Configure extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln(
            '<fg=green>Humbug configuration tool.' . PHP_EOL .
            'It will guide you through Humbug configuration in few seconds.</fg=green>' . PHP_EOL
        );

        if ($this->isAlreadyConfigured($input)) {
            $output->writeln(
                '<bg=red>Humbug configuration file "humbug.json.dist" already exists. '
                . 'You may use the --force option to start over.</bg=red>' . PHP_EOL
            );
            return 0;
        }

        $output->writeln('When choosing directories, you may enter each directory and press return.');
        $output->writeln('To exit directory selection, please leave the next answer blank and press return.');
        $output->write(PHP_EOL);

        $chDir = $this->resolveChDir($input, $output);

        $sourcesDirs = $this->getDirs($input, $output, 'What source directories do you want to include? : ');

        if (empty($sourcesDirs)) {
            $output->writeln('A source directory was not provided. Unable to generate "humbug.json.dist".');
            return 0;
        }

        $excludeDirs = $this->getDirs(
            $input,
            $output,
            'Any directories to exclude from within your source directories? :'
        );

        $timeout = $this->getTimeout($input, $output);

        $textLogFile = $this->getTextLogFile($input, $output);

        $jsonLogFile = $this->getJsonLogFile($input, $output);

        if (!$this->isGenerationConfirmed($input, $output)) {
            $output->writeln('<fg=red>Aborted.</fg=red>' .PHP_EOL);
            return 0;
        }

        $configuration = $this->createConfiguration(
            $sourcesDirs,
            $excludeDirs,
            $chDir,
            $timeout,
            $textLogFile,
            $jsonLogFile
        );

        $this->saveConfiguration($configuration);

        $output->writeln('Configuration file "humbug.json.dist" was created.');
    }

    protected function configure()
    {
        $this->setName('configure')
            ->addOption(
                'force',
                'f',
                InputOption::VALUE_NONE,
                'If it already exists, recreate the configuration anyway.'
            )
        ;
    }

    /**
     * @return QuestionHelper
     */
    private function getQuestionHelper()
    {
        return $this->getHelper('question');
    }

    private function isAlreadyConfigured(InputInterface $input)
    {
        if ($input->getOption('force')) {
            return false;
        }
        return file_exists('humbug.json.dist') || file_exists('humbug.json');
    }

    /**
     * @param $sourcesDirs
     * @param $excludeDirs
     * @param $chDir
     * @param $timeout
     * @param $textLogFile
     * @param $jsonLogFile
     *
     * @return \stdClass
     */
    private function createConfiguration(
        $sourcesDirs,
        $excludeDirs,
        $chDir,
        $timeout,
        $textLogFile,
        $jsonLogFile
    ) {
        $source = new \stdClass();
        $source->directories = $sourcesDirs;

        if (!empty($excludeDirs)) {
            $source->excludes = $excludeDirs;
        }

        $configuration = new \stdClass();
        $configuration->source = $source;

        if ($chDir) {
            $configuration->chdir = $chDir;
        }

        if ($timeout) {
            $configuration->timeout = $timeout;
        }

        $logs = $this->prepareLogs($textLogFile, $jsonLogFile);

        if ($logs) {
            $configuration->logs = $logs;
        }

        return $configuration;
    }

    /**
     * @param $configuration
     */
    private function saveConfiguration($configuration)
    {
        file_put_contents('humbug.json.dist', json_encode($configuration, JSON_PRETTY_PRINT));
    }

    /**
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return string|null
     *
     * @throws \RuntimeException
     */
    private function resolveChDir(InputInterface $input, OutputInterface $output)
    {
        $configurationLocator = new ConfigurationLocator();

        try {
            $configurationLocator->locate('.');
            return null;
        } catch (\RuntimeException $e) {
            $output->writeln($e->getMessage());
            $frameworkConfigurationQuestion = $this->createFrameworkConfigurationQuestion($configurationLocator);
            $chDir = $this->getQuestionHelper()->ask($input, $output, $frameworkConfigurationQuestion);

            if (!$chDir) {
                throw new RuntimeException('Could not create "humbug.json.dist". Cannot locate phpunit.xml(.dist) file.');
            }

            return $chDir;
        }
    }

    /**
     * @param ConfigurationLocator $configurationLocator
     * @return Question
     */
    private function createFrameworkConfigurationQuestion(ConfigurationLocator $configurationLocator)
    {
        $frameworkConfigurationQuestion = new Question('Where is your phpunit.xml(.dist) configuration located? : ');
        $frameworkConfigurationQuestion->setValidator(function ($answer) use ($configurationLocator) {
            $answer = trim($answer);

            if (!$answer) {
                return $answer;
            }

            if (!is_dir($answer)) {
                throw new RuntimeException(sprintf('Could not find "%s" directory.', $answer));
            }

            $configurationLocator->locate($answer);

            return $answer;
        });

        return $frameworkConfigurationQuestion;
    }

    /**
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return array
     */
    private function getDirs(InputInterface $input, OutputInterface $output, $question)
    {
        $sourcesDirs = [];

        $sourceQuestion = $this->createSourceQuestion($question);

        while ($sourceDir = $this->getQuestionHelper()->ask($input, $output, $sourceQuestion)) {
            if ($sourceDir) {
                $sourcesDirs[] = $sourceDir;
            }
        }

        return $sourcesDirs;
    }

    /**
     * @param $question
     * @return Question
     */
    private function createSourceQuestion($question)
    {
        $sourceQuestion = new Question($question);
        $sourceQuestion->setValidator(function ($answer) {
            if (trim($answer) && !is_dir($answer)) {
                throw new RuntimeException(sprintf('Could not find "%s" directory.', $answer));
            }

            return $answer;
        });
        return $sourceQuestion;
    }

    /**
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return string
     */
    private function getTimeout(InputInterface $input, OutputInterface $output)
    {
        $timeoutQuestion = $this->createTimeoutQuestion();

        $timeout = $this->getQuestionHelper()->ask($input, $output, $timeoutQuestion);

        return $timeout;
    }

    /**
     * @return Question
     */
    private function createTimeoutQuestion()
    {
        $timeoutQuestion = new Question('Single test suite timeout in seconds [10] : ', 10);
        $timeoutQuestion->setValidator(function ($answer) {
            if (!$answer || !is_numeric($answer) || (int) $answer <= 0) {
                throw new RuntimeException('Timeout should be an integer greater than 0');
            }

            return (int)$answer;
        });
        return $timeoutQuestion;
    }

    /**
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return string
     */
    private function getTextLogFile(InputInterface $input, OutputInterface $output)
    {
        $textLogQuestion = new Question('Where do you want to store the text log? [humbuglog.txt] : ', 'humbuglog.txt');
        $textLogFile = $this->getQuestionHelper()->ask($input, $output, $textLogQuestion);
        return $textLogFile;
    }

    /**
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return string
     */
    private function getJsonLogFile(InputInterface $input, OutputInterface $output)
    {
        $textLogQuestion = new Question('Where do you want to store the json log (if you need it)? : ');
        $textLogFile = $this->getQuestionHelper()->ask($input, $output, $textLogQuestion);
        return $textLogFile;
    }

    /**
     * @param $textLogFile
     * @param $jsonLogFile
     * @return null|\stdClass
     */
    private function prepareLogs($textLogFile, $jsonLogFile)
    {
        $logs = null;

        if ($textLogFile || $jsonLogFile) {
            $logs = new \stdClass();

            if ($textLogFile) {
                $logs->text = $textLogFile;
            }

            if ($jsonLogFile) {
                $logs->json = $jsonLogFile;
            }
        }

        return $logs;
    }

    /**
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return string
     */
    private function isGenerationConfirmed(InputInterface $input, OutputInterface $output)
    {
        $question = new ConfirmationQuestion('Generate "humbug.json.dist"? [Y]: ', true);
        $generationConfirmed = $this->getQuestionHelper()->ask($input, $output, $question);
        return $generationConfirmed;
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 */

namespace Humbug\Mutator\Number;

use Humbug\Mutator\MutatorAbstract;

class IntegerValue extends MutatorAbstract
{

    /**
     * Replace 1 with 0, 0 with 1, or increment.
     *
     * @param array $tokens
     * @param int $index
     * @return array
     */
    public static function getMutation(array &$tokens, $index)
    {
        $num = (integer) $tokens[$index][1];
        if ($num == 0) {
            $replace = 1;
        } elseif ($num == 1) {
            $replace = 0;
        } else {
            $replace = $num + 1;
        }
        $tokens[$index] = [
            T_LNUMBER,
            (string) $replace
        ];
    }

    public static function mutates(array &$tokens, $index)
    {
        $t = $tokens[$index];
        if (is_array($t) && $t[0] == T_LNUMBER) {
            return true;
        }
        return false;
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 */

namespace Humbug\Utility;

class Tokenizer
{
    const T_NEWLINE = -1;

    /**
     * Get tokens use token_get_all() but post process to interpolate new line
     * markers so we can check the line number of each token.
     *
     * @param string $source
     * @return array
     */
    public static function getTokens(&$source)
    {
        $newline = 0;
        $tokens = token_get_all($source);
        $ntokens = [];
        foreach ($tokens as $token) {
            $tname = is_array($token) ? $token[0] : null;
            $tdata = is_array($token) ? $token[1] : $token;
            $split = preg_split("%(\r\n|\n)%", $tdata, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
            if ($tname == T_CONSTANT_ENCAPSED_STRING || substr($tdata, 0, 2) == '/*') {
                $ntokens[] = [$tname, $tdata];
                foreach ($split as $value) {
                    if ($value == "\r\n" || $value == "\n") {
                        $newline++;
                    }
                }
                continue;
            }
            foreach ($split as $data) {
                if ($data == "\r\n" || $data == "\n") {
                    $newline++;
                    $ntokens[] = [self::T_NEWLINE, $data, $newline];
                } else {
                    $ntokens[] = is_array($token) ? [$tname, $data] : $data;
                }
            }
        }
        return $ntokens;
    }

    /**
     * Reconstruct a string of source code from its constituent tokens
     *
     * @param array $tokens
     * @return string
     */
    public static function reconstructFromTokens(array &$tokens)
    {
        $str = '';
        foreach ($tokens as $token) {
            if (is_string($token)) {
                $str .= $token;
            } else {
                $str .= $token[1];
            }
        }
        return $str;
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 */

namespace Humbug;

use Humbug\Utility\Tokenizer;

class Mutable
{

    /**
     * Name and relative path of the file to be mutated
     *
     * @var string
     */
    protected $filename = null;

    /**
     *  An array of generated mutations to be sequentially tested
     *
     * @var Mutation[]
     */
    protected $mutations = [];

    /**
     * Array of Mutators currently enabled to generate mutations
     *
     * @var array
     */
    protected $mutators = [
        // Booleans
        '\Humbug\Mutator\Boolean\TrueValue',
        '\Humbug\Mutator\Boolean\FalseValue',
        '\Humbug\Mutator\Boolean\LogicalNot',
        '\Humbug\Mutator\Boolean\LogicalAnd',
        '\Humbug\Mutator\Boolean\LogicalOr',
        '\Humbug\Mutator\Boolean\LogicalLowerAnd',
        '\Humbug\Mutator\Boolean\LogicalLowerOr',
        // Conditional Boundaries
        '\Humbug\Mutator\ConditionalBoundary\GreaterThan',
        '\Humbug\Mutator\ConditionalBoundary\GreaterThanOrEqualTo',
        '\Humbug\Mutator\ConditionalBoundary\LessThan',
        '\Humbug\Mutator\ConditionalBoundary\LessThanOrEqualTo',
        // Binary Arithmetic
        '\Humbug\Mutator\Arithmetic\Addition',
        '\Humbug\Mutator\Arithmetic\Subtraction',
        '\Humbug\Mutator\Arithmetic\Multiplication',
        '\Humbug\Mutator\Arithmetic\Division',
        '\Humbug\Mutator\Arithmetic\Modulus',
        '\Humbug\Mutator\Arithmetic\Exponentiation',
        '\Humbug\Mutator\Arithmetic\PlusEqual',
        '\Humbug\Mutator\Arithmetic\MinusEqual',
        '\Humbug\Mutator\Arithmetic\MulEqual',
        '\Humbug\Mutator\Arithmetic\DivEqual',
        '\Humbug\Mutator\Arithmetic\ModEqual',
        '\Humbug\Mutator\Arithmetic\PowEqual',
        '\Humbug\Mutator\Arithmetic\BitwiseAnd',
        '\Humbug\Mutator\Arithmetic\BitwiseOr',
        '\Humbug\Mutator\Arithmetic\BitwiseXor',
        '\Humbug\Mutator\Arithmetic\Not',
        '\Humbug\Mutator\Arithmetic\ShiftRight',
        '\Humbug\Mutator\Arithmetic\ShiftLeft',
        // Increments
        '\Humbug\Mutator\Increment\Increment',
        '\Humbug\Mutator\Increment\Decrement',
        // Negation of Conditionals
        '\Humbug\Mutator\ConditionalNegation\Equal',
        '\Humbug\Mutator\ConditionalNegation\NotEqual',
        '\Humbug\Mutator\ConditionalNegation\Identical',
        '\Humbug\Mutator\ConditionalNegation\NotIdentical',
        '\Humbug\Mutator\ConditionalNegation\GreaterThan',
        '\Humbug\Mutator\ConditionalNegation\GreaterThanOrEqualTo',
        '\Humbug\Mutator\ConditionalNegation\LessThan',
        '\Humbug\Mutator\ConditionalNegation\LessThanOrEqualTo',
        // Explicit Numbers
        '\Humbug\Mutator\Number\IntegerValue',
        '\Humbug\Mutator\Number\FloatValue',
        // Return Values
        '\Humbug\Mutator\ReturnValue\This',
        '\Humbug\Mutator\ReturnValue\IntegerValue',
        //'\Humbug\Mutator\ReturnValue\Float',
        '\Humbug\Mutator\ReturnValue\IntegerNegation',
        '\Humbug\Mutator\ReturnValue\FloatNegation',
        '\Humbug\Mutator\ReturnValue\NewObject',
        '\Humbug\Mutator\ReturnValue\FunctionCall',
        '\Humbug\Mutator\ReturnValue\BracketedStatement',
        // Negation of native function calls in if statements (assuming no ops/comps)
        '\Humbug\Mutator\IfStatement\FunctionCallNegation',
    ];

    /**
     * Constructor; sets name and relative path of the file being mutated
     *
     * @param string $filename
     */
    public function __construct($filename = null)
    {
        $this->setFilename($filename);
    }

    /**
     * Based on the current file, generate mutations
     *
     * @return void
     */
    public function generate()
    {
        $source = file_get_contents($this->getFilename());
        $tokens = Tokenizer::getTokens($source);
        $lineNumber = 1;
        $methodName = '???';
        $className = '???';
        $namespace = '';
        $inMethod = false;
        $inMethodBlock = false;
        $methodCurlyCount = 0;
        $tokenCount = count($tokens);
        foreach ($tokens as $index => $token) {
            if (is_array($token) && $token[0] == Tokenizer::T_NEWLINE) {
                $lineNumber = $token[2] + 1;
                continue;
            }

            if (is_array($token) && $token[0] == T_NAMESPACE) {
                for ($j=$index+1; $j<$tokenCount; $j++) {
                    if ($tokens[$j][0] == T_STRING) {
                        $namespace .= '\\' . $tokens[$j][1];
                    } elseif ($tokens[$j] == '{' || $tokens[$j] == ';') {
                        break;
                    }
                }
                continue;
            }

            if (is_array($token) && ($token[0] == T_CLASS || $token[0] == T_INTERFACE || $token[0] == T_TRAIT)
            && $tokens[$index-1][0] !== T_DOUBLE_COLON) {
                $className = $namespace . '\\' . $tokens[$index+2][1];
                continue;
            }

            //TODO: handle whitespace!
            if (is_array($token) && $token[0] == T_FUNCTION && '???' !== $className) {
                if (!isset($tokens[$index+2][1])) {
                    continue; // ignore closure
                }
                $methodName = $tokens[$index+2][1];
                $inMethod = true;
                continue;
            }

            /**
             * Limit mutation generation to the interior of methods (for now!)
             */
            if (true === $inMethod && false === $inMethodBlock && $methodCurlyCount == 0
            && !($token == '{' || (is_array($token) && $token[0] == T_CURLY_OPEN))) {
                continue; //skip over argument list
            }
            if ($inMethod) {
                if ($token == '{' || (is_array($token) && $token[0] == T_CURLY_OPEN)) {
                    $methodCurlyCount += 1;
                    $inMethodBlock = true;
                    continue;
                } elseif ($token == '}') {
                    $methodCurlyCount -= 1;
                    continue;
                }
                if ($methodCurlyCount == 0) {
                    $inMethodBlock = false;
                    $inMethod = false;
                    continue;
                }
            }

            if (true === $inMethodBlock) {
                foreach ($this->mutators as $mutator) {
                    if ($mutator::mutates($tokens, $index)) {
                        $this->mutations[] = new Mutation(
                            $this->getFilename(),
                            $lineNumber,
                            $className,
                            $methodName,
                            $index,
                            $mutator
                        );
                    }
                }
            }
        }
        return $this;
    }

    /**
     * Cleanup routines
     */
    public function cleanup()
    {
        unset($this->mutations);
        gc_collect_cycles();
    }

    /**
     * Set the file path of the file which is currently being assessed for
     * mutations.
     *
     * @param string $filename
     */
    public function setFilename($filename)
    {
        $this->filename = $filename;
    }

    /**
     * Return the file path of the file which is currently being assessed for
     * mutations.
     *
     * @return string
     */
    public function getFilename()
    {
        return $this->filename;
    }

    /**
     * Get an array of Class & Method indexed mutations containing the mutated
     * token and that token's index in the method's block code.
     *
     * @return Mutation[]
     */
    public function getMutations()
    {
        return $this->mutations;
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 */

namespace Humbug\Mutator\ReturnValue;

use Humbug\Mutator\MutatorAbstract;

class This extends MutatorAbstract
{

    /**
     * Replace (return $this;) with (return null;)
     *
     * @param array $tokens
     * @param int $index
     * @return array
     */
    public static function getMutation(array &$tokens, $index)
    {
        $tokenCount = count($tokens);
        for ($i=$index+1; $i < $tokenCount; $i++) {
            if (is_array($tokens[$i]) && $tokens[$i][0] == T_WHITESPACE) {
                continue;
            } elseif (is_array($tokens[$i]) && $tokens[$i][0] == T_VARIABLE && $tokens[$i][1] == '$this') {
                $tokens[$i] = [
                    T_STRING,
                    'null'
                ];
                break;
            }
        }
    }

    public static function mutates(array &$tokens, $index)
    {
        $t = $tokens[$index];
        if (is_array($t) && $t[0] == T_RETURN) {
            $hasThis = false;
            // effectively, we look for 'return $this;'. Anything else in there and we get out.
            $tokenCount = count($tokens);
            for ($i=$index+1; $i < $tokenCount; $i++) {
                if (is_array($tokens[$i]) && $tokens[$i][0] == T_WHITESPACE) {
                    continue;
                } elseif (is_array($tokens[$i]) && $tokens[$i][0] == T_VARIABLE && $tokens[$i][1] == '$this') {
                    $hasThis = true;
                    continue;
                } elseif (!is_array($tokens[$i]) && $tokens[$i] == ';') {
                    // return statement terminated
                    if ($hasThis === true) {
                        return true;
                    }
                    return false;
                } else {
                    $hasThis = false;
                }
            }
        }
        return false;
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 */

namespace Humbug\Mutator\ReturnValue;

use Humbug\Mutator\MutatorAbstract;
use Humbug\Utility\Tokenizer;

class BracketedStatement extends MutatorAbstract
{

    /**
     * Replace any return statement contained in brackets with null (but retain
     * the statement and move to before the return call).
     * This isn't perfect - the statement might evaluate to null anyway.
     *
     * @param array $tokens
     * @param int $index
     * @return array
     */
    public static function getMutation(array &$tokens, $index)
    {
        $replace = [];
        $last = null;
        $tokenCount = count($tokens);
        for ($i=$index+1; $i < $tokenCount; $i++) {
            if (is_array($tokens[$i]) && $tokens[$i][0] == T_WHITESPACE) {
                continue;
            } elseif (!is_array($tokens[$i]) && $tokens[$i] == '(') {
                // collect statement tokens (skipping one whitespace after 'return')
                for ($j=$index+2; $j < $tokenCount; $j++) {
                    if (!is_array($tokens[$j]) && $tokens[$j] == ';') {
                        $last = $j - 1;
                        break;
                    }
                    $replace[$j] = $tokens[$j];
                }
                // replace them all with blanks and set last to 'null'
                foreach ($replace as $k => $t) {
                    if ($k == $last) {
                        $tokens[$k] = [
                            T_STRING,
                            'null'
                        ];
                    } else {
                        $tokens[$k] = '';
                    }
                }
                // shift the instantiation prior to the return statement to
                // preserve instantiation behaviour without overwriting anything
                // and without upsetting line count.
                $replace[] = ';';
                $replace[] = [T_WHITESPACE, ' '];
                $string = ['-1' => Tokenizer::reconstructFromTokens($replace)];
                array_splice($tokens, $index, 0, $string);
                break;
            }
        }
    }

    public static function mutates(array &$tokens, $index)
    {
        $t = $tokens[$index];
        if (is_array($t) && $t[0] == T_RETURN) {
            if (self::getNextToken($tokens, $index, [T_WHITESPACE]) === '(') {
                return true;
            }
        }
        return false;
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 */

namespace Humbug\Mutator\Number;

use Humbug\Mutator\MutatorAbstract;

class FloatValue extends MutatorAbstract
{

    /**
     * Replace 0.0 with 1.0, 1.0 with 0.0, and float between 1 and 2 is incremented
     * by one, and any float greater than 2 is replaced with 1.0.
     *
     * @param array $tokens
     * @param int $index
     * @return array
     */
    public static function getMutation(array &$tokens, $index)
    {
        $num = (float) $tokens[$index][1];
        if ($num == 0) {
            $replace = 1.0;
        } elseif ($num == 1) {
            $replace = 0.0;
        } elseif ($num < 2) {
            $replace = $num + 1;
        } else {
            $replace = 1.0;
        }
        $tokens[$index] = [
            T_DNUMBER,
            sprintf("%.2f", $replace)
        ];
    }

    public static function mutates(array &$tokens, $index)
    {
        $t = $tokens[$index];
        if (is_array($t) && $t[0] == T_DNUMBER) {
            return true;
        }
        return false;
    }
}


<?php
/**
 * Humbug
 *
 * @category   Humbug
 * @package    Humbug
 * @copyright  Copyright (c) 2015 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    https://github.com/padraic/humbug/blob/master/LICENSE New BSD License
 */

namespace Humbug\Adapter;

use Humbug\Container;
use Symfony\Component\Process\Process;

abstract class AdapterAbstract
{
    protected $okCount = 0;

    /**
     * Runs the tests suite according to Runner set options and the execution
     * order of test case (if any). It then returns an array of two elements.
     * First element is a boolean result value indicating if tests passed or not.
     * Second element is an array containing the key "stdout" which stores the
     * output from the last test run.
     *
     * @param   Container         $container
     * @param   bool              $firstRun
     * @param   null|string       $interceptFile
     * @param   null|string       $mutantFile
     * @param   array             $testCases
     * @return  Process
     */
    abstract public function getProcess(
        Container $container,
        $firstRun = false,
        $interceptFile = null,
        $mutantFile = null,
        array $testCases = []
    );

    abstract public function getClassFile($class, Container $container);

    /**
     * Parse the test adapter result output to see if there were any failures.
     * In the context of mutation testing, a test failure is good (i.e. the
     * mutation was detected by the test suite).
     *
     * This assumes the output is in Test Anywhere Protocol (TAP) format.
     *
     * @param string $output
     * @return bool True if the test passed, false if it failed
     */
    public function ok($output)
    {
        return 0 === preg_match("%##teamcity\[testFailed%", $output);
    }

    /**
     * Parse the test adapter result output and count ok results.
     *
     * This assumes the output is in Test Anywhere Protocol (TAP) format.
     *
     * @param string $output
     * @return bool|int
     */
    public function hasOks($output)
    {
        $result = preg_match_all("%##teamcity\[testFinished%", $output);
        if ($result) {
            $this->okCount += $result;
            return $this->okCount;
        }
        return false;
    }
}