Use PHP built-in web server instead of Apache in Drone

Instead of running an additional Drone service with the Nextcloud server
now the Nextcloud server is run in the same Drone step as the acceptance
tests themselves using the PHP built-in web server.

Thanks to this, the Nextcloud server control is no longer needed, as the
acceptance tests can now directly reset, start and stop the Nextcloud
server. Also, the "nextcloudci/php7.0:php7.0-7" image provides
everything needed to run and manage the Nextcloud server (including the
Git command used to restore the directory to a saved state), so the
custom image is no longer needed either.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
This commit is contained in:
Daniel Calviño Sánchez 2017-04-18 20:24:46 +02:00
parent 593118204a
commit 72310cdac1
3 changed files with 80 additions and 231 deletions

View File

@ -478,19 +478,6 @@ pipeline:
when: when:
matrix: matrix:
TESTS: integration-trashbin TESTS: integration-trashbin
# As it needs access to the cloned Git repository it must be defined in the
# pipeline as "detached" instead of in the services.
service-acceptance-nextcloud-server:
image: nextcloudci/acceptance-nextcloud-server-php7.1-apache
detach: true
commands:
# "nextcloud-server-control-setup.sh" can not be set as the entry point in
# the image because Drone overrides it.
- /usr/local/bin/nextcloud-server-control-setup.sh
- su --shell "/bin/sh" --command "php /usr/local/bin/nextcloud-server-control.php 12345" - www-data
when:
matrix:
TESTS: acceptance
acceptance-access-levels: acceptance-access-levels:
image: nextcloudci/php7.0:php7.0-7 image: nextcloudci/php7.0:php7.0-7
commands: commands:

View File

@ -21,198 +21,63 @@
* *
*/ */
namespace NextcloudServerControl {
class SocketException extends \Exception {
public function __construct($message) {
parent::__construct($message);
}
}
/** /**
* Common class for communication between client and server. * Helper to manage a Nextcloud test server when acceptance tests are run in a
* Drone step.
* *
* Clients and server communicate through messages: a client sends a request and * The Nextcloud test server is executed using the PHP built-in web server
* the server answers with a response. Requests and responses all have the same * directly from the grandparent directory of the acceptance tests directory
* common structure composed by a mandatory header and optional data. The header * (that is, the root directory of the Nextcloud server); note that the
* contains a code that identifies the type of request or response followed by * acceptance tests must be run from the acceptance tests directory. The "setUp"
* the length of the data (which can be 0). The data is a free form string that * method resets the Nextcloud server to its initial state and starts it, while
* depends on each request and response type. * the "cleanUp" method stops it. To be able to reset the Nextcloud server to
* its initial state a Git repository must be provided in the root directory of
* the Nextcloud server; the last commit in that Git repository must provide the
* initial state for the Nextcloud server expected by the acceptance tests.
* *
* The Messenger abstracts all that and provides two public methods: readMessage * The Nextcloud server is available at "127.0.0.1", so it is expected to see
* and writeMessage. For each connection a client first writes the request * "127.0.0.1" as a trusted domain (which would be the case if it was installed
* message and then reads the response message, while the server first reads the * by running "occ maintenance:install"). The base URL to access the Nextcloud
* request message and then writes the response message. If the client needs to * server can be got from "getBaseUrl".
* send another request it must connect again to the server.
*
* The Messenger class in the server must be kept in sync with the Messenger
* class in the client. Due to the size of the code and its current use it was
* more practical, at least for the time being, to keep two copies of the code
* than creating a library that had to be downloaded and included in the client
* and in the server.
*/
class Messenger {
/**
* Reset the Nextcloud server.
*
* -Request data: empty
* -OK response data: empty.
* -Failed response data: error information.
*/
const CODE_REQUEST_RESET = 0;
const CODE_RESPONSE_OK = 0;
const CODE_RESPONSE_FAILED = 1;
const HEADER_LENGTH = 5;
/**
* Reads a message from the given socket.
*
* The message is returned as an indexed array with keys "code" and "data".
*
* @param resource $socket the socket to read the message from.
* @return array the message read.
* @throws SocketException if an error occurs while reading the socket.
*/
public static function readMessage($socket) {
$header = self::readSocket($socket, self::HEADER_LENGTH);
$header = unpack("Ccode/VdataLength", $header);
$data = self::readSocket($socket, $header["dataLength"]);
return [ "code" => $header["code"], "data" => $data ];
}
/**
* Reads content from the given socket.
*
* It blocks until the specified number of bytes were read.
*
* @param resource $socket the socket to read the message from.
* @param int $length the number of bytes to read.
* @return string the content read.
* @throws SocketException if an error occurs while reading the socket.
*/
private static function readSocket($socket, $length) {
if ($socket == null) {
throw new SocketException("Null socket can not be read from");
}
$pendingLength = $length;
$content = "";
while ($pendingLength > 0) {
$readContent = socket_read($socket, $pendingLength);
if ($readContent === "") {
throw new SocketException("Socket could not be read: $pendingLength bytes are pending, but there is no more data to read");
} else if ($readContent == false) {
throw new SocketException("Socket could not be read: " . socket_strerror(socket_last_error()));
}
$pendingLength -= strlen($readContent);
$content = $content . $readContent;
}
return $content;
}
/**
* Writes a message to the given socket.
*
* @param resource $socket the socket to write the message to.
* @param int $code the message code.
* @param string $data the message data, if any.
* @throws SocketException if an error occurs while reading the socket.
*/
public static function writeMessage($socket, $code, $data = "") {
if ($socket == null) {
throw new SocketException("Null socket can not be written to");
}
$header = pack("CV", $code, strlen($data));
$message = $header . $data;
$pendingLength = strlen($message);
while ($pendingLength > 0) {
$sent = socket_write($socket, $message, $pendingLength);
if ($sent !== 0 && $sent == false) {
throw new SocketException("Message ($message) could not be written: " . socket_strerror(socket_last_error()));
}
$pendingLength -= $sent;
$message = substr($message, $sent);
}
}
}
}
namespace {
use NextcloudServerControl\Messenger;
use NextcloudServerControl\SocketException;
/**
* Helper to manage the Nextcloud test server running in a Drone service.
*
* The NextcloudTestServerDroneHelper controls a Nextcloud test server running
* in a Drone service. The "setUp" method resets the Nextcloud server to its
* initial state; nothing needs to be done in the "cleanUp" method. To be able
* to control the remote Nextcloud server the Drone service must provide the
* Nextcloud server control server; the port in which the server listens on can
* be set with the $nextcloudTestServerControlPort parameter of the constructor.
*
* Drone services are available at "127.0.0.1", so the Nextcloud server is
* expected to see "127.0.0.1" as a trusted domain (which would be the case if
* it was installed by running "occ maintenance:install"). Note, however, that
* the Nextcloud server does not listen on port "80" but on port "8000" due to
* internal issues of the Nextcloud server control. In any case, the base URL to
* access the Nextcloud server can be got from "getBaseUrl".
*/ */
class NextcloudTestServerDroneHelper implements NextcloudTestServerHelper { class NextcloudTestServerDroneHelper implements NextcloudTestServerHelper {
/** /**
* @var int * @var string
*/ */
private $nextcloudTestServerControlPort; private $phpServerPid;
/** /**
* Creates a new NextcloudTestServerDroneHelper. * Creates a new NextcloudTestServerDroneHelper.
*
* @param int $nextcloudTestServerControlPort the port in which the
* Nextcloud server control is listening.
*/ */
public function __construct($nextcloudTestServerControlPort) { public function __construct() {
$this->nextcloudTestServerControlPort = $nextcloudTestServerControlPort; $this->phpServerPid = "";
} }
/** /**
* Sets up the Nextcloud test server. * Sets up the Nextcloud test server.
* *
* It resets the Nextcloud test server through the control system provided * It resets the Nextcloud test server restoring its last saved Git state
* by its Drone service and waits for the Nextcloud test server to be * and then waits for the Nextcloud test server to start again; if the
* started again; if the server can not be reset or if it does not start * server can not be reset or if it does not start again after some time an
* again after some time an exception is thrown (as it is just a warning for * exception is thrown (as it is just a warning for the test runner and
* the test runner and nothing to be explicitly catched a plain base * nothing to be explicitly catched a plain base Exception is used).
* Exception is used).
* *
* @throws \Exception if the Nextcloud test server in the Drone service can * @throws \Exception if the Nextcloud test server can not be reset or
* not be reset or started again. * started again.
*/ */
public function setUp() { public function setUp() {
$resetNextcloudServerCallback = function($socket) { // Ensure that previous PHP server is not running (as cleanUp may not
Messenger::writeMessage($socket, Messenger::CODE_REQUEST_RESET); // have been called).
$this->killPhpServer();
$response = Messenger::readMessage($socket); $this->execOrException("cd ../../ && git reset --hard HEAD");
$this->execOrException("cd ../../ && git clean -d --force");
if ($response["code"] == Messenger::CODE_RESPONSE_FAILED) { // execOrException is not used because the server is started in the
throw new Exception("Request to reset Nextcloud server failed: " . $response["data"]); // background, so the command will always succeed even if the server
} // itself fails.
}; $this->phpServerPid = exec("php -S 127.0.0.1:80 -t ../../ >/dev/null 2>&1 & echo $!");
$this->sendRequestAndHandleResponse($resetNextcloudServerCallback);
$timeout = 60; $timeout = 60;
if (!Utils::waitForServer($this->getBaseUrl(), $timeout)) { if (!Utils::waitForServer($this->getBaseUrl(), $timeout)) {
@ -223,9 +88,10 @@ class NextcloudTestServerDroneHelper implements NextcloudTestServerHelper {
/** /**
* Cleans up the Nextcloud test server. * Cleans up the Nextcloud test server.
* *
* Nothing needs to be done when using the Drone service. * It kills the running Nextcloud test server, if any.
*/ */
public function cleanUp() { public function cleanUp() {
$this->killPhpServer();
} }
/** /**
@ -234,39 +100,35 @@ class NextcloudTestServerDroneHelper implements NextcloudTestServerHelper {
* @return string the base URL of the Nextcloud test server. * @return string the base URL of the Nextcloud test server.
*/ */
public function getBaseUrl() { public function getBaseUrl() {
return "http://127.0.0.1:8000/index.php"; return "http://127.0.0.1/index.php";
} }
/** /**
* Executes the given callback to communicate with the Nextcloud test server * Executes the given command, throwing an Exception if it fails.
* control.
* *
* A socket is created with the Nextcloud test server control and passed to * @param string $command the command to execute.
* the callback to send the request and handle its response. * @throws \Exception if the command fails to execute.
*
* @param \Closure $nextcloudServerControlCallback the callback to call with
* the communication socket.
* @throws \Exception if any socket-related operation fails.
*/ */
private function sendRequestAndHandleResponse($nextcloudServerControlCallback) { private function execOrException($command) {
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); exec($command . " 2>&1", $output, $returnValue);
if ($socket === false) { if ($returnValue != 0) {
throw new Exception("Request socket to reset Nextcloud server could not be created: " . socket_strerror(socket_last_error())); throw new Exception("'$command' could not be executed: " . implode("\n", $output));
}
} }
try { /**
if (socket_connect($socket, "127.0.0.1", $this->nextcloudTestServerControlPort) === false) { * Kills the PHP built-in web server started in setUp, if any.
throw new Exception("Request socket to reset Nextcloud server could not be connected: " . socket_strerror(socket_last_error())); */
private function killPhpServer() {
if ($this->phpServerPid == "") {
return;
} }
$nextcloudServerControlCallback($socket); // execOrException is not used because the PID may no longer exist when
} catch (SocketException $exception) { // trying to kill it.
throw new Exception("Request socket to reset Nextcloud server failed: " . $exception->getMessage()); exec("kill " . $this->phpServerPid);
} finally {
socket_close($socket); $this->phpServerPid = "";
}
} }
} }
}

View File

@ -22,11 +22,13 @@
# #
# The acceptance tests are written in Behat so, besides running the tests, this # The acceptance tests are written in Behat so, besides running the tests, this
# script installs Behat, its dependencies, and some related packages in the # script installs Behat, its dependencies, and some related packages in the
# "vendor" subdirectory of the acceptance tests. The acceptance tests also use # "vendor" subdirectory of the acceptance tests. The acceptance tests expect
# the Selenium server to control a web browser, and they require a Nextcloud # that the last commit in the Git repository provides the default state of the
# server to be available, so this script waits for the Selenium server and the # Nextcloud server, so the script installs the Nextcloud server and saves a
# Nextcloud server (both provided in their own Drone service) to be ready before # snapshot of the whole grandparent directory (no .gitignore file is used) in
# running the tests. # the Git repository. Finally, the acceptance tests also use the Selenium server
# to control a web browser, so this script waits for the Selenium server
# (provided in its own Drone service) to be ready before running the tests.
# Exit immediately on errors. # Exit immediately on errors.
set -o errexit set -o errexit
@ -50,26 +52,24 @@ ORIGINAL="\
REPLACEMENT="\ REPLACEMENT="\
- NextcloudTestServerContext:\n\ - NextcloudTestServerContext:\n\
nextcloudTestServerHelper: NextcloudTestServerDroneHelper\n\ nextcloudTestServerHelper: NextcloudTestServerDroneHelper\n\
nextcloudTestServerHelperParameters:\n\ nextcloudTestServerHelperParameters:"
- $NEXTCLOUD_SERVER_CONTROL_PORT"
sed "s/$ORIGINAL/$REPLACEMENT/" config/behat.yml > config/behat-drone.yml sed "s/$ORIGINAL/$REPLACEMENT/" config/behat.yml > config/behat-drone.yml
# Both the Selenium server and the Nextcloud server control should be ready by cd ../../
# now, as Composer typically takes way longer to execute than their startup
# (which is done in parallel in Drone services), but just in case.
echo "Installing and configuring Nextcloud server"
build/acceptance/installAndConfigureServer.sh
echo "Saving the default state so acceptance tests can reset to it"
find . -name ".gitignore" -exec rm --force {} \;
git add --all && echo 'Default state' | git -c user.name='John Doe' -c user.email='john@doe.org' commit --quiet --file=-
cd build/acceptance
# The Selenium server should be ready by now, as Composer typically takes way
# longer to execute than its startup (which is done in parallel in a Drone
# service), but just in case.
echo "Waiting for Selenium" echo "Waiting for Selenium"
timeout 60s bash -c "while ! curl 127.0.0.1:4444 >/dev/null 2>&1; do sleep 1; done" timeout 60s bash -c "while ! curl 127.0.0.1:4444 >/dev/null 2>&1; do sleep 1; done"
# This just checks if it can connect to the port in which the Nextcloud server
# control should be listening on.
NEXTCLOUD_SERVER_CONTROL_PORT="12345"
PHP_CHECK_NEXTCLOUD_SERVER="\
if ((\\\$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) { exit(1); } \
if (socket_connect(\\\$socket, \\\"127.0.0.1\\\", \\\"$NEXTCLOUD_SERVER_CONTROL_PORT\\\") === false) { exit(1); } \
socket_close(\\\$socket);"
echo "Waiting for Nextcloud server control"
timeout 60s bash -c "while ! php -r \"$PHP_CHECK_NEXTCLOUD_SERVER\" >/dev/null 2>&1; do sleep 1; done"
vendor/bin/behat --config=config/behat-drone.yml $SCENARIO_TO_RUN vendor/bin/behat --config=config/behat-drone.yml $SCENARIO_TO_RUN