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; }