Files
paradigms/clojure/cljtest/ClojureScript.java
2026-04-13 20:12:01 +03:00

125 lines
4.8 KiB
Java

package cljtest;
import clojure.java.api.Clojure;
import clojure.lang.ArraySeq;
import clojure.lang.IFn;
import common.Engine;
import common.EngineException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* Utility class for Clojure tests.
*
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
*/
public final class ClojureScript {
public static final IFn LOAD_STRING = var("clojure.core/load-string");
public static final IFn LOAD_FILE = asUser("load-file");
public static final IFn LOAD_STRING_IN = asUser("load-string");
public static Path CLOJURE_ROOT = Path.of(".");
private ClojureScript() {
}
private static IFn asUser(final String function) {
return (IFn) LOAD_STRING.invoke(
"(fn " + function + "-in [arg]" +
" (binding [*ns* *ns*]" +
" (in-ns 'user)" +
" (" + function + " arg)))"
);
}
public static void loadScript(final String script) {
final String escaped = CLOJURE_ROOT.toString().replace("\\", "\\\\");
LOAD_STRING_IN.invoke("(defn load-file [file] (clojure.core/load-file (str \"" + escaped + "/\" file)))");
LOAD_FILE.invoke(CLOJURE_ROOT.resolve(script).toString());
}
static <T> Engine.Result<T> call(final IFn f, final Class<T> type, final String context, final Object[] args) {
final Object result;
try {
result = callUnsafe(f, args);
} catch (final AssertionError e) {
throw e;
} catch (final Throwable e) {
throw new EngineException("No error expected in " + context, e);
}
if (result == null) {
throw new EngineException("Expected %s, found null\n%s".formatted(type.getSimpleName(), context), new NullPointerException());
}
if (!type.isInstance(result)) {
throw new EngineException("Expected %s, found %s (%s)\n%s".formatted(type.getSimpleName(), result, result.getClass().getSimpleName(), context), null);
}
return new Engine.Result<>(context, type.cast(result));
}
private static Object callUnsafe(final IFn f, final Object[] args) {
return switch (args.length) {
case 0 -> f.invoke();
case 1 -> f.invoke(args[0]);
case 2 -> f.invoke(args[0], args[1]);
case 3 -> f.invoke(args[0], args[1], args[2]);
case 4 -> f.invoke(args[0], args[1], args[2], args[3]);
case 5 -> f.invoke(args[0], args[1], args[2], args[3], args[4]);
case 6 -> f.invoke(args[0], args[1], args[2], args[3], args[4], args[5]);
case 7 -> f.invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
case 8 -> f.invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
case 9 -> f.invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]);
default -> f.applyTo(ArraySeq.create(args));
};
}
public static Engine.Result<Throwable> expectException(final IFn f, final Object[] args, final String context) {
try {
callUnsafe(f, args);
} catch (final Throwable e) {
return new Engine.Result<>(context, e);
}
assert false : "Exception expected in " + context;
return null;
}
public static <T> F<T> function(final String name, final Class<T> type) {
return new F<>(name, type);
}
/**
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
*/
public record F<T>(String name, Class<T> type, IFn f) {
public F(final String name, final Class<T> type) {
this(name.substring(name.indexOf("/") + 1), type, var(name));
}
public Engine.Result<T> call(final Engine.Result<?>... args) {
return ClojureScript.call(
f,
type,
callToString(args),
Arrays.stream(args).map(Engine.Result::value).toArray()
);
}
public String callToString(final Engine.Result<?>[] args) {
return "(" + name + Arrays.stream(args).map(arg -> " " + arg.context()).collect(Collectors.joining()) + ")";
}
public Engine.Result<Throwable> expectException(final Engine.Result<?>... args) {
return ClojureScript.expectException(
f,
Arrays.stream(args).map(Engine.Result::value).toArray(),
"(" + name + " " + Arrays.stream(args).map(Engine.Result::context).collect(Collectors.joining(" ")) + ")"
);
}
}
public static IFn var(final String name) {
final String qualifiedName = (name.contains("/") ? "" : "user/") + name;
return Clojure.var(qualifiedName);
}
}