linux-trace-devel.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: "Tzvetomir Stoyanov (VMware)" <tz.stoyanov@gmail.com>
To: rostedt@goodmis.org
Cc: linux-trace-devel@vger.kernel.org
Subject: [PATCH 08/12] libtracefs: Extend kprobes unit test
Date: Thu, 28 Oct 2021 15:09:03 +0300	[thread overview]
Message-ID: <20211028120907.101847-9-tz.stoyanov@gmail.com> (raw)
In-Reply-To: <20211028120907.101847-1-tz.stoyanov@gmail.com>

As there are a lot of changes in the libtracefs kprobes APIs, the unit
test of that functionality should be updated. A new test section is
added, for testing the all kprobes APIs.

Signed-off-by: Tzvetomir Stoyanov (VMware) <tz.stoyanov@gmail.com>
---
 utest/tracefs-utest.c | 414 +++++++++++++++++++++++++-----------------
 1 file changed, 244 insertions(+), 170 deletions(-)

diff --git a/utest/tracefs-utest.c b/utest/tracefs-utest.c
index 7c1a84e..0a17b82 100644
--- a/utest/tracefs-utest.c
+++ b/utest/tracefs-utest.c
@@ -28,22 +28,6 @@
 #define TRACE_ON	"tracing_on"
 #define TRACE_CLOCK	"trace_clock"
 
-#define KPROBE_EVENTS	"kprobe_events"
-
-#define KPROBE_1_NAME	"mkdir"
-#define KPROBE_1_GROUP	"kprobes"
-#define KPROBE_1_ADDR	"do_mkdirat"
-#define KPROBE_1_FMT	"path=+u0($arg2):ustring"
-
-#define KPROBE_2_NAME	"open"
-#define KPROBE_2_GROUP	"myprobe"
-#define KPROBE_2_ADDR	"do_sys_openat2"
-#define KPROBE_2_FMT	"file=+u0($arg2):ustring flags=+0($arg3):x64"
-
-#define KRETPROBE_NAME	"retopen"
-#define KRETPROBE_ADDR	"do_sys_openat2"
-#define KRETPROBE_FMT	"ret=$retval"
-
 #define SQL_1_EVENT	"wakeup_1"
 #define SQL_1_SQL	"select sched_switch.next_pid as woke_pid, sched_waking.common_pid as waking_pid from sched_waking join sched_switch on sched_switch.next_pid = sched_waking.pid"
 
@@ -457,28 +441,264 @@ static void test_instance_file_read(struct tracefs_instance *inst, const char *f
 	free(file);
 }
 
+#define KPROBE_DEFAULT_GROUP "kprobes"
+struct kprobe_test {
+	enum tracefs_kprobe_type type;
+	char *prefix;
+	char *system;
+	char *event;
+	char *address;
+	char *format;
+};
+
+static bool check_kprobes(struct kprobe_test *kprobes, int count,
+			  struct tracefs_dynevent **devents, bool in_system)
+{
+
+	enum tracefs_kprobe_type ktype;
+	char *ename;
+	char *kaddress;
+	char *kevent;
+	char *ksystem;
+	char *kformat;
+	char *kprefix;
+	int found = 0;
+	int ret;
+	int i, j;
+
+	for (i = 0; devents[i]; i++) {
+		ktype = tracefs_kprobe_info(devents[i], &ksystem,
+					   &kevent, &kprefix, &kaddress, &kformat);
+		for (j = 0; j < count; j++) {
+			if (ktype != kprobes[j].type)
+				continue;
+			if (kprobes[j].event)
+				ename = kprobes[j].event;
+			else
+				ename = kprobes[j].address;
+			if (strcmp(ename, kevent))
+				continue;
+			if (kprobes[j].system) {
+				CU_TEST(strcmp(kprobes[j].system, ksystem) == 0);
+			} else {
+				CU_TEST(strcmp(KPROBE_DEFAULT_GROUP, ksystem) == 0);
+			}
+			CU_TEST(strcmp(kprobes[j].address, kaddress) == 0);
+			CU_TEST(strcmp(kprobes[j].format, kformat) == 0);
+			if (kprobes[j].prefix)
+				CU_TEST(strcmp(kprobes[j].prefix, kprefix) == 0);
+			ret = tracefs_event_enable(test_instance, ksystem, kevent);
+			if (in_system) {
+				CU_TEST(ret == 0);
+			} else {
+				CU_TEST(ret != 0);
+			}
+			ret = tracefs_event_disable(test_instance, ksystem, kevent);
+			if (in_system) {
+				CU_TEST(ret == 0);
+			} else {
+				CU_TEST(ret != 0);
+			}
+
+			found++;
+			break;
+		}
+		free(ksystem);
+		free(kevent);
+		free(kprefix);
+		free(kaddress);
+		free(kformat);
+	}
+
+	CU_TEST(found == count);
+	if (found != count)
+		return false;
+
+	return true;
+}
+
+#define KPROBE_COUNT	2
+#define KRETPROBE_COUNT	2
+static void test_kprobes(void)
+{
+	char *tmp;
+
+	struct kprobe_test ktests[KPROBE_COUNT] = {
+		{ TRACEFS_KPROBE, "p:", NULL, "mkdir", "do_mkdirat", "path=+u0($arg2):ustring" },
+		{ TRACEFS_KPROBE, "p:", "ptest", "open", "do_sys_openat2",
+				  "file=+u0($arg2):ustring flags=+0($arg3):x64" },
+	};
+	struct kprobe_test kretests[KRETPROBE_COUNT] = {
+		{ TRACEFS_KRETPROBE, NULL, NULL, "retopen", "do_sys_openat2", "ret=$retval" },
+		{ TRACEFS_KRETPROBE, NULL, NULL, NULL, "do_sys_open", "ret=$retval" },
+	};
+	struct tracefs_dynevent *dkretprobe[KRETPROBE_COUNT + 1];
+	struct tracefs_dynevent *dkprobe[KPROBE_COUNT + 1];
+	struct tracefs_dynevent **devents;
+	int ret;
+	int i;
+
+	/* Invalid parameters */
+	CU_TEST(tracefs_kprobe_alloc("test", NULL, NULL, "test") == NULL);
+	CU_TEST(tracefs_kretprobe_alloc("test", NULL, NULL, "test", 0) == NULL);
+	CU_TEST(tracefs_kprobe_alloc("test", NULL, "test", NULL) == NULL);
+	CU_TEST(tracefs_kretprobe_alloc("test", NULL, "test", NULL, 0) == NULL);
+	CU_TEST(tracefs_kprobe_create(NULL) != 0);
+	CU_TEST(tracefs_kprobe_info(NULL, &tmp, &tmp, &tmp, &tmp, &tmp) == TRACEFS_ALL_KPROBES);
+	CU_TEST(tracefs_kprobe_raw("test", "test", NULL, "test") != 0);
+	CU_TEST(tracefs_kretprobe_raw("test", "test", NULL, "test") != 0);
+	CU_TEST(tracefs_kprobe_raw("test", "test", "test", NULL) != 0);
+	CU_TEST(tracefs_kretprobe_raw("test", "test", "test", NULL) != 0);
+
+	/* kprobes APIs */
+	ret = tracefs_kprobe_destroy(NULL, true);
+	CU_TEST(ret == 0);
+	ret = tracefs_kprobes_get(TRACEFS_ALL_KPROBES, &devents);
+	CU_TEST(ret == 0);
+	CU_TEST(devents == NULL);
+
+	for (i = 0; i < KPROBE_COUNT; i++) {
+		dkprobe[i] = tracefs_kprobe_alloc(ktests[i].system, ktests[i].event,
+						  ktests[i].address, ktests[i].format);
+		CU_TEST(dkprobe[i] != NULL);
+	}
+	dkprobe[i] = NULL;
+	ret = tracefs_kprobes_get(TRACEFS_ALL_KPROBES, &devents);
+	CU_TEST(ret == 0);
+	CU_TEST(devents == NULL);
+	CU_TEST(check_kprobes(ktests, KPROBE_COUNT, dkprobe, false));
+
+	for (i = 0; i < KRETPROBE_COUNT; i++) {
+		dkretprobe[i] = tracefs_kretprobe_alloc(kretests[i].system, kretests[i].event,
+							kretests[i].address, kretests[i].format, 0);
+		CU_TEST(dkretprobe[i] != NULL);
+	}
+	dkretprobe[i] = NULL;
+	ret = tracefs_kprobes_get(TRACEFS_ALL_KPROBES, &devents);
+	CU_TEST(ret == 0);
+	CU_TEST(devents == NULL);
+	CU_TEST(check_kprobes(kretests, KRETPROBE_COUNT, dkretprobe, false));
+
+	for (i = 0; i < KPROBE_COUNT; i++) {
+		CU_TEST(tracefs_kprobe_create(dkprobe[i]) == 0);
+	}
+	ret = tracefs_kprobes_get(TRACEFS_ALL_KPROBES, &devents);
+	CU_TEST(ret == KPROBE_COUNT);
+	CU_TEST(devents != NULL);
+	CU_TEST(check_kprobes(ktests, KPROBE_COUNT, devents, true));
+	CU_TEST(check_kprobes(kretests, KRETPROBE_COUNT, dkretprobe, false));
+	tracefs_dynevent_list_free(&devents);
+	devents = NULL;
+
+	for (i = 0; i < KRETPROBE_COUNT; i++) {
+		CU_TEST(tracefs_kprobe_create(dkretprobe[i]) == 0);
+	}
+	ret = tracefs_kprobes_get(TRACEFS_ALL_KPROBES, &devents);
+	CU_TEST(ret == (KPROBE_COUNT + KRETPROBE_COUNT));
+	CU_TEST(devents != NULL);
+	CU_TEST(check_kprobes(ktests, KPROBE_COUNT, devents, true));
+	CU_TEST(check_kprobes(kretests, KRETPROBE_COUNT, devents, true));
+	tracefs_dynevent_list_free(&devents);
+	devents = NULL;
+
+	for (i = 0; i < KRETPROBE_COUNT; i++) {
+		CU_TEST(tracefs_kprobe_destroy(dkretprobe[i], false) == 0);
+	}
+	ret = tracefs_kprobes_get(TRACEFS_ALL_KPROBES, &devents);
+	CU_TEST(ret == KPROBE_COUNT);
+	CU_TEST(devents != NULL);
+	CU_TEST(check_kprobes(ktests, KPROBE_COUNT, devents, true));
+	CU_TEST(check_kprobes(kretests, KRETPROBE_COUNT, dkretprobe, false));
+	tracefs_dynevent_list_free(&devents);
+	devents = NULL;
+
+	for (i = 0; i < KPROBE_COUNT; i++) {
+		CU_TEST(tracefs_kprobe_destroy(dkprobe[i], false) == 0);
+	}
+	ret = tracefs_kprobes_get(TRACEFS_ALL_KPROBES, &devents);
+	CU_TEST(ret == 0);
+	CU_TEST(devents == NULL);
+	CU_TEST(check_kprobes(ktests, KPROBE_COUNT, dkprobe, false));
+	CU_TEST(check_kprobes(kretests, KRETPROBE_COUNT, dkretprobe, false));
+	tracefs_dynevent_list_free(&devents);
+	devents = NULL;
+
+	for (i = 0; i < KPROBE_COUNT; i++)
+		tracefs_kprobe_free(dkprobe[i]);
+	for (i = 0; i < KRETPROBE_COUNT; i++)
+		tracefs_kprobe_free(dkretprobe[i]);
+
+	/* kprobes raw APIs */
+	ret = tracefs_kprobe_destroy(NULL, true);
+	CU_TEST(ret == 0);
+	ret = tracefs_kprobes_get(TRACEFS_ALL_KPROBES, &devents);
+	CU_TEST(ret == 0);
+	CU_TEST(devents == NULL);
+
+	for (i = 0; i < KPROBE_COUNT; i++) {
+		ret = tracefs_kprobe_raw(ktests[i].system, ktests[i].event,
+					 ktests[i].address, ktests[i].format);
+		CU_TEST(ret == 0);
+	}
+
+	ret = tracefs_kprobes_get(TRACEFS_KPROBE, &devents);
+	CU_TEST(ret == KPROBE_COUNT);
+	CU_TEST(devents != NULL);
+	CU_TEST(check_kprobes(ktests, KPROBE_COUNT, devents, true));
+	tracefs_dynevent_list_free(&devents);
+	devents = NULL;
+
+	for (i = 0; i < KRETPROBE_COUNT; i++) {
+		ret = tracefs_kretprobe_raw(kretests[i].system, kretests[i].event,
+					    kretests[i].address, kretests[i].format);
+		CU_TEST(ret == 0);
+	}
+
+	ret = tracefs_kprobes_get(TRACEFS_KPROBE, &devents);
+	CU_TEST(ret == KPROBE_COUNT);
+	CU_TEST(devents != NULL);
+	CU_TEST(check_kprobes(ktests, KPROBE_COUNT, devents, true));
+	tracefs_dynevent_list_free(&devents);
+	devents = NULL;
+
+	ret = tracefs_kprobes_get(TRACEFS_KRETPROBE, &devents);
+	CU_TEST(ret == KRETPROBE_COUNT);
+	CU_TEST(devents != NULL);
+	CU_TEST(check_kprobes(kretests, KRETPROBE_COUNT, devents, true));
+	tracefs_dynevent_list_free(&devents);
+	devents = NULL;
+
+	ret = tracefs_kprobes_get(TRACEFS_ALL_KPROBES, &devents);
+	CU_TEST(ret == (KPROBE_COUNT + KRETPROBE_COUNT));
+	CU_TEST(devents != NULL);
+	CU_TEST(check_kprobes(ktests, KPROBE_COUNT, devents, true));
+	CU_TEST(check_kprobes(kretests, KRETPROBE_COUNT, devents, true));
+	tracefs_dynevent_list_free(&devents);
+	devents = NULL;
+
+	ret = tracefs_kprobe_destroy(NULL, true);
+	CU_TEST(ret == 0);
+	ret = tracefs_kprobes_get(TRACEFS_ALL_KPROBES, &devents);
+	CU_TEST(ret == 0);
+	CU_TEST(devents == NULL);
+}
+
 static void test_instance_file(void)
 {
 	struct tracefs_instance *instance = NULL;
 	struct tracefs_instance *second = NULL;
-	enum tracefs_kprobe_type type;
 	const char *name = get_rand_str();
 	const char *inst_name = NULL;
 	const char *tdir;
 	char *inst_file;
 	char *inst_dir;
 	struct stat st;
-	char **kprobes;
-	char *kformat;
-	char *ktype;
-	char *kaddr;
-	char *fname;
 	char *file1;
 	char *file2;
 	char *tracer;
+	char *fname;
 	int size;
 	int ret;
-	int i;
 
 	tdir  = tracefs_tracing_dir();
 	CU_TEST(tdir != NULL);
@@ -541,153 +761,6 @@ static void test_instance_file(void)
 	free(file1);
 	free(file2);
 
-	ret = tracefs_kprobe_clear_all(true);
-	CU_TEST(ret == 0);
-	ret = tracefs_kprobe_raw(NULL, KPROBE_1_NAME, KPROBE_1_ADDR, KPROBE_1_FMT);
-	CU_TEST(ret == 0);
-	ret = tracefs_kprobe_raw(KPROBE_2_GROUP, KPROBE_2_NAME, KPROBE_2_ADDR,
-				 KPROBE_2_FMT);
-	CU_TEST(ret == 0);
-
-	ret = tracefs_kretprobe_raw(KPROBE_2_GROUP, KRETPROBE_NAME, KRETPROBE_ADDR,
-				 KRETPROBE_FMT);
-	CU_TEST(ret == 0);
-
-	type = tracefs_kprobe_info(KPROBE_1_GROUP, KPROBE_1_NAME, &ktype,
-				   &kaddr, &kformat);
-	CU_TEST(type == TRACEFS_KPROBE);
-	CU_TEST(ktype && *ktype == 'p');
-	CU_TEST(kaddr && !strcmp(kaddr, KPROBE_1_ADDR));
-	CU_TEST(kformat && !strcmp(kformat, KPROBE_1_FMT));
-	free(ktype);
-	free(kaddr);
-	free(kformat);
-
-	type = tracefs_kprobe_info(KPROBE_2_GROUP, KPROBE_2_NAME, &ktype,
-				   &kaddr, &kformat);
-	CU_TEST(type == TRACEFS_KPROBE);
-	CU_TEST(ktype && *ktype == 'p');
-	CU_TEST(kaddr && !strcmp(kaddr, KPROBE_2_ADDR));
-	CU_TEST(kformat && !strcmp(kformat, KPROBE_2_FMT));
-	free(ktype);
-	free(kaddr);
-	free(kformat);
-
-	type = tracefs_kprobe_info(KPROBE_2_GROUP, KRETPROBE_NAME, &ktype,
-				   &kaddr, &kformat);
-	CU_TEST(type == TRACEFS_KRETPROBE);
-	CU_TEST(ktype && *ktype == 'r');
-	CU_TEST(kaddr && !strcmp(kaddr, KRETPROBE_ADDR));
-	CU_TEST(kformat && !strcmp(kformat, KRETPROBE_FMT));
-	free(ktype);
-	free(kaddr);
-	free(kformat);
-
-	kprobes = tracefs_kprobes_get(TRACEFS_ALL_KPROBES);
-	CU_TEST(kprobes != NULL);
-
-	for (i = 0; kprobes[i]; i++) {
-		char *system = strtok(kprobes[i], "/");
-		char *event = strtok(NULL, "");
-		bool found = false;
-		if (!strcmp(system, KPROBE_1_GROUP)) {
-			CU_TEST(!strcmp(event, KPROBE_1_NAME));
-			found = true;
-		} else if (!strcmp(system, KPROBE_2_GROUP)) {
-			switch (tracefs_kprobe_info(system, event, NULL, NULL, NULL)) {
-			case TRACEFS_KPROBE:
-				CU_TEST(!strcmp(event, KPROBE_2_NAME));
-				found = true;
-				break;
-			case TRACEFS_KRETPROBE:
-				CU_TEST(!strcmp(event, KRETPROBE_NAME));
-				found = true;
-				break;
-			default:
-				break;
-			}
-		}
-		CU_TEST(found);
-	}
-	tracefs_list_free(kprobes);
-	CU_TEST(i == 3);
-
-	kprobes = tracefs_kprobes_get(TRACEFS_KPROBE);
-	CU_TEST(kprobes != NULL);
-
-	for (i = 0; kprobes[i]; i++) {
-		char *system = strtok(kprobes[i], "/");
-		char *event = strtok(NULL, "");
-		bool found = false;
-		if (!strcmp(system, KPROBE_1_GROUP)) {
-			CU_TEST(!strcmp(event, KPROBE_1_NAME));
-			found = true;
-		} else if (!strcmp(system, KPROBE_2_GROUP)) {
-			CU_TEST(!strcmp(event, KPROBE_2_NAME));
-			found = true;
-		}
-		CU_TEST(found);
-	}
-	tracefs_list_free(kprobes);
-	CU_TEST(i == 2);
-
-	kprobes = tracefs_kprobes_get(TRACEFS_KRETPROBE);
-	CU_TEST(kprobes != NULL);
-
-	for (i = 0; kprobes[i]; i++) {
-		char *system = strtok(kprobes[i], "/");
-		char *event = strtok(NULL, "");
-		bool found = false;
-		if (!strcmp(system, KPROBE_2_GROUP)) {
-			CU_TEST(!strcmp(event, KRETPROBE_NAME));
-			found = true;
-		}
-		CU_TEST(found);
-	}
-	tracefs_list_free(kprobes);
-	CU_TEST(i == 1);
-
-	ret = tracefs_event_enable(instance, KPROBE_1_GROUP, KPROBE_1_NAME);
-	CU_TEST(ret == 0);
-	ret = tracefs_event_enable(instance, KPROBE_2_GROUP, KPROBE_2_NAME);
-	CU_TEST(ret == 0);
-	ret = tracefs_event_enable(instance, KPROBE_2_GROUP, KRETPROBE_NAME);
-	CU_TEST(ret == 0);
-
-	ret = tracefs_kprobe_clear_all(false);
-	CU_TEST(ret < 0);
-
-	ret = tracefs_kprobe_clear_probe(KPROBE_2_GROUP, NULL, false);
-	CU_TEST(ret < 0);
-
-	ret = tracefs_kprobe_clear_probe(KPROBE_2_GROUP, NULL, true);
-	CU_TEST(ret == 0);
-
-	kprobes = tracefs_kprobes_get(TRACEFS_ALL_KPROBES);
-	CU_TEST(kprobes != NULL);
-
-	for (i = 0; kprobes[i]; i++) {
-		char *system = strtok(kprobes[i], "/");
-		char *event = strtok(NULL, "");
-		bool found = false;
-		if (!strcmp(system, KPROBE_1_GROUP)) {
-			CU_TEST(!strcmp(event, KPROBE_1_NAME));
-			found = true;
-		}
-		CU_TEST(found);
-	}
-	tracefs_list_free(kprobes);
-	CU_TEST(i == 1);
-
-	ret = tracefs_kprobe_clear_all(true);
-	CU_TEST(ret == 0);
-
-	kprobes = tracefs_kprobes_get(TRACEFS_ALL_KPROBES);
-	CU_TEST(kprobes != NULL);
-
-	CU_TEST(kprobes[0] == NULL);
-	tracefs_list_free(kprobes);
-
 	tracefs_put_tracing_file(inst_file);
 	free(fname);
 
@@ -1442,4 +1515,5 @@ void test_tracefs_lib(void)
 		    test_custom_trace_dir);
 	CU_add_test(suite, "ftrace marker",
 		    test_ftrace_marker);
+	CU_add_test(suite, "kprobes", test_kprobes);
 }
-- 
2.31.1


  parent reply	other threads:[~2021-10-28 12:09 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-10-28 12:08 [PATCH 00/12] libtracefs dynamic events support Tzvetomir Stoyanov (VMware)
2021-10-28 12:08 ` [PATCH 01/12] libtracefs: Add new internal APIs for dynamic events Tzvetomir Stoyanov (VMware)
2021-10-28 21:41   ` Steven Rostedt
2021-10-29  2:46     ` Tzvetomir Stoyanov
2021-10-29  3:09       ` Steven Rostedt
2021-10-28 12:08 ` [PATCH 02/12] libtracefs: Rename tracefs_get_kprobes API Tzvetomir Stoyanov (VMware)
2021-10-28 12:08 ` [PATCH 03/12] libtracefs: New kprobes APIs Tzvetomir Stoyanov (VMware)
2021-10-29  2:55   ` Steven Rostedt
2021-10-28 12:08 ` [PATCH 04/12] libtracefs: Remove redundant " Tzvetomir Stoyanov (VMware)
2021-10-28 12:09 ` [PATCH 05/12] libtracefs: Reimplement tracefs_kprobes_get API Tzvetomir Stoyanov (VMware)
2021-10-29  3:01   ` Steven Rostedt
2021-10-28 12:09 ` [PATCH 06/12] libtracefs: Change tracefs_kprobe_info API Tzvetomir Stoyanov (VMware)
2021-10-28 12:09 ` [PATCH 07/12] libtracefs: Reimplement kprobe raw APIs Tzvetomir Stoyanov (VMware)
2021-10-28 12:09 ` Tzvetomir Stoyanov (VMware) [this message]
2021-10-28 12:09 ` [PATCH 09/12] libtracefs: Update kprobes man pages Tzvetomir Stoyanov (VMware)
2021-10-28 12:09 ` [PATCH 10/12] libtracefs: Rename tracefs_synth_init API Tzvetomir Stoyanov (VMware)
2021-10-28 12:09 ` [PATCH 11/12] libtracefs: Use the internal dynamic events API when creating synthetic events Tzvetomir Stoyanov (VMware)
2021-10-28 12:09 ` [PATCH 12/12] libtracefs: Document tracefs_dynevent_list_free() API Tzvetomir Stoyanov (VMware)

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20211028120907.101847-9-tz.stoyanov@gmail.com \
    --to=tz.stoyanov@gmail.com \
    --cc=linux-trace-devel@vger.kernel.org \
    --cc=rostedt@goodmis.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).