This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit 3d7659ff2036bc005784ce109184748944840ed8 Author: Andrus Adamchik <[email protected]> AuthorDate: Sun May 24 11:50:54 2026 -0400 CAY-2949 CayenneModeler MCP: dbimport_run tool tests --- .claude/rules/cayenne-mcp-server/testing.md | 43 ++++ .../java/org/apache/cayenne/mcp/TestMcpClient.java | 141 ++++++++++++ ...{InProcessMcpServer.java => TestMcpServer.java} | 8 +- .../cayenne/mcp/tools/cgen/CgenRunMcpIT.java | 108 ++-------- .../mcp/tools/dbimport/DbImportRunMcpIT.java | 237 +++++++++++++++++++++ 5 files changed, 444 insertions(+), 93 deletions(-) diff --git a/.claude/rules/cayenne-mcp-server/testing.md b/.claude/rules/cayenne-mcp-server/testing.md new file mode 100644 index 000000000..6c5a0c04d --- /dev/null +++ b/.claude/rules/cayenne-mcp-server/testing.md @@ -0,0 +1,43 @@ +--- +paths: + - "cayenne-mcp-server/**" +description: cayenne-mcp-server testing conventions +--- + +## Integration Test Conventions + +### `TestMcpClient` — shared test extension + +`TestMcpClient` is a JUnit 5 extension (`BeforeEachCallback` + `AfterEachCallback`). Declare it as +an instance field annotated with `@RegisterExtension`; JUnit starts the server before each test +and shuts it down after: + +```java +@RegisterExtension +TestMcpClient client = new TestMcpClient(); +``` + +Key methods: + +| Method | Purpose | +|---------------------------------------------------|---| +| `client.send(json)` | Send a raw JSON-RPC message | +| `client.sendToolCall(id, tool, project, dataMap)` | Send a `tools/call` request for tools that take `projectPath` + `dataMap` args | +| `client.readLine(ms)` | Read next non-blank response line, fail after timeout | +| `client.readLineAsJson(ms)` | Read next response line and return pretty-printed tool-result JSON | + +### Full JSON comparisons + +Prefer comparing the entire pretty-printed JSON payload as a string rather than asserting +individual fields. Variable parts (file paths, JDBC URLs) are formatted in with `%s`; everything +else is written literally. This makes assertions self-documenting and catches unexpected field +additions or serialization regressions that field-by-field checks miss: + +```java +assertEquals(""" + { + "status" : "up_to_date", + ... + }""".formatted(varPath, varUrl, DRIVER, ADAPTER), + client.readLineAsJson(15_000)); +``` diff --git a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/TestMcpClient.java b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/TestMcpClient.java new file mode 100644 index 000000000..61ad21f52 --- /dev/null +++ b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/TestMcpClient.java @@ -0,0 +1,141 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://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 org.apache.cayenne.mcp; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * JUnit 5 extension that wraps the in-process MCP server lifecycle and provides methods + * for sending JSON-RPC requests, reading responses, and extracting tool-result payloads. + * Declare as an instance field annotated with {@code @RegisterExtension}. + */ +public class TestMcpClient implements BeforeEachCallback, AfterEachCallback { + + private static final ObjectMapper MAPPER = JsonMapper.builder().build(); + + private TestMcpServer server; + private BufferedWriter writer; + private BufferedReader reader; + + @Override + public void beforeEach(ExtensionContext context) { + server = TestMcpServer.start(); + writer = new BufferedWriter(new OutputStreamWriter(server.getOutputStream())); + reader = new BufferedReader(new InputStreamReader(server.getInputStream())); + + send(""" + {"jsonrpc":"2.0","id":1,"method":"initialize","params":{\ + "protocolVersion":"2024-11-05",\ + "capabilities":{},\ + "clientInfo":{"name":"test","version":"1.0"}}}"""); + String initResponse = readLine(5_000); + assertTrue(initResponse.contains("\"id\":1"), "initialize response missing: " + initResponse); + send(""" + {"jsonrpc":"2.0","method":"notifications/initialized","params":{}}"""); + } + + @Override + public void afterEach(ExtensionContext context) { + try { + writer.close(); + assertTrue(server.waitFor(10, TimeUnit.SECONDS), "Server thread did not stop after stdin was closed"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for server to stop", e); + } catch (IOException e) { + throw new RuntimeException("Failed to close client", e); + } + } + + /** + * Sends a JSON-RPC message to the server. + */ + public void send(String json) { + try { + writer.write(json); + writer.newLine(); + writer.flush(); + } catch (IOException e) { + throw new RuntimeException("Failed to send message", e); + } + } + + /** + * Reads the next non-blank response line, blocking up to {@code timeoutMs} milliseconds. + */ + public String readLine(long timeoutMs) { + long deadline = System.currentTimeMillis() + timeoutMs; + try { + while (System.currentTimeMillis() < deadline) { + if (reader.ready()) { + String line = reader.readLine(); + if (line != null && !line.isBlank()) { + return line; + } + } + Thread.sleep(20); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for response", e); + } catch (IOException e) { + throw new RuntimeException("Failed to read response", e); + } + throw new AssertionError("No response within " + timeoutMs + "ms"); + } + + /** + * Reads the next response line and returns the pretty-printed tool result JSON payload. + */ + public String readLineAsJson(long timeoutMs) { + String mcpResponse = readLine(timeoutMs); + JsonNode root = MAPPER.readTree(mcpResponse); + String text = root.at("/result/content/0/text").asString(); + JsonNode payload = MAPPER.readTree(text); + return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(payload) + .replace("\r\n", "\n"); + } + + /** + * Sends a {@code tools/call} JSON-RPC request for a tool that accepts + * {@code projectPath} and {@code dataMap} arguments. + */ + public void sendToolCall(int id, String toolName, Path project, String dataMap) { + String escapedPath = project.toString().replace("\\", "\\\\"); + send(""" + {"jsonrpc":"2.0","id":%d,"method":"tools/call","params":\ + {"name":"%s","arguments":\ + {"projectPath":"%s","dataMap":"%s"}}}""".formatted(id, toolName, escapedPath, dataMap)); + } +} diff --git a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/InProcessMcpServer.java b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/TestMcpServer.java similarity index 88% rename from cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/InProcessMcpServer.java rename to cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/TestMcpServer.java index 2fd346ca6..cb8834cbb 100644 --- a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/InProcessMcpServer.java +++ b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/TestMcpServer.java @@ -10,13 +10,13 @@ import java.util.concurrent.TimeUnit; /** * Handles the lifecycle of a test in-process MCP server. */ -public class InProcessMcpServer { +public class TestMcpServer { private final OutputStream outputStream; private final InputStream inputStream; private final Thread serverThread; - public static InProcessMcpServer start() { + public static TestMcpServer start() { try { PipedOutputStream clientOut = new PipedOutputStream(); PipedInputStream serverIn = new PipedInputStream(clientOut); @@ -29,13 +29,13 @@ public class InProcessMcpServer { serverThread.setDaemon(true); serverThread.start(); - return new InProcessMcpServer(clientOut, clientIn, serverThread); + return new TestMcpServer(clientOut, clientIn, serverThread); } catch (IOException e) { throw new RuntimeException("Failed to start in-process MCP server", e); } } - private InProcessMcpServer(OutputStream outputStream, InputStream inputStream, Thread serverThread) { + private TestMcpServer(OutputStream outputStream, InputStream inputStream, Thread serverThread) { this.outputStream = outputStream; this.inputStream = inputStream; this.serverThread = serverThread; diff --git a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunMcpIT.java b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunMcpIT.java index 898ac3319..bbe80cecf 100644 --- a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunMcpIT.java +++ b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunMcpIT.java @@ -18,23 +18,15 @@ ****************************************************************/ package org.apache.cayenne.mcp.tools.cgen; -import org.apache.cayenne.mcp.InProcessMcpServer; -import org.junit.jupiter.api.AfterEach; +import org.apache.cayenne.mcp.TestMcpClient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; import java.nio.file.Files; import java.nio.file.Path; -import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -46,57 +38,34 @@ import static org.junit.jupiter.api.Assertions.assertTrue; */ public class CgenRunMcpIT { - private static final ObjectMapper MAPPER = JsonMapper.builder().build(); + @RegisterExtension + TestMcpClient client = new TestMcpClient(); @TempDir Path tempDir; - private InProcessMcpServer server; - private BufferedWriter writer; - private BufferedReader reader; private Path projectFile; private Path destDir; @BeforeEach - void setUp() throws Exception { + void setUp() throws IOException { destDir = tempDir.resolve("generated"); writeFixture("PersonMap", "com.example", destDir, true); projectFile = tempDir.resolve("cayenne-project.xml"); - - server = InProcessMcpServer.start(); - writer = new BufferedWriter(new OutputStreamWriter(server.getOutputStream())); - reader = new BufferedReader(new InputStreamReader(server.getInputStream())); - - send(""" - {"jsonrpc":"2.0","id":1,"method":"initialize","params":{\ - "protocolVersion":"2024-11-05",\ - "capabilities":{},\ - "clientInfo":{"name":"test","version":"1.0"}}}"""); - String initResponse = readLine(5_000); - assertTrue(initResponse.contains("\"id\":1"), "initialize response missing: " + initResponse); - - send(""" - {"jsonrpc":"2.0","method":"notifications/initialized","params":{}}"""); - } - - @AfterEach - void stopServer() throws Exception { - writer.close(); - assertTrue(server.waitFor(10, TimeUnit.SECONDS), "Server thread did not stop after stdin was closed"); } @Test - public void toolsListIncludesCgenRun() throws Exception { - send(""" + public void toolsListIncludesCgenRun() { + client.send(""" {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"""); - String response = readLine(5_000); + String response = client.readLine(5_000); assertTrue(response.contains("\"cgen_run\""), "tools/list missing 'cgen_run': " + response); } @Test - public void projectNotFoundReturnsValidationError() throws Exception { - send(""" + public void projectNotFoundReturnsValidationError() { + client.send(""" {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{\ "name":"cgen_run","arguments":{\ "projectPath":"/no/such/file.xml","dataMap":"X"}}}"""); @@ -122,12 +91,12 @@ public class CgenRunMcpIT { "code" : "project_not_found", "message" : "No readable file at /no/such/file.xml" } - }""", extractPayload(readLine(5_000))); + }""", client.readLineAsJson(5_000)); } @Test - public void generatesFilesOnFirstRun() throws Exception { - send(callJson(4, projectFile, "PersonMap")); + public void generatesFilesOnFirstRun() { + client.sendToolCall(4, "cgen_run", projectFile, "PersonMap"); // JSON-escape backslashes so Windows paths round-trip correctly through Jackson serialization String superPath = destDir.resolve("com/example/auto/_Person.java").toAbsolutePath().toString().replace("\\", "\\\\"); @@ -162,15 +131,15 @@ public class CgenRunMcpIT { "destDirWritable" : true }, "error" : null - }""".formatted(superPath, subPath, destPath), extractPayload(readLine(15_000))); + }""".formatted(superPath, subPath, destPath), client.readLineAsJson(15_000)); } @Test - public void upToDateOnSecondRun() throws Exception { - send(callJson(5, projectFile, "PersonMap")); - readLine(15_000); + public void upToDateOnSecondRun() { + client.sendToolCall(5, "cgen_run", projectFile, "PersonMap"); + client.readLine(15_000); - send(callJson(6, projectFile, "PersonMap")); + client.sendToolCall(6, "cgen_run", projectFile, "PersonMap"); String destPath = destDir.toAbsolutePath().toString().replace("\\", "\\\\"); @@ -194,46 +163,7 @@ public class CgenRunMcpIT { "destDirWritable" : true }, "error" : null - }""".formatted(destPath), extractPayload(readLine(15_000))); - } - - private String extractPayload(String mcpResponse) throws Exception { - JsonNode root = MAPPER.readTree(mcpResponse); - String text = root.at("/result/content/0/text").asText(); - JsonNode payload = MAPPER.readTree(text); - // Normalize CRLF → LF: Jackson's DefaultPrettyPrinter uses System.lineSeparator() - // which is \r\n on Windows, but Java text blocks always use \n. - return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(payload) - .replace("\r\n", "\n"); - } - - private String callJson(int id, Path project, String dataMap) { - String escapedPath = project.toString().replace("\\", "\\\\"); - return String.format( - "{\"jsonrpc\":\"2.0\",\"id\":%d,\"method\":\"tools/call\",\"params\":{" + - "\"name\":\"cgen_run\",\"arguments\":{" + - "\"projectPath\":\"%s\",\"dataMap\":\"%s\"}}}", - id, escapedPath, dataMap); - } - - private void send(String json) throws IOException { - writer.write(json); - writer.newLine(); - writer.flush(); - } - - private String readLine(long timeoutMs) throws Exception { - long deadline = System.currentTimeMillis() + timeoutMs; - while (System.currentTimeMillis() < deadline) { - if (reader.ready()) { - String line = reader.readLine(); - if (line != null && !line.isBlank()) { - return line; - } - } - Thread.sleep(20); - } - throw new AssertionError("No response within " + timeoutMs + "ms"); + }""".formatted(destPath), client.readLineAsJson(15_000)); } private void writeFixture(String mapName, String pkg, Path destDir, boolean makePairs) throws IOException { diff --git a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/dbimport/DbImportRunMcpIT.java b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/dbimport/DbImportRunMcpIT.java new file mode 100644 index 000000000..10ffa610e --- /dev/null +++ b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/dbimport/DbImportRunMcpIT.java @@ -0,0 +1,237 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://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 org.apache.cayenne.mcp.tools.dbimport; + +import org.apache.cayenne.dba.hsqldb.HSQLDBNoSchemaAdapter; +import org.apache.cayenne.mcp.TestMcpClient; +import org.apache.cayenne.modeler.pref.PreferenceNodeIds; +import org.apache.cayenne.modeler.pref.PrefsLocator; +import org.apache.cayenne.modeler.pref.adapters.DataMapPrefs; +import org.apache.cayenne.modeler.pref.dbconnector.DBConnector; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; +import java.util.UUID; +import java.util.prefs.Preferences; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Integration tests for the {@code dbimport_run} tool over the MCP stdio protocol. + * Complements {@link DbImportRunIT}, which calls the tool directly; this class exercises + * the full JSON-RPC round-trip through the in-process MCP server. + */ +public class DbImportRunMcpIT { + + private static final String HSQL_ADAPTER = HSQLDBNoSchemaAdapter.class.getName(); + private static final String HSQL_DRIVER = "org.hsqldb.jdbc.JDBCDriver"; + + @RegisterExtension + TestMcpClient client = new TestMcpClient(); + + @TempDir + Path tempDir; + + private String dbUrl; + private Connection dbConn; + private Path projectFile; + private Path dataMapFile; + private Preferences dataMapPrefsNode; + + @BeforeEach + void setUp() throws Exception { + dbUrl = "jdbc:hsqldb:mem:dbimport_mcp_%s;shutdown=true" + .formatted(UUID.randomUUID().toString().replace("-", "")); + dbConn = DriverManager.getConnection(dbUrl, "SA", ""); + try (Statement s = dbConn.createStatement()) { + s.execute(""" + CREATE TABLE artist ( + id INTEGER NOT NULL PRIMARY KEY, + name VARCHAR(255) + )"""); + s.execute(""" + CREATE TABLE painting ( + id INTEGER NOT NULL PRIMARY KEY, + title VARCHAR(255), + artist_id INTEGER, + FOREIGN KEY (artist_id) REFERENCES artist(id) + )"""); + } + + projectFile = DbImportRunValidationTest.copyFixture("with-dbimport", tempDir); + dataMapFile = tempDir.resolve("TestMap.map.xml"); + + // The in-process MCP server uses new PrefsLocator() → Preferences.userRoot(). + // Write the connector there so the server can find it when the tool runs. + String dataMapId = PreferenceNodeIds.idForPath(dataMapFile.toAbsolutePath().toString()); + PrefsLocator locator = new PrefsLocator(Preferences.userRoot()); + dataMapPrefsNode = locator.dataMapNode(dataMapId); + DBConnector connector = new DBConnector(); + connector.setUrl(dbUrl); + connector.setUserName("SA"); + connector.setPassword(""); + connector.setJdbcDriver(HSQL_DRIVER); + connector.setDbAdapter(HSQL_ADAPTER); + new DataMapPrefs(dataMapPrefsNode).setConnector(connector); + } + + @AfterEach + void tearDown() throws Exception { + if (dataMapPrefsNode != null) { + try { + dataMapPrefsNode.removeNode(); + } catch (Exception ignored) { + } + } + if (dbConn != null && !dbConn.isClosed()) { + dbConn.close(); + } + } + + @Test + public void toolsListIncludesDbImportRun() { + client.send(""" + {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"""); + + String response = client.readLine(5_000); + assertTrue(response.contains("\"dbimport_run\""), "tools/list missing 'dbimport_run': " + response); + } + + @Test + public void projectNotFoundReturnsValidationError() { + client.send(""" + {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{\ + "name":"dbimport_run","arguments":{\ + "projectPath":"/no/such/file.xml","dataMap":"TestMap"}}}"""); + + assertEquals(""" + { + "status" : "validation_failed", + "summary" : { + "tokensConsidered" : 0, + "tokensApplied" : 0, + "entitiesAdded" : 0, + "entitiesRemoved" : 0, + "entitiesModified" : 0, + "relationshipsAdded" : 0 + }, + "resolved" : null, + "warnings" : [ ], + "validation" : { + "projectFound" : false, + "dataMapFound" : null, + "reverseEngineeringConfigPresent" : null, + "dbConnectorPresent" : null, + "jdbcDriverLoadable" : null, + "jdbcConnectionOpened" : null + }, + "error" : { + "code" : "project_not_found", + "message" : "No readable file at /no/such/file.xml" + } + }""", client.readLineAsJson(5_000)); + } + + @Test + public void happyPathImportsEntities() { + client.sendToolCall(4, "dbimport_run", projectFile, "TestMap"); + + String dataMapPath = dataMapFile.toAbsolutePath().toString().replace("\\", "\\\\"); + String escapedDbUrl = dbUrl.replace("\\", "\\\\"); + + assertEquals(""" + { + "status" : "imported", + "summary" : { + "tokensConsidered" : 2, + "tokensApplied" : 2, + "entitiesAdded" : 2, + "entitiesRemoved" : 0, + "entitiesModified" : 0, + "relationshipsAdded" : 0 + }, + "resolved" : { + "dataMapFile" : "%s", + "jdbcUrl" : "%s", + "jdbcDriver" : "%s", + "dbAdapter" : "%s" + }, + "warnings" : [ "Can't find ObjEntity for PAINTING", "Db Relationship (Db Relationship : toMany (ARTIST.ID, PAINTING.ARTIST_ID)) will have GUESSED Obj Relationship reflection. " ], + "validation" : { + "projectFound" : true, + "dataMapFound" : true, + "reverseEngineeringConfigPresent" : true, + "dbConnectorPresent" : true, + "jdbcDriverLoadable" : true, + "jdbcConnectionOpened" : true + }, + "error" : null + }""".formatted(dataMapPath, escapedDbUrl, HSQL_DRIVER, HSQL_ADAPTER), + client.readLineAsJson(30_000)); + } + + @Test + public void upToDateOnSecondRun() { + client.sendToolCall(5, "dbimport_run", projectFile, "TestMap"); + client.readLine(30_000); + + client.sendToolCall(6, "dbimport_run", projectFile, "TestMap"); + + String dataMapPath = dataMapFile.toAbsolutePath().toString().replace("\\", "\\\\"); + String escapedDbUrl = dbUrl.replace("\\", "\\\\"); + + assertEquals(""" + { + "status" : "up_to_date", + "summary" : { + "tokensConsidered" : 0, + "tokensApplied" : 0, + "entitiesAdded" : 0, + "entitiesRemoved" : 0, + "entitiesModified" : 0, + "relationshipsAdded" : 0 + }, + "resolved" : { + "dataMapFile" : "%s", + "jdbcUrl" : "%s", + "jdbcDriver" : "%s", + "dbAdapter" : "%s" + }, + "warnings" : [ ], + "validation" : { + "projectFound" : true, + "dataMapFound" : true, + "reverseEngineeringConfigPresent" : true, + "dbConnectorPresent" : true, + "jdbcDriverLoadable" : true, + "jdbcConnectionOpened" : true + }, + "error" : null + }""".formatted(dataMapPath, escapedDbUrl, HSQL_DRIVER, HSQL_ADAPTER), + client.readLineAsJson(30_000)); + } +}
