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()

Reply via email to