This is an automated email from the ASF dual-hosted git repository.
bcall pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new a1ac1db41d Stabilize post and slow_post autests under parallel runs
(#12886)
a1ac1db41d is described below
commit a1ac1db41d8d428274f575e709aaeeb8922b4083
Author: Bryan Call <[email protected]>
AuthorDate: Thu Feb 19 09:50:26 2026 -0800
Stabilize post and slow_post autests under parallel runs (#12886)
* Stabilize thread_config autest startup ordering -- gate check_threads
on ATS port readiness to avoid validation races.
* Stabilize post/slow_post autests -- add explicit origin readiness checks
in post-early-return; accept microserver exception variant for Fedora 43.
* Revert thread_config PortOpen change per review -- keep ATS startup gating
on existing readiness behavior, move upstream readiness to server Ready
fields.
* Replace nc-based server with mock_origin.py -- server1.sh only accepts one
TCP connection (consumed by readiness probe). mock_origin.py absorbs
probes,
sends configured response, and drains request data to prevent TCP RST /
502.
* Remove unused server1.sh replaced by mock_origin.py
---
tests/gold_tests/post/post-early-return.test.py | 21 ++-
tests/gold_tests/post/server1.sh | 41 ------
tests/gold_tests/slow_post/server_abort.test.py | 3 +-
tests/tools/mock_origin.py | 167 ++++++++++++++++++++++++
4 files changed, 184 insertions(+), 48 deletions(-)
diff --git a/tests/gold_tests/post/post-early-return.test.py
b/tests/gold_tests/post/post-early-return.test.py
index 82879eb401..a8e85e2c06 100644
--- a/tests/gold_tests/post/post-early-return.test.py
+++ b/tests/gold_tests/post/post-early-return.test.py
@@ -61,18 +61,27 @@ ts.Disk.records_config.update(
'proxy.config.diags.debug.tags': 'http',
})
+mock_origin = os.path.join(Test.Variables.AtsTestToolsDir, 'mock_origin.py')
+mock_origin_args = '--status 420 --reason "Be Calm"'
+
server1 = Test.Processes.Process(
- "server1", "bash -c '" + Test.TestDirectory + "/server1.sh {}
outserver1'".format(Test.Variables.upstream_port1))
+ "server1", f"python3 {mock_origin} {Test.Variables.upstream_port1}
{mock_origin_args} --output outserver1")
server2 = Test.Processes.Process(
- "server2", "bash -c '" + Test.TestDirectory + "/server1.sh {}
outserver1'".format(Test.Variables.upstream_port2))
+ "server2", f"python3 {mock_origin} {Test.Variables.upstream_port2}
{mock_origin_args} --output outserver1")
server3 = Test.Processes.Process(
- "server3", "bash -c '" + Test.TestDirectory + "/server1.sh {}
outserver1'".format(Test.Variables.upstream_port3))
+ "server3", f"python3 {mock_origin} {Test.Variables.upstream_port3}
{mock_origin_args} --output outserver1")
server4 = Test.Processes.Process(
- "server4", "bash -c '" + Test.TestDirectory + "/server1.sh {}
outserver1'".format(Test.Variables.upstream_port4))
+ "server4", f"python3 {mock_origin} {Test.Variables.upstream_port4}
{mock_origin_args} --output outserver1")
server5 = Test.Processes.Process(
- "server5", "bash -c '" + Test.TestDirectory + "/server1.sh {}
outserver1'".format(Test.Variables.upstream_port5))
+ "server5", f"python3 {mock_origin} {Test.Variables.upstream_port5}
{mock_origin_args} --output outserver1")
server6 = Test.Processes.Process(
- "server6", "bash -c '" + Test.TestDirectory + "/server1.sh {}
outserver1'".format(Test.Variables.upstream_port6))
+ "server6", f"python3 {mock_origin} {Test.Variables.upstream_port6}
{mock_origin_args} --output outserver1")
+server1.Ready = When.PortOpen(Test.Variables.upstream_port1)
+server2.Ready = When.PortOpen(Test.Variables.upstream_port2)
+server3.Ready = When.PortOpen(Test.Variables.upstream_port3)
+server4.Ready = When.PortOpen(Test.Variables.upstream_port4)
+server5.Ready = When.PortOpen(Test.Variables.upstream_port5)
+server6.Ready = When.PortOpen(Test.Variables.upstream_port6)
big_post_body = "0123456789" * 231070
big_post_body_file = open(os.path.join(Test.RunDirectory, "big_post_body"),
"w")
diff --git a/tests/gold_tests/post/server1.sh b/tests/gold_tests/post/server1.sh
deleted file mode 100755
index 13d9bab695..0000000000
--- a/tests/gold_tests/post/server1.sh
+++ /dev/null
@@ -1,41 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# A very simple cleartext server for one HTTP transaction. Does no validation
of the Request message.
-# Sends a fixed response message
-
-
-response ()
-{
- # Wait for end of Request message.
- #
- while (( 1 == 1 ))
- do
- if [[ -f $outfile ]] ; then
- if tr '\r\n' '=!' < $outfile | grep '=!=!' > /dev/null
- then
- break;
- fi
- fi
- sleep 1
- done
-
-
- printf "HTTP/1.1 420 Be Calm\r\nContent-Length: 0\r\n\r\n"
-
-}
-outfile=$2
-response | nc -l $1 > "$outfile"
diff --git a/tests/gold_tests/slow_post/server_abort.test.py
b/tests/gold_tests/slow_post/server_abort.test.py
index 1004bcdb6a..0586785983 100644
--- a/tests/gold_tests/slow_post/server_abort.test.py
+++ b/tests/gold_tests/slow_post/server_abort.test.py
@@ -47,4 +47,5 @@ tr.ReturnCode = 0
tr.StillRunningAfter = server
tr.StillRunningAfter = ts
server.Streams.stderr += Testers.ContainsExpression(
- "UnicodeDecodeError", "Verify that the server raises an exception when
processing the request.")
+ "(UnicodeDecodeError|IndexError: list index out of range)",
+ "Verify that the server raises an exception when processing the request.")
diff --git a/tests/tools/mock_origin.py b/tests/tools/mock_origin.py
new file mode 100644
index 0000000000..f2390ed7fd
--- /dev/null
+++ b/tests/tools/mock_origin.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python3
+'''
+A reusable mock origin server for ATS autests.
+
+Replaces the various ad-hoc nc-based shell scripts (post/server1.sh,
+chunked_encoding/server2..4.sh, post_slow_server/server.sh) with a single
+Python tool that:
+
+ - Handles When.PortOpen() readiness probes gracefully (nc -l cannot).
+ - Accepts one real HTTP request, optionally saves it to a file, and sends
+ a configurable response.
+ - Drains remaining request data after responding so that ATS does not see
+ a connection reset while still forwarding a POST body (avoids HTTP/2 502).
+ - Supports Content-Length bodies, chunked transfer encoding, and arbitrary
+ response delays.
+
+Usage examples mapping to the original shell scripts:
+
+ # post/server1.sh PORT OUTFILE
+ mock_origin.py PORT --output OUTFILE --status 420 --reason "Be Calm"
+
+ # chunked_encoding/server2.sh PORT OUTFILE (Content-Length body)
+ mock_origin.py PORT --output OUTFILE --body "123456789012345"
+
+ # chunked_encoding/server3.sh PORT OUTFILE (Chunked body)
+ mock_origin.py PORT --output OUTFILE --body "123456789012345" --chunked
+
+ # post_slow_server/server.sh PORT (Delayed 200KB response)
+ mock_origin.py PORT --output rcv_file --delay 120 --body-size 204800
+'''
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import socket
+import sys
+import time
+
+FILLER_LINE_WIDTH = 8
+
+
+def build_response(args):
+ '''Build the complete HTTP response bytes from CLI arguments.'''
+
+ body = b''
+ if args.body is not None:
+ body = args.body.encode()
+ elif args.body_size and args.body_size > 0:
+ lines = []
+ offset = 0
+ while offset < args.body_size:
+ offset += FILLER_LINE_WIDTH
+ lines.append(f'{offset:07d}\n'.encode())
+ body = b''.join(lines)[:args.body_size]
+
+ status_line = f'HTTP/1.1 {args.status} {args.reason}\r\n'.encode()
+
+ if args.chunked:
+ headers = b'Transfer-Encoding: chunked\r\n'
+ for h in (args.header or []):
+ headers += h.encode() + b'\r\n'
+ headers += b'\r\n'
+ chunk = f'{len(body):X}\r\n'.encode() + body + b'\r\n'
+ terminator = b'0\r\n\r\n'
+ return status_line + headers + chunk + terminator
+ else:
+ headers = f'Content-Length: {len(body)}\r\n'.encode()
+ for h in (args.header or []):
+ headers += h.encode() + b'\r\n'
+ headers += b'\r\n'
+ return status_line + headers + body
+
+
+def serve_one(args):
+ '''Listen, absorb readiness probes, serve one real HTTP transaction,
exit.'''
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind(('', args.port))
+ sock.listen(1)
+
+ response = build_response(args)
+
+ while True:
+ conn, addr = sock.accept()
+ data = b''
+ try:
+ while True:
+ chunk = conn.recv(65536)
+ if not chunk:
+ break
+ data += chunk
+ if b'\r\n\r\n' in data:
+ break
+ except ConnectionError:
+ pass
+
+ if not data:
+ # Readiness probe (e.g. When.PortOpen) -- connected and
+ # disconnected without sending data. Go back to waiting.
+ conn.close()
+ continue
+
+ # Real HTTP request arrived.
+ if args.output:
+ with open(args.output, 'wb') as f:
+ f.write(data)
+
+ if args.delay > 0:
+ time.sleep(args.delay)
+
+ try:
+ conn.sendall(response)
+ except ConnectionError:
+ pass
+
+ # Drain remaining request data (e.g. a large POST body that is still
+ # being forwarded by ATS). Closing without draining causes a TCP RST
+ # which makes ATS return 502 on HTTP/2 streams.
+ try:
+ while True:
+ if not conn.recv(65536):
+ break
+ except ConnectionError:
+ pass
+
+ conn.close()
+ break
+
+ sock.close()
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Mock origin server for ATS autests. '
+ 'Listens on PORT, serves one HTTP transaction, then exits. '
+ 'Compatible with When.PortOpen() readiness probes.')
+
+ parser.add_argument('port', type=int, help='TCP port to listen on')
+ parser.add_argument('--output', '-o', help='Write received request data to
FILE')
+ parser.add_argument('--status', '-s', type=int, default=200, help='HTTP
status code (default: 200)')
+ parser.add_argument('--reason', '-r', default='OK', help='HTTP reason
phrase (default: OK)')
+ parser.add_argument('--header', action='append', help='Additional response
header (repeatable), e.g. "X-Foo: bar"')
+ parser.add_argument('--body', '-b', help='Response body string')
+ parser.add_argument('--body-size', type=int, default=0, help='Generate N
bytes of filler body data')
+ parser.add_argument('--chunked', action='store_true', help='Use chunked
transfer encoding')
+ parser.add_argument('--delay', '-d', type=float, default=0, help='Seconds
to delay before sending response')
+
+ args = parser.parse_args()
+ serve_one(args)
+
+
+if __name__ == '__main__':
+ main()