-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathrtc-test.sh
More file actions
executable file
·2148 lines (1906 loc) · 85 KB
/
rtc-test.sh
File metadata and controls
executable file
·2148 lines (1906 loc) · 85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
# rtc-test.sh -- WordPress RTC HTTP polling load tester
#
# Two-file package: drop rtc-test.php into mu-plugins, then use this script.
#
# Requirements: bash, curl
# WP-CLI (wp) is used by setup/teardown if available; not needed for test commands.
# python3 is required for the capture-sanitize and replay commands only.
#
# Workflow A -- run everything on the web host (WP-CLI available):
# bash rtc-test.sh setup
# bash rtc-test.sh baseline
# bash rtc-test.sh sustain
# bash rtc-test.sh teardown
#
# Workflow B -- setup on web host, run tests from localhost:
# On web host: bash rtc-test.sh setup
# cat .env # copy output to clipboard
# On localhost: paste into .env, then:
# bash rtc-test.sh refresh-auth # re-login from this host
# bash rtc-test.sh baseline
# bash rtc-test.sh sustain
# On web host: bash rtc-test.sh teardown
#
# Commands:
# setup Install MU-plugin, create rtctest user + test post, write .env
# teardown Delete test post, remove cookie jar, strip generated section from .env
# baseline Measure ambient WP REST overhead (run before scenarios)
# single-idle 1 client, POLLS polls, no updates
# two-idle 2 clients alternating, awareness propagation check
# two-editing 2 clients: sync handshake + update exchange (1 pass)
# one-idle-one-editing 2 clients: 1 editor sends updates, 1 idle watches
# n-idle N_CLIENTS clients round-robin, awareness only
# compaction-trigger Send updates until should_compact fires, then compact
# report Fetch log from plugin and print summary table
# clear Delete all log entries (table intact)
# reset Drop and recreate the log table
set -euo pipefail
# -------------------------------------------------------------------------
# Config file auto-load
# Source .env (written by setup) before applying defaults so that
# environment variables set by the caller still take precedence.
# -------------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# resolve_env_file: prints the path of the active env file.
# Uses .env if it exists, otherwise falls back to .env.example.
resolve_env_file() {
if [ -f "${SCRIPT_DIR}/.env" ]; then
printf '%s' "${SCRIPT_DIR}/.env"
else
printf '%s' "${SCRIPT_DIR}/.env.example"
fi
}
_env_file="$(resolve_env_file)"
if [ -f "${_env_file}" ]; then
# Snapshot any variables the caller already set in the environment so we
# can restore them after sourcing -- env vars take precedence over the file.
_pre_url="${WP_URL:-}"
_pre_user="${WP_USER:-}"
_pre_pass="${WP_PASS:-}"
_pre_jar="${WP_COOKIE_JAR:-}"
_pre_nonce="${WP_NONCE:-}"
_pre_path="${WP_PATH:-}"
_pre_post="${POST_ID:-}"
_pre_approach="${APPROACH:-}"
_pre_reporter_url="${REPORTER_URL:-}"
_pre_api_key="${REPORTER_API_KEY:-}"
_pre_env_name="${ENVIRONMENT_NAME:-}"
_pre_has_poll_delay=0
[ "${POLL_DELAY+x}" = x ] && _pre_has_poll_delay=1 && _pre_poll_delay="${POLL_DELAY}"
_pre_has_update_size=0
[ "${UPDATE_SIZE+x}" = x ] && _pre_has_update_size=1 && _pre_update_size="${UPDATE_SIZE}"
# shellcheck source=/dev/null
. "${_env_file}"
[ -n "${_pre_url}" ] && WP_URL="${_pre_url}"
[ -n "${_pre_user}" ] && WP_USER="${_pre_user}"
[ -n "${_pre_pass}" ] && WP_PASS="${_pre_pass}"
[ -n "${_pre_jar}" ] && WP_COOKIE_JAR="${_pre_jar}"
[ -n "${_pre_nonce}" ] && WP_NONCE="${_pre_nonce}"
[ -n "${_pre_path}" ] && WP_PATH="${_pre_path}"
[ -n "${_pre_post}" ] && POST_ID="${_pre_post}"
[ -n "${_pre_approach}" ] && APPROACH="${_pre_approach}"
[ -n "${_pre_reporter_url}" ] && REPORTER_URL="${_pre_reporter_url}"
[ -n "${_pre_api_key}" ] && REPORTER_API_KEY="${_pre_api_key}"
[ -n "${_pre_env_name}" ] && ENVIRONMENT_NAME="${_pre_env_name}"
[ "${_pre_has_poll_delay}" = 1 ] && POLL_DELAY="${_pre_poll_delay}"
[ "${_pre_has_update_size}" = 1 ] && UPDATE_SIZE="${_pre_update_size}"
unset _pre_url _pre_user _pre_pass _pre_jar _pre_nonce _pre_path _pre_post \
_pre_approach _pre_reporter_url _pre_api_key _pre_env_name \
_pre_has_poll_delay _pre_poll_delay _pre_has_update_size _pre_update_size
fi
unset _env_file
# die MESSAGE — defined before config so POLL_DELAY / UPDATE_SIZE validation can use it.
die() { printf 'ERROR: %s\n' "$1" >&2; exit 1; }
# rtc_require_poll_delay VALUE -- echo decimal 0..86400 (matches server clamp) or die.
# Rejects empty, non-numeric, newlines, and header-injection characters for safe curl -H / URLs.
rtc_require_poll_delay() {
local v="$1"
case "${v}" in
''|*[!0-9]*) die "POLL_DELAY must be a non-negative integer (refusing unsafe or empty value)" ;;
esac
if [ "${v}" -gt 86400 ] 2>/dev/null; then
die "POLL_DELAY must be <= 86400 (got: ${v})"
fi
printf '%s' "${v}"
}
# rtc_require_update_size VALUE -- echo small|medium|large or die.
rtc_require_update_size() {
case "$1" in
small|medium|large) printf '%s' "$1" ;;
*) die "UPDATE_SIZE must be small, medium, or large (refusing unsafe value)" ;;
esac
}
# -------------------------------------------------------------------------
# Configuration (all overridable via environment variables or .env)
# -------------------------------------------------------------------------
WP_URL="${WP_URL:-http://localhost}"
WP_USER="${WP_USER:-admin}"
WP_PASS="${WP_PASS:-}" # WP login password (set by setup)
WP_COOKIE_JAR="${WP_COOKIE_JAR:-${SCRIPT_DIR}/rtc-test-cookies.txt}" # cookie jar path (set by setup)
WP_NONCE="${WP_NONCE:-}" # wp_rest nonce (set by setup, ~12h TTL)
WP_PATH="${WP_PATH:-}" # Absolute path to WordPress root; required by setup
REQUIRED_WP_VERSION="${REQUIRED_WP_VERSION:-nightly}" # WordPress version required by these tests
POST_ID="${POST_ID:-1}"
POLLS="${POLLS:-10}"
POLL_DELAY="${POLL_DELAY:-1}" # Seconds between polls per client (0 = immediate re-poll / stress mode)
N_CLIENTS="${N_CLIENTS:-3}"
DURATION="${DURATION:-30}" # Seconds to run for sustain command
UPDATE_SIZE="${UPDATE_SIZE:-small}"
APPROACH="${APPROACH:-}" # Storage approach label (e.g. post-meta, custom-table); written to log
REPORTER_URL="${REPORTER_URL:-https://make.wordpress.org/hosting}"
# run.sh may set comma-separated lists in .env; a single rtc-test.sh command uses the first value only.
case "${POLL_DELAY}" in
*,*)
POLL_DELAY="${POLL_DELAY%%,*}"
POLL_DELAY="${POLL_DELAY//[[:space:]]/}"
printf 'NOTICE: POLL_DELAY is a list; using first value for this command: %s\n' "${POLL_DELAY}" >&2
;;
esac
case "${UPDATE_SIZE}" in
*,*)
UPDATE_SIZE="${UPDATE_SIZE%%,*}"
UPDATE_SIZE="${UPDATE_SIZE//[[:space:]]/}"
printf 'NOTICE: UPDATE_SIZE is a list; using first value for this command: %s\n' "${UPDATE_SIZE}" >&2
;;
esac
POLL_DELAY="$(rtc_require_poll_delay "${POLL_DELAY}")"
UPDATE_SIZE="$(rtc_require_update_size "${UPDATE_SIZE}")"
# -------------------------------------------------------------------------
# Deterministic test constants
# -------------------------------------------------------------------------
# Client IDs are integers (REST schema: minimum 1).
CLIENT_A=10001
CLIENT_B=10002
# Room identifier.
ROOM="postType/post:${POST_ID}"
# Fixed awareness payloads per client (minimal structure).
AWARENESS_A='{"name":"RTC-Test-Client-A","color":"#cc0000"}'
AWARENESS_B='{"name":"RTC-Test-Client-B","color":"#0000cc"}'
# CRDT sync protocol payloads (base64-encoded, opaque to the server).
# The server stores and relays these without CRDT validation.
SYNC_STEP1_DATA="AA==" # base64 of 0x00 -- minimal Yjs state vector (empty doc)
SYNC_STEP2_DATA="AAAA" # base64 of 0x00 0x00 0x00 -- minimal sync step 2 response
# Update payloads by size (fixed, so tests are deterministic and reproducible).
# small = 3 decoded bytes -- baseline latency
# medium = 384 decoded bytes -- typical edit delta
# large = 3072 decoded bytes -- large paste / bulk edit
UPDATE_SMALL="AQAB"
# shellcheck disable=SC2016
UPDATE_MEDIUM="$(printf '%0.sAQABAgACAwADBAA' {1..30} | head -c 512)"
# shellcheck disable=SC2016
UPDATE_LARGE="$(printf '%0.sAQABAgACAwADBAA' {1..280} | head -c 4096)"
# Select payload based on UPDATE_SIZE.
case "${UPDATE_SIZE}" in
medium) UPDATE_DATA="${UPDATE_MEDIUM}" ;;
large) UPDATE_DATA="${UPDATE_LARGE}" ;;
*) UPDATE_DATA="${UPDATE_SMALL}" ;;
esac
# -------------------------------------------------------------------------
# Identification headers (present on every /wp-sync/ request)
# -------------------------------------------------------------------------
# X-RTC-Test: 1 -- tells the plugin to record this request
# X-RTC-Scenario: <s> -- labels the scenario in the log
# -b sends cookies read-only (does not save new Set-Cookie headers), which is
# safe for concurrent curl requests since all instances only read the jar.
BASE_CURL_OPTS=(
--silent
--show-error
-b "${WP_COOKIE_JAR}"
-H "X-WP-Nonce: ${WP_NONCE}"
-H "X-RTC-Test: 1"
-H "Content-Type: application/json"
-H "X-RTC-Poll-Delay: ${POLL_DELAY}"
-H "X-RTC-Update-Size: ${UPDATE_SIZE}"
)
[ -n "${APPROACH}" ] && BASE_CURL_OPTS+=( -H "X-RTC-Approach: ${APPROACH}" )
RTC_ENDPOINT="${WP_URL}/wp-json/wp-sync/v1/updates"
PLUGIN_LOG_URL="${WP_URL}/wp-json/rtc-test/v1/log"
PLUGIN_ENV_URL="${WP_URL}/wp-json/rtc-test/v1/env"
PLUGIN_TABLE_URL="${WP_URL}/wp-json/rtc-test/v1/table"
PLUGIN_REPORT_URL="${WP_URL}/wp-json/rtc-test/v1/report"
PLUGIN_REPORT_ALL_URL="${WP_URL}/wp-json/rtc-test/v1/report-all"
PLUGIN_SUBMIT_URL="${WP_URL}/wp-json/rtc-test/v1/submit"
CAPTURE_SESSION_URL="${WP_URL}/wp-json/rtc-capture/v1/session"
CAPTURE_SESSIONS_URL="${WP_URL}/wp-json/rtc-capture/v1/sessions"
# -------------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------------
# require_auth
# Confirms that cookie jar and nonce are available before running a test command.
require_auth() {
if [ ! -f "${WP_COOKIE_JAR}" ] || [ -z "${WP_NONCE}" ]; then
die "Auth not configured. Run: bash rtc-test.sh setup"
fi
}
# do_login USER PASS URL JAR
# Logs in via wp-login.php and saves cookies to JAR.
# Returns 0 if wordpress_logged_in cookie is present in JAR, 1 otherwise.
do_login() {
local _user="$1" _pass="$2" _url="$3" _jar="$4"
# GET login page first to seed the test cookie that WP checks on POST.
curl -s -c "${_jar}" "${_url}/wp-login.php" -o /dev/null
# POST credentials. rememberme=forever gives a 14-day cookie vs 2-day default.
curl -s -c "${_jar}" -b "${_jar}" -L -o /dev/null \
--data-urlencode "log=${_user}" \
--data-urlencode "pwd=${_pass}" \
--data "wp-submit=Log+In&redirect_to=%2Fwp-admin%2F&testcookie=1&rememberme=forever" \
"${_url}/wp-login.php"
grep -q "wordpress_logged_in" "${_jar}" 2>/dev/null
}
# do_get_nonce URL JAR
# Calls the rtctest_nonce AJAX handler and prints the wp_rest nonce.
# Prints nothing if the call fails (caller should check for empty output).
do_get_nonce() {
local _url="$1" _jar="$2"
curl -s -b "${_jar}" -X POST \
--data "action=rtctest_nonce" \
"${_url}/wp-admin/admin-ajax.php" \
| grep -o '"nonce":"[^"]*"' | head -1 | cut -d'"' -f4 || true
}
# rtc_post SCENARIO JSON_BODY
# Posts to the RTC endpoint with the given scenario label.
# Test metadata is sent as both request headers and query parameters so the
# plugin can receive it even when a reverse proxy strips custom headers.
# _wpnonce is sent in the URL as a fallback in case X-WP-Nonce is also stripped.
# Prints the raw response body followed by __HTTP_STATUS__:NNN on its own line.
rtc_post() {
local scenario="$1"
local body="$2"
local url="${RTC_ENDPOINT}?_rtctest=1&_rtcscenario=${scenario}&_wpnonce=${WP_NONCE}"
[ -n "${APPROACH}" ] && url="${url}&_rtcapproach=${APPROACH}"
url="${url}&_rtcpolldelay=${POLL_DELAY}&_rtcupdatesize=${UPDATE_SIZE}"
curl "${BASE_CURL_OPTS[@]}" \
-H "X-RTC-Scenario: ${scenario}" \
-X POST "${url}" \
-w '\n__HTTP_STATUS__:%{http_code}' \
-d "${body}"
}
# rtc_post_timed SCENARIO JSON_BODY
# Same as rtc_post but appends timing breakdown lines measured by curl:
# __CLIENT_MS__:<total_ms>
# __CLIENT_TIMING__:<connect_ms>:<tls_ms>:<server_ms>:<transfer_ms>
# where server_ms = time_starttransfer - time_pretransfer (PHP/DB processing only).
rtc_post_timed() {
local scenario="$1"
local body="$2"
local url="${RTC_ENDPOINT}?_rtctest=1&_rtcscenario=${scenario}&_wpnonce=${WP_NONCE}"
[ -n "${APPROACH}" ] && url="${url}&_rtcapproach=${APPROACH}"
url="${url}&_rtcpolldelay=${POLL_DELAY}&_rtcupdatesize=${UPDATE_SIZE}"
local timing
timing=$(curl "${BASE_CURL_OPTS[@]}" \
-H "X-RTC-Scenario: ${scenario}" \
-X POST "${url}" \
-d "${body}" \
-w '\n__CURL_TIME__:%{time_namelookup}:%{time_connect}:%{time_appconnect}:%{time_pretransfer}:%{time_starttransfer}:%{time_total}' \
-D /tmp/rtctest_last_headers.txt \
-o /tmp/rtctest_last_response.json 2>&1) || true
cat /tmp/rtctest_last_response.json
# Parse the six timing fields and derive the four useful deltas.
local ms connect_ms tls_ms server_ms transfer_ms
ms=$(printf '%s' "${timing}" | grep '__CURL_TIME__' | awk -F: '{
dns=$2; conn=$3; tls=$4; pre=$5; ttfb=$6; total=$7
printf "%.0f", total*1000
}')
connect_ms=$(printf '%s' "${timing}" | grep '__CURL_TIME__' | awk -F: '{
printf "%.0f", ($3-$2)*1000
}')
tls_ms=$(printf '%s' "${timing}" | grep '__CURL_TIME__' | awk -F: '{
printf "%.0f", ($4-$3)*1000
}')
server_ms=$(printf '%s' "${timing}" | grep '__CURL_TIME__' | awk -F: '{
printf "%.0f", ($6-$5)*1000
}')
transfer_ms=$(printf '%s' "${timing}" | grep '__CURL_TIME__' | awk -F: '{
printf "%.0f", ($7-$6)*1000
}')
printf '\n__CLIENT_MS__:%s\n' "${ms}"
printf '__CLIENT_TIMING__:%s:%s:%s:%s\n' "${connect_ms}" "${tls_ms}" "${server_ms}" "${transfer_ms}"
}
# extract_cursor RESPONSE_JSON
# Prints the end_cursor value from the first room in the response.
extract_cursor() {
printf '%s' "$1" | grep -o '"end_cursor":[0-9]*' | head -1 | grep -o '[0-9]*' || true
}
# extract_update_count RESPONSE_JSON
# Prints the total_updates value from the first room in the response.
extract_update_count() {
printf '%s' "$1" | grep -o '"total_updates":[0-9]*' | head -1 | grep -o '[0-9]*' || true
}
# extract_should_compact RESPONSE_JSON
# Prints "true" or "false".
extract_should_compact() {
printf '%s' "$1" | grep -o '"should_compact":\(true\|false\)' | head -1 | grep -o '\(true\|false\)' || true
}
# extract_client_ms TIMED_RESPONSE
# Prints total elapsed ms from rtc_post_timed output.
extract_client_ms() {
printf '%s' "$1" | grep '__CLIENT_MS__' | grep -o '[0-9]*$'
}
# extract_server_ms TIMED_RESPONSE
# Prints server processing ms (TTFB - pretransfer) from rtc_post_timed output.
# This is PHP+DB time only, with network and TLS removed.
extract_server_ms() {
printf '%s' "$1" | grep '__CLIENT_TIMING__' | awk -F: '{print $4}'
}
# extract_connect_ms TIMED_RESPONSE
# Prints TCP connect ms. Near-zero means loopback; >5ms means real network.
extract_connect_ms() {
printf '%s' "$1" | grep '__CLIENT_TIMING__' | awk -F: '{print $2}'
}
# extract_tls_ms TIMED_RESPONSE
# Prints TLS handshake ms (time_appconnect - time_connect).
extract_tls_ms() {
printf '%s' "$1" | grep '__CLIENT_TIMING__' | awk -F: '{print $3}'
}
# extract_transfer_ms TIMED_RESPONSE
# Prints response body transfer ms (time_total - time_starttransfer).
extract_transfer_ms() {
printf '%s' "$1" | grep '__CLIENT_TIMING__' | awk -F: '{print $5}'
}
# extract_status RESPONSE
# Prints the HTTP status code appended by rtc_post (__HTTP_STATUS__:NNN).
# Returns empty string if not present (e.g. network error / curl failure).
extract_status() {
printf '%s' "$1" | grep '__HTTP_STATUS__' | grep -o '[0-9]*$'
}
# count_awareness RESPONSE_JSON
# Counts how many client awareness entries are in the response.
# The server returns awareness as {"<client_id>": <state>} -- each state has a
# "name" field from the client's awareness payload, so we count those.
count_awareness() {
printf '%s' "$1" | grep -o '"name"' | wc -l | tr -d ' ' || true
}
# check_rtc_response RESPONSE_JSON CONTEXT
# Prints a diagnostic and returns 1 if the response is not a valid RTC reply.
check_rtc_response() {
local resp="$1"
local ctx="${2:-}"
if printf '%s' "${resp}" | grep -q '"end_cursor"'; then
return 0
fi
printf 'ERROR%s: RTC endpoint did not return a valid response.\n' \
"${ctx:+ (${ctx})}"
printf 'Raw response: %s\n' "${resp}" | head -3
printf '\nPossible causes:\n'
printf ' 1. Gutenberg RTC feature not enabled (check: wp option get wp_collaboration_enabled)\n'
printf ' 2. The /wp-sync/v1/updates endpoint does not exist on this install\n'
printf ' 3. Authentication failed (check WP_USER and WP_PASS in .env)\n'
printf ' If you copied .env from another host, run: bash rtc-test.sh refresh-auth\n'
printf ' 4. POST_ID=%s may not exist or user lacks edit permission\n' "${POST_ID}"
return 1
}
# build_room_json CLIENT_ID AWARENESS_JSON CURSOR UPDATES_JSON
build_room_json() {
local cid="$1"
local awareness="$2"
local cursor="$3"
local updates="$4"
printf '{"room":"%s","client_id":%s,"awareness":%s,"after":%s,"updates":[%s]}' \
"${ROOM}" "${cid}" "${awareness}" "${cursor}" "${updates}"
}
# build_rooms_json ROOM_JSON [ROOM_JSON ...]
build_rooms_json() {
local rooms=""
for r in "$@"; do
[ -n "${rooms}" ] && rooms="${rooms},"
rooms="${rooms}${r}"
done
printf '{"rooms":[%s]}' "${rooms}"
}
# build_update_json TYPE DATA
build_update_json() {
printf '{"type":"%s","data":"%s"}' "$1" "$2"
}
# print_header LABEL
print_header() {
printf '\n=== %s ===\n' "$1"
}
# print_test_run_conditions STORAGE_APPROACH
# Summarizes parameters sent on each logged RTC request (headers / query / DB columns).
# Call after apply-approach (or whenever you want a clear record of the active matrix).
print_test_run_conditions() {
local storage="${1:-}"
printf '\n'
printf '── Test conditions (logged per request'
[ -n "${storage}" ] && printf '; storage approach: %s' "${storage}"
printf ') ──\n'
printf ' POLL_DELAY: %s\n' "${POLL_DELAY}"
printf ' UPDATE_SIZE: %s\n' "${UPDATE_SIZE}"
printf ' POLLS: %s\n' "${POLLS}"
printf ' N_CLIENTS: %s\n' "${N_CLIENTS}"
printf ' DURATION: %s s\n' "${DURATION}"
printf ' (Sent as X-RTC-Poll-Delay / X-RTC-Update-Size and _rtcpolldelay / _rtcupdatesize.)\n'
}
# -------------------------------------------------------------------------
# WordPress version enforcement
# -------------------------------------------------------------------------
# ensure_wp_version -- verify the installed WordPress version matches
# REQUIRED_WP_VERSION; download and install it via WP-CLI if not.
# When REQUIRED_WP_VERSION is "nightly", any alpha/beta/RC build is accepted
# and a fresh nightly is only downloaded if nothing is installed yet.
# Pass all WP-CLI flags as arguments (e.g. --path=... --allow-root --url=...).
ensure_wp_version() {
local current
current="$(wp "$@" core version 2>/dev/null)" \
|| { printf 'ERROR: Could not read WordPress version via WP-CLI.\n'; return 1; }
if [ "${REQUIRED_WP_VERSION}" = "nightly" ]; then
# Any pre-release build satisfies the nightly requirement. Re-download
# only if the site reports no version at all.
if [ -n "${current}" ]; then
printf 'WordPress: %s (nightly or later accepted)\n' "${current}"
return 0
fi
printf 'WordPress: not installed. Downloading nightly...\n'
wp "$@" core download --version=nightly --skip-content \
|| { printf 'ERROR: WP nightly download failed.\n'; return 1; }
wp "$@" core update-db \
|| printf 'WARNING: Database update step failed or was not needed.\n'
return 0
fi
if [ "${current}" = "${REQUIRED_WP_VERSION}" ]; then
printf 'WordPress: %s (matches required version)\n' "${current}"
return 0
fi
printf 'WordPress: %s installed, %s required. Updating...\n' \
"${current}" "${REQUIRED_WP_VERSION}"
wp "$@" core update --version="${REQUIRED_WP_VERSION}" --force \
|| { printf 'ERROR: WP core update to %s failed.\n' "${REQUIRED_WP_VERSION}"; return 1; }
wp "$@" core update-db \
|| printf 'WARNING: Database update step failed or was not needed.\n'
local updated
updated="$(wp "$@" core version 2>/dev/null)"
if [ "${updated}" = "${REQUIRED_WP_VERSION}" ]; then
printf 'WordPress: updated to %s\n' "${updated}"
else
printf 'ERROR: Version after update is %s, expected %s.\n' \
"${updated}" "${REQUIRED_WP_VERSION}"
return 1
fi
}
cmd_print_test_conditions() {
local storage="${1:-}"
print_header "print-test-conditions${storage:+ (${storage})}"
print_test_run_conditions "${storage}"
}
cmd_ensure_wp_version() {
print_header "ensure-wp-version"
command -v wp >/dev/null 2>&1 || die "WP-CLI is required for ensure-wp-version."
[ -n "${WP_PATH:-}" ] || die "WP_PATH is not set. Add it to your .env file."
local WP_FLAGS=()
[ "$(id -u)" = "0" ] && WP_FLAGS+=( "--allow-root" )
WP_FLAGS+=( "--path=${WP_PATH}" )
[ -n "${WP_URL:-}" ] && WP_FLAGS+=( "--url=${WP_URL}" )
ensure_wp_version "${WP_FLAGS[@]}"
}
# -------------------------------------------------------------------------
# Approach helpers
# -------------------------------------------------------------------------
# approach_patch_file APPROACH -- prints the absolute patch file path, or empty
# string if the approach is the RC2 baseline (no patch required).
approach_patch_file() {
case "$1" in
custom-table) printf '%s' "${SCRIPT_DIR}/patches/02-custom-table.patch" ;;
post-meta-transients) printf '%s' "${SCRIPT_DIR}/patches/03-post-meta-transients.patch" ;;
custom-table-with-transients) printf '%s' "${SCRIPT_DIR}/patches/04-custom-table-with-transients.patch" ;;
*) printf '' ;; # post-meta (RC2 baseline) or empty
esac
}
# approach_has_schema_change APPROACH -- returns 0 if the approach adds the
# wp_collaboration table (requires wp core update-db and table teardown).
approach_has_schema_change() {
case "$1" in
custom-table|custom-table-with-transients) return 0 ;;
*) return 1 ;;
esac
}
# _build_wp_flags -- populates a local array named WP_FLAGS from the current
# environment. Call as: local WP_FLAGS=(); _build_wp_flags
_build_wp_flags() {
[ "$(id -u)" = "0" ] && WP_FLAGS+=( "--allow-root" )
WP_FLAGS+=( "--path=${WP_PATH}" )
[ -n "${WP_URL:-}" ] && WP_FLAGS+=( "--url=${WP_URL}" )
}
# _clear_rtc_data WP_FLAGS... -- deletes all RTC collaboration data so the next
# approach starts from a clean state. Handles all three storage types safely:
# post meta rows, the collaboration table (if present), and awareness transients.
_clear_rtc_data() {
printf 'Clearing RTC collaboration data...\n'
wp "$@" eval '
global $wpdb;
// Remove post meta rows written by the post-meta storage implementation.
$deleted = (int) $wpdb->query(
"DELETE FROM {$wpdb->postmeta}
WHERE meta_key IN (\"wp_sync_update\", \"wp_sync_awareness_state\")"
);
echo " Post meta RTC rows deleted: {$deleted}\n";
// Truncate the collaboration table if it exists (custom-table approaches).
$collab = $wpdb->prefix . "collaboration";
$exists = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $collab ) );
if ( $exists ) {
$wpdb->query( "TRUNCATE TABLE `{$collab}`" );
echo " Collaboration table truncated.\n";
}
// Remove awareness transients (post-meta-transients approach).
$deleted = (int) $wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE \"_transient_wp_sync_awareness%\"
OR option_name LIKE \"_transient_timeout_wp_sync_awareness%\""
);
if ( $deleted > 0 ) {
echo " Awareness transients deleted: {$deleted}\n";
}
' 2>/dev/null || printf ' WARNING: Could not clear RTC data via WP-CLI.\n'
}
cmd_apply_approach() {
local approach="${1:-}"
[ -n "${approach}" ] || die "Usage: bash rtc-test.sh apply-approach <approach>
Approaches: post-meta custom-table post-meta-transients custom-table-with-transients"
case "${approach}" in
post-meta|custom-table|post-meta-transients|custom-table-with-transients) ;;
*) die "Unknown approach '${approach}'. Valid: post-meta custom-table post-meta-transients custom-table-with-transients" ;;
esac
print_header "apply-approach (${approach})"
command -v wp >/dev/null 2>&1 || die "WP-CLI is required for apply-approach."
[ -n "${WP_PATH:-}" ] || die "WP_PATH is not set. Add it to your .env file."
local WP_FLAGS=()
_build_wp_flags
# Step 1: Reset to a clean nightly build so every approach starts from identical files.
printf 'Downloading WordPress nightly...\n'
wp "${WP_FLAGS[@]}" core download --force --version=nightly --skip-content \
|| die "Failed to download WordPress nightly."
# Step 2: Re-copy the MU-plugin (nightly download does not touch wp-content, but
# re-copying ensures the plugin version matches this repo).
local mu_dir="${WP_PATH}/wp-content/mu-plugins"
mkdir -p "${mu_dir}"
cp "${SCRIPT_DIR}/rtc-test.php" "${mu_dir}/rtc-test.php" \
&& printf 'MU-plugin: re-copied\n' \
|| printf 'WARNING: Could not re-copy MU-plugin.\n'
# Step 3: Baseline DB upgrade (nightly may carry a newer schema than what is in the DB).
wp "${WP_FLAGS[@]}" core update-db >/dev/null 2>&1 || true
# Step 4: Apply the approach's patch (post-meta is the nightly baseline, no patch needed).
local new_patch
new_patch="$(approach_patch_file "${approach}")"
if [ -n "${new_patch}" ]; then
[ -f "${new_patch}" ] || die "Patch file not found: ${new_patch}"
# Remove any files that this patch would create fresh. wp core download
# does not delete files that are absent from the nightly package, so files
# added by a previous approach's patch would still be on disk and cause
# patch to detect an "already exists" conflict.
# New-file hunks have "--- /dev/null" as their source; the destination line
# is "+++ b/src/<path>" which, with -p2, maps to <path> under WP_PATH.
local _del_count=0
while IFS= read -r _rel; do
local _target="${WP_PATH}/${_rel}"
if [ -f "${_target}" ]; then
rm -f "${_target}"
_del_count=$(( _del_count + 1 ))
fi
done < <(grep -A1 '^--- /dev/null' "${new_patch}" \
| grep '^\+\+\+ ' \
| sed 's|^\+\+\+ b/src/||')
[ "${_del_count}" -gt 0 ] && \
printf 'Removed %d file(s) added by a previous patch.\n' "${_del_count}"
printf 'Applying patch for %s...\n' "${approach}"
local _fwd_dry
if ! _fwd_dry=$(patch --dry-run --batch -p2 --ignore-whitespace -d "${WP_PATH}" < "${new_patch}" 2>&1); then
printf '%s\n' "${_fwd_dry}"
die "Patch dry-run failed. The patch context does not match the nightly files.
Check which file/hunk is listed above."
fi
patch --batch -p2 --ignore-whitespace -d "${WP_PATH}" < "${new_patch}" \
|| die "Patch failed. WordPress files may be in an inconsistent state."
printf 'Patch applied.\n'
else
printf 'Approach %s is the nightly baseline — no patch needed.\n' "${approach}"
fi
# Step 5: Run DB upgrade again if the approach introduces a schema change (adds the
# wp_collaboration table). wp_is_collaboration_enabled() requires db_version >= 61841.
if approach_has_schema_change "${approach}"; then
printf 'Running database upgrade (adds collaboration table)...\n'
wp "${WP_FLAGS[@]}" core update-db || die "Database upgrade failed."
local db_ver
db_ver="$(wp "${WP_FLAGS[@]}" option get db_version 2>/dev/null)"
if [ "${db_ver:-0}" -ge 61841 ] 2>/dev/null; then
printf 'Database: version %s (collaboration table ready)\n' "${db_ver}"
else
printf 'WARNING: db_version is %s, expected >= 61841. RTC may not activate.\n' "${db_ver}"
fi
fi
# Step 6: Clear all RTC data so this run starts from a clean state.
_clear_rtc_data "${WP_FLAGS[@]}"
# Step 7: Flush the object cache.
wp "${WP_FLAGS[@]}" cache flush 2>/dev/null \
&& printf 'Object cache: flushed\n' \
|| printf 'Object cache: no external cache to flush\n'
# Step 8: Ensure RTC is enabled.
wp "${WP_FLAGS[@]}" option update wp_collaboration_enabled 1 >/dev/null 2>&1 \
&& printf 'RTC: enabled\n' \
|| printf 'WARNING: Could not enable RTC. Verify in Settings > Writing.\n'
printf '\nApproach "%s" is ready. Run tests with APPROACH="%s".\n' \
"${approach}" "${approach}"
}
cmd_reset_approach() {
print_header "reset-approach"
command -v wp >/dev/null 2>&1 || die "WP-CLI is required for reset-approach."
[ -n "${WP_PATH:-}" ] || die "WP_PATH is not set. Add it to your .env file."
local WP_FLAGS=()
_build_wp_flags
printf 'Downloading WordPress nightly...\n'
wp "${WP_FLAGS[@]}" core download --force --version=nightly --skip-content \
|| die "Failed to download WordPress nightly."
# Remove any files added by approach patches — wp core download leaves them behind.
local _patch_file _del_count=0 _rel _target
for _patch_file in "${SCRIPT_DIR}/patches/"*.patch; do
[ -f "${_patch_file}" ] || continue
while IFS= read -r _rel; do
_target="${WP_PATH}/${_rel}"
if [ -f "${_target}" ]; then
rm -f "${_target}"
_del_count=$(( _del_count + 1 ))
fi
done < <(grep -A1 '^--- /dev/null' "${_patch_file}" \
| grep '^\+\+\+ ' \
| sed 's|^\+\+\+ b/src/||')
done
[ "${_del_count}" -gt 0 ] && \
printf 'Removed %d file(s) added by approach patches.\n' "${_del_count}"
local mu_dir="${WP_PATH}/wp-content/mu-plugins"
mkdir -p "${mu_dir}"
cp "${SCRIPT_DIR}/rtc-test.php" "${mu_dir}/rtc-test.php" \
&& printf 'MU-plugin: re-copied\n' \
|| printf 'WARNING: Could not re-copy MU-plugin.\n'
wp "${WP_FLAGS[@]}" core update-db >/dev/null 2>&1 || true
wp "${WP_FLAGS[@]}" cache flush 2>/dev/null || true
wp "${WP_FLAGS[@]}" option update wp_collaboration_enabled 1 >/dev/null 2>&1 || true
printf '\nReset to clean nightly.\n'
}
# -------------------------------------------------------------------------
# Commands
# -------------------------------------------------------------------------
cmd_setup() {
print_header "setup"
if command -v wp >/dev/null 2>&1; then
setup_wpcli
else
setup_manual
fi
}
setup_wpcli() {
printf 'WP-CLI found. Auto-configuring...\n\n'
# WP_PATH is required for setup; no auto-detection.
if [ -z "${WP_PATH:-}" ]; then
printf 'ERROR: WP_PATH is not set.\n'
printf 'Add it to your .env file (copy .env.example if you have not already):\n'
printf ' WP_PATH="/var/www/html"\n'
printf '\nThen re-run: bash rtc-test.sh setup\n\n'
setup_manual
return 1
fi
local WP_FLAGS=()
[ "$(id -u)" = "0" ] && WP_FLAGS+=( "--allow-root" )
WP_FLAGS+=( "--path=${WP_PATH}" )
if ! wp "${WP_FLAGS[@]}" core version >/dev/null 2>&1; then
printf 'WP-CLI cannot reach WordPress at: %s\n' "${WP_PATH}"
printf 'Verify WP_PATH points to the directory containing wp-config.php.\n\n'
setup_manual
return 1
fi
printf 'WordPress root: %s\n' "${WP_PATH}"
# Pull the authoritative site URL from the database, then add it to flags
# so multisite / subdomain installs resolve correctly.
local site_url
site_url="$(wp "${WP_FLAGS[@]}" option get siteurl 2>/dev/null)" || site_url="${WP_URL}"
WP_FLAGS+=( "--url=${site_url}" )
# Verify the URL is reachable before writing it to config. HTTP 200 or 401
# both confirm the REST API is present; anything else (including curl error 7
# for "connection refused") means test requests will fail.
local http_code
http_code="$(curl --silent --max-time 5 -o /dev/null -w '%{http_code}' \
"${site_url}/wp-json/" 2>/dev/null)" || http_code="000"
case "${http_code}" in
200|401)
printf 'Site URL: %s (reachable, HTTP %s)\n' "${site_url}" "${http_code}" ;;
*)
printf 'Site URL: %s\n' "${site_url}"
printf 'WARNING: %s/wp-json/ returned HTTP %s.\n' "${site_url}" "${http_code}"
printf 'Test requests will likely fail. Check that this URL is reachable\n'
printf 'from the host running this script.\n' ;;
esac
# Ensure the required WordPress version is installed before proceeding.
ensure_wp_version "${WP_FLAGS[@]}" || die "WordPress version requirement not met. Aborting setup."
# Copy the MU-plugin now, before login/nonce steps that depend on it being active.
local wp_content_dir mu_plugins_dir
wp_content_dir="$(wp "${WP_FLAGS[@]}" eval 'echo WP_CONTENT_DIR;' 2>/dev/null)"
mu_plugins_dir="${wp_content_dir}/mu-plugins"
if mkdir -p "${mu_plugins_dir}" 2>/dev/null \
&& cp "${SCRIPT_DIR}/rtc-test.php" "${mu_plugins_dir}/rtc-test.php" 2>/dev/null; then
printf 'MU-plugin: copied to %s\n' "${mu_plugins_dir}"
else
printf 'WARNING: Could not copy rtc-test.php to %s\n' "${mu_plugins_dir}"
printf ' Copy it manually:\n'
printf ' cp "%s/rtc-test.php" "%s/"\n' "${SCRIPT_DIR}" "${mu_plugins_dir}"
fi
# Always use the dedicated rtctest user so setup controls the password.
# We generate the password here and either create or reset the account.
local rtctest_wp_pass
rtctest_wp_pass="$(openssl rand -hex 16 2>/dev/null)" \
|| rtctest_wp_pass="RtcTest$(date +%s)"
if wp "${WP_FLAGS[@]}" user get rtctest --fields=ID --format=csv >/dev/null 2>&1; then
# rtctest already exists -- reset password so we have a known credential.
wp "${WP_FLAGS[@]}" user update rtctest --user_pass="${rtctest_wp_pass}" \
>/dev/null 2>&1 || die "Failed to reset rtctest password."
printf 'User: rtctest (password reset)\n'
else
printf 'Creating dedicated "rtctest" user (editor role)...\n'
wp "${WP_FLAGS[@]}" user create rtctest rtctest@example.com \
--role=editor \
--user_pass="${rtctest_wp_pass}" \
--porcelain >/dev/null \
|| die "Failed to create rtctest user."
printf 'User: rtctest (created)\n'
fi
WP_USER="rtctest"
# Fetch numeric ID -- wp post create --post_author requires an ID, not a login.
local user_id
user_id="$(wp "${WP_FLAGS[@]}" user get rtctest --field=ID 2>/dev/null)"
# Log in via wp-login.php to obtain a session cookie.
local jar="${SCRIPT_DIR}/rtc-test-cookies.txt"
rm -f "${jar}"
printf 'Logging in...\n'
do_login "rtctest" "${rtctest_wp_pass}" "${site_url}" "${jar}" \
|| die "Cookie login failed. Check site URL and that rtctest@example.com is reachable."
printf 'Cookies: obtained (%s)\n' "${jar}"
# Get the wp_rest nonce via the rtctest_nonce AJAX handler in the monitor plugin.
printf 'Fetching nonce...\n'
local nonce
nonce=$(do_get_nonce "${site_url}" "${jar}")
[ -n "${nonce}" ] || die "Empty nonce. Is rtc-test.php deployed and active?"
printf 'Nonce: obtained (%s...)\n' "${nonce:0:8}"
# Create a dedicated test post so test traffic is isolated from real content.
printf 'Creating test post...\n'
local post_id
post_id="$(wp "${WP_FLAGS[@]}" post create \
--post_title="RTC Test Post [rtc-test.sh]" \
--post_status=publish \
--post_type=post \
--post_author="${user_id}" \
--porcelain 2>/dev/null)" \
|| die "Failed to create test post."
printf 'Test post ID: %s\n' "${post_id}"
# Enable Real Time Collaboration.
if wp "${WP_FLAGS[@]}" option update wp_collaboration_enabled 1 >/dev/null 2>&1; then
printf 'RTC: enabled\n'
else
printf 'RTC: could not update option -- verify in Settings > Writing\n'
fi
# Check if SAVEQUERIES is already defined and enabled in wp-config.php.
savequeries_value=$(wp "${WP_FLAGS[@]}" config get SAVEQUERIES 2>/dev/null)
if [ "$savequeries_value" = "true" ] || [ "$savequeries_value" = "1" ]; then
printf 'SAVEQUERIES: already enabled in wp-config.php\n'
# Enable SAVEQUERIES so the plugin can record per-request DB time.
elif wp "${WP_FLAGS[@]}" config set SAVEQUERIES true --raw >/dev/null 2>&1; then
printf 'SAVEQUERIES: enabled in wp-config.php\n'
else
die "Could not set SAVEQUERIES in wp-config.php. Ensure the file is writable and re-run setup."
fi
# Gutenberg must not be active during tests — it ships its own RTC implementation
# that would interfere with the approaches under test.
if wp "${WP_FLAGS[@]}" plugin is-installed gutenberg >/dev/null 2>&1; then
printf 'Gutenberg: installed\n'
if wp "${WP_FLAGS[@]}" plugin is-active gutenberg >/dev/null 2>&1; then
printf 'Gutenberg: active -- deactivating...\n'
if wp "${WP_FLAGS[@]}" plugin deactivate gutenberg >/dev/null 2>&1; then
printf 'Gutenberg: deactivated\n'
else
die "Could not deactivate Gutenberg. Deactivate it manually and re-run setup."
fi
else
printf 'Gutenberg: not active\n'
fi
else
printf 'Gutenberg: not installed\n'
fi
# Write generated values to .env (or .env.example if .env does not exist yet).
# Strip any previous generated section first (from the marker line to EOF),
# then append the fresh block.
local env_file
env_file="$(resolve_env_file)"
local tmp
tmp="$(mktemp)"
awk '/Generated by setup/{found=1} !found{print}' "${env_file}" > "${tmp}" \
&& mv "${tmp}" "${env_file}"
{
printf '\n# ── Generated by setup ─────────────────────────────────────────────────────\n'
printf '# The values below are written automatically by "bash rtc-test.sh setup".\n'
printf '# Do not edit them manually — re-run setup or refresh-auth to regenerate.\n'
printf '\n'
printf 'WP_URL="%s"\n' "${site_url}"
printf 'WP_USER="rtctest"\n'
printf 'WP_PASS="%s"\n' "${rtctest_wp_pass}"
printf 'WP_NONCE="%s"\n' "${nonce}"
printf 'POST_ID="%s"\n' "${post_id}"
printf '_RTC_POST_ID_AUTO=1\n'
} >> "${env_file}"
printf '\nGenerated values written to %s\n' "${env_file}"
printf '\nNext steps:\n'
printf ' bash rtc-test.sh baseline\n'
printf ' bash rtc-test.sh single-idle\n'
printf ' bash rtc-test.sh report\n'
printf ' bash rtc-test.sh teardown # when done\n'
}
setup_manual() {
printf 'WP-CLI not available. Manual setup steps:\n\n'
printf '1. Copy rtc-test.php to the site'"'"'s mu-plugins directory:\n'
printf ' cp rtc-test.php /path/to/wp-content/mu-plugins/\n\n'
printf '2. Enable RTC: WP Admin > Settings > Writing > "Enable early access to\n'
printf ' real-time collaboration"\n\n'
printf '3. Note a post ID for an existing editor-role user you want to test with.\n\n'
printf '4. Copy .env.example to .env and fill in the required values:\n\n'
printf ' cp .env.example .env\n\n'
printf ' Required values:\n'
printf ' WP_URL="%s"\n' "${WP_URL}"
printf ' WP_USER="<login>"\n'
printf ' WP_PASS="<password>"\n'
printf ' WP_PATH="<absolute path to WordPress root>"\n'
printf ' POST_ID=<post_id>\n\n'
printf '5. Then run:\n'
printf ' bash rtc-test.sh refresh-auth # logs in and writes cookie jar + nonce\n\n'
printf 'After that, all test commands are available.\n'
}
cmd_teardown() {
print_header "teardown"
# Re-source config so teardown works even if vars are not in the environment.
local config_file
config_file="$(resolve_env_file)"
# shellcheck source=/dev/null
[ -f "${config_file}" ] && . "${config_file}"
# Remove cookie jar if present.
local jar="${WP_COOKIE_JAR:-${SCRIPT_DIR}/rtc-test-cookies.txt}"
if [ -f "${jar}" ]; then
rm "${jar}"
printf 'Removed %s\n' "${jar}"
fi
if command -v wp >/dev/null 2>&1; then
local WP_FLAGS=()
[ "$(id -u)" = "0" ] && WP_FLAGS+=( "--allow-root" )
[ -n "${WP_URL:-}" ] && WP_FLAGS+=( "--url=${WP_URL}" )
[ -n "${WP_PATH:-}" ] && WP_FLAGS+=( "--path=${WP_PATH}" )
if [ "${_RTC_POST_ID_AUTO:-0}" = "1" ] && [ -n "${POST_ID:-}" ]; then
printf 'Deleting test post %s...\n' "${POST_ID}"
wp "${WP_FLAGS[@]}" post delete "${POST_ID}" --force \
>/dev/null 2>&1 && printf ' Done.\n' || printf ' Already removed.\n'
fi
fi
# Strip the generated section from the env file, leaving user config intact.
if [ -f "${config_file}" ]; then
local tmp
tmp="$(mktemp)"
awk '/Generated by setup/{found=1} !found{print}' "${config_file}" > "${tmp}" \
&& mv "${tmp}" "${config_file}"
printf 'Stripped generated values from %s\n' "${config_file}"
fi
printf '\nTeardown complete.\n'