From 3f082cea81fa4e6082d178e7a2944d3d7c1f8d1a Mon Sep 17 00:00:00 2001 From: Bergur Ragnarsson Date: Thu, 4 Jun 2026 10:37:48 +0000 Subject: [PATCH 1/4] fix: add gorpipe testserver for speedy use in tests --- .../scala/gorsat/Outputs/ColorStdOut.scala | 6 +- .../scala/gorsat/Outputs/NorColorStdOut.scala | 5 +- .../main/scala/gorsat/Outputs/NorStdOut.scala | 4 +- .../main/scala/gorsat/Outputs/StdOut.scala | 4 +- .../process/CLIGorExecutionEngine.scala | 26 +++--- .../main/scala/gorsat/process/GorPipe.scala | 4 + .../scala/gorsat/process/GorTestServer.scala | 82 +++++++++++++++++++ .../scala/gorsat/process/PipeOptions.scala | 10 ++- 8 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 gortools/src/main/scala/gorsat/process/GorTestServer.scala diff --git a/gortools/src/main/scala/gorsat/Outputs/ColorStdOut.scala b/gortools/src/main/scala/gorsat/Outputs/ColorStdOut.scala index fd810dac6..22345ded9 100644 --- a/gortools/src/main/scala/gorsat/Outputs/ColorStdOut.scala +++ b/gortools/src/main/scala/gorsat/Outputs/ColorStdOut.scala @@ -22,12 +22,12 @@ package gorsat.Outputs -import gorsat.Commands.RowHeader import gorsat.process.PipeInstance +import java.io.OutputStream import org.gorpipe.gor.model.{Row, RowColorize} -case class ColorStdOut(instance: PipeInstance = null, colorFormatter: RowColorize) - extends OutStream(null , System.out) { +case class ColorStdOut(instance: PipeInstance = null, colorFormatter: RowColorize, dest: OutputStream = System.out) + extends OutStream(null, dest) { var headerPrinted : Boolean = false diff --git a/gortools/src/main/scala/gorsat/Outputs/NorColorStdOut.scala b/gortools/src/main/scala/gorsat/Outputs/NorColorStdOut.scala index c319a5b0b..5f791b6c8 100644 --- a/gortools/src/main/scala/gorsat/Outputs/NorColorStdOut.scala +++ b/gortools/src/main/scala/gorsat/Outputs/NorColorStdOut.scala @@ -23,10 +23,11 @@ package gorsat.Outputs import gorsat.process.PipeInstance +import java.io.OutputStream import org.gorpipe.gor.model.{Row, RowColorize} -case class NorColorStdOut(instance: PipeInstance = null, colorFormatter: RowColorize) - extends NorOutStream(null, System.out) { +case class NorColorStdOut(instance: PipeInstance = null, colorFormatter: RowColorize, dest: OutputStream = System.out) + extends NorOutStream(null, dest) { var headerPrinted : Boolean = false diff --git a/gortools/src/main/scala/gorsat/Outputs/NorStdOut.scala b/gortools/src/main/scala/gorsat/Outputs/NorStdOut.scala index 401bd9329..3c2f52430 100644 --- a/gortools/src/main/scala/gorsat/Outputs/NorStdOut.scala +++ b/gortools/src/main/scala/gorsat/Outputs/NorStdOut.scala @@ -22,5 +22,7 @@ package gorsat.Outputs -case class NorStdOut(override val header: String = null) extends NorOutStream(header, System.out) { +import java.io.OutputStream + +case class NorStdOut(override val header: String = null, dest: OutputStream = System.out) extends NorOutStream(header, dest) { } diff --git a/gortools/src/main/scala/gorsat/Outputs/StdOut.scala b/gortools/src/main/scala/gorsat/Outputs/StdOut.scala index 80a5c253e..29b3c141e 100644 --- a/gortools/src/main/scala/gorsat/Outputs/StdOut.scala +++ b/gortools/src/main/scala/gorsat/Outputs/StdOut.scala @@ -22,6 +22,8 @@ package gorsat.Outputs -case class StdOut( val header: String = null) extends OutStream (header, System.out) { +import java.io.OutputStream + +case class StdOut(val header: String = null, dest: OutputStream = System.out) extends OutStream(header, dest) { } \ No newline at end of file diff --git a/gortools/src/main/scala/gorsat/process/CLIGorExecutionEngine.scala b/gortools/src/main/scala/gorsat/process/CLIGorExecutionEngine.scala index 6c2907afd..5cb8013fb 100644 --- a/gortools/src/main/scala/gorsat/process/CLIGorExecutionEngine.scala +++ b/gortools/src/main/scala/gorsat/process/CLIGorExecutionEngine.scala @@ -31,6 +31,7 @@ import org.gorpipe.gor.RequestStats import org.gorpipe.gor.driver.meta.DataType import org.gorpipe.gor.util.DataUtil import org.gorpipe.gor.model.{RowRotatingColorize, RowTypeColorize} +import java.io.OutputStream /** * Execution engine for GOR running as command line. This class takes as input the command line options, construct a @@ -39,11 +40,16 @@ import org.gorpipe.gor.model.{RowRotatingColorize, RowTypeColorize} * @param pipeOptions GorPipe command line options * @param whitelistedCmdFiles File containing whitelisted commands * @param securityContext Security context if needed + * @param outputStream Stream to write query results to (defaults to stdout) */ -class CLIGorExecutionEngine(pipeOptions: PipeOptions, whitelistedCmdFiles:String = null, securityContext:String = null) extends GorExecutionEngine { +class CLIGorExecutionEngine(pipeOptions: PipeOptions, whitelistedCmdFiles:String, securityContext:String, outputStream: OutputStream) extends GorExecutionEngine { + + def this(pipeOptions: PipeOptions, whitelistedCmdFiles: String, securityContext: String) = { + this(pipeOptions, whitelistedCmdFiles, securityContext, System.out) + } def this(args:Array[String], whitelistedCmdFiles:String, securityContext:String) = { - this(PipeOptions.parseInputArguments(args), whitelistedCmdFiles, securityContext) + this(PipeOptions.parseInputArguments(args), whitelistedCmdFiles, securityContext, System.out) } override protected def createSession(): GorSession = { @@ -66,7 +72,7 @@ class CLIGorExecutionEngine(pipeOptions: PipeOptions, whitelistedCmdFiles:String if (MacroUtilities.isLastCommandWrite(pipeOptions.query)) instance = null iterator.thePipeStep = iterator.thePipeStep | - createStdOut(session.getNorContext || iterator.isNorContext, pipeOptions.color, iterator) + createStdOut(session.getNorContext || iterator.isNorContext, pipeOptions.color, iterator, outputStream) iterator } @@ -83,24 +89,24 @@ class CLIGorExecutionEngine(pipeOptions: PipeOptions, whitelistedCmdFiles:String } } - private def createStdOut(isNor: Boolean, color: String, iterator: PipeInstance): OutStream = { + private def createStdOut(isNor: Boolean, color: String, iterator: PipeInstance, out: OutputStream): OutStream = { val c = color.toLowerCase() if (isNor) { if (c.startsWith("r")) { - NorColorStdOut(iterator, new RowRotatingColorize()) + NorColorStdOut(iterator, new RowRotatingColorize(), out) } else if(c.startsWith("t")) { - NorColorStdOut(iterator, new RowTypeColorize()) + NorColorStdOut(iterator, new RowTypeColorize(), out) } else { - NorStdOut(if (iterator == null) null else iterator.getHeader()) + NorStdOut(if (iterator == null) null else iterator.getHeader(), out) } } else { if (c.startsWith("r")) { - ColorStdOut(iterator, new RowRotatingColorize()) + ColorStdOut(iterator, new RowRotatingColorize(), out) } else if (c.startsWith("t")) { - ColorStdOut(iterator, new RowTypeColorize()) + ColorStdOut(iterator, new RowTypeColorize(), out) } else { - StdOut(if (iterator == null) null else iterator.getHeader()) + StdOut(if (iterator == null) null else iterator.getHeader(), out) } } } diff --git a/gortools/src/main/scala/gorsat/process/GorPipe.scala b/gortools/src/main/scala/gorsat/process/GorPipe.scala index 75f67e72b..7433da751 100644 --- a/gortools/src/main/scala/gorsat/process/GorPipe.scala +++ b/gortools/src/main/scala/gorsat/process/GorPipe.scala @@ -80,6 +80,10 @@ object GorPipe extends GorPipeFirstOrderCommands { // Initialize database connections DbConnection.initInConsoleApp() + if (commandlineOptions.testServer) { + GorTestServer.start(commandlineOptions) + System.exit(0) + } var exitCode = 0 //todo find a better way to construct diff --git a/gortools/src/main/scala/gorsat/process/GorTestServer.scala b/gortools/src/main/scala/gorsat/process/GorTestServer.scala new file mode 100644 index 000000000..84358c7e9 --- /dev/null +++ b/gortools/src/main/scala/gorsat/process/GorTestServer.scala @@ -0,0 +1,82 @@ +/* + * BEGIN_COPYRIGHT + * + * Copyright (C) 2011-2013 deCODE genetics Inc. + * Copyright (C) 2013-2019 WuXi NextCode Inc. + * All Rights Reserved. + * + * GORpipe is free software: you can redistribute it and/or modify + * it under the terms of the AFFERO GNU General Public License as published by + * the Free Software Foundation. + * + * GORpipe is distributed "AS-IS" AND WITHOUT ANY WARRANTY OF ANY KIND, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, + * NON-INFRINGEMENT, OR FITNESS FOR A PARTICULAR PURPOSE. See + * the AFFERO GNU General Public License for the complete license terms. + * + * You should have received a copy of the AFFERO GNU General Public License + * along with GORpipe. If not, see + * + * END_COPYRIGHT + */ + +package gorsat.process + +import com.sun.net.httpserver.{HttpExchange, HttpServer} +import java.io.ByteArrayOutputStream +import java.net.InetSocketAddress +import org.gorpipe.exceptions.{ExceptionUtilities, GorException} +import org.slf4j.LoggerFactory + +object GorTestServer { + + private val logger = LoggerFactory.getLogger(this.getClass) + + def start(baseOptions: PipeOptions): Unit = { + val server = HttpServer.create(new InetSocketAddress(baseOptions.port), 0) + + server.createContext("/query", (exchange: HttpExchange) => { + if (exchange.getRequestMethod.equalsIgnoreCase("POST")) { + val query = new String(exchange.getRequestBody.readAllBytes(), "UTF-8").trim + val (status, body) = executeQuery(query, baseOptions) + val bytes = body.getBytes("UTF-8") + exchange.getResponseHeaders.set("Content-Type", "text/plain; charset=UTF-8") + exchange.sendResponseHeaders(status, bytes.length) + val os = exchange.getResponseBody + os.write(bytes) + os.close() + } else { + exchange.sendResponseHeaders(405, -1) + exchange.getResponseBody.close() + } + }) + + server.start() + System.err.println(s"GOR test server listening on port ${baseOptions.port}") + logger.info(s"GOR test server started on port ${baseOptions.port}") + + Thread.currentThread().join() + } + + private def executeQuery(query: String, baseOptions: PipeOptions): (Int, String) = { + val opts = new PipeOptions() + opts.query = query + opts.gorRoot = baseOptions.gorRoot + opts.configFile = baseOptions.configFile + opts.aliasFile = baseOptions.aliasFile + opts.cacheDir = baseOptions.cacheDir + opts.color = "none" + + val out = new ByteArrayOutputStream() + val engine = new CLIGorExecutionEngine(opts, null, null, out) + try { + engine.execute() + (200, out.toString("UTF-8")) + } catch { + case ge: GorException => + (500, ExceptionUtilities.gorExceptionToString(ge)) + case ex: Throwable => + (500, Option(ex.getMessage).getOrElse(ex.getClass.getName)) + } + } +} diff --git a/gortools/src/main/scala/gorsat/process/PipeOptions.scala b/gortools/src/main/scala/gorsat/process/PipeOptions.scala index ea68b767d..5a9eb7835 100644 --- a/gortools/src/main/scala/gorsat/process/PipeOptions.scala +++ b/gortools/src/main/scala/gorsat/process/PipeOptions.scala @@ -90,7 +90,9 @@ object PipeOptions { "-gorroot", "-requestid", "-stats", - "-color") + "-color", + "-test-server", + "-port") } /** @@ -136,6 +138,10 @@ class PipeOptions { var stats: Boolean = false // Colored output for stdout var color: String = _ + // Start as a test HTTP server instead of executing a query + var testServer: Boolean = false + // Port for the test server + var port: Int = 4242 def parseOptions(args: Array[String]): Unit = { this.aliasFile = CommandParseUtilities.stringValueOfOptionWithDefault(args, "-aliases", null) @@ -153,6 +159,8 @@ class PipeOptions { this.version = CommandParseUtilities.hasOption(args, "-version") this.stats = CommandParseUtilities.hasOption(args, "-stats") this.color = CommandParseUtilities.stringValueOfOptionWithDefault(args, "-color", "none") + this.testServer = CommandParseUtilities.hasOption(args, "-test-server") + this.port = CommandParseUtilities.stringValueOfOptionWithDefault(args, "-port", "4242").toInt // Following options should not be part of the documentation this.prePipe = CommandParseUtilities.hasOption(args, "-prepipe") From 78809328c561dc787b617df9665cb8ed73cfb650 Mon Sep 17 00:00:00 2001 From: Bergur Ragnarsson Date: Thu, 4 Jun 2026 11:33:15 +0000 Subject: [PATCH 2/4] moved to gor test-server subcommand --- .../main/java/org/gorpipe/gor/cli/GorCLI.java | 3 +- .../gor/cli/server/TestServerCommand.java | 71 +++++++++++++++++++ .../main/scala/gorsat/process/GorPipe.scala | 5 -- .../scala/gorsat/process/GorTestServer.scala | 8 +-- .../scala/gorsat/process/PipeOptions.scala | 10 +-- 5 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 gorscripts/src/main/java/org/gorpipe/gor/cli/server/TestServerCommand.java diff --git a/gorscripts/src/main/java/org/gorpipe/gor/cli/GorCLI.java b/gorscripts/src/main/java/org/gorpipe/gor/cli/GorCLI.java index f99e3a971..1707896e9 100644 --- a/gorscripts/src/main/java/org/gorpipe/gor/cli/GorCLI.java +++ b/gorscripts/src/main/java/org/gorpipe/gor/cli/GorCLI.java @@ -30,6 +30,7 @@ import org.gorpipe.gor.cli.manager.ManagerCommand; import org.gorpipe.gor.cli.migrator.FolderMigratorCommand; import org.gorpipe.gor.cli.query.QueryCommand; +import org.gorpipe.gor.cli.server.TestServerCommand; import org.gorpipe.gor.cli.render.RenderCommand; import org.gorpipe.logging.GorLogbackUtil; import picocli.CommandLine; @@ -38,7 +39,7 @@ @CommandLine.Command(name="gor", version="version 1.0", description = "Command line interface for gor query language and processes.", - subcommands = {QueryCommand.class, FolderMigratorCommand.class}) + subcommands = {QueryCommand.class, FolderMigratorCommand.class, TestServerCommand.class}) public class GorCLI extends GorExecCLI implements Runnable { public static void main(String[] args) { GorLogbackUtil.initLog("gor"); diff --git a/gorscripts/src/main/java/org/gorpipe/gor/cli/server/TestServerCommand.java b/gorscripts/src/main/java/org/gorpipe/gor/cli/server/TestServerCommand.java new file mode 100644 index 000000000..135f84d9d --- /dev/null +++ b/gorscripts/src/main/java/org/gorpipe/gor/cli/server/TestServerCommand.java @@ -0,0 +1,71 @@ +/* + * BEGIN_COPYRIGHT + * + * Copyright (C) 2011-2013 deCODE genetics Inc. + * Copyright (C) 2013-2019 WuXi NextCode Inc. + * All Rights Reserved. + * + * GORpipe is free software: you can redistribute it and/or modify + * it under the terms of the AFFERO GNU General Public License as published by + * the Free Software Foundation. + * + * GORpipe is distributed "AS-IS" AND WITHOUT ANY WARRANTY OF ANY KIND, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, + * NON-INFRINGEMENT, OR FITNESS FOR A PARTICULAR PURPOSE. See + * the AFFERO GNU General Public License for the complete license terms. + * + * You should have received a copy of the AFFERO GNU General Public License + * along with GORpipe. If not, see + * + * END_COPYRIGHT + */ + +package org.gorpipe.gor.cli.server; + +import gorsat.process.GorTestServer; +import gorsat.process.PipeOptions; +import org.gorpipe.gor.cli.HelpOptions; +import org.gorpipe.gor.model.DbConnection; +import org.gorpipe.gor.session.ProjectContext; +import org.gorpipe.util.ConfigUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; + +@CommandLine.Command(name = "test-server", + description = "Start an HTTP server that accepts GOR queries via POST /query", + header = "Start a GOR test server") +public class TestServerCommand extends HelpOptions implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(TestServerCommand.class); + + @CommandLine.Option(names = {"-p", "--port"}, defaultValue = "4242", + description = "Port to listen on (default: 4242)") + private int port; + + @CommandLine.Option(names = {"-d", "--cachedir"}, + description = "Path to cache directory for query execution") + private String cacheDir; + + @CommandLine.Option(names = {"-w", "--workers"}, defaultValue = "0", + description = "Number of parallel workers for query execution") + private int workers; + + @Override + public void run() { + try { + ConfigUtil.loadConfig("gor"); + DbConnection.initInConsoleApp(); + + PipeOptions opts = new PipeOptions(); + opts.cacheDir_$eq(cacheDir != null ? cacheDir : ProjectContext.DEFAULT_CACHE_DIR); + opts.workers_$eq(workers); + opts.color_$eq("none"); + + GorTestServer.start(port, opts); + } catch (Exception e) { + log.error("Failed to start test server: {}", e.getMessage(), e); + System.exit(-1); + } + } +} diff --git a/gortools/src/main/scala/gorsat/process/GorPipe.scala b/gortools/src/main/scala/gorsat/process/GorPipe.scala index 7433da751..b340ac13b 100644 --- a/gortools/src/main/scala/gorsat/process/GorPipe.scala +++ b/gortools/src/main/scala/gorsat/process/GorPipe.scala @@ -80,11 +80,6 @@ object GorPipe extends GorPipeFirstOrderCommands { // Initialize database connections DbConnection.initInConsoleApp() - if (commandlineOptions.testServer) { - GorTestServer.start(commandlineOptions) - System.exit(0) - } - var exitCode = 0 //todo find a better way to construct diff --git a/gortools/src/main/scala/gorsat/process/GorTestServer.scala b/gortools/src/main/scala/gorsat/process/GorTestServer.scala index 84358c7e9..3e661396d 100644 --- a/gortools/src/main/scala/gorsat/process/GorTestServer.scala +++ b/gortools/src/main/scala/gorsat/process/GorTestServer.scala @@ -32,8 +32,8 @@ object GorTestServer { private val logger = LoggerFactory.getLogger(this.getClass) - def start(baseOptions: PipeOptions): Unit = { - val server = HttpServer.create(new InetSocketAddress(baseOptions.port), 0) + def start(port: Int, baseOptions: PipeOptions): Unit = { + val server = HttpServer.create(new InetSocketAddress(port), 0) server.createContext("/query", (exchange: HttpExchange) => { if (exchange.getRequestMethod.equalsIgnoreCase("POST")) { @@ -52,8 +52,8 @@ object GorTestServer { }) server.start() - System.err.println(s"GOR test server listening on port ${baseOptions.port}") - logger.info(s"GOR test server started on port ${baseOptions.port}") + System.err.println(s"GOR test server listening on port $port") + logger.info(s"GOR test server started on port $port") Thread.currentThread().join() } diff --git a/gortools/src/main/scala/gorsat/process/PipeOptions.scala b/gortools/src/main/scala/gorsat/process/PipeOptions.scala index 5a9eb7835..ea68b767d 100644 --- a/gortools/src/main/scala/gorsat/process/PipeOptions.scala +++ b/gortools/src/main/scala/gorsat/process/PipeOptions.scala @@ -90,9 +90,7 @@ object PipeOptions { "-gorroot", "-requestid", "-stats", - "-color", - "-test-server", - "-port") + "-color") } /** @@ -138,10 +136,6 @@ class PipeOptions { var stats: Boolean = false // Colored output for stdout var color: String = _ - // Start as a test HTTP server instead of executing a query - var testServer: Boolean = false - // Port for the test server - var port: Int = 4242 def parseOptions(args: Array[String]): Unit = { this.aliasFile = CommandParseUtilities.stringValueOfOptionWithDefault(args, "-aliases", null) @@ -159,8 +153,6 @@ class PipeOptions { this.version = CommandParseUtilities.hasOption(args, "-version") this.stats = CommandParseUtilities.hasOption(args, "-stats") this.color = CommandParseUtilities.stringValueOfOptionWithDefault(args, "-color", "none") - this.testServer = CommandParseUtilities.hasOption(args, "-test-server") - this.port = CommandParseUtilities.stringValueOfOptionWithDefault(args, "-port", "4242").toInt // Following options should not be part of the documentation this.prePipe = CommandParseUtilities.hasOption(args, "-prepipe") From 65b2f892e152dc5f89629a80fcf0b9fbd807a367 Mon Sep 17 00:00:00 2001 From: Bergur Ragnarsson Date: Fri, 5 Jun 2026 12:22:31 +0000 Subject: [PATCH 3/4] return error msg --- .../src/main/scala/gorsat/process/GorTestServer.scala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gortools/src/main/scala/gorsat/process/GorTestServer.scala b/gortools/src/main/scala/gorsat/process/GorTestServer.scala index 3e661396d..16e41be34 100644 --- a/gortools/src/main/scala/gorsat/process/GorTestServer.scala +++ b/gortools/src/main/scala/gorsat/process/GorTestServer.scala @@ -60,7 +60,7 @@ object GorTestServer { private def executeQuery(query: String, baseOptions: PipeOptions): (Int, String) = { val opts = new PipeOptions() - opts.query = query + opts.query = PipeOptions.cleanUpQueryAndSplit(query).mkString(";") opts.gorRoot = baseOptions.gorRoot opts.configFile = baseOptions.configFile opts.aliasFile = baseOptions.aliasFile @@ -74,9 +74,13 @@ object GorTestServer { (200, out.toString("UTF-8")) } catch { case ge: GorException => - (500, ExceptionUtilities.gorExceptionToString(ge)) + val msg = ExceptionUtilities.gorExceptionToString(ge) + logger.error("Query failed: {}", msg) + (500, msg) case ex: Throwable => - (500, Option(ex.getMessage).getOrElse(ex.getClass.getName)) + val msg = Option(ex.getMessage).getOrElse(ex.getClass.getName) + logger.error("Query failed: {}", msg, ex) + (500, msg) } } } From 57db5b6fbec2c45a5989bac67f36bd8c7092ee96 Mon Sep 17 00:00:00 2001 From: Bergur Ragnarsson Date: Fri, 5 Jun 2026 14:34:44 +0000 Subject: [PATCH 4/4] support config file option to get ref build --- .../org/gorpipe/gor/cli/server/TestServerCommand.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gorscripts/src/main/java/org/gorpipe/gor/cli/server/TestServerCommand.java b/gorscripts/src/main/java/org/gorpipe/gor/cli/server/TestServerCommand.java index 135f84d9d..3bdeea9fb 100644 --- a/gorscripts/src/main/java/org/gorpipe/gor/cli/server/TestServerCommand.java +++ b/gorscripts/src/main/java/org/gorpipe/gor/cli/server/TestServerCommand.java @@ -32,6 +32,8 @@ import org.slf4j.LoggerFactory; import picocli.CommandLine; +import java.io.File; + @CommandLine.Command(name = "test-server", description = "Start an HTTP server that accepts GOR queries via POST /query", header = "Start a GOR test server") @@ -51,6 +53,10 @@ public class TestServerCommand extends HelpOptions implements Runnable { description = "Number of parallel workers for query execution") private int workers; + @CommandLine.Option(names = {"-c", "--config"}, + description = "Loads configuration from external file") + private File configFile; + @Override public void run() { try { @@ -61,6 +67,8 @@ public void run() { opts.cacheDir_$eq(cacheDir != null ? cacheDir : ProjectContext.DEFAULT_CACHE_DIR); opts.workers_$eq(workers); opts.color_$eq("none"); + if (configFile != null) + opts.configFile_$eq(configFile.toString()); GorTestServer.start(port, opts); } catch (Exception e) {