OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / swarmd / test / test_client.cpp

213 lines · cpp ref:
// Gate: the C++ client library, against a real Daemon over a real socket.
//
// docs/SWARMD-DESIGN.md §9 step 5. Until now the wire had two client
// implementations — a Python harness and swarmd-probe — and both were wrong at
// least once in ways the daemon could not distinguish from a bad daemon. This
// asserts the one that replaces them.
//
// ⛔ NO NETWORK. The daemon is pointed at a bootstrap that answers nothing, so
// it never has a peer. Everything here is about the LOCAL contract: the calls,
// their replies, and the events. Peer-to-peer behaviour is
// test_channel_e2e.py's and test_content_e2e.py's job, on the real DHT.
//
// The daemon runs on its own thread because that is how a daemon runs. A client
// that blocks for a reply has nobody to pump the thing it is waiting for
// otherwise, and pretending otherwise is what a single-threaded gate would be
// asserting instead.

#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <chrono>
#include <thread>
#include <vector>

#include <unistd.h>
#include <sys/stat.h>
#include <cerrno>

#include "client.h"
#include "daemon.h"

static int checks = 0, failures = 0;
static void check(bool c, const char *what) {
	checks++;
	if (c) { printf("  PASS: %s\n", what); return; }
	failures++;
	printf("  FAIL: %s\n", what);
}

int main() {
	char sock[128], file[128];
	snprintf(sock, sizeof(sock), "/tmp/swarmd-client-%d.sock", (int)getpid());
	snprintf(file, sizeof(file), "/tmp/swarmd-client-%d.bin", (int)getpid());

	// A file for the content calls to name. Small: this gate is about the
	// contract, not about throughput.
	{
		FILE *f = fopen(file, "wb");
		if (!f) { printf("cannot write %s\n", file); return 1; }
		std::vector<uint8_t> bytes(64 * 1024);
		for (size_t i = 0; i < bytes.size(); i++) bytes[i] = (uint8_t)(i * 31 + 7);
		fwrite(bytes.data(), 1, bytes.size(), f);
		fclose(f);
	}

	char store[128];
	snprintf(store, sizeof(store), "/tmp/swarmd-client-store-%d", (int)getpid());
	if (mkdir(store, 0755) != 0 && errno != EEXIST) { printf("cannot make %s\n", store); return 1; }

	swarmd::Daemon d;
	if (!d.start(sock, "127.0.0.1:1", store)) { printf("daemon start failed\n"); return 1; }

	std::atomic<bool> stop(false);
	std::thread pump([&d, &stop]() { while (!stop.load()) d.poll(5); });

	int rc = 0;
	{
		swarmd::Client c;

		// Events the daemon raises on its own.
		int content_failed = 0;
		std::string last_fail;
		int failed_during_status = 0;
		bool counting = false;
		c.on.on_content_failed = [&](const uint8_t *, const std::string &why) {
			content_failed++;
			last_fail = why;
			if (counting) failed_during_status++;
		};
		int disconnects = 0;
		c.on.on_disconnect = [&](const std::string &) { disconnects++; };

		uint8_t identity[32];
		memset(identity, 0x5c, 32);

		check(c.connect(sock, "gate", identity), "connect() reached the daemon and said HELLO");
		check(c.connected(), "and reports itself connected");

		// ⛔ THESE TWO ARE THE POINT OF HELLO. §5 says the reply carries
		// {client_id, daemon_pubkey}; §7 item 4 says client ids exist so a client
		// can suppress its own echo. A client that cannot learn its own id cannot
		// do that, and a zeroed daemon key tells it nothing about who it reached.
		check(c.client_id() != 0, "HELLO answered with this client's OWN id");
		{
			static const uint8_t zero[32] = { 0 };
			check(memcmp(c.daemon_pubkey(), zero, 32) != 0,
				  "and with the daemon's real public key, not 32 zeroes");
		}

		uint8_t topic[32];
		memset(topic, 0x3a, 32);
		check(c.join(topic), "join() was accepted");

		const uint64_t ch = c.open_channel("client-gate/v1", {}, 2);
		check(ch != 0, "open_channel() returned a handle");

		// The send-truth contract, now through the library: with no peer there
		// is nowhere to put the frame, and saying so is the whole point.
		bool writable = true;
		const char *payload = "nowhere-to-go";
		check(!c.send(ch, 0, 0, (const uint8_t *)payload, strlen(payload), &writable),
			  "send() on a channel no peer has open is REFUSED, not silently dropped");
		check(!writable, "and the channel is not reported writable");

		// ⛔ GATE D7, the half that can be asserted without a network: STATUS must
		// say the DHT is NOT up when it is not. #72's panel said Connected while
		// the daemon was gone, and a status that only ever reports health is
		// worse than no status — it is a reason to stop looking. This daemon is
		// pointed at a bootstrap that answers nothing, so `up` here would be a
		// lie with nothing behind it.
		swarmd::Status st;
		check(c.status(st), "status() answered");
		check(st.local_clients >= 1, "and counts this client");
		check(st.peers_connected == 0, "and reports no peers, which is the truth here");
		check(!st.bootstrapped, "D7: it does NOT claim to be bootstrapped against a dead bootstrap");
		check(st.dht_state != 2, "D7: and does not report the DHT up");

		// ---- content ----
		uint8_t hash[32], hash2[32];
		check(c.content_hash(file, hash), "content_hash() hashed a real file");
		check(c.content_hash(file, hash2) && memcmp(hash, hash2, 32) == 0,
			  "and the same bytes hash the same way twice");
		{
			static const uint8_t zero[32] = { 0 };
			check(memcmp(hash, zero, 32) != 0, "and the hash is not all zeroes");
		}
		check(!c.content_hash("/nonexistent/path/at/all", hash2),
			  "hashing a file that is not there FAILS rather than returning zeroes");

		check(c.content_serve(hash, file), "content_serve() registered the file");

		// Cancelling a fetch nobody started is refused by liboverlay, and the
		// daemon reports that refusal as a FAILED event rather than swallowing it.
		uint8_t missing[32];
		memset(missing, 0x77, 32);
		check(c.content_cancel(missing, false),
			  "content_cancel() was accepted by the daemon (the RPC, not the outcome)");

		// ⛔ THE INTERLEAVE — and the wait is what MAKES it one, not a patch over
		// a flake.
		//
		// The cancel's ok reply is written by the control handler synchronously;
		// the FAILED event is produced later, on the UDX loop, and broadcast by a
		// subsequent Daemon::poll. Asserting straight after the cancel asserts a
		// race: status() is answered just as fast, and the first version of this
		// gate failed against a client that was working. Waiting until the event
		// is known to be sitting in our socket is what establishes the condition
		// the assertion is about — a frame queued AHEAD of the reply a blocking
		// call is waiting for. A client that drops what it reads on the way to
		// its own answer loses it, invisibly: no error, just an event that never
		// fires.
		{
			const uint64_t deadline = 3000;
			uint64_t waited = 0;
			while (content_failed == 0 && waited < deadline) {
				std::this_thread::sleep_for(std::chrono::milliseconds(20));
				waited += 20;
				// Deliberately NOT polling the client: the point is to leave the
				// frame unread so the next blocking call has to handle it.
			}
			// It is in our socket now, unread, because nothing has read it.
			std::this_thread::sleep_for(std::chrono::milliseconds(50));
		}
		counting = true;
		check(c.status(st), "a second status() answered");
		counting = false;
		check(content_failed >= 1, "the daemon reported the refused cancel at all");
		check(failed_during_status >= 1,
			  "an event queued AHEAD of a reply was delivered by the blocking call "
			  "that read past it, not dropped");
		check(last_fail.find("no such fetch") != std::string::npos,
			  "and it is the event the daemon actually sent");

		check(c.close_channel(ch), "close_channel() was accepted");
		check(disconnects == 0, "nothing in that sequence looked like a disconnect");

		// ---- what a closed client does ----
		c.close();
		check(!c.connected(), "close() closes");
		check(!c.join(topic), "and a call on a closed client fails rather than crashing");
		check(c.last_error() == "not connected", "with an error that says why");

		// ---- a daemon that is not there ----
		swarmd::Client absent;
		check(!absent.connect("/tmp/swarmd-does-not-exist.sock", "gate"),
			  "connecting to a socket with no daemon fails");
		check(absent.last_error().find("no daemon at") != std::string::npos,
			  "and says so");

		rc = failures ? 1 : 0;
	}

	stop.store(true);
	pump.join();
	d.stop();
	unlink(file);

	printf("\n%d/%d passed\n", checks - failures, checks);
	return rc;
}