diff --git a/.travis.yml b/.travis.yml index a538f58..acdf808 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,11 @@ php: - 5.5 - 5.4 +matrix: + include: + - php: 5.4 + env: RPC_TEST=true BITCOIN_VERSION=0.15.0 + branches: only: - 1.x @@ -17,11 +22,30 @@ notifications: irc: "chat.freenode.net#dspacelabs" install: + - | + if [ "$BITCOIN_VERSION" != "" ] && [ ! -e "${HOME}/bitcoin" ]; then + mkdir ${HOME}/bitcoin + fi + - | + if [ "$BITCOIN_VERSION" != "" ] && [ ! -e "${HOME}/bitcoin/bitcoin-$BITCOIN_VERSION" ]; then + cd ${HOME}/bitcoin && + rm bitcoin-* -rf && + wget https://bitcoin.org/bin/bitcoin-core-${BITCOIN_VERSION}/bitcoin-${BITCOIN_VERSION}-x86_64-linux-gnu.tar.gz && + mv bitcoin-${BITCOIN_VERSION}-x86_64-linux-gnu.tar.gz bitcoin.tar.gz && + tar xvf bitcoin.tar.gz && + cd ${TRAVIS_BUILD_DIR} + else + echo "Had bitcoind" + fi - composer require "codeclimate/php-test-reporter:*" -n - composer install script: - php bin/phpunit + - | + if [ "$RPC_TEST" != "" ]; then + BITCOIND_PATH="$HOME/bitcoin/bitcoin-$BITCOIN_VERSION/bin/bitcoind" php bin/phpunit -c rpc.phpunit.xml + fi after_script: - bin/test-reporter --stdout > codeclimate.json diff --git a/composer.json b/composer.json index e59f571..d0de1ed 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,10 @@ "psr-4": { "Nbobtc\\": "src/" } }, "autoload-dev": { - "psr-4": { "Tests\\Nbobtc\\": "tests/" } + "psr-4": { + "Tests\\Nbobtc\\": "tests/", + "RpcTests\\Nbobtc\\": "tests-rpc/" + } }, "extra": { "branch-alias": { diff --git a/rpc.phpunit.xml b/rpc.phpunit.xml new file mode 100644 index 0000000..279a319 --- /dev/null +++ b/rpc.phpunit.xml @@ -0,0 +1,37 @@ + + + + + + tests-rpc/ + + + + + + src/ + + + + + + + + + + + + diff --git a/tests-rpc/Bitcoind.php b/tests-rpc/Bitcoind.php new file mode 100644 index 0000000..a2cd22d --- /dev/null +++ b/tests-rpc/Bitcoind.php @@ -0,0 +1,208 @@ + 1, + "server" => 1, + "regtest" => 1, + ]; + + private $options = []; + + /** + * RpcServer constructor. + * @param $bitcoind + * @param string $dataDir + * @param Credential $credential + * @param array $options + */ + public function __construct($bitcoind, $dataDir, Credential $credential, array $options = []) + { + $this->bitcoind = $bitcoind; + $this->dataDir = $dataDir; + $this->credential = $credential; + $this->options = array_merge($options, $this->defaultOptions); + } + + /** + * @return string + */ + private function getPidFile() + { + return "{$this->dataDir}/regtest/bitcoind.pid"; + } + + /** + * @return string + */ + private function getConfigFile() + { + return "{$this->dataDir}/bitcoin.conf"; + } + + /** + * @param Credential $rpcCredential + */ + private function writeConfigToFile(Credential $rpcCredential) + { + $fd = fopen($this->getConfigFile(), "w"); + if (!$fd) { + throw new \RuntimeException("Failed to open bitcoin.conf for writing"); + } + + $config = array_merge( + $this->options, + $rpcCredential->getConfigArray() + ); + + $iniConfig = implode("\n", array_map(function ($value, $key) { + return "{$key}={$value}"; + }, $config, array_keys($config))); + + if (!fwrite($fd, $iniConfig)) { + throw new \RuntimeException("Failed to write to bitcoin.conf"); + } + + fclose($fd); + } + + /** + * Start bitcoind and + * @return void + */ + public function start() + { + if ($this->isRunning()) { + return; + } + + $this->writeConfigToFile($this->credential); + $res = 0; + $out = ''; + exec(sprintf("%s -datadir=%s", $this->bitcoind, $this->dataDir), $out, $res); + + if ($res !== 0) { + throw new \RuntimeException("Failed to start bitcoind: {$this->dataDir}\n"); + } + + $start = microtime(true); + $limit = 10; + $connected = false; + + $conn = new Client($this->credential->getDsn()); + + do { + try { + $result = json_decode($conn->sendCommand(new Command("getchaintips"))->getBody()->getContents(), true); + if ($result['error'] === null) { + $connected = true; + } else { + if ($result['error']['code'] !== self::ERROR_STARTUP && $result['error']['code'] !== self::ERROR_UNKNOWN_COMMAND) { + throw new \RuntimeException("Unexpected error code during startup: {$result['error']['code']}"); + } + + sleep(0.2); + } + + } catch (\Exception $e) { + sleep(0.2); + } + + if (microtime(true) > $start + $limit) { + throw new \RuntimeException("Timeout elapsed, never made connection to bitcoind"); + } + } while (!$connected); + } + + /** + * Recursive delete of datadir. + * @param string $src + */ + private function recursiveDelete($src) + { + $dir = opendir($src); + while(false !== ( $file = readdir($dir)) ) { + if (( $file != '.' ) && ( $file != '..' )) { + $full = $src . '/' . $file; + if ( is_dir($full) ) { + $this->recursiveDelete($full); + } + else { + unlink($full); + } + } + } + closedir($dir); + rmdir($src); + } + + /** + * @return void + */ + public function destroy() + { + if ($this->isRunning()) { + $this->makeClient()->sendCommand(new Command("stop")); + + do { + sleep(0.2); + } while($this->isRunning()); + + $this->recursiveDelete($this->dataDir); + } + } + + /** + * @return bool + */ + public function isRunning() + { + return file_exists($this->getPidFile()); + } + + /** + * @return Client + */ + public function makeClient() + { + if (!$this->isRunning()) { + throw new \RuntimeException("No client, server not running"); + } + + if (null === $this->client) { + $this->client = new Client($this->credential->getDsn()); + } + + return $this->client; + } +} diff --git a/tests-rpc/BitcoindFactory.php b/tests-rpc/BitcoindFactory.php new file mode 100644 index 0000000..62b9dbb --- /dev/null +++ b/tests-rpc/BitcoindFactory.php @@ -0,0 +1,115 @@ +testsDirPath = $this->envOrDefault("BITCOIND_TEST_DIR", "/tmp"); + $this->bitcoindPath = $this->envOrDefault("BITCOIND_PATH"); + if (null === $this->bitcoindPath) { + throw new \RuntimeException("Missing BITCOIND_PATH variable"); + } + + $this->credential = new Credential("127.0.0.1", 18332, "rpcuser", "rpcpass"); + } + + /** + * @param $var + * @param null $default + * @return array|false|null|string + */ + private function envOrDefault($var, $default = null) + { + $value = getenv($var); + if (in_array($value, [null, false, ""])) { + $value = $default; + } + return $value; + } + + /** + * Generates a new datadir for bitcoind, which will be + * cleaned up after tests finish. + * + * @return string + */ + protected function createRandomTestDir() + { + $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + $randFile = substr(str_shuffle($chars),0, 9); + $this->testDir[] = $dir = "{$this->testsDirPath}/{$randFile}"; + if (!mkdir($dir)) { + throw new \RuntimeException("Failed to create test dir!"); + } + return $dir; + } + + /** + * Creates a new regtest bitcoind server + * @param array $options + * @return Bitcoind + */ + public function createServer($options = []) + { + $testDir = $this->createRandomTestDir(); + $rpcServer = new Bitcoind($this->bitcoindPath, $testDir, $this->credential, $options); + $rpcServer->start(); + $this->server[] = $rpcServer; + return $rpcServer; + } + + /** + * Removes any still running bitcoind instances. + */ + protected function cleanup() + { + $servers = 0; + $dirs = 0; + foreach ($this->server as $server) { + if ($server->isRunning()) { + $servers++; + $server->destroy(); + } + } + + echo "Cleaned up {$servers} servers, and {$dirs} directories\n"; + } + + public function __destruct() + { + $this->cleanup(); + } +} diff --git a/tests-rpc/Credential.php b/tests-rpc/Credential.php new file mode 100644 index 0000000..15e0619 --- /dev/null +++ b/tests-rpc/Credential.php @@ -0,0 +1,122 @@ +host = $host; + $this->username = $user; + $this->port = $port; + $this->password = $pass; + $this->isHttps = $isHttps; + } + + /** + * @return array + */ + public function getConfigArray() + { + return [ + "rpcuser" => $this->username, + "rpcpassword" => $this->password, + "rpcport" => $this->port, + "rpcallowip" => "127.0.0.1", + ]; + } + + /** + * @return string + */ + public function getDsn() + { + $prefix = "http" . ($this->isHttps ? "s" : ""); + return "$prefix://{$this->username}:{$this->password}@{$this->host}:{$this->port}"; + } + + /** + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * @return int + */ + public function getPort() + { + return $this->port; + } + + /** + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * @return string + */ + public function getPassword() + { + return $this->password; + } + + /** + * @return bool + */ + public function isHttps() + { + return $this->isHttps; + } + +} diff --git a/tests-rpc/Test/GetChainTipsTest.php b/tests-rpc/Test/GetChainTipsTest.php new file mode 100644 index 0000000..161ff69 --- /dev/null +++ b/tests-rpc/Test/GetChainTipsTest.php @@ -0,0 +1,45 @@ +rpcFactory = $rpcFactory; + } + + public function testGetBlockChainInfo() + { + $bitcoind = $this->rpcFactory->createServer(); + $this->assertTrue($bitcoind->isRunning()); + + $client = $bitcoind->makeClient(); + $response = $client->sendCommand(new Command("getblockchaininfo", [])); + $this->assertEquals(200, $response->getStatusCode()); + + $decoded = json_decode($response->getBody()->getContents(), true); + $this->assertInternalType('array', $decoded); + $this->assertArrayHasKey('error', $decoded); + $this->assertNull($decoded['error']); + + $this->assertArrayHasKey('result', $decoded); + $this->assertInternalType('array', $decoded['result']); + + $bitcoind->destroy(); + } +}