From dad782b73621395b57df6f3e3dc2246cfc0647f9 Mon Sep 17 00:00:00 2001 From: navidaminnn Date: Wed, 30 Jul 2025 01:43:06 -0400 Subject: [PATCH 1/4] debug adapter test --- src/manip/DebugAdapter.test.scala | 220 ++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 src/manip/DebugAdapter.test.scala diff --git a/src/manip/DebugAdapter.test.scala b/src/manip/DebugAdapter.test.scala new file mode 100644 index 0000000..7e351e3 --- /dev/null +++ b/src/manip/DebugAdapter.test.scala @@ -0,0 +1,220 @@ +// Copyright 2024-2025 Forja Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package forja.manip + +import forja.* +import forja.source.{Source, SourceRange} +import forja.dsl.* +import forja.langs.tla.{TLAReader, TLAParser} + +import java.net.{InetSocketAddress, Socket} +import java.nio.charset.StandardCharsets +import java.util.concurrent.LinkedBlockingQueue +import scala.collection.mutable +import scala.compiletime.uninitialized + +class DebugAdapterTests extends munit.FunSuite: + case class DAPMessage(messageType: String, content: String, timestamp: Long = System.currentTimeMillis()) + + class DAPClient(host: String, port: Int) extends AutoCloseable: + private val socket: Socket = Socket() + private var seqNum = 1 + private val asyncQueue = new LinkedBlockingQueue[DAPMessage]() + @volatile private var running = false + private var readerThread: Thread = uninitialized + + def connect(): Unit = + socket.connect(InetSocketAddress(host, port), 5000) + running = true + readerThread = new Thread(() => { + try { + while (running && !socket.isClosed) { + val msg = readResponse() + asyncQueue.put(msg) + } + } catch { + case _: java.io.EOFException | _: java.net.SocketTimeoutException | _: ujson.IncompleteParseException => + // Normal shutdown + case ex: Exception => + println(s"[DAPClient] Reader thread exception: $ex") + } + }, "dap-client-reader") + readerThread.setDaemon(true) + readerThread.start() + + def close(): Unit = + running = false + if ((readerThread: AnyRef) ne null) readerThread.interrupt() + socket.close() + + def sendRequest(command: String, args: ujson.Value = ujson.Obj()): Unit = + val requestObj = ujson.Obj( + "seq" -> seqNum, + "type" -> "request", + "command" -> command, + "arguments" -> args + ) + val payloadBytes = ujson.writeToByteArray(requestObj) + socket.getOutputStream.write( + s"Content-Length: ${payloadBytes.length}\r\n\r\n" + .getBytes(StandardCharsets.UTF_8), + ) + socket.getOutputStream.write(payloadBytes) + seqNum += 1 + + def takeNMessages(n: Int): Seq[DAPMessage] = + val messages = mutable.Buffer[DAPMessage]() + var count = 0 + while count < n do + val msg = asyncQueue.take() + if msg != null then + messages += msg + count += 1 + messages.toSeq + + private def readResponse(): DAPMessage = + val input = socket.getInputStream + val buffer = StringBuilder() + var contentLength = -1 + + while contentLength == -1 do + val read = input.read() + if read == -1 then + // If nothing was read, treat as normal shutdown, not a protocol error + socket.close() + if buffer.isEmpty then + throw new java.io.EOFException("Socket closed while reading headers (empty buffer)") + else + // Only fail if there was partial, non-header data + fail("Socket closed with partial, non-header data: " + buffer.toString) + val char = read.toChar + buffer.append(char) + if buffer.toString.endsWith("\r\n\r\n") then + val headers = buffer.toString + headers match + case s if s.contains("Content-Length:") => + val lengthPattern = """Content-Length: (\d+)""".r + lengthPattern.findFirstMatchIn(headers) match + case Some(m) => contentLength = m.group(1).toInt + case None => + socket.close() + fail("Malformed DAP header: Content-Length not found") + case _ => + socket.close() + fail("Malformed DAP header: Content-Length not found") + + val jsonBytes = Array.ofDim[Byte](contentLength) + val bytesRead = input.readNBytes(jsonBytes, 0, contentLength) + if bytesRead != contentLength then + socket.close() + fail(s"Expected $contentLength bytes, got $bytesRead") + val json = String(jsonBytes, StandardCharsets.UTF_8) + + val js = ujson.read(json) + val messageType = + js.obj.get("type").map(_.str) match + case Some("response") => "response" + case Some("event") => "event" + case _ => "unknown" + + val dapMessage = DAPMessage(messageType, json) + dapMessage + + val debugAdapterFixture = FunFixture[DebugAdapter]( + setup = { _ => + val host = "localhost" + val port = 4711 + val adapter = DebugAdapter(host, port) + Thread.sleep(1000) + adapter + }, + teardown = { adapter => + adapter.close() + } + ) + + def createTestTLASource(): Source = + val tlaContent = """ + |---- MODULE TestModule ---- + |EXTENDS Naturals + | + |VARIABLE x + | + |Init == x = 0 + | + |Next == x' = x + 1 + | + |Spec == Init /\ [][Next]_x + | + |==== + """.stripMargin + + Source.fromString(tlaContent) + + debugAdapterFixture.test("handles Step Over request with actual compiler execution") { debugAdapter => + val client = DAPClient("localhost", 4711) + try { + client.connect() + + val compilerThread = new Thread(() => { + val src = createTestTLASource() + val top = TLAReader(SourceRange.entire(src)) + instrumentWithTracer(debugAdapter) { + TLAParser(top) + } + }) + + compilerThread.start() + + // Send all requests up front, without blocking for responses + client.sendRequest("initialize", ujson.Obj( + "clientID" -> "test", + "adapterID" -> "forja" + )) + var messages = client.takeNMessages(1) + assert(messages.nonEmpty, "Expected an initialize response") + assert(messages.exists(_.messageType == "response"), "Expected an initialize response") + println(s"${messages(0).messageType}: ${messages(0).content}\n") + + client.sendRequest("attach") + messages = client.takeNMessages(2) + assert(messages.nonEmpty, "Expected an initialize response") + assert(messages.exists(_.messageType == "response"), "Expected an attach response") + println(s"${messages(0).messageType}: ${messages(0).content}") + assert(messages.exists(_.messageType == "event"), "Expected a next event") + println(s"${messages(1).messageType}: ${messages(1).content}\n") + + client.sendRequest("next", ujson.Obj("threadId" -> 1)) + messages = client.takeNMessages(3) + assert(messages.nonEmpty, "Expected a next response") + assert(messages.exists(_.messageType == "response"), "Expected a next response") + println(s"${messages(0).messageType}: ${messages(0).content}") + assert(messages.exists(_.messageType == "event"), "Expected a loadedSource event") + println(s"${messages(1).messageType}: ${messages(1).content}") + assert(messages.exists(_.messageType == "event"), "Expected a stopped event") + println(s"${messages(2).messageType}: ${messages(2).content}\n") + + client.sendRequest("continue", ujson.Obj("threadId" -> 1)) + messages = client.takeNMessages(1) + assert(messages.nonEmpty, "Expected a continue response") + println(s"${messages(0).messageType}: ${messages(0).content}\n") + + println("Successfully got all expected DAP messages") + + compilerThread.join() + } finally { + client.close() + } + } \ No newline at end of file From cabb4f0cf893ce18e16aa5f398d2dffb3c6a2d0d Mon Sep 17 00:00:00 2001 From: navidaminnn Date: Wed, 30 Jul 2025 01:44:31 -0400 Subject: [PATCH 2/4] added eof spacing --- src/manip/DebugAdapter.test.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/manip/DebugAdapter.test.scala b/src/manip/DebugAdapter.test.scala index 7e351e3..648ce61 100644 --- a/src/manip/DebugAdapter.test.scala +++ b/src/manip/DebugAdapter.test.scala @@ -217,4 +217,4 @@ class DebugAdapterTests extends munit.FunSuite: } finally { client.close() } - } \ No newline at end of file + } From f98555190145fd21589869f67cf7f97c8f742e9d Mon Sep 17 00:00:00 2001 From: navidaminnn Date: Wed, 30 Jul 2025 01:53:04 -0400 Subject: [PATCH 3/4] auto formatter --- src/manip/DebugAdapter.test.scala | 114 +++++++++++++++++++----------- 1 file changed, 73 insertions(+), 41 deletions(-) diff --git a/src/manip/DebugAdapter.test.scala b/src/manip/DebugAdapter.test.scala index 648ce61..54240dd 100644 --- a/src/manip/DebugAdapter.test.scala +++ b/src/manip/DebugAdapter.test.scala @@ -26,8 +26,12 @@ import scala.collection.mutable import scala.compiletime.uninitialized class DebugAdapterTests extends munit.FunSuite: - case class DAPMessage(messageType: String, content: String, timestamp: Long = System.currentTimeMillis()) - + case class DAPMessage( + messageType: String, + content: String, + timestamp: Long = System.currentTimeMillis(), + ) + class DAPClient(host: String, port: Int) extends AutoCloseable: private val socket: Socket = Socket() private var seqNum = 1 @@ -38,19 +42,23 @@ class DebugAdapterTests extends munit.FunSuite: def connect(): Unit = socket.connect(InetSocketAddress(host, port), 5000) running = true - readerThread = new Thread(() => { - try { - while (running && !socket.isClosed) { - val msg = readResponse() - asyncQueue.put(msg) - } - } catch { - case _: java.io.EOFException | _: java.net.SocketTimeoutException | _: ujson.IncompleteParseException => + readerThread = new Thread( + () => { + try { + while (running && !socket.isClosed) { + val msg = readResponse() + asyncQueue.put(msg) + } + } catch { + case _: java.io.EOFException | _: java.net.SocketTimeoutException | + _: ujson.IncompleteParseException => // Normal shutdown - case ex: Exception => - println(s"[DAPClient] Reader thread exception: $ex") - } - }, "dap-client-reader") + case ex: Exception => + println(s"[DAPClient] Reader thread exception: $ex") + } + }, + "dap-client-reader", + ) readerThread.setDaemon(true) readerThread.start() @@ -64,7 +72,7 @@ class DebugAdapterTests extends munit.FunSuite: "seq" -> seqNum, "type" -> "request", "command" -> command, - "arguments" -> args + "arguments" -> args, ) val payloadBytes = ujson.writeToByteArray(requestObj) socket.getOutputStream.write( @@ -95,10 +103,14 @@ class DebugAdapterTests extends munit.FunSuite: // If nothing was read, treat as normal shutdown, not a protocol error socket.close() if buffer.isEmpty then - throw new java.io.EOFException("Socket closed while reading headers (empty buffer)") + throw new java.io.EOFException( + "Socket closed while reading headers (empty buffer)", + ) else // Only fail if there was partial, non-header data - fail("Socket closed with partial, non-header data: " + buffer.toString) + fail( + "Socket closed with partial, non-header data: " + buffer.toString, + ) val char = read.toChar buffer.append(char) if buffer.toString.endsWith("\r\n\r\n") then @@ -142,28 +154,30 @@ class DebugAdapterTests extends munit.FunSuite: }, teardown = { adapter => adapter.close() - } + }, ) def createTestTLASource(): Source = val tlaContent = """ - |---- MODULE TestModule ---- - |EXTENDS Naturals - | - |VARIABLE x - | - |Init == x = 0 - | - |Next == x' = x + 1 - | - |Spec == Init /\ [][Next]_x - | - |==== + |---- MODULE TestModule ---- + |EXTENDS Naturals + | + |VARIABLE x + | + |Init == x = 0 + | + |Next == x' = x + 1 + | + |Spec == Init /\ [][Next]_x + | + |==== """.stripMargin - + Source.fromString(tlaContent) - debugAdapterFixture.test("handles Step Over request with actual compiler execution") { debugAdapter => + debugAdapterFixture.test( + "handles Step Over request with actual compiler execution", + ) { debugAdapter => val client = DAPClient("localhost", 4711) try { client.connect() @@ -179,19 +193,28 @@ class DebugAdapterTests extends munit.FunSuite: compilerThread.start() // Send all requests up front, without blocking for responses - client.sendRequest("initialize", ujson.Obj( - "clientID" -> "test", - "adapterID" -> "forja" - )) + client.sendRequest( + "initialize", + ujson.Obj( + "clientID" -> "test", + "adapterID" -> "forja", + ), + ) var messages = client.takeNMessages(1) assert(messages.nonEmpty, "Expected an initialize response") - assert(messages.exists(_.messageType == "response"), "Expected an initialize response") + assert( + messages.exists(_.messageType == "response"), + "Expected an initialize response", + ) println(s"${messages(0).messageType}: ${messages(0).content}\n") client.sendRequest("attach") messages = client.takeNMessages(2) assert(messages.nonEmpty, "Expected an initialize response") - assert(messages.exists(_.messageType == "response"), "Expected an attach response") + assert( + messages.exists(_.messageType == "response"), + "Expected an attach response", + ) println(s"${messages(0).messageType}: ${messages(0).content}") assert(messages.exists(_.messageType == "event"), "Expected a next event") println(s"${messages(1).messageType}: ${messages(1).content}\n") @@ -199,11 +222,20 @@ class DebugAdapterTests extends munit.FunSuite: client.sendRequest("next", ujson.Obj("threadId" -> 1)) messages = client.takeNMessages(3) assert(messages.nonEmpty, "Expected a next response") - assert(messages.exists(_.messageType == "response"), "Expected a next response") + assert( + messages.exists(_.messageType == "response"), + "Expected a next response", + ) println(s"${messages(0).messageType}: ${messages(0).content}") - assert(messages.exists(_.messageType == "event"), "Expected a loadedSource event") + assert( + messages.exists(_.messageType == "event"), + "Expected a loadedSource event", + ) println(s"${messages(1).messageType}: ${messages(1).content}") - assert(messages.exists(_.messageType == "event"), "Expected a stopped event") + assert( + messages.exists(_.messageType == "event"), + "Expected a stopped event", + ) println(s"${messages(2).messageType}: ${messages(2).content}\n") client.sendRequest("continue", ujson.Obj("threadId" -> 1)) From 87cc692eaed402d2ec98ab1140ab6fc1b842cb7a Mon Sep 17 00:00:00 2001 From: navidaminnn Date: Wed, 30 Jul 2025 01:57:57 -0400 Subject: [PATCH 4/4] re ordered import for linting --- src/manip/DebugAdapter.test.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/manip/DebugAdapter.test.scala b/src/manip/DebugAdapter.test.scala index 54240dd..9a1e20b 100644 --- a/src/manip/DebugAdapter.test.scala +++ b/src/manip/DebugAdapter.test.scala @@ -14,14 +14,15 @@ package forja.manip -import forja.* -import forja.source.{Source, SourceRange} -import forja.dsl.* -import forja.langs.tla.{TLAReader, TLAParser} - import java.net.{InetSocketAddress, Socket} import java.nio.charset.StandardCharsets import java.util.concurrent.LinkedBlockingQueue + +import forja.* +import forja.dsl.* +import forja.langs.tla.{TLAParser, TLAReader} +import forja.source.{Source, SourceRange} + import scala.collection.mutable import scala.compiletime.uninitialized