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 Engine.Result call(final IFn f, final Class 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 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 F function(final String name, final Class type) { return new F<>(name, type); } /** * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) */ public record F(String name, Class type, IFn f) { public F(final String name, final Class type) { this(name.substring(name.indexOf("/") + 1), type, var(name)); } public Engine.Result 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 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); } }