Files
prog-intro/java/base/Runner.java
2026-04-13 10:38:59 +03:00

282 lines
11 KiB
Java

package base;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
*/
@SuppressWarnings("unused")
@FunctionalInterface
public interface Runner {
List<String> run(final TestCounter counter, final List<String> input);
default List<String> run(final TestCounter counter, final String... input) {
return run(counter, List.of(input));
}
default void testEquals(
final TestCounter counter,
final List<String> input,
final List<String> expected
) {
counter.test(() -> {
final List<String> actual = run(counter, input);
for (int i = 0; i < Math.min(expected.size(), actual.size()); i++) {
final String exp = expected.get(i);
final String act = actual.get(i);
if (!exp.equalsIgnoreCase(act)) {
Asserts.assertEquals("Line " + (i + 1), exp, act);
return;
}
}
Asserts.assertEquals(
"Number of lines",
expected.size(),
actual.size()
);
});
}
static Packages packages(final String... packages) {
return new Packages(List.of(packages));
}
@FunctionalInterface
interface CommentRunner {
List<String> run(
String comment,
TestCounter counter,
List<String> input
);
}
final class Packages {
private final List<String> packages;
private Packages(final List<String> packages) {
this.packages = packages;
}
public Runner std(final String className) {
final CommentRunner main = main(className);
return (counter, input) ->
counter.call("io", () -> {
final ByteArrayOutputStream baos =
new ByteArrayOutputStream();
try (
final PrintWriter writer = new PrintWriter(
baos,
false,
StandardCharsets.UTF_8
)
) {
input.forEach(writer::println);
}
final InputStream oldIn = System.in;
try {
System.setIn(
new ByteArrayInputStream(baos.toByteArray())
);
return main.run(
String.format("[%d input lines]", input.size()),
counter,
List.of()
);
} finally {
System.setIn(oldIn);
}
});
}
@SuppressWarnings("ConfusingMainMethod")
public CommentRunner main(final String className) {
final Method method = findMain(className);
return (comment, counter, input) -> {
counter.format(
"Running test %02d: java %s %s%n",
counter.getTestNo(),
method.getDeclaringClass().getName(),
comment
);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
@SuppressWarnings("UseOfSystemOutOrSystemErr")
final PrintStream oldOut = System.out;
try {
System.setOut(
new PrintStream(out, false, StandardCharsets.UTF_8)
);
method.invoke(
null,
new Object[] { input.toArray(String[]::new) }
);
final BufferedReader reader = new BufferedReader(
new InputStreamReader(
new ByteArrayInputStream(out.toByteArray()),
StandardCharsets.UTF_8
)
);
final List<String> result = new ArrayList<>();
while (true) {
final String line = reader.readLine();
if (line == null) {
if (result.isEmpty()) {
result.add("");
}
return result;
}
result.add(line.trim());
}
} catch (final InvocationTargetException e) {
final Throwable cause = e.getCause();
throw Asserts.error(
"main thrown exception %s: %s",
cause.getClass().getSimpleName(),
cause
);
} catch (final Exception e) {
throw Asserts.error(
"Cannot invoke main: %s: %s",
e.getClass().getSimpleName(),
e.getMessage()
);
} finally {
System.setOut(oldOut);
}
};
}
private Method findMain(final String className) {
try {
final URL url = new File(".").toURI().toURL();
final List<String> candidates = packages
.stream()
.flatMap(pkg -> {
final String prefix = pkg.isEmpty() ? pkg : pkg + ".";
return Stream.of(
prefix + className,
prefix + "$" + className
);
})
.toList();
//noinspection ClassLoaderInstantiation,resource,IOResourceOpenedButNotSafelyClosed
final URLClassLoader classLoader = new URLClassLoader(
new URL[] { url }
);
for (final String candidate : candidates) {
try {
final Class<?> loaded = classLoader.loadClass(
candidate
);
if (!Modifier.isPublic(loaded.getModifiers())) {
throw Asserts.error(
"Class %s is not public",
candidate
);
}
final Method main = loaded.getMethod(
"main",
String[].class
);
if (
!Modifier.isPublic(main.getModifiers()) ||
!Modifier.isStatic(main.getModifiers())
) {
throw Asserts.error(
"Method main of class %s should be public and static",
candidate
);
}
return main;
} catch (final ClassNotFoundException e) {
// Ignore
} catch (final NoSuchMethodException e) {
throw Asserts.error(
"Could not find method main(String[]) in class %s",
candidate,
e
);
}
}
throw Asserts.error(
"Could not find neither of classes %s",
candidates
);
} catch (final MalformedURLException e) {
throw Asserts.error("Invalid path", e);
}
}
private static String getClassName(
final String pkg,
final String className
) {
return pkg.isEmpty() ? className : pkg + "." + className;
}
public Runner args(final String className) {
final CommentRunner main = main(className);
// final AtomicReference<String> prev = new AtomicReference<>("");
return (counter, input) -> {
final int total =
input.stream().mapToInt(String::length).sum() +
input.size() * 3;
final String comment =
total <= 300
? input
.stream()
.collect(Collectors.joining("\" \"", "\"", "\""))
: input.size() <= 100
? String.format(
"[%d arguments, sizes: %s]",
input.size(),
input
.stream()
.mapToInt(String::length)
.mapToObj(String::valueOf)
.collect(Collectors.joining(", "))
)
: String.format(
"[%d arguments, total size: %d]",
input.size(),
total
);
// assert comment.length() <= 5 || !prev.get().equals(comment) : "Duplicate tests " + comment;
// prev.set(comment);
return main.run(comment, counter, input);
};
}
public Runner files(final String className) {
final Runner args = args(className);
return (counter, input) ->
counter.call("io", () -> {
final Path inf = counter.getFile("in");
final Path ouf = counter.getFile("out");
Files.write(inf, input);
args.run(counter, List.of(inf.toString(), ouf.toString()));
final List<String> output = Files.readAllLines(ouf);
Files.delete(inf);
Files.delete(ouf);
return output;
});
}
}
}