From d4235547b63211937bfab14792d9288430ba80b5 Mon Sep 17 00:00:00 2001 From: Bell Le Date: Fri, 9 Aug 2024 10:00:38 -0700 Subject: [PATCH 1/5] Use IOUtils in PosixPluginFrontendSpec --- .../frontend/OsSpecificFrontendSpec.scala | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala b/bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala index 619d1a8..906825f 100644 --- a/bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala +++ b/bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala @@ -1,5 +1,6 @@ package protocbridge.frontend +import org.apache.commons.io.IOUtils import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.must.Matchers import protocbridge.{ExtraEnv, ProtocCodeGenerator} @@ -30,17 +31,13 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers { writeInput.close() }, processOutput => { - val buffer = new Array[Byte](4096) - var bytesRead = 0 - while (bytesRead != -1) { - bytesRead = processOutput.read(buffer) - if (bytesRead != -1) { - actualOutput.write(buffer, 0, bytesRead) - } - } + IOUtils.copy(processOutput, actualOutput) processOutput.close() }, - _.close() + processError => { + IOUtils.copy(processError, System.err) + processError.close() + } ) ) process.exitValue() From 687db56a3cf6a5ec607a1217a88e2cb351ef9f10 Mon Sep 17 00:00:00 2001 From: Bell Le Date: Fri, 9 Aug 2024 10:02:56 -0700 Subject: [PATCH 2/5] Stress test PosixPluginFrontendSpec --- .../frontend/OsSpecificFrontendSpec.scala | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala b/bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala index 906825f..87a9a1b 100644 --- a/bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala +++ b/bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala @@ -1,11 +1,15 @@ package protocbridge.frontend import org.apache.commons.io.IOUtils +import org.scalatest.exceptions.TestFailedException import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.must.Matchers import protocbridge.{ExtraEnv, ProtocCodeGenerator} import java.io.ByteArrayOutputStream +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.DurationInt +import scala.concurrent.{Await, Future, TimeoutException} import scala.sys.process.ProcessIO import scala.util.Random @@ -40,7 +44,13 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers { } ) ) - process.exitValue() + try { + Await.result(Future { process.exitValue() }, 5.seconds) + } catch { + case _: TimeoutException => + System.err.println(s"Timeout") + process.destroy() + } frontend.cleanup(state) (state, actualOutput.toByteArray) } @@ -59,9 +69,27 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers { toReceive } } + // Repeat 100,000 times since named pipes on macOS are flaky. + val repeatCount = 100000 + for (i <- 1 until repeatCount) { + if (i % 100 == 1) println(s"Running iteration $i of $repeatCount") + val (state, response) = + testPluginFrontend(frontend, fakeGenerator, env, toSend) + try { + response mustBe response + } catch { + case e: TestFailedException => + System.err.println(s"""Failed on iteration $i of $repeatCount: ${e.getMessage}""") + } + } val (state, response) = testPluginFrontend(frontend, fakeGenerator, env, toSend) - response mustBe toReceive + try { + response mustBe response + } catch { + case e: TestFailedException => + System.err.println(s"""Failed on iteration $repeatCount of $repeatCount: ${e.getMessage}""") + } state } From aebccb76ed6a08f06d2bd469ed67af3614a66eca Mon Sep 17 00:00:00 2001 From: Bell Le Date: Fri, 13 Sep 2024 08:55:09 -0700 Subject: [PATCH 3/5] Fix response validation --- .../scala/protocbridge/frontend/OsSpecificFrontendSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala b/bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala index 87a9a1b..65f84ce 100644 --- a/bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala +++ b/bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala @@ -76,7 +76,7 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers { val (state, response) = testPluginFrontend(frontend, fakeGenerator, env, toSend) try { - response mustBe response + response mustBe toReceive } catch { case e: TestFailedException => System.err.println(s"""Failed on iteration $i of $repeatCount: ${e.getMessage}""") @@ -85,7 +85,7 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers { val (state, response) = testPluginFrontend(frontend, fakeGenerator, env, toSend) try { - response mustBe response + response mustBe toReceive } catch { case e: TestFailedException => System.err.println(s"""Failed on iteration $repeatCount of $repeatCount: ${e.getMessage}""") From 84d64f9294c7a3fcd09b40fd58af6f93218bdf68 Mon Sep 17 00:00:00 2001 From: Bell Le Date: Fri, 13 Sep 2024 10:09:34 -0700 Subject: [PATCH 4/5] Switch to MacPluginFrontend in tests --- .../scala/protocbridge/frontend/PosixPluginFrontendSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bridge/src/test/scala/protocbridge/frontend/PosixPluginFrontendSpec.scala b/bridge/src/test/scala/protocbridge/frontend/PosixPluginFrontendSpec.scala index 4a6dd99..1c615d2 100644 --- a/bridge/src/test/scala/protocbridge/frontend/PosixPluginFrontendSpec.scala +++ b/bridge/src/test/scala/protocbridge/frontend/PosixPluginFrontendSpec.scala @@ -3,11 +3,11 @@ package protocbridge.frontend class PosixPluginFrontendSpec extends OsSpecificFrontendSpec { if (!PluginFrontend.isWindows && !PluginFrontend.isMac) { it must "execute a program that forwards input and output to given stream" in { - testSuccess(PosixPluginFrontend) + testSuccess(MacPluginFrontend) } it must "not hang if there is an OOM in generator" in { - testFailure(PosixPluginFrontend) + testFailure(MacPluginFrontend) } } } From 2a57d0ca447a79a0fdf53834a6d54514f9680dc8 Mon Sep 17 00:00:00 2001 From: Bell Le Date: Fri, 13 Sep 2024 13:23:32 -0700 Subject: [PATCH 5/5] Add SocketAllocationSpec --- .../frontend/SocketAllocationSpec.scala | 50 ++++++++++++++ port_conflict.py | 68 +++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 bridge/src/test/scala/protocbridge/frontend/SocketAllocationSpec.scala create mode 100644 port_conflict.py diff --git a/bridge/src/test/scala/protocbridge/frontend/SocketAllocationSpec.scala b/bridge/src/test/scala/protocbridge/frontend/SocketAllocationSpec.scala new file mode 100644 index 0000000..531b44b --- /dev/null +++ b/bridge/src/test/scala/protocbridge/frontend/SocketAllocationSpec.scala @@ -0,0 +1,50 @@ +package protocbridge.frontend +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.must.Matchers + +import java.lang.management.ManagementFactory +import java.net.ServerSocket +import scala.collection.mutable +import scala.sys.process._ +import scala.util.{Failure, Success, Try} + +class SocketAllocationSpec extends AnyFlatSpec with Matchers { + it must "allocate an unused port" in { + val repeatCount = 100000 + + val currentPid = getCurrentPid + val portConflictCount = mutable.Map[Int, Int]() + + for (i <- 1 to repeatCount) { + if (i % 100 == 1) println(s"Running iteration $i of $repeatCount") + + val serverSocket = new ServerSocket(0) // Bind to any available port. + try { + val port = serverSocket.getLocalPort + Try { + s"lsof -i :$port -t".!!.trim + } match { + case Success(output) => + if (output.nonEmpty) { + val pids = output.split("\n").filterNot(_ == currentPid.toString) + if (pids.nonEmpty) { + System.err.println("Port conflict detected on port " + port + " with PIDs: " + pids.mkString(", ")) + portConflictCount(port) = portConflictCount.getOrElse(port, 0) + 1 + } + } + case Failure(_) => // Ignore failure and continue + } + } finally { + serverSocket.close() + } + } + + assert(portConflictCount.isEmpty, s"Found the following ports in use out of $repeatCount: $portConflictCount") + } + + private def getCurrentPid: Int = { + val jvmName = ManagementFactory.getRuntimeMXBean.getName + val pid = jvmName.split("@")(0) + pid.toInt + } +} diff --git a/port_conflict.py b/port_conflict.py new file mode 100644 index 0000000..d7033c0 --- /dev/null +++ b/port_conflict.py @@ -0,0 +1,68 @@ +import os +import socket +import subprocess +import sys + +def is_port_in_use(port, current_pid): + """ + Check if the given port is in use by other processes, excluding the current process. + + :param port: Port number to check + :param pid: Current process ID to exclude from the result + :return: True if the port is in use by another process, False otherwise + """ + try: + # Run lsof command to check if any process is using the port + result = subprocess.run( + ['lsof', '-i', f':{port}', '-t'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + output = result.stdout.strip() + + if output: + # Check if the output contains lines with processes other than the current one + return [ + line + for line in output.split('\n') + if line != str(current_pid) + ] + return [] + except subprocess.CalledProcessError as e: + print(f"Error checking port: {e}", file=sys.stderr) + return [] + +def main(): + repeat_count = 10000 + + current_pid = os.getpid() # Get the current process ID + port_conflict_count = {} + + for i in range(1, repeat_count + 1): + if i % 100 == 1: + print(f"Running iteration {i} of {repeat_count}") + + # Bind to an available port (port 0) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(('', 0)) # Bind to port 0 to get an available port + port = sock.getsockname()[1] # Get the actual port number assigned + + # Check if the port is in use by any other process + pids = is_port_in_use(port, current_pid) + if pids: + print(f"Port conflict detected on port {port} with PIDs: {', '.join(pids)}", file=sys.stderr) + port_conflict_count[port] = port_conflict_count.get(port, 0) + 1 + + # Close the socket after checking + sock.close() + + if port_conflict_count: + print("Ports that were found to be in use and their collision counts:") + for port, count in port_conflict_count.items(): + print(f"Port {port} was found in use {count} times") + else: + print("No ports were found to be in use.") + +if __name__ == '__main__': + main()