*
*/
-namespace NextcloudServerControl {
-
-class SocketException extends \Exception {
- public function __construct($message) {
- parent::__construct($message);
- }
-}
-
-/**
- * Common class for communication between client and server.
- *
- * Clients and server communicate through messages: a client sends a request and
- * the server answers with a response. Requests and responses all have the same
- * common structure composed by a mandatory header and optional data. The header
- * contains a code that identifies the type of request or response followed by
- * the length of the data (which can be 0). The data is a free form string that
- * depends on each request and response type.
- *
- * The Messenger abstracts all that and provides two public methods: readMessage
- * and writeMessage. For each connection a client first writes the request
- * message and then reads the response message, while the server first reads the
- * request message and then writes the response message. If the client needs to
- * 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.
+ * Helper to manage a Nextcloud test server when acceptance tests are run in a
+ * Drone step.
*
- * 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.
+ * The Nextcloud test server is executed using the PHP built-in web server
+ * directly from the grandparent directory of the acceptance tests directory
+ * (that is, the root directory of the Nextcloud server); note that the
+ * acceptance tests must be run from the acceptance tests directory. The "setUp"
+ * method resets the Nextcloud server to its initial state and starts it, while
+ * 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.
*
- * 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".
+ * The Nextcloud server is available at "127.0.0.1", so it 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"). The base URL to access the Nextcloud
+ * server can be got from "getBaseUrl".
*/
class NextcloudTestServerDroneHelper implements NextcloudTestServerHelper {
/**
- * @var int
+ * @var string
*/
- private $nextcloudTestServerControlPort;
+ private $phpServerPid;
/**
* Creates a new NextcloudTestServerDroneHelper.
- *
- * @param int $nextcloudTestServerControlPort the port in which the
- * Nextcloud server control is listening.
*/
- public function __construct($nextcloudTestServerControlPort) {
- $this->nextcloudTestServerControlPort = $nextcloudTestServerControlPort;
+ public function __construct() {
+ $this->phpServerPid = "";
}
/**
* Sets up the Nextcloud test server.
*
- * It resets the Nextcloud test server through the control system provided
- * by its Drone service and waits for the Nextcloud test server to be
- * started again; if the server can not be reset or if it does not start
- * again after some time an exception is thrown (as it is just a warning for
- * the test runner and nothing to be explicitly catched a plain base
- * Exception is used).
+ * It resets the Nextcloud test server restoring its last saved Git state
+ * and then waits for the Nextcloud test server to start again; if the
+ * server can not be reset or if it does not start again after some time an
+ * exception is thrown (as it is just a warning for the test runner and
+ * nothing to be explicitly catched a plain base Exception is used).
*
- * @throws \Exception if the Nextcloud test server in the Drone service can
- * not be reset or started again.
+ * @throws \Exception if the Nextcloud test server can not be reset or
+ * started again.
*/
public function setUp() {
- $resetNextcloudServerCallback = function($socket) {
- Messenger::writeMessage($socket, Messenger::CODE_REQUEST_RESET);
+ // Ensure that previous PHP server is not running (as cleanUp may not
+ // 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) {
- throw new Exception("Request to reset Nextcloud server failed: " . $response["data"]);
- }
- };
- $this->sendRequestAndHandleResponse($resetNextcloudServerCallback);
+ // execOrException is not used because the server is started in the
+ // 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 $!");
$timeout = 60;
if (!Utils::waitForServer($this->getBaseUrl(), $timeout)) {
/**
* 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() {
+ $this->killPhpServer();
}
/**
* @return string the base URL of the Nextcloud test server.
*/
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
- * control.
+ * Executes the given command, throwing an Exception if it fails.
*
- * A socket is created with the Nextcloud test server control and passed to
- * the callback to send the request and handle its response.
- *
- * @param \Closure $nextcloudServerControlCallback the callback to call with
- * the communication socket.
- * @throws \Exception if any socket-related operation fails.
+ * @param string $command the command to execute.
+ * @throws \Exception if the command fails to execute.
*/
- private function sendRequestAndHandleResponse($nextcloudServerControlCallback) {
- $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
- if ($socket === false) {
- throw new Exception("Request socket to reset Nextcloud server could not be created: " . socket_strerror(socket_last_error()));
+ private function execOrException($command) {
+ exec($command . " 2>&1", $output, $returnValue);
+ if ($returnValue != 0) {
+ throw new Exception("'$command' could not be executed: " . implode("\n", $output));
}
+ }
- try {
- if (socket_connect($socket, "127.0.0.1", $this->nextcloudTestServerControlPort) === false) {
- throw new Exception("Request socket to reset Nextcloud server could not be connected: " . socket_strerror(socket_last_error()));
- }
-
- $nextcloudServerControlCallback($socket);
- } catch (SocketException $exception) {
- throw new Exception("Request socket to reset Nextcloud server failed: " . $exception->getMessage());
- } finally {
- socket_close($socket);
+ /**
+ * Kills the PHP built-in web server started in setUp, if any.
+ */
+ private function killPhpServer() {
+ if ($this->phpServerPid == "") {
+ return;
}
- }
-}
+ // execOrException is not used because the PID may no longer exist when
+ // trying to kill it.
+ exec("kill " . $this->phpServerPid);
+
+ $this->phpServerPid = "";
+ }
}
#
# The acceptance tests are written in Behat so, besides running the tests, this
# script installs Behat, its dependencies, and some related packages in the
-# "vendor" subdirectory of the acceptance tests. The acceptance tests also use
-# the Selenium server to control a web browser, and they require a Nextcloud
-# server to be available, so this script waits for the Selenium server and the
-# Nextcloud server (both provided in their own Drone service) to be ready before
-# running the tests.
+# "vendor" subdirectory of the acceptance tests. The acceptance tests expect
+# that the last commit in the Git repository provides the default state of the
+# Nextcloud server, so the script installs the Nextcloud server and saves a
+# snapshot of the whole grandparent directory (no .gitignore file is used) in
+# 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.
set -o errexit
REPLACEMENT="\
- NextcloudTestServerContext:\n\
nextcloudTestServerHelper: NextcloudTestServerDroneHelper\n\
- nextcloudTestServerHelperParameters:\n\
- - $NEXTCLOUD_SERVER_CONTROL_PORT"
+ nextcloudTestServerHelperParameters:"
sed "s/$ORIGINAL/$REPLACEMENT/" config/behat.yml > config/behat-drone.yml
-# Both the Selenium server and the Nextcloud server control should be ready by
-# now, as Composer typically takes way longer to execute than their startup
-# (which is done in parallel in Drone services), but just in case.
+cd ../../
-echo "Waiting for Selenium"
-timeout 60s bash -c "while ! curl 127.0.0.1:4444 >/dev/null 2>&1; do sleep 1; done"
+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=-
-# 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);"
+cd build/acceptance
-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"
+# 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"
+timeout 60s bash -c "while ! curl 127.0.0.1:4444 >/dev/null 2>&1; do sleep 1; done"
vendor/bin/behat --config=config/behat-drone.yml $SCENARIO_TO_RUN