diff --git a/IntegrationTests/plugin_echo.sh b/IntegrationTests/plugin_echo.sh
new file mode 100644
index 0000000..6eed284
--- /dev/null
+++ b/IntegrationTests/plugin_echo.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the SwiftNIO open source project
+##
+## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+## Licensed under Apache License v2.0
+##
+## See LICENSE.txt for license information
+## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+##
+## SPDX-License-Identifier: Apache-2.0
+##
+##===----------------------------------------------------------------------===##
+
+function plugin_echo_test_suite_begin() {
+ echo "Running test suite '$1'"
+}
+
+function plugin_echo_test_suite_end() {
+ true
+}
+
+# test_name
+function plugin_echo_test_begin() {
+ echo -n "Running test '$1'... "
+}
+
+function plugin_echo_test_skip() {
+ echo "Skipping test '$1'"
+}
+
+function plugin_echo_test_ok() {
+ echo "OK (${1}s)"
+}
+
+function plugin_echo_test_fail() {
+ echo "FAILURE ($1)"
+ echo "--- OUTPUT BEGIN ---"
+ cat "$2"
+ echo "--- OUTPUT END ---"
+}
+
+function plugin_echo_test_end() {
+ true
+}
+
+function plugin_echo_summary_ok() {
+ echo "OK (ran $1 tests successfully)"
+}
+
+function plugin_echo_summary_fail() {
+ echo "FAILURE (oks: $1, failures: $2)"
+}
+
+function plugin_echo_init() {
+ true
+}
diff --git a/IntegrationTests/plugin_junit_xml.sh b/IntegrationTests/plugin_junit_xml.sh
new file mode 100644
index 0000000..d88a51c
--- /dev/null
+++ b/IntegrationTests/plugin_junit_xml.sh
@@ -0,0 +1,119 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the SwiftNIO open source project
+##
+## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+## Licensed under Apache License v2.0
+##
+## See LICENSE.txt for license information
+## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+##
+## SPDX-License-Identifier: Apache-2.0
+##
+##===----------------------------------------------------------------------===##
+
+junit_testsuite_time=0
+
+function junit_output_write() {
+ extra_flags=""
+ if [[ "$1" == "-n" ]]; then
+ extra_flags="-n"
+ shift
+ fi
+ test -n "$junit_xml_output"
+ echo $extra_flags "$*" >> "$junit_xml_output"
+}
+
+function junit_output_cat() {
+ cat "$@" >> "$junit_xml_output"
+}
+
+# search, replace
+function junit_output_replace() {
+ test -n "$junit_xml_output"
+ case "$(uname -s)" in
+ Linux)
+ sed -i "s/$1/$2/g" "$junit_xml_output"
+ ;;
+ *)
+ sed -i "" "s/$1/$2/g" "$junit_xml_output"
+ ;;
+ esac
+}
+
+function plugin_junit_xml_test_suite_begin() {
+ junit_testsuite_time=0
+ junit_output_write ""
+}
+
+function plugin_junit_xml_test_suite_end() {
+ junit_repl_success_and_fail "$1" "$2"
+ junit_output_write ""
+}
+
+# test_name
+function plugin_junit_xml_test_begin() {
+ junit_output_write -n " "
+ junit_testsuite_time=$((junit_testsuite_time + time_ms))
+}
+
+function plugin_junit_xml_test_fail() {
+ time_ms=$1
+ junit_output_write " time='$time_ms'>"
+ junit_output_write " "
+ junit_output_write " "
+ junit_output_write ' '
+ junit_output_write " "
+ junit_output_write " "
+}
+
+function plugin_junit_xml_test_end() {
+ junit_output_write " "
+}
+
+function junit_repl_success_and_fail() {
+ junit_output_replace XXX-TESTS-XXX "$(($1 + $2))"
+ junit_output_replace XXX-FAILURES-XXX "$2"
+ junit_output_replace XXX-TIME-XXX "$junit_testsuite_time"
+}
+
+function plugin_junit_xml_summary_ok() {
+ junit_output_write ""
+}
+
+function plugin_junit_xml_summary_fail() {
+ junit_output_write ""
+}
+
+function plugin_junit_xml_init() {
+ junit_xml_output=""
+ for f in "$@"; do
+ if [[ "$junit_xml_output" = "PLACEHOLDER" ]]; then
+ junit_xml_output="$f"
+ fi
+ if [[ "$f" == "--junit-xml" && -z "$junit_xml_output" ]]; then
+ junit_xml_output="PLACEHOLDER"
+ fi
+ done
+
+ if [[ -z "$junit_xml_output" || "$junit_xml_output" = "PLACEHOLDER" ]]; then
+ echo >&2 "ERROR: you need to specify the output after the --junit-xml argument"
+ false
+ fi
+ echo "" > "$junit_xml_output"
+}
diff --git a/IntegrationTests/run-single-test.sh b/IntegrationTests/run-single-test.sh
new file mode 100755
index 0000000..3650102
--- /dev/null
+++ b/IntegrationTests/run-single-test.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the SwiftNIO open source project
+##
+## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+## Licensed under Apache License v2.0
+##
+## See LICENSE.txt for license information
+## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+##
+## SPDX-License-Identifier: Apache-2.0
+##
+##===----------------------------------------------------------------------===##
+
+(
+# this sub-shell is where the actual test is run
+set -eu
+set -x
+set -o pipefail
+
+test="$1"
+tmp="$2"
+root="$3"
+g_show_info="$4"
+here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+source "$here/test_functions.sh"
+source "$test"
+wait
+)
+exit_code=$?
+exit $exit_code
diff --git a/IntegrationTests/run-tests.sh b/IntegrationTests/run-tests.sh
new file mode 100755
index 0000000..9250b83
--- /dev/null
+++ b/IntegrationTests/run-tests.sh
@@ -0,0 +1,159 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the SwiftNIO open source project
+##
+## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+## Licensed under Apache License v2.0
+##
+## See LICENSE.txt for license information
+## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+##
+## SPDX-License-Identifier: Apache-2.0
+##
+##===----------------------------------------------------------------------===##
+
+set -eu
+
+shopt -s nullglob
+
+here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+tmp=$(mktemp -d /tmp/.swift-nio-http1-server-sh-tests_XXXXXX)
+
+# start_time
+function time_diff_to_now() {
+ echo "$(( $(date +%s) - $1 ))"
+}
+
+function plugins_do() {
+ local method
+ method="$1"
+ shift
+ for plugin in $plugins; do
+ cd "$orig_cwd"
+ "plugin_${plugin}_${method}" "$@"
+ cd - > /dev/null
+ done
+}
+
+source "$here/plugin_echo.sh"
+source "$here/plugin_junit_xml.sh"
+
+plugins="echo"
+plugin_opts_ind=0
+if [[ "${1-default}" == "--junit-xml" ]]; then
+ plugins="echo junit_xml"
+ plugin_opts_ind=2
+fi
+
+function usage() {
+ echo >&2 "Usage: $0 [OPTIONS]"
+ echo >&2
+ echo >&2 "OPTIONS:"
+ echo >&2 " -f FILTER: Only run tests matching FILTER (regex)"
+}
+
+orig_cwd=$(pwd)
+cd "$here"
+
+plugins_do init "$@"
+shift $plugin_opts_ind
+
+filter="."
+verbose=false
+show_info=false
+debug=false
+while getopts "f:vid" opt; do
+ case $opt in
+ f)
+ filter="$OPTARG"
+ ;;
+ v)
+ verbose=true
+ ;;
+ i)
+ show_info=true
+ ;;
+ d)
+ debug=true
+ ;;
+ \?)
+ usage
+ exit 1
+ ;;
+ esac
+done
+
+function run_test() {
+ if $verbose; then
+ "$@" 2>&1 | tee -a "$out"
+ # we need to return the return value of the first command
+ return ${PIPESTATUS[0]}
+ else
+ "$@" >> "$out" 2>&1
+ fi
+}
+
+exec 3>&1 4>&2 # copy stdout/err to fd 3/4 to we can output control messages
+cnt_ok=0
+cnt_fail=0
+for f in tests_*; do
+ suite_ok=0
+ suite_fail=0
+ plugins_do test_suite_begin "$f"
+ start_suite=$(date +%s)
+ cd "$f"
+ for t in test_*.sh; do
+ if [[ ! "$f/$t" =~ $filter ]]; then
+ plugins_do test_skip "$t"
+ continue
+ fi
+ out=$(mktemp "$tmp/test.out_XXXXXX")
+ test_tmp=$(mktemp -d "$tmp/test.tmp_XXXXXX")
+ plugins_do test_begin "$t" "$f"
+ start=$(date +%s)
+ if run_test "$here/run-single-test.sh" "$here/$f/$t" "$test_tmp" "$here/.." "$show_info"; then
+ plugins_do test_ok "$(time_diff_to_now $start)"
+ suite_ok=$((suite_ok+1))
+ if $verbose; then
+ cat "$out"
+ fi
+ else
+ plugins_do test_fail "$(time_diff_to_now $start)" "$out"
+ suite_fail=$((suite_fail+1))
+ fi
+ if ! $debug; then
+ rm "$out"
+ rm -rf "$test_tmp"
+ fi
+ plugins_do test_end
+ done
+ cnt_ok=$((cnt_ok + suite_ok))
+ cnt_fail=$((cnt_fail + suite_fail))
+ cd ..
+ plugins_do test_suite_end "$(time_diff_to_now $start_suite)" "$suite_ok" "$suite_fail"
+done
+
+if ! $debug; then
+ rm -rf "$tmp"
+else
+ echo >&2 "debug mode, not deleting '$tmp'"
+fi
+
+
+# report
+if [[ $cnt_fail > 0 ]]; then
+ # kill leftovers (the whole process group)
+ trap '' TERM
+ kill 0
+
+ plugins_do summary_fail "$cnt_ok" "$cnt_fail"
+else
+ plugins_do summary_ok "$cnt_ok" "$cnt_fail"
+fi
+
+if [[ $cnt_fail > 0 ]]; then
+ exit 1
+else
+ exit 0
+fi
diff --git a/IntegrationTests/test_functions.sh b/IntegrationTests/test_functions.sh
new file mode 100644
index 0000000..2eafeb7
--- /dev/null
+++ b/IntegrationTests/test_functions.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the SwiftNIO open source project
+##
+## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+## Licensed under Apache License v2.0
+##
+## See LICENSE.txt for license information
+## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+##
+## SPDX-License-Identifier: Apache-2.0
+##
+##===----------------------------------------------------------------------===##
+function fail() {
+ echo >&2 "FAILURE: $*"
+ false
+}
+
+function assert_equal() {
+ if [[ "$1" != "$2" ]]; then
+ fail "expected '$1', got '$2' ${3-}"
+ fi
+}
+
+function assert_equal_files() {
+ if ! cmp -s "$1" "$2"; then
+ diff -u "$1" "$2" || true
+ echo
+ echo "--- SNIP ($1, size=$(wc "$1"), SHA=$(shasum "$1")) ---"
+ cat "$1"
+ echo "--- SNAP ($1)---"
+ echo "--- SNIP ($2, size=$(wc "$2"), SHA=$(shasum "$2")) ---"
+ cat "$2"
+ echo "--- SNAP ($2) ---"
+ fail "file '$1' not equal to '$2'"
+ fi
+}
+
+function assert_less_than() {
+ if [[ ! "$1" -lt "$2" ]]; then
+ fail "assertion '$1' < '$2' failed"
+ fi
+}
+
+function assert_less_than_or_equal() {
+ if [[ ! "$1" -le "$2" ]]; then
+ fail "assertion '$1' <= '$2' failed"
+ fi
+}
+
+function assert_greater_than() {
+ if [[ ! "$1" -gt "$2" ]]; then
+ fail "assertion '$1' > '$2' failed"
+ fi
+}
+
+function assert_greater_than_or_equal() {
+ if [[ ! "$1" -ge "$2" ]]; then
+ fail "assertion '$1' >= '$2' failed"
+ fi
+}
+
+g_has_previously_infoed=false
+
+function info() {
+ if $g_show_info; then
+ if ! $g_has_previously_infoed; then
+ echo >&3 || true # echo an extra newline so it looks better
+ g_has_previously_infoed=true
+ fi
+ echo >&3 "info: $*" || true
+ fi
+}
+
+function warn() {
+ echo >&4 "warning: $*"
+}
diff --git a/IntegrationTests/tests_01_allocation_counters/defines.sh b/IntegrationTests/tests_01_allocation_counters/defines.sh
new file mode 100644
index 0000000..c99a521
--- /dev/null
+++ b/IntegrationTests/tests_01_allocation_counters/defines.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the SwiftNIO open source project
+##
+## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+## Licensed under Apache License v2.0
+##
+## See LICENSE.txt for license information
+## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+##
+## SPDX-License-Identifier: Apache-2.0
+##
+##===----------------------------------------------------------------------===##
diff --git a/IntegrationTests/tests_01_allocation_counters/test_01_allocation_counts.sh b/IntegrationTests/tests_01_allocation_counters/test_01_allocation_counts.sh
new file mode 100644
index 0000000..6e7fd31
--- /dev/null
+++ b/IntegrationTests/tests_01_allocation_counters/test_01_allocation_counts.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the SwiftNIO open source project
+##
+## Copyright (c) 2017-2023 Apple Inc. and the SwiftNIO project authors
+## Licensed under Apache License v2.0
+##
+## See LICENSE.txt for license information
+## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+##
+## SPDX-License-Identifier: Apache-2.0
+##
+##===----------------------------------------------------------------------===##
+
+source defines.sh
+
+set -eu
+here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+all_tests=()
+for file in "$here/test_01_resources/"test_*.swift; do
+ test_name=$(basename "$file")
+ test_name=${test_name#test_*}
+ test_name=${test_name%*.swift}
+ all_tests+=( "$test_name" )
+done
+
+"$here/test_01_resources/run-nio-ssh-alloc-counter-tests.sh" -t "$tmp" > "$tmp/output"
+
+for test in "${all_tests[@]}"; do
+ cat "$tmp/output" # helps debugging
+
+ while read -r test_case; do
+ test_case=${test_case#test_*}
+ total_allocations=$(grep "^test_$test_case.total_allocations:" "$tmp/output" | cut -d: -f2 | sed 's/ //g')
+ not_freed_allocations=$(grep "^test_$test_case.remaining_allocations:" "$tmp/output" | cut -d: -f2 | sed 's/ //g')
+ max_allowed_env_name="MAX_ALLOCS_ALLOWED_$test_case"
+
+ info "$test_case: allocations not freed: $not_freed_allocations"
+ info "$test_case: total number of mallocs: $total_allocations"
+
+ assert_less_than "$not_freed_allocations" 5 # allow some slack
+ assert_greater_than "$not_freed_allocations" -5 # allow some slack
+ if [[ -z "${!max_allowed_env_name+x}" ]]; then
+ if [[ -z "${!max_allowed_env_name+x}" ]]; then
+ warn "no reference number of allocations set (set to \$$max_allowed_env_name)"
+ warn "to set current number:"
+ warn " export $max_allowed_env_name=$total_allocations"
+ fi
+ else
+ max_allowed=${!max_allowed_env_name}
+ assert_less_than_or_equal "$total_allocations" "$max_allowed"
+ assert_greater_than "$total_allocations" "$(( max_allowed - 1000))"
+ fi
+ done < <(grep "^test_$test[^\W]*.total_allocations:" "$tmp/output" | cut -d: -f1 | cut -d. -f1 | sort | uniq)
+done
diff --git a/IntegrationTests/tests_01_allocation_counters/test_01_resources/run-nio-ssh-alloc-counter-tests.sh b/IntegrationTests/tests_01_allocation_counters/test_01_resources/run-nio-ssh-alloc-counter-tests.sh
new file mode 100755
index 0000000..d62bbbb
--- /dev/null
+++ b/IntegrationTests/tests_01_allocation_counters/test_01_resources/run-nio-ssh-alloc-counter-tests.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the SwiftNIO open source project
+##
+## Copyright (c) 2019-2023 Apple Inc. and the SwiftNIO project authors
+## Licensed under Apache License v2.0
+##
+## See LICENSE.txt for license information
+## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+##
+## SPDX-License-Identifier: Apache-2.0
+##
+##===----------------------------------------------------------------------===##
+
+set -eu
+here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+tmp_dir="/tmp"
+
+while getopts "t:" opt; do
+ case "$opt" in
+ t)
+ tmp_dir="$OPTARG"
+ ;;
+ *)
+ exit 1
+ ;;
+ esac
+done
+
+nio_checkout=$(mktemp -d "$tmp_dir/.swift-nio_XXXXXX")
+(
+cd "$nio_checkout"
+git clone --depth 1 https://github.com/apple/swift-nio
+)
+
+shift $((OPTIND-1))
+
+tests_to_run=("$here"/test_*.swift)
+
+if [[ $# -gt 0 ]]; then
+ tests_to_run=("$@")
+fi
+
+"$nio_checkout/swift-nio/IntegrationTests/allocation-counter-tests-framework/run-allocation-counter.sh" \
+ -p "$here/../../.." \
+ -m NIOCore \
+ -m NIOEmbedded \
+ -m NIOPosix \
+ -m NIOSSH \
+ -s "$here/shared.swift" \
+ -t "$tmp_dir" \
+ -d <( echo '.package(url: "https://github.com/apple/swift-nio.git", from: "2.32.0"),' ) \
+ "${tests_to_run[@]}"
diff --git a/IntegrationTests/tests_01_allocation_counters/test_01_resources/shared.swift b/IntegrationTests/tests_01_allocation_counters/test_01_resources/shared.swift
new file mode 100644
index 0000000..3b8d05e
--- /dev/null
+++ b/IntegrationTests/tests_01_allocation_counters/test_01_resources/shared.swift
@@ -0,0 +1,74 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+import NIOCore
+import NIOEmbedded
+import NIOSSH
+
+final class AcceptAllHostKeysDelegate: NIOSSHClientServerAuthenticationDelegate {
+ func validateHostKey(hostKey: NIOSSHPublicKey, validationCompletePromise: EventLoopPromise) {
+ // Do not replicate this in your own code: validate host keys! This is a
+ // choice made for expedience, not for any other reason.
+ validationCompletePromise.succeed(())
+ }
+}
+
+final class HardcodedClientPasswordDelegate: NIOSSHClientUserAuthenticationDelegate {
+ func nextAuthenticationType(availableMethods: NIOSSHAvailableUserAuthenticationMethods, nextChallengePromise: EventLoopPromise) {
+ precondition(availableMethods.contains(.password))
+ nextChallengePromise.succeed(NIOSSHUserAuthenticationOffer(username: "username", serviceName: "", offer: .password(.init(password: "password"))))
+ }
+}
+
+final class HardcodedServerPasswordDelegate: NIOSSHServerUserAuthenticationDelegate {
+ var supportedAuthenticationMethods: NIOSSHAvailableUserAuthenticationMethods {
+ .password
+ }
+
+ func requestReceived(request: NIOSSHUserAuthenticationRequest, responsePromise: EventLoopPromise) {
+ guard request.username == "username", case .password(let passwordRequest) = request.request else {
+ responsePromise.succeed(.failure)
+ return
+ }
+
+ if passwordRequest.password == "password" {
+ responsePromise.succeed(.success)
+ } else {
+ responsePromise.succeed(.failure)
+ }
+ }
+}
+
+/// Have two `EmbeddedChannel` objects send and receive data from each other until
+/// they make no forward progress.
+func interactInMemory(_ first: EmbeddedChannel, _ second: EmbeddedChannel) throws {
+ var operated: Bool
+
+ func readBytesFromChannel(_ channel: EmbeddedChannel) throws -> ByteBuffer? {
+ try channel.readOutbound(as: ByteBuffer.self)
+ }
+
+ repeat {
+ operated = false
+ first.embeddedEventLoop.run()
+
+ if let data = try readBytesFromChannel(first) {
+ operated = true
+ try second.writeInbound(data)
+ }
+ if let data = try readBytesFromChannel(second) {
+ operated = true
+ try first.writeInbound(data)
+ }
+ } while operated
+}
diff --git a/IntegrationTests/tests_01_allocation_counters/test_01_resources/test_client_server_many_small_commands_per_connection.swift b/IntegrationTests/tests_01_allocation_counters/test_01_resources/test_client_server_many_small_commands_per_connection.swift
new file mode 100644
index 0000000..ac7971a
--- /dev/null
+++ b/IntegrationTests/tests_01_allocation_counters/test_01_resources/test_client_server_many_small_commands_per_connection.swift
@@ -0,0 +1,133 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2019-2023 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import NIOCore
+import NIOEmbedded
+import NIOSSH
+
+final class ServerHandler: ChannelInboundHandler {
+ typealias InboundIn = SSHChannelData
+ typealias OutboundOut = SSHChannelData
+
+ func channelRead(context: ChannelHandlerContext, data: NIOAny) {
+ context.write(data, promise: nil)
+ }
+
+ func channelReadComplete(context: ChannelHandlerContext) {
+ context.flush()
+ }
+}
+
+final class ClientHandler: ChannelInboundHandler {
+ typealias InboundIn = SSHChannelData
+ typealias OutboundOut = SSHChannelData
+
+ private var didSend: Bool = false
+ private let message: ByteBuffer = ByteBuffer(string: "hello")
+ var readBytes: Int = 0
+
+ func handlerAdded(context: ChannelHandlerContext) {
+ if context.channel.isActive {
+ self.sendInitialMessage(context: context)
+ }
+ }
+
+ func channelActive(context: ChannelHandlerContext) {
+ self.sendInitialMessage(context: context)
+ }
+
+ private func sendInitialMessage(context: ChannelHandlerContext) {
+ if self.didSend { return }
+
+ self.didSend = true
+ let data = SSHChannelData(type: .channel, data: .byteBuffer(message))
+ context.writeAndFlush(self.wrapOutboundOut(data), promise: nil)
+ }
+
+ func channelRead(context: ChannelHandlerContext, data: NIOAny) {
+ let data = self.unwrapInboundIn(data)
+ guard case .byteBuffer(let buffer) = data.data else {
+ fatalError()
+ }
+ self.readBytes += buffer.readableBytes
+
+ if self.readBytes == self.message.readableBytes {
+ context.close(promise: nil)
+ }
+ }
+}
+
+func run(identifier: String) {
+ let loop = EmbeddedEventLoop()
+ let hostKey = NIOSSHPrivateKey(ed25519Key: .init())
+
+ measure(identifier: identifier) {
+ var sumOfReadBytes = 0
+
+ let clientChannel = EmbeddedChannel(loop: loop)
+ let serverChannel = EmbeddedChannel(loop: loop)
+
+ try! clientChannel.pipeline.addHandler(
+ NIOSSHHandler(
+ role: .client(.init(
+ userAuthDelegate: HardcodedClientPasswordDelegate(),
+ serverAuthDelegate: AcceptAllHostKeysDelegate()
+ )
+ ),
+ allocator: clientChannel.allocator,
+ inboundChildChannelInitializer: nil
+ )
+ ).wait()
+ try! serverChannel.pipeline.addHandler(
+ NIOSSHHandler(
+ role: .server(.init(
+ hostKeys: [hostKey],
+ userAuthDelegate: HardcodedServerPasswordDelegate()
+ )
+ ),
+ allocator: serverChannel.allocator,
+ inboundChildChannelInitializer: { channel, _ in
+ channel.pipeline.addHandler(ServerHandler())
+ }
+ )
+ ).wait()
+
+ try! clientChannel.connect(to: SocketAddress(ipAddress: "1.2.3.4", port: 5678)).wait()
+ try! serverChannel.connect(to: SocketAddress(ipAddress: "1.2.3.4", port: 5678)).wait()
+
+ for _ in 0 ..< 1000 {
+ let clientHandler = ClientHandler()
+
+ let childChannelFuture: EventLoopFuture = clientChannel.pipeline.handler(type: NIOSSHHandler.self).flatMap { sshHandler in
+ let promise = clientChannel.eventLoop.makePromise(of: Channel.self)
+ sshHandler.createChannel(promise) { childChannel, _ in
+ childChannel.pipeline.addHandlers([clientHandler])
+ }
+ return promise.futureResult
+ }
+ clientChannel.embeddedEventLoop.run()
+ try! interactInMemory(clientChannel, serverChannel)
+
+ let childChannel = try! childChannelFuture.wait()
+
+ try! childChannel.closeFuture.wait()
+ sumOfReadBytes = clientHandler.readBytes
+ }
+
+ try! clientChannel.close().wait()
+ try! serverChannel.close().wait()
+
+ return sumOfReadBytes
+ }
+}
diff --git a/IntegrationTests/tests_01_allocation_counters/test_01_resources/test_client_server_one_command_per_connection.swift b/IntegrationTests/tests_01_allocation_counters/test_01_resources/test_client_server_one_command_per_connection.swift
new file mode 100644
index 0000000..75f8b0b
--- /dev/null
+++ b/IntegrationTests/tests_01_allocation_counters/test_01_resources/test_client_server_one_command_per_connection.swift
@@ -0,0 +1,133 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2019-2023 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import NIOCore
+import NIOEmbedded
+import NIOSSH
+
+final class ServerHandler: ChannelInboundHandler {
+ typealias InboundIn = SSHChannelData
+ typealias OutboundOut = SSHChannelData
+
+ func channelRead(context: ChannelHandlerContext, data: NIOAny) {
+ context.write(data, promise: nil)
+ }
+
+ func channelReadComplete(context: ChannelHandlerContext) {
+ context.flush()
+ }
+}
+
+final class ClientHandler: ChannelInboundHandler {
+ typealias InboundIn = SSHChannelData
+ typealias OutboundOut = SSHChannelData
+
+ private var didSend: Bool = false
+ private let message: ByteBuffer = ByteBuffer(string: "hello")
+ var readBytes: Int = 0
+
+ func handlerAdded(context: ChannelHandlerContext) {
+ if context.channel.isActive {
+ self.sendInitialMessage(context: context)
+ }
+ }
+
+ func channelActive(context: ChannelHandlerContext) {
+ self.sendInitialMessage(context: context)
+ }
+
+ private func sendInitialMessage(context: ChannelHandlerContext) {
+ if self.didSend { return }
+
+ self.didSend = true
+ let data = SSHChannelData(type: .channel, data: .byteBuffer(message))
+ context.writeAndFlush(self.wrapOutboundOut(data), promise: nil)
+ }
+
+ func channelRead(context: ChannelHandlerContext, data: NIOAny) {
+ let data = self.unwrapInboundIn(data)
+ guard case .byteBuffer(let buffer) = data.data else {
+ fatalError()
+ }
+ self.readBytes += buffer.readableBytes
+
+ if self.readBytes == self.message.readableBytes {
+ context.close(promise: nil)
+ }
+ }
+}
+
+func run(identifier: String) {
+ let loop = EmbeddedEventLoop()
+ let hostKey = NIOSSHPrivateKey(ed25519Key: .init())
+
+ measure(identifier: identifier) {
+ var sumOfReadBytes = 0
+
+ for _ in 0 ..< 1000 {
+ let clientChannel = EmbeddedChannel(loop: loop)
+ let serverChannel = EmbeddedChannel(loop: loop)
+
+ try! clientChannel.pipeline.addHandler(
+ NIOSSHHandler(
+ role: .client(.init(
+ userAuthDelegate: HardcodedClientPasswordDelegate(),
+ serverAuthDelegate: AcceptAllHostKeysDelegate()
+ )
+ ),
+ allocator: clientChannel.allocator,
+ inboundChildChannelInitializer: nil
+ )
+ ).wait()
+ try! serverChannel.pipeline.addHandler(
+ NIOSSHHandler(
+ role: .server(.init(
+ hostKeys: [hostKey],
+ userAuthDelegate: HardcodedServerPasswordDelegate()
+ )
+ ),
+ allocator: serverChannel.allocator,
+ inboundChildChannelInitializer: { channel, _ in
+ channel.pipeline.addHandler(ServerHandler())
+ }
+ )
+ ).wait()
+
+ try! clientChannel.connect(to: SocketAddress(ipAddress: "1.2.3.4", port: 5678)).wait()
+ try! serverChannel.connect(to: SocketAddress(ipAddress: "1.2.3.4", port: 5678)).wait()
+
+ let clientHandler = ClientHandler()
+
+ let childChannelFuture: EventLoopFuture = clientChannel.pipeline.handler(type: NIOSSHHandler.self).flatMap { sshHandler in
+ let promise = clientChannel.eventLoop.makePromise(of: Channel.self)
+ sshHandler.createChannel(promise) { childChannel, _ in
+ childChannel.pipeline.addHandlers([clientHandler])
+ }
+ return promise.futureResult
+ }
+ clientChannel.embeddedEventLoop.run()
+ try! interactInMemory(clientChannel, serverChannel)
+
+ let childChannel = try! childChannelFuture.wait()
+
+ try! childChannel.closeFuture.wait()
+ sumOfReadBytes = clientHandler.readBytes
+
+ try! clientChannel.close().wait()
+ try! serverChannel.close().wait()
+ }
+
+ return sumOfReadBytes
+ }
+}
diff --git a/IntegrationTests/tests_01_allocation_counters/test_01_resources/test_client_server_streaming_large_message_in_small_chunks.swift b/IntegrationTests/tests_01_allocation_counters/test_01_resources/test_client_server_streaming_large_message_in_small_chunks.swift
new file mode 100644
index 0000000..77bc0db
--- /dev/null
+++ b/IntegrationTests/tests_01_allocation_counters/test_01_resources/test_client_server_streaming_large_message_in_small_chunks.swift
@@ -0,0 +1,148 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2019-2023 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import NIOCore
+import NIOEmbedded
+import NIOSSH
+
+final class ServerHandler: ChannelInboundHandler {
+ typealias InboundIn = SSHChannelData
+ typealias OutboundOut = SSHChannelData
+
+ func channelRead(context: ChannelHandlerContext, data: NIOAny) {
+ context.write(data, promise: nil)
+ }
+
+ func channelReadComplete(context: ChannelHandlerContext) {
+ context.flush()
+ }
+}
+
+final class ClientHandler: ChannelInboundHandler {
+ typealias InboundIn = SSHChannelData
+ typealias OutboundOut = SSHChannelData
+
+ private var didSend: Bool = false
+ private let message: ByteBuffer
+ private let chunkSize: Int
+ var readBytes: Int = 0
+
+ init(message: ByteBuffer, chunkSize: Int) {
+ self.message = message
+ self.chunkSize = chunkSize
+ }
+
+ func handlerAdded(context: ChannelHandlerContext) {
+ if context.channel.isActive {
+ self.sendInitialMessage(context: context)
+ }
+ }
+
+ func channelActive(context: ChannelHandlerContext) {
+ self.sendInitialMessage(context: context)
+ }
+
+ private func sendInitialMessage(context: ChannelHandlerContext) {
+ if self.didSend { return }
+
+ self.didSend = true
+
+ var message = self.message
+
+ while let chunk = message.readSlice(length: self.chunkSize) {
+ let data = SSHChannelData(type: .channel, data: .byteBuffer(chunk))
+ context.write(self.wrapOutboundOut(data), promise: nil)
+ }
+
+ context.flush()
+ }
+
+ func channelRead(context: ChannelHandlerContext, data: NIOAny) {
+ let data = self.unwrapInboundIn(data)
+ guard case .byteBuffer(let buffer) = data.data else {
+ fatalError()
+ }
+ self.readBytes += buffer.readableBytes
+
+ if self.readBytes == self.message.readableBytes {
+ context.close(promise: nil)
+ }
+ }
+}
+
+func run(identifier: String) {
+ let loop = EmbeddedEventLoop()
+ let hostKey = NIOSSHPrivateKey(ed25519Key: .init())
+
+ // The factor of 1000 is harder to see here, but it's that we send 1000 chunks.
+ let chunkSize = 16 * 1000
+ let message = ByteBuffer(repeating: 0x00, count: chunkSize * 1000)
+
+ measure(identifier: identifier) {
+ var sumOfReadBytes = 0
+
+ let clientChannel = EmbeddedChannel(loop: loop)
+ let serverChannel = EmbeddedChannel(loop: loop)
+
+ try! clientChannel.pipeline.addHandler(
+ NIOSSHHandler(
+ role: .client(.init(
+ userAuthDelegate: HardcodedClientPasswordDelegate(),
+ serverAuthDelegate: AcceptAllHostKeysDelegate()
+ )
+ ),
+ allocator: clientChannel.allocator,
+ inboundChildChannelInitializer: nil
+ )
+ ).wait()
+ try! serverChannel.pipeline.addHandler(
+ NIOSSHHandler(
+ role: .server(.init(
+ hostKeys: [hostKey],
+ userAuthDelegate: HardcodedServerPasswordDelegate()
+ )
+ ),
+ allocator: serverChannel.allocator,
+ inboundChildChannelInitializer: { channel, _ in
+ channel.pipeline.addHandler(ServerHandler())
+ }
+ )
+ ).wait()
+
+ try! clientChannel.connect(to: SocketAddress(ipAddress: "1.2.3.4", port: 5678)).wait()
+ try! serverChannel.connect(to: SocketAddress(ipAddress: "1.2.3.4", port: 5678)).wait()
+
+ let clientHandler = ClientHandler(message: message, chunkSize: chunkSize)
+
+ let childChannelFuture: EventLoopFuture = clientChannel.pipeline.handler(type: NIOSSHHandler.self).flatMap { sshHandler in
+ let promise = clientChannel.eventLoop.makePromise(of: Channel.self)
+ sshHandler.createChannel(promise) { childChannel, _ in
+ childChannel.pipeline.addHandlers([clientHandler])
+ }
+ return promise.futureResult
+ }
+ clientChannel.embeddedEventLoop.run()
+ try! interactInMemory(clientChannel, serverChannel)
+
+ let childChannel = try! childChannelFuture.wait()
+
+ try! childChannel.closeFuture.wait()
+ sumOfReadBytes = clientHandler.readBytes
+
+ try! clientChannel.close().wait()
+ try! serverChannel.close().wait()
+
+ return sumOfReadBytes
+ }
+}
diff --git a/dev/alloc-limits-from-test-output b/dev/alloc-limits-from-test-output
new file mode 100755
index 0000000..1be4924
--- /dev/null
+++ b/dev/alloc-limits-from-test-output
@@ -0,0 +1,88 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the SwiftNIO open source project
+##
+## Copyright (c) 2021 Apple Inc. and the SwiftNIO project authors
+## Licensed under Apache License v2.0
+##
+## See LICENSE.txt for license information
+## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+##
+## SPDX-License-Identifier: Apache-2.0
+##
+##===----------------------------------------------------------------------===##
+
+# This script allows you to consume any Jenkins/alloc counter run output and
+# convert it into the right for for the docker-compose script.
+
+set -eu
+
+mode_flag=${1---docker-compose}
+
+function usage() {
+ echo >&1 "Usage: $0 [--docker-compose|--export]"
+ echo >&1
+ echo >&1 "Example:"
+ echo >&1 " # copy the output from the Jenkins CI into your clipboard, then"
+ echo >&1 " pbpaste | $0 --docker-compose"
+}
+
+function die() {
+ echo >&2 "ERROR: $*"
+ exit 1
+}
+
+case "$mode_flag" in
+ --docker-compose)
+ mode=docker
+ ;;
+ --export)
+ mode=export
+ ;;
+ *)
+ usage
+ exit 1
+ ;;
+esac
+
+function allow_slack() {
+ raw="$1"
+ if [[ ! "$raw" =~ ^[0-9]+$ ]]; then
+ die "not a malloc count: '$raw'"
+ fi
+ if [[ "$raw" -lt 1000 ]]; then
+ echo "$raw"
+ return
+ fi
+
+ allocs=$raw
+ while true; do
+ allocs=$(( allocs + 1 ))
+ if [[ "$allocs" =~ [0-9]+00$ || "$allocs" =~ [0-9]+50$ ]]; then
+ echo "$allocs"
+ return
+ fi
+ done
+}
+
+grep -e "total number of mallocs" -e ".total_allocations" -e "export MAX_ALLOCS_ALLOWED_" | \
+ sed -e "s/: total number of mallocs: /=/g" \
+ -e "s/.total_allocations: /=/g" \
+ -e "s/info: /test_/g" \
+ -e "s/export MAX_ALLOCS_ALLOWED_/test_/g" | \
+ grep -Eo 'test_[a-zA-Z0-9_-]+=[0-9]+' | sort | uniq | while read info; do
+ test_name=$(echo "$info" | sed "s/test_//g" | cut -d= -f1 )
+ allocs=$(allow_slack "$(echo "$info" | cut -d= -f2 | sed "s/ //g")")
+ case "$mode" in
+ docker)
+ echo " - MAX_ALLOCS_ALLOWED_$test_name=$allocs"
+ ;;
+ export)
+ echo "export MAX_ALLOCS_ALLOWED_$test_name=$allocs"
+ ;;
+ *)
+ die "Unexpected mode: $mode"
+ ;;
+ esac
+done
diff --git a/dev/update-alloc-limits-to-last-completed-ci-build b/dev/update-alloc-limits-to-last-completed-ci-build
new file mode 100755
index 0000000..17cc63b
--- /dev/null
+++ b/dev/update-alloc-limits-to-last-completed-ci-build
@@ -0,0 +1,49 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the SwiftNIO open source project
+##
+## Copyright (c) 2021-2023 Apple Inc. and the SwiftNIO project authors
+## Licensed under Apache License v2.0
+##
+## See LICENSE.txt for license information
+## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+##
+## SPDX-License-Identifier: Apache-2.0
+##
+##===----------------------------------------------------------------------===##
+
+set -eu
+set -o pipefail
+
+here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+url_prefix=${1-"https://ci.swiftserver.group/job/swift-nio-ssh-"}
+target_repo=${2-"$here/.."}
+tmpdir=$(mktemp -d /tmp/.last-build_XXXXXX)
+
+for f in 55 56 57 58 nightly; do
+ echo "swift$f"
+ if [[ "$f" == "nightly" ]]; then
+ url="$url_prefix$f-prb/lastCompletedBuild/consoleFull"
+ else
+ url="${url_prefix}swift${f}-prb/lastCompletedBuild/consoleFull"
+ fi
+ echo "$url"
+ curl -s "$url" | "$here/alloc-limits-from-test-output" > "$tmpdir/limits$f"
+
+ if [[ "$(wc -l < "$tmpdir/limits$f")" -lt 3 ]]; then
+ echo >&2 "ERROR: fewer than 3 limits found, something's not right"
+ exit 1
+ fi
+
+ docker_file=$(if [[ "$f" == "nightly" ]]; then f=main; fi && ls "$target_repo/docker/docker-compose."*"$f"*".yaml")
+
+ echo "$docker_file"
+ cat "$tmpdir/limits$f"
+ cat "$docker_file" | grep -v MAX_ALLOCS_ALLOWED | grep -B10000 "^ environment:" > "$tmpdir/pre$f"
+ cat "$docker_file" | grep -v MAX_ALLOCS_ALLOWED | grep -A10000 "^ environment:" | sed 1d > "$tmpdir/post$f"
+ cat "$tmpdir/pre$f" "$tmpdir/limits$f" "$tmpdir/post$f" > "$docker_file"
+done
+
+rm -rf "$tmpdir"
diff --git a/docker/docker-compose.2004.55.yaml b/docker/docker-compose.2004.55.yaml
index 6a9e42f..b8bba2d 100644
--- a/docker/docker-compose.2004.55.yaml
+++ b/docker/docker-compose.2004.55.yaml
@@ -11,7 +11,10 @@ services:
test:
image: swift-nio-ssh:20.04-5.5
- environment: []
+ environment:
+ - MAX_ALLOCS_ALLOWED_client_server_many_small_commands_per_connection=270900
+ - MAX_ALLOCS_ALLOWED_client_server_one_command_per_connection=1158050
+ - MAX_ALLOCS_ALLOWED_client_server_streaming_large_message_in_small_chunks=65150
#- SANITIZER_ARG=--sanitize=thread
#- WARN_AS_ERROR_ARG=-Xswiftc -warnings-as-errors
diff --git a/docker/docker-compose.2004.56.yaml b/docker/docker-compose.2004.56.yaml
index 81cc96f..04746f7 100644
--- a/docker/docker-compose.2004.56.yaml
+++ b/docker/docker-compose.2004.56.yaml
@@ -11,7 +11,10 @@ services:
test:
image: swift-nio-ssh:20.04-5.6
- environment: []
+ environment:
+ - MAX_ALLOCS_ALLOWED_client_server_many_small_commands_per_connection=267850
+ - MAX_ALLOCS_ALLOWED_client_server_one_command_per_connection=1100050
+ - MAX_ALLOCS_ALLOWED_client_server_streaming_large_message_in_small_chunks=65100
#- SANITIZER_ARG=--sanitize=thread
#- WARN_AS_ERROR_ARG=-Xswiftc -warnings-as-errors
diff --git a/docker/docker-compose.2204.57.yaml b/docker/docker-compose.2204.57.yaml
index ff0b7ee..eead550 100644
--- a/docker/docker-compose.2204.57.yaml
+++ b/docker/docker-compose.2204.57.yaml
@@ -11,7 +11,10 @@ services:
test:
image: swift-nio-ssh:22.04-5.7
- environment: []
+ environment:
+ - MAX_ALLOCS_ALLOWED_client_server_many_small_commands_per_connection=267850
+ - MAX_ALLOCS_ALLOWED_client_server_one_command_per_connection=1100050
+ - MAX_ALLOCS_ALLOWED_client_server_streaming_large_message_in_small_chunks=65100
#- SANITIZER_ARG=--sanitize=thread
#- WARN_AS_ERROR_ARG=-Xswiftc -warnings-as-errors
diff --git a/docker/docker-compose.2204.58.yaml b/docker/docker-compose.2204.58.yaml
index d5157c4..54bbe2f 100644
--- a/docker/docker-compose.2204.58.yaml
+++ b/docker/docker-compose.2204.58.yaml
@@ -11,6 +11,9 @@ services:
test:
image: swift-nio-ssh:22.04-5.8
environment:
+ - MAX_ALLOCS_ALLOWED_client_server_many_small_commands_per_connection=261850
+ - MAX_ALLOCS_ALLOWED_client_server_one_command_per_connection=1087050
+ - MAX_ALLOCS_ALLOWED_client_server_streaming_large_message_in_small_chunks=63100
- IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error
#- SANITIZER_ARG=--sanitize=thread
- WARN_AS_ERROR_ARG=-Xswiftc -warnings-as-errors
diff --git a/docker/docker-compose.2204.main.yaml b/docker/docker-compose.2204.main.yaml
index 142d85a..99fcb0a 100644
--- a/docker/docker-compose.2204.main.yaml
+++ b/docker/docker-compose.2204.main.yaml
@@ -11,6 +11,9 @@ services:
test:
image: swift-nio-ssh:22.04-main
environment:
+ - MAX_ALLOCS_ALLOWED_client_server_many_small_commands_per_connection=261850
+ - MAX_ALLOCS_ALLOWED_client_server_one_command_per_connection=1087050
+ - MAX_ALLOCS_ALLOWED_client_server_streaming_large_message_in_small_chunks=63100
- IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error
#- SANITIZER_ARG=--sanitize=thread
- WARN_AS_ERROR_ARG=-Xswiftc -warnings-as-errors
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index f25440f..91dfac8 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -28,7 +28,7 @@ services:
test:
<<: *common
- command: /bin/bash -xcl "swift test --enable-test-discovery $${WARN_AS_ERROR_ARG-} $${SANITIZER_ARG-} $${IMPORT_CHECK_ARG-}"
+ command: /bin/bash -xcl "swift test --enable-test-discovery $${WARN_AS_ERROR_ARG-} $${SANITIZER_ARG-} $${IMPORT_CHECK_ARG-} && ./scripts/integration_tests.sh $${INTEGRATION_TESTS_ARG-}"
# util
diff --git a/scripts/integration_tests.sh b/scripts/integration_tests.sh
new file mode 100755
index 0000000..ca630bd
--- /dev/null
+++ b/scripts/integration_tests.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the SwiftNIO open source project
+##
+## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+## Licensed under Apache License v2.0
+##
+## See LICENSE.txt for license information
+## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+##
+## SPDX-License-Identifier: Apache-2.0
+##
+##===----------------------------------------------------------------------===##
+
+set +ex
+
+mkdir -p .build # for the junit.xml file
+./IntegrationTests/run-tests.sh --junit-xml .build/junit-sh-tests.xml -i $@
diff --git a/scripts/soundness.sh b/scripts/soundness.sh
index 49706d1..103b115 100755
--- a/scripts/soundness.sh
+++ b/scripts/soundness.sh
@@ -18,7 +18,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
function replace_acceptable_years() {
# this needs to replace all acceptable forms with 'YEARS'
- sed -e 's/20[12][78901]-20[12][8901]/YEARS/' -e 's/2019-2020/YEARS/' -e 's/2019/YEARS/' -e 's/2020/YEARS/' -e 's/2021/YEARS/' -e 's/2022/YEARS/'
+ sed -e 's/20[12][7890123]-20[12][890123]/YEARS/' -e 's/2019-2020/YEARS/' -e 's/20[12][90123]/YEARS/'
}
command -v swiftformat >/dev/null 2>&1 || { echo >&2 "swiftformat needs to be installed but is not available in the path."; exit 1; }