From c438562ad30c88dc5c2fc364a53ce6771b967ef0 Mon Sep 17 00:00:00 2001
From: Alexandre Felipe <o.alexandre.felipe@gmail.com>
Date: Thu, 17 Sep 2026 08:43:52 +0100
Subject: [PATCH-v2.1] pg_regress: multi-line test groups

This support multi-line test groups
instead of requiring
test: all test in a single line
it also support
test:
  tests in
  different
  lines

this will make it more readable, and showing line numbers
having one test per line makes it much easier to see how many
tests are in a group.

SYNTAX
======

1. An unindented line is a syntax error

1.a unindented line before any `test:`
Example:
```
 9 # ----------
10 top
11 # required setup steps
```
Error:
line 10: expected "test:", indented test group continuation, got "top"
1.b unindented line after a `test:`
Example:
```
16 test:
16    boolean
18 char
```
Error:
line 18: expected "test:", indented test group continuation, got "char"

2. blank lines are OK
Example:
```
test:

    boolean
    char
```

3. mixing tests in the same line and next lines is OK

Example:
```
test: boolean
    char
```

4. empty tests are errors
4.a `test:` followed by empty lines or comments
Example:
```
12 test: test_setup
13 test:
14 # ----------
15 # The first group of parallel tests
16 # ----------
17 test: boolean
```
Error:
empty test group in schedule file "./parallel_schedule" line 13

Reporting the line where the empty test started not when it was
confirmed empty.

4.b empty `test:` at the end of the file
Example:
```
364 test: tablespace
365 test:
```
Error:
empty test group in schedule file "./parallel_schedule" line 365

4.c Empty `test:` immediately followed by `test:`
Example:
```
12 test:
13 test: test_setup
```
Error:
empty test group in schedule file "./parallel_schedule" line 12

EFFECTIVE CONCURRENCY
=====================
This also add for each group a note about the average execution
concurrency achieved during the test.

 # parallel group (2 tests):  brin_bloom brin_multi
 # effective concurrency 1.46 / 2
---
 src/test/regress/parallel_schedule | 270 ++++++++++++++++++++++++++---
 src/test/regress/pg_regress.c      | 112 ++++++++++--
 2 files changed, 346 insertions(+), 36 deletions(-)

diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 75063f87a4a..6cbcc3375da 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -14,21 +14,80 @@ test: test_setup
 # ----------
 # The first group of parallel tests
 # ----------
-test: boolean char name varchar text int2 int4 int8 oid float4 float8 bit numeric txid uuid enum money rangetypes pg_lsn regproc
+test:
+    boolean
+    char
+    name
+    varchar
+    text
+    int2
+    int4
+    int8
+    oid
+    float4
+    float8
+    bit
+    numeric
+    txid
+    uuid
+    enum
+    money
+    rangetypes
+    pg_lsn
+    regproc
 
 # ----------
 # The second group of parallel tests
 # multirangetypes depends on rangetypes
 # multirangetypes shouldn't run concurrently with type_sanity
 # ----------
-test: strings md5 numerology point lseg line box path polygon circle date time timetz timestamp timestamptz interval inet macaddr macaddr8 multirangetypes
+test:
+    strings
+    md5
+    numerology
+    point
+    lseg
+    line
+    box
+    path
+    polygon
+    circle
+    date
+    time
+    timetz
+    timestamp
+    timestamptz
+    interval
+    inet
+    macaddr
+    macaddr8
+    multirangetypes
 
 # ----------
 # Another group of parallel tests
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct pg_dependencies oid8 encoding euc_kr
+test:
+    geometry
+    horology
+    tstypes
+    regex
+    type_sanity
+    opr_sanity
+    misc_sanity
+    comments
+    expressions
+    unicode
+    xid
+    mvcc
+    database
+    stats_import
+    pg_ndistinct
+    pg_dependencies
+    oid8
+    encoding
+    euc_kr
 
 # ----------
 # Load huge amounts of data
@@ -36,19 +95,54 @@ test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comment
 # execute two copy tests in parallel, to check that copy itself
 # is concurrent safe.
 # ----------
-test: copy copyselect copydml copyencoding insert insert_conflict
+test:
+    copy
+    copyselect
+    copydml
+    copyencoding
+    insert
+    insert_conflict
 
 # ----------
 # More groups of parallel tests
 # Note: many of the tests in later groups depend on create_index
 # ----------
-test: create_function_c create_misc create_operator create_procedure create_table create_type create_schema
-test: create_index create_index_spgist create_view index_including index_including_gist
+test:
+    create_function_c
+    create_misc
+    create_operator
+    create_procedure
+    create_table
+    create_type
+    create_schema
+
+test:
+    create_index
+    create_index_spgist
+    create_view
+    index_including
+    index_including_gist
 
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test:
+    create_aggregate
+    create_function_sql
+    create_cast
+    constraints
+    triggers
+    select
+    inherit
+    typed_table
+    vacuum
+    drop_if_exists
+    updatable_views
+    roleattributes
+    create_am
+    hash_func
+    errors
+    infinite_recurse
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
@@ -61,27 +155,89 @@ test: sanity_check
 # aggregates depends on create_aggregate
 # join depends on create_misc
 # ----------
-test: select_into select_distinct select_distinct_on select_implicit select_having subselect union case join aggregates transactions random portals arrays btree_index hash_index update delete namespace prepared_xacts
+test:
+    select_into
+    select_distinct
+    select_distinct_on
+    select_implicit
+    select_having
+    subselect
+    union
+    case
+    join
+    aggregates
+    transactions
+    random
+    portals
+    arrays
+    btree_index
+    hash_index
+    update
+    delete
+    namespace
+    prepared_xacts
 
 # ----------
 # Another group of parallel tests
 # ----------
-test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated_stored join_hash
+test:
+    brin
+    gin
+    gist
+    spgist
+    privileges
+    init_privs
+    security_label
+    collate
+    matview
+    lock
+    replica_identity
+    rowsecurity
+    object_address
+    tablesample
+    groupingsets
+    drop_operator
+    password
+    identity
+    generated_stored
+    join_hash
 
 # ----------
 # Additional BRIN tests
 # ----------
-test: brin_bloom brin_multi
+test:
+    brin_bloom
+    brin_multi
 
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions nls sysviews tsrf tid tidscan tidrangescan collate.utf8 collate.icu.utf8 incremental_sort create_role without_overlaps generated_virtual
+test:
+    alter_generic alter_operator
+    create_table_like
+    create_role
+    misc misc_functions
+    async
+    dbsize
+    merge
+    nls
+    sysviews
+    tsrf
+    tid tidscan tidrangescan
+    collate.utf8 collate.icu.utf8
+    incremental_sort
+    without_overlaps
+    generated_virtual
 
 # collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
 # psql depends on create_am
 # amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test:
+    amutils
+    psql psql_crosstab psql_pipeline
+    collate.linux.utf8 collate.windows.win1252
+    rules
+    stats_ext
 
 # ----------
 # Run these alone so they don't run out of parallel workers
@@ -96,18 +252,46 @@ test: vacuum_parallel
 test: maintain_every
 
 # no relation related tests can be put in this group
-test: publication subscription
+test:
+    publication
+    subscription
 
 # ----------
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite
+test:
+    select_views
+    portals_p2
+    foreign_key
+    dependency
+    guc
+    bitmapops
+    combocid
+    tsearch
+    tsdicts
+    foreign_data
+    window
+    xmlmap
+    functional_deps
+    advisory_lock
+    indirect_toast
+    equivclass
+    stats_rewrite
 
 # ----------
 # Another group of parallel tests (JSON related)
 # ----------
-test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson sqljson_queryfuncs sqljson_jsontable
+test:
+    json
+    jsonb
+    json_encoding
+    jsonpath
+    jsonpath_encoding
+    jsonb_jsonpath
+    sqljson
+    sqljson_queryfuncs
+    sqljson_jsontable
 
 # ----------
 # Another group of parallel tests
@@ -115,7 +299,25 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
 # NB: temp.sql does reconnects which transiently uses 2 connections,
 # so keep this parallel group to at most 19 tests
 # ----------
-test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
+test:
+    plancache
+    limit
+    plpgsql
+    copy2
+    temp
+    domain
+    rangefuncs
+    prepare
+    conversion
+    truncate
+    alter_table
+    sequence
+    polymorphism
+    rowtypes
+    returning
+    largeobject
+    with
+    xml
 
 # ----------
 # Another group of parallel tests
@@ -123,26 +325,50 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain memoize stats predicate numa eager_aggregate planner_est
+test:
+    partition_join
+    partition_prune
+    reloptions
+    hash_part
+    indexing
+    partition_aggregate
+    partition_info
+    tuplesort
+    explain
+    memoize
+    stats
+    predicate
+    numa
+    eager_aggregate
+    planner_est
 
 # ----------
 # Another group of parallel tests (compression)
 # ----------
-test: compression compression_lz4 compression_pglz cluster
+test:
+    compression
+    compression_lz4
+    compression_pglz
+    cluster
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
 # oidjoins is read-only, though, and should run late for best coverage
-test: oidjoins event_trigger
+test:
+    oidjoins
+    event_trigger
 
 
 # event_trigger_login cannot run concurrently with any other tests because
 # on-login event handling could catch connection of a concurrent test.
-test: event_trigger_login
+test:
+    event_trigger_login
 
 # this test also uses event triggers, so likewise run it by itself
-test: fast_default
+test:
+    fast_default
 
 # run tablespace test at the end because it drops the tablespace created during
 # setup that other tests may use.
-test: tablespace
+test:
+    tablespace
diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c
index 13944701bc7..ad516174bf6 100644
--- a/src/test/regress/pg_regress.c
+++ b/src/test/regress/pg_regress.c
@@ -1709,8 +1709,40 @@ log_child_failure(int exitstatus)
 		diag("(test process exited with unrecognized status %d)", exitstatus);
 }
 
+/*
+ * During  the test execution the concurrency level varies
+ * this computes the average concurrency from the moment the
+ * first test starts to the moment the last test stops.
+ */
+static void
+log_group_concurrency(instr_time *test_start, instr_time *test_stop, int num_tests)
+{
+	double group_start = INSTR_TIME_GET_NANOSEC(test_start[0]);
+	double group_stop = INSTR_TIME_GET_NANOSEC(test_stop[0]);
+	double total = (group_stop - group_start);
+	for(int i = 1; i < num_tests; i++)
+	{
+		double t0 = INSTR_TIME_GET_NANOSEC(test_start[i]);
+		double tf = INSTR_TIME_GET_NANOSEC(test_stop[i]);
+		if (i == 0 || (t0 < group_start))
+			group_start = t0;
+		if (i == 0 || (tf > group_stop))
+			group_stop = tf;
+		total += tf - t0;
+	}
+	note("effective concurrency %.2f / %d", total / (group_stop - group_start), num_tests);
+}
+
 /*
  * Run all the tests specified in one schedule file
+ *
+ * Schedule file syntax:
+ *   Schedule = (Blank | Comment | Group)*
+ *   Group    = "test:" (Line | Comment) (Indent Line)*
+ *   Line     = token (Space+ token)* Comment?
+ *   Blank    = '\n'
+ *   Comment  = '#' [^\n]* \n
+ *   Indent   = [\t ]+
  */
 static void
 run_schedule(const char *schedule, test_start_function startfunc,
@@ -1728,45 +1760,84 @@ run_schedule(const char *schedule, test_start_function startfunc,
 	char		scbuf[1024];
 	FILE	   *scf;
 	int			line_num = 0;
+	int			test_start = 0;
+	int			num_tests;
+	bool		in_group;
+	bool		eof = false;
+	int			i;
 
 	memset(tests, 0, sizeof(tests));
 	memset(resultfiles, 0, sizeof(resultfiles));
 	memset(expectfiles, 0, sizeof(expectfiles));
 	memset(tags, 0, sizeof(tags));
 
+
 	scf = fopen(schedule, "r");
 	if (!scf)
 		bail("could not open file \"%s\" for reading: %m", schedule);
 
-	while (fgets(scbuf, sizeof(scbuf), scf))
+	num_tests = 0;
+	in_group = false;
+
+	while (!eof)
 	{
 		char	   *test = NULL;
 		char	   *c;
-		int			num_tests;
 		bool		inword;
-		int			i;
+
+		if (!fgets(scbuf, sizeof(scbuf), scf))
+		{
+			eof = true;
+			goto run_group;
+		}
 
 		line_num++;
 
+
 		/* strip trailing whitespace, especially the newline */
 		i = strlen(scbuf);
 		while (i > 0 && isspace((unsigned char) scbuf[i - 1]))
 			scbuf[--i] = '\0';
 
-		if (scbuf[0] == '\0' || scbuf[0] == '#')
+		if (scbuf[0] == '#' || scbuf[0] == '\0')
 			continue;
-		if (strncmp(scbuf, "test: ", 6) == 0)
-			test = scbuf + 6;
+
+		if (scbuf[0] == ' ' || scbuf[0] == '\t')
+		{
+			if (!in_group)
+			{
+				bail("indented test name in schedule file \"%s\" line %d "
+					 "without a preceding \"test:\" line: %s",
+					 schedule, line_num, scbuf);
+			}
+			test = scbuf;
+		}
 		else
 		{
-			bail("syntax error in schedule file \"%s\" line %d: %s",
-				 schedule, line_num, scbuf);
+			if (strncmp(scbuf, "test:", 5) != 0)
+			{
+				bail("syntax error in schedule file \"%s\" line %d: "
+					 "expected \"test:\", indented test group continuation, got \"%s\"",
+					 schedule, line_num, scbuf);
+			}
+			if (in_group)
+				goto run_group;
+
+
+test_line:
+			test_start = line_num;
+			in_group = true;
+			num_tests = 0;
+			test = scbuf + 5;
 		}
 
-		num_tests = 0;
+		while (*test == ' ' || *test == '\t')
+			test++;
 		inword = false;
 		for (c = test;; c++)
 		{
+			if (*c == '#')
+				break;
 			if (*c == '\0' || isspace((unsigned char) *c))
 			{
 				if (inword)
@@ -1796,12 +1867,15 @@ run_schedule(const char *schedule, test_start_function startfunc,
 				inword = true;
 			}
 		}
+		continue;
 
+run_group:
+		if (!in_group)
+			bail("pg_regress error while reading schedule file \"%s\" line %d",
+				 schedule, test_start);
 		if (num_tests == 0)
-		{
-			bail("syntax error in schedule file \"%s\" line %d: %s",
-				 schedule, line_num, scbuf);
-		}
+			bail("empty test group in schedule file \"%s\" line %d",
+				 schedule, test_start);
 
 		if (num_tests == 1)
 		{
@@ -1850,6 +1924,9 @@ run_schedule(const char *schedule, test_start_function startfunc,
 			note_end();
 		}
 
+		if(num_tests > 1)
+			log_group_concurrency(starttimes, stoptimes, num_tests);
+
 		/* Check results for all tests */
 		for (i = 0; i < num_tests; i++)
 		{
@@ -1901,7 +1978,6 @@ run_schedule(const char *schedule, test_start_function startfunc,
 				}
 			}
 		}
-
 		for (i = 0; i < num_tests; i++)
 		{
 			pg_free(tests[i]);
@@ -1910,11 +1986,19 @@ run_schedule(const char *schedule, test_start_function startfunc,
 			free_stringlist(&expectfiles[i]);
 			free_stringlist(&tags[i]);
 		}
+		/* return */
+		if (strncmp(scbuf, "test:", 5) == 0)
+			goto test_line;
+
+		in_group = false;
+		num_tests = 0;
 	}
 
 	fclose(scf);
 }
 
+
+
 /*
  * Run a single test
  */
-- 
2.53.0

