mirror of
https://github.com/sbt/sbt.git
synced 2026-09-03 08:34:34 +02:00
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:
@@ -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>
|
||||
* {
|
||||
* @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>
|
||||
* {
|
||||
* @code
|
||||
* RuntimeTypeAdapterFactory<Shape> 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>
|
||||
* {
|
||||
* @code
|
||||
* Gson gson = new GsonBuilder().registerTypeAdapterFactory(shapeAdapterFactory).create();
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* Like {@code GsonBuilder}, this API supports chaining:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* @code
|
||||
* RuntimeTypeAdapterFactory<Shape> 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>
|
||||
* {
|
||||
* @code
|
||||
* Diamond diamond = new Diamond();
|
||||
* String json = gson.toJson(diamond, Shape.class);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* And then:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user