Reimplement test agent as a worker command

This sends JSON-RPC over stdin as opposed to using ObjectStream over socket.
This commit is contained in:
Eugene Yokota
2025-07-04 01:28:56 -04:00
parent eb74554ec1
commit b247e2620f
21 changed files with 950 additions and 354 deletions
@@ -0,0 +1,344 @@
/*
* Copyright (C) 2011 Google Inc.
*
* 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 com.google.gson.typeadapters;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Adapts values whose runtime type may differ from their declaration type. This is necessary when a
* field's type is not the same type that GSON should create when deserializing that field. For
* example, consider these types:
*
* <pre>
* {
* &#64;code
* abstract class Shape {
* int x;
* int y;
* }
* class Circle extends Shape {
* int radius;
* }
* class Rectangle extends Shape {
* int width;
* int height;
* }
* class Diamond extends Shape {
* int width;
* int height;
* }
* class Drawing {
* Shape bottomShape;
* Shape topShape;
* }
* }
* </pre>
*
* <p>Without additional type information, the serialized JSON is ambiguous. Is the bottom shape in
* this drawing a rectangle or a diamond?
*
* <pre>{@code
* {
* "bottomShape": {
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }
* }</pre>
*
* This class addresses this problem by adding type information to the serialized JSON and honoring
* that type information when the JSON is deserialized:
*
* <pre>{@code
* {
* "bottomShape": {
* "type": "Diamond",
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "type": "Circle",
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }
* }</pre>
*
* Both the type field name ({@code "type"}) and the type labels ({@code "Rectangle"}) are
* configurable.
*
* <h2>Registering Types</h2>
*
* Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field name to the
* {@link #of} factory method. If you don't supply an explicit type field name, {@code "type"} will
* be used.
*
* <pre>
* {
* &#64;code
* RuntimeTypeAdapterFactory&lt;Shape&gt; shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class, "type");
* }
* </pre>
*
* Next register all of your subtypes. Every subtype must be explicitly registered. This protects
* your application from injection attacks. If you don't supply an explicit type label, the type's
* simple name will be used.
*
* <pre>{@code
* shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
* shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
* shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
* }</pre>
*
* Finally, register the type adapter factory in your application's GSON builder:
*
* <pre>
* {
* &#64;code
* Gson gson = new GsonBuilder().registerTypeAdapterFactory(shapeAdapterFactory).create();
* }
* </pre>
*
* Like {@code GsonBuilder}, this API supports chaining:
*
* <pre>
* {
* &#64;code
* RuntimeTypeAdapterFactory&lt;Shape&gt; shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
* .registerSubtype(Rectangle.class).registerSubtype(Circle.class).registerSubtype(Diamond.class);
* }
* </pre>
*
* <h2>Serialization and deserialization</h2>
*
* In order to serialize and deserialize a polymorphic object, you must specify the base type
* explicitly.
*
* <pre>
* {
* &#64;code
* Diamond diamond = new Diamond();
* String json = gson.toJson(diamond, Shape.class);
* }
* </pre>
*
* And then:
*
* <pre>
* {
* &#64;code
* Shape shape = gson.fromJson(json, Shape.class);
* }
* </pre>
*/
public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Class<?> baseType;
private final String typeFieldName;
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<>();
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<>();
private final boolean maintainType;
private boolean recognizeSubtypes;
private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName, boolean maintainType) {
if (typeFieldName == null || baseType == null) {
throw new NullPointerException();
}
this.baseType = baseType;
this.typeFieldName = typeFieldName;
this.maintainType = maintainType;
}
/**
* Creates a new runtime type adapter for {@code baseType} using {@code typeFieldName} as the type
* field name. Type field names are case sensitive.
*
* @param maintainType true if the type field should be included in deserialized objects
*/
public static <T> RuntimeTypeAdapterFactory<T> of(
Class<T> baseType, String typeFieldName, boolean maintainType) {
return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType);
}
/**
* Creates a new runtime type adapter for {@code baseType} using {@code typeFieldName} as the type
* field name. Type field names are case sensitive.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false);
}
/**
* Creates a new runtime type adapter for {@code baseType} using {@code "type"} as the type field
* name.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
return new RuntimeTypeAdapterFactory<>(baseType, "type", false);
}
/**
* Ensures that this factory will handle not just the given {@code baseType}, but any subtype of
* that type.
*/
@CanIgnoreReturnValue
public RuntimeTypeAdapterFactory<T> recognizeSubtypes() {
this.recognizeSubtypes = true;
return this;
}
/**
* Registers {@code type} identified by {@code label}. Labels are case sensitive.
*
* @throws IllegalArgumentException if either {@code type} or {@code label} have already been
* registered on this type adapter.
*/
@CanIgnoreReturnValue
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
if (type == null || label == null) {
throw new NullPointerException();
}
if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
throw new IllegalArgumentException("types and labels must be unique");
}
labelToSubtype.put(label, type);
subtypeToLabel.put(type, label);
return this;
}
/**
* Registers {@code type} identified by its {@link Class#getSimpleName simple name}. Labels are
* case sensitive.
*
* @throws IllegalArgumentException if either {@code type} or its simple name have already been
* registered on this type adapter.
*/
@CanIgnoreReturnValue
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
return registerSubtype(type, type.getSimpleName());
}
@Override
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
if (type == null) {
return null;
}
Class<?> rawType = type.getRawType();
boolean handle =
recognizeSubtypes ? baseType.isAssignableFrom(rawType) : baseType.equals(rawType);
if (!handle) {
return null;
}
TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<>();
Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = new LinkedHashMap<>();
for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
labelToDelegate.put(entry.getKey(), delegate);
subtypeToDelegate.put(entry.getValue(), delegate);
}
return new TypeAdapter<R>() {
@Override
public R read(JsonReader in) throws IOException {
JsonElement jsonElement = jsonElementAdapter.read(in);
JsonElement labelJsonElement;
if (maintainType) {
labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);
} else {
labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
}
if (labelJsonElement == null) {
throw new JsonParseException(
"cannot deserialize "
+ baseType
+ " because it does not define a field named "
+ typeFieldName);
}
String label = labelJsonElement.getAsString();
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
if (delegate == null) {
throw new JsonParseException(
"cannot deserialize "
+ baseType
+ " subtype named "
+ label
+ "; did you forget to register a subtype?");
}
return delegate.fromJsonTree(jsonElement);
}
@Override
public void write(JsonWriter out, R value) throws IOException {
Class<?> srcType = value.getClass();
String label = subtypeToLabel.get(srcType);
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
if (delegate == null) {
throw new JsonParseException(
"cannot serialize " + srcType.getName() + "; did you forget to register a subtype?");
}
JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
if (maintainType) {
jsonElementAdapter.write(out, jsonObject);
return;
}
JsonObject clone = new JsonObject();
if (jsonObject.has(typeFieldName)) {
throw new JsonParseException(
"cannot serialize "
+ srcType.getName()
+ " because it already defines a field named "
+ typeFieldName);
}
clone.add(typeFieldName, new JsonPrimitive(label));
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
jsonElementAdapter.write(out, clone);
}
}.nullSafe();
}
}
@@ -0,0 +1,17 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.worker1;
public enum ForkTags {
Error,
Warn,
Info,
Debug,
Done
}
@@ -0,0 +1,509 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.worker1;
import com.google.gson.Gson;
import sbt.testing.*;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.LinkedHashSet;
import java.util.concurrent.*;
public class ForkTestMain {
// serializables
// -----------------------------------------------------------------------------
public static final class SubclassFingerscan implements SubclassFingerprint, Serializable {
private final boolean isModule;
private final String superclassName;
private final boolean requireNoArgConstructor;
public SubclassFingerscan(final SubclassFingerprint print) {
isModule = print.isModule();
superclassName = print.superclassName();
requireNoArgConstructor = print.requireNoArgConstructor();
}
public boolean isModule() {
return isModule;
}
public String superclassName() {
return superclassName;
}
public boolean requireNoArgConstructor() {
return requireNoArgConstructor;
}
}
public static final class AnnotatedFingerscan implements AnnotatedFingerprint, Serializable {
private final boolean isModule;
private final String annotationName;
public AnnotatedFingerscan(final AnnotatedFingerprint print) {
isModule = print.isModule();
annotationName = print.annotationName();
}
public boolean isModule() {
return isModule;
}
public String annotationName() {
return annotationName;
}
}
public static final class ForkEvent implements Event, Serializable {
private final String fullyQualifiedName;
private final Fingerprint fingerprint;
private final Selector selector;
private final Status status;
private final OptionalThrowable throwable;
private final long duration;
ForkEvent(final Event e) {
this.fullyQualifiedName = e.fullyQualifiedName();
final Fingerprint rawFingerprint = e.fingerprint();
if (rawFingerprint instanceof SubclassFingerprint)
this.fingerprint = new SubclassFingerscan((SubclassFingerprint) rawFingerprint);
else this.fingerprint = new AnnotatedFingerscan((AnnotatedFingerprint) rawFingerprint);
this.selector = e.selector();
checkSerializableSelector(selector);
this.status = e.status();
final OptionalThrowable originalThrowable = e.throwable();
if (originalThrowable.isDefined())
this.throwable = new OptionalThrowable(new ForkError(originalThrowable.get()));
else this.throwable = originalThrowable;
this.duration = e.duration();
}
public String fullyQualifiedName() {
return fullyQualifiedName;
}
public Fingerprint fingerprint() {
return fingerprint;
}
public Selector selector() {
return selector;
}
public Status status() {
return status;
}
public OptionalThrowable throwable() {
return throwable;
}
public long duration() {
return duration;
}
private static void checkSerializableSelector(final Selector selector) {
if (!(selector instanceof Serializable)) {
throw new UnsupportedOperationException(
"Selector implementation must be Serializable, but "
+ selector.getClass().getName()
+ " is not.");
}
}
}
public static class ForkEventsInfo implements Serializable {
public long id;
public String group;
public ArrayList<ForkEvent> events;
public ForkEventsInfo(long id, String group, ArrayList<ForkEvent> events) {
this.id = id;
this.group = group;
this.events = events;
}
}
// -----------------------------------------------------------------------------
public static final class ForkError extends Exception {
private final String originalMessage;
private final String originalName;
private ForkError cause1;
ForkError(final Throwable t) {
originalMessage = t.getMessage();
originalName = t.getClass().getName();
setStackTrace(t.getStackTrace());
if (t.getCause() != null) cause1 = new ForkError(t.getCause());
}
public String getMessage() {
return originalName + ": " + originalMessage;
}
public Exception getCause() {
return cause1;
}
}
public static class ForkErrorInfo implements Serializable {
public final long id;
public final ForkError error;
public ForkErrorInfo(long id, ForkError error) {
this.id = id;
this.error = error;
}
}
// main
// ----------------------------------------------------------------------------------------------------------------
public static void main(long id, TestInfo info, PrintStream originalOut, ClassLoader classLoader)
throws Exception {
new Run(originalOut, id).run(info, classLoader);
}
// ----------------------------------------------------------------------------------------------------------------
public static final class Run {
final PrintStream originalOut;
final long id;
final Gson gson;
Run(PrintStream originalOut, long id) {
this.originalOut = originalOut;
this.id = id;
this.gson = WorkerMain.mkGson();
}
private void run(TestInfo info, ClassLoader classLoader) {
try {
runTests(info, classLoader);
} catch (final RunAborted e) {
internalError(e);
} catch (final Throwable t) {
try {
logError("Uncaught exception when running tests: " + t.toString());
writeError(new ForkError(t));
} catch (final Throwable t2) {
internalError(t2);
}
}
}
private boolean matches(final Fingerprint f1, final Fingerprint f2) {
if (f1 instanceof SubclassFingerprint && f2 instanceof SubclassFingerprint) {
final SubclassFingerprint sf1 = (SubclassFingerprint) f1;
final SubclassFingerprint sf2 = (SubclassFingerprint) f2;
return sf1.isModule() == sf2.isModule()
&& sf1.superclassName().equals(sf2.superclassName());
} else if (f1 instanceof AnnotatedFingerprint && f2 instanceof AnnotatedFingerprint) {
final AnnotatedFingerprint af1 = (AnnotatedFingerprint) f1;
final AnnotatedFingerprint af2 = (AnnotatedFingerprint) f2;
return af1.isModule() == af2.isModule()
&& af1.annotationName().equals(af2.annotationName());
}
return false;
}
class RunAborted extends RuntimeException {
RunAborted(final Exception e) {
super(e);
}
}
private void writeError(ForkError error) {
ForkErrorInfo info = new ForkErrorInfo(this.id, error);
String params = this.gson.toJson(info, ForkErrorInfo.class);
String notification =
String.format(
"{ \"jsonrpc\": \"2.0\", \"method\": \"forkError\", \"params\": %s }", params);
this.originalOut.println(notification);
this.originalOut.flush();
}
private void log(final String message, final ForkTags level) {
TestLogInfo info = new TestLogInfo(this.id, level, message);
String params = this.gson.toJson(info, TestLogInfo.class);
String notification =
String.format(
"{ \"jsonrpc\": \"2.0\", \"method\": \"testLog\", \"params\": %s }", params);
this.originalOut.println(notification);
this.originalOut.flush();
}
private void logDebug(final String message) {
log(message, ForkTags.Debug);
}
private void logInfo(final String message) {
log(message, ForkTags.Info);
}
private void logWarn(final String message) {
log(message, ForkTags.Warn);
}
private void logError(final String message) {
log(message, ForkTags.Error);
}
private Logger remoteLogger(final boolean ansiCodesSupported) {
return new Logger() {
public boolean ansiCodesSupported() {
return ansiCodesSupported;
}
public void error(final String s) {
logError(s);
}
public void warn(final String s) {
logWarn(s);
}
public void info(final String s) {
logInfo(s);
}
public void debug(final String s) {
logDebug(s);
}
public void trace(final Throwable t) {
writeError(new ForkError(t));
}
};
}
private void writeEvents(final TaskDef taskDef, final ForkEvent[] events) {
ForkEventsInfo info =
new ForkEventsInfo(
this.id,
taskDef.fullyQualifiedName(),
new ArrayList<ForkEvent>(Arrays.asList(events)));
String params = this.gson.toJson(info, ForkEventsInfo.class);
String notification =
String.format(
"{ \"jsonrpc\": \"2.0\", \"method\": \"testEvents\", \"params\": %s }", params);
this.originalOut.println(notification);
this.originalOut.flush();
}
private ExecutorService executorService(final boolean parallel) {
if (parallel) {
final int nbThreads = Runtime.getRuntime().availableProcessors();
logDebug("Create a test executor with a thread pool of " + nbThreads + " threads.");
// more options later...
// TODO we might want to configure the blocking queue with size #proc
return Executors.newFixedThreadPool(nbThreads);
} else {
logDebug("Create a single-thread test executor");
return Executors.newSingleThreadExecutor();
}
}
private void runTests(TestInfo info, ClassLoader classLoader) throws Exception {
final ExecutorService executor = executorService(info.parallel);
final TaskDef[] tests = info.taskDefs.toArray(new TaskDef[] {});
final int nFrameworks = info.testRunners.size();
final Logger[] loggers = {remoteLogger(info.ansiCodesSupported)};
for (TestInfo.TestRunner testRunner : info.testRunners) {
final String[] frameworkArgs = testRunner.mainRunnerArgs.toArray(new String[] {});
final String[] remoteFrameworkArgs =
testRunner.mainRunnerRemoteArgs.toArray(new String[] {});
Framework framework = null;
for (final String implClassName : testRunner.implClassNames) {
try {
final Object rawFramework =
classLoader.loadClass(implClassName).getDeclaredConstructor().newInstance();
if (rawFramework instanceof Framework) framework = (Framework) rawFramework;
else framework = new FrameworkWrapper((org.scalatools.testing.Framework) rawFramework);
break;
} catch (final ClassNotFoundException e) {
logError("Framework implementation '" + implClassName + "' not present.");
}
}
if (framework == null) continue;
final LinkedHashSet<TaskDef> filteredTests = new LinkedHashSet<>();
for (final Fingerprint testFingerprint : framework.fingerprints()) {
for (final TaskDef test : tests) {
// TODO: To pass in correct explicitlySpecified and selectors
if (matches(testFingerprint, test.fingerprint()))
filteredTests.add(
new TaskDef(
test.fullyQualifiedName(),
test.fingerprint(),
test.explicitlySpecified(),
test.selectors()));
}
}
final Runner runner = framework.runner(frameworkArgs, remoteFrameworkArgs, classLoader);
final Task[] tasks = runner.tasks(filteredTests.toArray(new TaskDef[filteredTests.size()]));
logDebug(
"Runner for "
+ framework.getClass().getName()
+ " produced "
+ tasks.length
+ " initial tasks for "
+ filteredTests.size()
+ " tests.");
Thread callDoneOnShutdown = new Thread(() -> runner.done());
Runtime.getRuntime().addShutdownHook(callDoneOnShutdown);
runTestTasks(executor, tasks, loggers);
runner.done();
Runtime.getRuntime().removeShutdownHook(callDoneOnShutdown);
}
}
private void runTestTasks(
final ExecutorService executor, final Task[] tasks, final Logger[] loggers) {
if (tasks.length > 0) {
final List<Future<Task[]>> futureNestedTasks = new ArrayList<>();
for (final Task task : tasks) {
futureNestedTasks.add(runTest(executor, task, loggers));
}
// Note: this could be optimized further, we could have a callback once a test finishes that
// executes immediately the nested tasks
// At the moment, I'm especially interested in JUnit, which doesn't have nested tasks.
final List<Task> nestedTasks = new ArrayList<>();
for (final Future<Task[]> futureNestedTask : futureNestedTasks) {
try {
nestedTasks.addAll(Arrays.asList(futureNestedTask.get()));
} catch (final Exception e) {
logError("Failed to execute task " + futureNestedTask);
}
}
runTestTasks(executor, nestedTasks.toArray(new Task[nestedTasks.size()]), loggers);
}
}
private Future<Task[]> runTest(
final ExecutorService executor, final Task task, final Logger[] loggers) {
return executor.submit(
() -> {
ForkEvent[] events;
Task[] nestedTasks;
final TaskDef taskDef = task.taskDef();
try {
final Collection<ForkEvent> eventList = new ConcurrentLinkedDeque<>();
final EventHandler handler =
new EventHandler() {
public void handle(final Event e) {
eventList.add(new ForkEvent(e));
}
};
logDebug(" Running " + taskDef);
nestedTasks = task.execute(handler, loggers);
if (nestedTasks.length > 0 || eventList.size() > 0)
logDebug(
" Produced "
+ nestedTasks.length
+ " nested tasks and "
+ eventList.size()
+ " events.");
events = eventList.toArray(new ForkEvent[eventList.size()]);
} catch (final Throwable t) {
nestedTasks = new Task[0];
events =
new ForkEvent[] {
testError(
taskDef,
"Uncaught exception when running "
+ taskDef.fullyQualifiedName()
+ ": "
+ t.toString(),
t)
};
}
writeEvents(taskDef, events);
return nestedTasks;
});
}
private void internalError(final Throwable t) {
System.err.println("Internal error when running tests: " + t.toString());
}
private ForkEvent testEvent(
final String fullyQualifiedName,
final Fingerprint fingerprint,
final Selector selector,
final Status r,
final ForkError err,
final long duration) {
final OptionalThrowable throwable;
if (err == null) throwable = new OptionalThrowable();
else throwable = new OptionalThrowable(err);
return new ForkEvent(
new Event() {
public String fullyQualifiedName() {
return fullyQualifiedName;
}
public Fingerprint fingerprint() {
return fingerprint;
}
public Selector selector() {
return selector;
}
public Status status() {
return r;
}
public OptionalThrowable throwable() {
return throwable;
}
public long duration() {
return duration;
}
});
}
private ForkEvent testError(final TaskDef taskDef, final String message, final Throwable t) {
logError(message);
final ForkError fe = new ForkError(t);
writeError(fe);
return testEvent(
taskDef.fullyQualifiedName(),
taskDef.fingerprint(),
new SuiteSelector(),
Status.Error,
fe,
0);
}
}
}
@@ -0,0 +1,328 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.worker1;
import sbt.testing.*;
/**
* Adapts the old {@link org.scalatools.testing.Framework} interface into the new {@link
* sbt.testing.Framework}
*/
public final class FrameworkWrapper implements Framework {
private final org.scalatools.testing.Framework oldFramework;
public FrameworkWrapper(final org.scalatools.testing.Framework oldFramework) {
this.oldFramework = oldFramework;
}
public String name() {
return oldFramework.name();
}
public Fingerprint[] fingerprints() {
final org.scalatools.testing.Fingerprint[] oldFingerprints = oldFramework.tests();
final int length = oldFingerprints.length;
final Fingerprint[] fingerprints = new Fingerprint[length];
for (int i = 0; i < length; i++) {
final org.scalatools.testing.Fingerprint oldFingerprint = oldFingerprints[i];
if (oldFingerprint instanceof org.scalatools.testing.TestFingerprint)
fingerprints[i] =
new SubclassFingerprintWrapper((org.scalatools.testing.TestFingerprint) oldFingerprint);
else if (oldFingerprint instanceof org.scalatools.testing.SubclassFingerprint)
fingerprints[i] =
new SubclassFingerprintWrapper(
(org.scalatools.testing.SubclassFingerprint) oldFingerprint);
else
fingerprints[i] =
new AnnotatedFingerprintWrapper(
(org.scalatools.testing.AnnotatedFingerprint) oldFingerprint);
}
return fingerprints;
}
public Runner runner(
final String[] args, final String[] remoteArgs, final ClassLoader testClassLoader) {
return new RunnerWrapper(oldFramework, testClassLoader, args);
}
}
final class SubclassFingerprintWrapper implements SubclassFingerprint {
private final String superclassName;
private final boolean isModule;
private final boolean requireNoArgConstructor;
SubclassFingerprintWrapper(final org.scalatools.testing.SubclassFingerprint oldFingerprint) {
superclassName = oldFingerprint.superClassName();
isModule = oldFingerprint.isModule();
requireNoArgConstructor =
false; // Old framework SubclassFingerprint does not require no arg constructor
}
public boolean isModule() {
return isModule;
}
public String superclassName() {
return superclassName;
}
public boolean requireNoArgConstructor() {
return requireNoArgConstructor;
}
}
final class AnnotatedFingerprintWrapper implements AnnotatedFingerprint {
private final String annotationName;
private final boolean isModule;
AnnotatedFingerprintWrapper(final org.scalatools.testing.AnnotatedFingerprint oldFingerprint) {
annotationName = oldFingerprint.annotationName();
isModule = oldFingerprint.isModule();
}
public boolean isModule() {
return isModule;
}
public String annotationName() {
return annotationName;
}
}
final class EventHandlerWrapper implements org.scalatools.testing.EventHandler {
private final EventHandler newEventHandler;
private final String fullyQualifiedName;
private final Fingerprint fingerprint;
EventHandlerWrapper(
final EventHandler newEventHandler,
final String fullyQualifiedName,
final Fingerprint fingerprint) {
this.newEventHandler = newEventHandler;
this.fullyQualifiedName = fullyQualifiedName;
this.fingerprint = fingerprint;
}
public void handle(final org.scalatools.testing.Event oldEvent) {
newEventHandler.handle(new EventWrapper(oldEvent, fullyQualifiedName, fingerprint));
}
}
final class EventWrapper implements Event {
private final org.scalatools.testing.Event oldEvent;
private final String className;
private final Fingerprint fingerprint;
private final OptionalThrowable throwable;
EventWrapper(
final org.scalatools.testing.Event oldEvent,
final String className,
final Fingerprint fingerprint) {
this.oldEvent = oldEvent;
this.className = className;
this.fingerprint = fingerprint;
final Throwable oldThrowable = oldEvent.error();
if (oldThrowable == null) throwable = new OptionalThrowable();
else throwable = new OptionalThrowable(oldThrowable);
}
public String fullyQualifiedName() {
return className;
}
public Fingerprint fingerprint() {
return fingerprint;
}
public Selector selector() {
return new TestSelector(oldEvent.testName());
}
public Status status() {
switch (oldEvent.result()) {
case Success:
return Status.Success;
case Error:
return Status.Error;
case Failure:
return Status.Failure;
case Skipped:
return Status.Skipped;
default:
throw new IllegalStateException("Invalid status.");
}
}
public OptionalThrowable throwable() {
return throwable;
}
public long duration() {
return -1; // Just return -1 as old event does not have duration.
}
}
final class RunnerWrapper implements Runner {
private final org.scalatools.testing.Framework oldFramework;
private final ClassLoader testClassLoader;
private final String[] args;
RunnerWrapper(
final org.scalatools.testing.Framework oldFramework,
final ClassLoader testClassLoader,
final String[] args) {
this.oldFramework = oldFramework;
this.testClassLoader = testClassLoader;
this.args = args;
}
public Task[] tasks(final TaskDef[] taskDefs) {
final int length = taskDefs.length;
final Task[] tasks = new Task[length];
for (int i = 0; i < length; i++) {
final TaskDef taskDef = taskDefs[i];
tasks[i] = createTask(taskDef);
}
return tasks;
}
private Task createTask(final TaskDef taskDef) {
return new Task() {
public String[] tags() {
return new String[0]; // Old framework does not support tags
}
private org.scalatools.testing.Logger createOldLogger(final Logger logger) {
return new org.scalatools.testing.Logger() {
public boolean ansiCodesSupported() {
return logger.ansiCodesSupported();
}
public void error(final String msg) {
logger.error(msg);
}
public void warn(final String msg) {
logger.warn(msg);
}
public void info(final String msg) {
logger.info(msg);
}
public void debug(final String msg) {
logger.debug(msg);
}
public void trace(final Throwable t) {
logger.trace(t);
}
};
}
private void runRunner(
final org.scalatools.testing.Runner runner,
final Fingerprint fingerprint,
final EventHandler eventHandler) {
// Old runner only support subclass fingerprint.
final SubclassFingerprint subclassFingerprint = (SubclassFingerprint) fingerprint;
final org.scalatools.testing.TestFingerprint oldFingerprint =
new org.scalatools.testing.TestFingerprint() {
public boolean isModule() {
return subclassFingerprint.isModule();
}
public String superClassName() {
return subclassFingerprint.superclassName();
}
};
final String name = taskDef.fullyQualifiedName();
runner.run(
name,
oldFingerprint,
new EventHandlerWrapper(eventHandler, name, subclassFingerprint),
args);
}
private void runRunner2(
final org.scalatools.testing.Runner2 runner,
final Fingerprint fingerprint,
final EventHandler eventHandler) {
final org.scalatools.testing.Fingerprint oldFingerprint;
if (fingerprint instanceof SubclassFingerprint) {
final SubclassFingerprint subclassFingerprint = (SubclassFingerprint) fingerprint;
oldFingerprint =
new org.scalatools.testing.SubclassFingerprint() {
public boolean isModule() {
return subclassFingerprint.isModule();
}
public String superClassName() {
return subclassFingerprint.superclassName();
}
};
} else {
final AnnotatedFingerprint annotatedFingerprint = (AnnotatedFingerprint) fingerprint;
oldFingerprint =
new org.scalatools.testing.AnnotatedFingerprint() {
public boolean isModule() {
return annotatedFingerprint.isModule();
}
public String annotationName() {
return annotatedFingerprint.annotationName();
}
};
}
final String name = taskDef.fullyQualifiedName();
runner.run(
name, oldFingerprint, new EventHandlerWrapper(eventHandler, name, fingerprint), args);
}
public Task[] execute(final EventHandler eventHandler, final Logger[] loggers) {
final int length = loggers.length;
final org.scalatools.testing.Logger[] oldLoggers =
new org.scalatools.testing.Logger[length];
for (int i = 0; i < length; i++) {
oldLoggers[i] = createOldLogger(loggers[i]);
}
final org.scalatools.testing.Runner runner =
oldFramework.testRunner(testClassLoader, oldLoggers);
final Fingerprint fingerprint = taskDef.fingerprint();
if (runner instanceof org.scalatools.testing.Runner2) {
runRunner2((org.scalatools.testing.Runner2) runner, fingerprint, eventHandler);
} else {
runRunner(runner, fingerprint, eventHandler);
}
return new Task[0];
}
public TaskDef taskDef() {
return taskDef;
}
};
}
public String done() {
return "";
}
public String[] args() {
return args;
}
public String[] remoteArgs() {
return new String[0]; // Old framework does not support remoteArgs
}
}
@@ -8,10 +8,11 @@
package sbt.internal.worker1;
import java.io.Serializable;
import java.util.ArrayList;
public class RunInfo {
public class JvmRunInfo {
public class RunInfo implements Serializable {
public static class JvmRunInfo implements Serializable {
public ArrayList<String> args;
public ArrayList<FilePath> classpath;
public String mainClass;
@@ -29,7 +30,7 @@ public class RunInfo {
}
}
public class NativeRunInfo {}
public static class NativeRunInfo implements Serializable {}
public boolean jvm;
public JvmRunInfo jvmRunInfo;
@@ -0,0 +1,55 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.worker1;
import java.io.Serializable;
import java.util.ArrayList;
import sbt.testing.TaskDef;
public class TestInfo implements Serializable {
public static class TestRunner implements Serializable {
public final ArrayList<String> implClassNames;
public final ArrayList<String> mainRunnerArgs;
public final ArrayList<String> mainRunnerRemoteArgs;
public TestRunner(
ArrayList<String> implClassNames,
ArrayList<String> mainRunnerArgs,
ArrayList<String> mainRunnerRemoteArgs) {
this.implClassNames = implClassNames;
this.mainRunnerArgs = mainRunnerArgs;
this.mainRunnerRemoteArgs = mainRunnerRemoteArgs;
}
}
public final boolean jvm;
public final RunInfo.JvmRunInfo jvmRunInfo;
public final RunInfo.NativeRunInfo nativeRunInfo;
public final boolean ansiCodesSupported;
public final boolean parallel;
public final ArrayList<TaskDef> taskDefs;
public final ArrayList<TestRunner> testRunners;
public TestInfo(
boolean jvm,
RunInfo.JvmRunInfo jvmRunInfo,
RunInfo.NativeRunInfo nativeRunInfo,
boolean ansiCodesSupported,
boolean parallel,
ArrayList<TaskDef> taskDefs,
ArrayList<TestRunner> testRunners) {
this.jvm = jvm;
this.jvmRunInfo = jvmRunInfo;
this.nativeRunInfo = nativeRunInfo;
this.ansiCodesSupported = ansiCodesSupported;
this.parallel = parallel;
this.taskDefs = taskDefs;
this.testRunners = testRunners;
}
}
@@ -0,0 +1,25 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.worker1;
import java.io.Serializable;
import java.util.ArrayList;
import sbt.testing.TaskDef;
public class TestLogInfo implements Serializable {
public final long id;
public final ForkTags tag;
public final String message;
public TestLogInfo(long id, ForkTags tag, String message) {
this.id = id;
this.tag = tag;
this.message = message;
}
}
@@ -0,0 +1,13 @@
package sbt.internal.worker1;
import java.io.Serializable;
public final class WorkerError implements Serializable {
public final int code;
public final String message;
public WorkerError(int code, String message) {
this.code = code;
this.message = message;
}
}
@@ -9,11 +9,14 @@
package sbt.internal.worker1;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Method;
@@ -22,15 +25,41 @@ import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Scanner;
import sbt.testing.*;
/**
* WorkerMain that communicates via the stdin and stdout using JSON-RPC
* (https://www.jsonrpc.org/specification).
*/
public final class WorkerMain {
private PrintStream originalOut;
private InputStream originalIn;
private Scanner inScanner;
public static Gson mkGson() {
RuntimeTypeAdapterFactory<Fingerprint> fingerprintFac =
RuntimeTypeAdapterFactory.of(Fingerprint.class, "type");
fingerprintFac.registerSubtype(ForkTestMain.SubclassFingerscan.class, "SubclassFingerscan");
fingerprintFac.registerSubtype(ForkTestMain.AnnotatedFingerscan.class, "AnnotatedFingerscan");
RuntimeTypeAdapterFactory<Selector> selectorFac =
RuntimeTypeAdapterFactory.of(Selector.class, "type");
selectorFac.registerSubtype(SuiteSelector.class, "SuiteSelector");
selectorFac.registerSubtype(TestSelector.class, "TestSelector");
selectorFac.registerSubtype(NestedSuiteSelector.class, "NestedSuiteSelector");
selectorFac.registerSubtype(NestedTestSelector.class, "NestedTestSelector");
selectorFac.registerSubtype(TestWildcardSelector.class, "TestWildcardSelector");
return new GsonBuilder()
.registerTypeAdapterFactory(fingerprintFac)
.registerTypeAdapterFactory(selectorFac)
.create();
}
public static void main(final String[] args) throws Exception {
try {
if (args.length == 0) {
WorkerMain app = new WorkerMain();
app.consoleWork();
System.exit(0);
} else {
System.err.println("missing args");
System.exit(1);
@@ -45,31 +74,49 @@ public final class WorkerMain {
this.originalOut = System.out;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
System.setOut(new PrintStream(baos));
this.originalIn = System.in;
this.inScanner = new Scanner(this.originalIn);
}
void consoleWork() throws Exception {
Scanner input = new Scanner(System.in);
if (input.hasNextLine()) {
String line = input.nextLine();
if (this.inScanner.hasNextLine()) {
String line = this.inScanner.nextLine();
process(line);
}
}
void process(String json) throws Exception {
/** This processes single request of supposed JSON line. */
void process(String json) {
JsonElement elem = JsonParser.parseString(json);
JsonObject o = elem.getAsJsonObject();
if (!o.has("jsonrpc")) {
throw new RuntimeException("missing jsonprc element");
return;
}
Gson g = WorkerMain.mkGson();
long id = o.getAsJsonPrimitive("id").getAsLong();
String method = o.getAsJsonPrimitive("method").getAsString();
JsonObject params = o.getAsJsonObject("params");
switch (method) {
case "run":
Gson g = new Gson();
RunInfo info = g.fromJson(params, RunInfo.class);
run(info);
break;
try {
String method = o.getAsJsonPrimitive("method").getAsString();
JsonObject params = o.getAsJsonObject("params");
switch (method) {
case "run":
RunInfo info = g.fromJson(params, RunInfo.class);
run(info);
break;
case "test":
TestInfo testInfo = g.fromJson(params, TestInfo.class);
test(id, testInfo);
break;
}
String response = String.format("{ \"jsonrpc\": \"2.0\", \"result\": 0, \"id\": %d }", id);
this.originalOut.println(response);
this.originalOut.flush();
} catch (Throwable e) {
WorkerError err = new WorkerError(1, e.getMessage());
String errMessage = g.toJson(err, err.getClass());
String errJson =
String.format("{ \"jsonrpc\": \"2.0\", \"error\": %s, \"id\": %d }", errMessage, id);
this.originalOut.println(errJson);
this.originalOut.flush();
}
}
@@ -79,20 +126,7 @@ public final class WorkerMain {
throw new RuntimeException("missing jvmRunInfo element");
}
RunInfo.JvmRunInfo jvmRunInfo = info.jvmRunInfo;
URL[] urls =
jvmRunInfo
.classpath
.stream()
.map(
filePath -> {
try {
return filePath.path.toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
})
.toArray(URL[]::new);
URLClassLoader cl = new URLClassLoader(urls, ClassLoader.getSystemClassLoader());
URLClassLoader cl = createClassLoader(jvmRunInfo, ClassLoader.getSystemClassLoader());
try {
Class<?> mainClass = cl.loadClass(jvmRunInfo.mainClass);
Method mainMethod = mainClass.getMethod("main", String[].class);
@@ -105,4 +139,37 @@ public final class WorkerMain {
throw new RuntimeException("only jvm is supported");
}
}
void test(long id, TestInfo info) throws Exception {
if (info.jvm) {
RunInfo.JvmRunInfo jvmRunInfo = info.jvmRunInfo;
ClassLoader parent = new ForkTestMain().getClass().getClassLoader();
ClassLoader cl = createClassLoader(jvmRunInfo, parent);
try {
ForkTestMain.main(id, info, this.originalOut, cl);
} finally {
if (cl instanceof URLClassLoader) {
((URLClassLoader) cl).close();
}
}
} else {
throw new RuntimeException("only jvm is supported");
}
}
private URLClassLoader createClassLoader(RunInfo.JvmRunInfo info, ClassLoader parent) {
URL[] urls =
info.classpath
.stream()
.map(
filePath -> {
try {
return filePath.path.toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
})
.toArray(URL[]::new);
return new URLClassLoader(urls, parent);
}
}