Add test
This commit is contained in:
42
Build.java
42
Build.java
@@ -10,6 +10,20 @@ import java.util.stream.Collectors;
|
||||
public class Build {
|
||||
static final String program = "jtags";
|
||||
|
||||
static void printUsage() {
|
||||
System.err.println(
|
||||
"""
|
||||
Usage: java Build <subcommand> [args...]
|
||||
Subcommands:
|
||||
build Build %1$s
|
||||
run [args...] Run %1$s with arguments
|
||||
package Package %1$s to a JAR file
|
||||
[file] Output JAR filename [Default: Jtags.jar]
|
||||
test Run %1$s tests
|
||||
"""
|
||||
.formatted(program));
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
rebuildSelf(Build.class, args);
|
||||
|
||||
@@ -47,6 +61,11 @@ public class Build {
|
||||
packageJar(Optional.ofNullable(arguments.poll()));
|
||||
break;
|
||||
|
||||
case "test":
|
||||
buildJtags(mainClass, sourcePaths, classPaths);
|
||||
runTests();
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.severe("Unknown subcommand: " + subcommand);
|
||||
printUsage();
|
||||
@@ -77,16 +96,17 @@ public class Build {
|
||||
".");
|
||||
}
|
||||
|
||||
static void printUsage() {
|
||||
System.err.println(
|
||||
"""
|
||||
Usage: java Build <subcommand> [args...]
|
||||
Subcommands:
|
||||
build Build %1$s
|
||||
run [args...] Run %1$s with arguments
|
||||
package Package %1$s to a JAR file
|
||||
[file] Output JAR filename [Default: Jtags.jar]
|
||||
"""
|
||||
.formatted(program));
|
||||
static void runTests() {
|
||||
String mainClass = "TestJtags";
|
||||
String[] sourcePaths = glob("src/test/java/**.java");
|
||||
|
||||
if (classNeedsRebuild(mainClass, sourcePaths)) {
|
||||
logger.info("Compiling %s...".formatted(mainClass));
|
||||
if (!compileJava(sourcePaths)) {
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
System.exit(runJava(new String[0], new String[] {"-enableassertions"}, mainClass));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.source.util.JavacTask;
|
||||
import com.sun.source.util.Trees;
|
||||
import java.io.IOException;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.AbstractQueue;
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.JavaFileObject;
|
||||
import javax.tools.StandardJavaFileManager;
|
||||
@@ -17,7 +17,7 @@ public class TagCollector {
|
||||
|
||||
static JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
|
||||
public static PriorityQueue<Tag> collectTags(Options options) {
|
||||
public static AbstractQueue<Tag> collectTags(Options options) {
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
|
||||
|
||||
Iterable<? extends JavaFileObject> compilationUnits =
|
||||
@@ -26,11 +26,14 @@ public class TagCollector {
|
||||
JavacTask task =
|
||||
(JavacTask) compiler.getTask(null, fileManager, null, null, null, compilationUnits);
|
||||
Iterable<? extends CompilationUnitTree> trees = task.parse();
|
||||
TreeVisitorContext context = new TreeVisitorContext(Trees.instance(task));
|
||||
|
||||
TreeVisitor treeVisitor = new TreeVisitor(options);
|
||||
TreeVisitorContext context = new TreeVisitorContext(Trees.instance(task));
|
||||
|
||||
for (CompilationUnitTree compilationUnitTree : trees) {
|
||||
treeVisitor.scan(compilationUnitTree, context);
|
||||
}
|
||||
|
||||
return treeVisitor.tags;
|
||||
|
||||
} catch (IOException ex) {
|
||||
|
||||
@@ -9,20 +9,20 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.AbstractQueue;
|
||||
import xyz.naofal.jtags.Jtags.Options;
|
||||
|
||||
public record TagsWriter(Options options) {
|
||||
private static final int MAX_PATTERN_LENGTH = 96;
|
||||
|
||||
public boolean writeTagsFile(PriorityQueue<Tag> tags) {
|
||||
public boolean writeTagsFile(AbstractQueue<Tag> tags) {
|
||||
|
||||
try (var outputStream = new FileOutputStream(options().output.toFile());
|
||||
var writer = new OutputStreamWriter(outputStream); ) {
|
||||
|
||||
writer.write(
|
||||
"""
|
||||
!_TAG_FILE_ENCODING\tutf-8\t
|
||||
!_TAG_FILE_ENCODING\tutf-8
|
||||
!_TAG_FILE_SORTED\t2\t/0=unsorted, 1=sorted, 2=foldcase/
|
||||
""");
|
||||
|
||||
@@ -48,7 +48,7 @@ public record TagsWriter(Options options) {
|
||||
? tag.location().toString()
|
||||
: options.output.getParent().toAbsolutePath().relativize(tag.location()).toString());
|
||||
writer.write("\t/^");
|
||||
writer.write(tag.line().substring(0, Math.min(tag.line().length(), MAX_PATTERN_LENGTH)));
|
||||
writer.write(tag.line());
|
||||
writer.write("$/;\"\t");
|
||||
writer.write(
|
||||
switch (tag.kind()) {
|
||||
|
||||
@@ -9,16 +9,17 @@ import com.sun.source.tree.PackageTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import com.sun.source.util.TreePathScanner;
|
||||
import java.util.AbstractQueue;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import xyz.naofal.jtags.Jtags.Options;
|
||||
|
||||
public class TreeVisitor extends TreePathScanner<Void, TreeVisitorContext> {
|
||||
public final Options options;
|
||||
public final PriorityQueue<Tag> tags = new PriorityQueue<>();
|
||||
public final AbstractQueue<Tag> tags = new PriorityBlockingQueue<>();
|
||||
|
||||
public TreeVisitor(Options options) {
|
||||
this.options = options;
|
||||
@@ -104,7 +105,7 @@ public class TreeVisitor extends TreePathScanner<Void, TreeVisitorContext> {
|
||||
|
||||
@Override
|
||||
public Void visitMethod(MethodTree node, TreeVisitorContext p) {
|
||||
ClassTree enclosingType = (ClassTree)getCurrentPath().getParentPath().getLeaf();
|
||||
ClassTree enclosingType = (ClassTree) getCurrentPath().getParentPath().getLeaf();
|
||||
|
||||
if (options.excludeNonPublic
|
||||
&& !node.getModifiers().getFlags().contains(Modifier.PUBLIC)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package xyz.naofal.jtags.example;
|
||||
|
||||
@interface T {
|
||||
static int t = 1;
|
||||
}
|
||||
|
||||
public class Example {
|
||||
|
||||
public static void lorem(
|
||||
String string1,
|
||||
String string2,
|
||||
String string3,
|
||||
String string4,
|
||||
String string5,
|
||||
String string6) {
|
||||
|
||||
Object o =
|
||||
new Object() {
|
||||
void f() {}
|
||||
;
|
||||
};
|
||||
|
||||
class T {
|
||||
void f() {}
|
||||
|
||||
private void fp() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/test/java/TestJtags.java
Normal file
34
src/test/java/TestJtags.java
Normal file
@@ -0,0 +1,34 @@
|
||||
import static notest.Test.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import notest.Test;
|
||||
|
||||
class TestJtags {
|
||||
public static final String Jtags = "xyz.naofal.jtags.Jtags";
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.exit(Util.runTests(TestJtags.class) ? 0 : 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
static void basicExample() throws IOException {
|
||||
Path file = Files.createTempFile("jtags", null);
|
||||
Util.runJava(
|
||||
new String[0],
|
||||
new String[] {"-Dlogger.level=CONFIG"},
|
||||
Jtags,
|
||||
"-o",
|
||||
file.toString(),
|
||||
"src/test/java/examples/BasicExample.java");
|
||||
assert Files.readString(file)
|
||||
.trim()
|
||||
.equals(
|
||||
"""
|
||||
ABC
|
||||
"""
|
||||
.trim())
|
||||
: "Unexpected Result:\n" + Files.readString(file);
|
||||
}
|
||||
}
|
||||
28
src/test/java/examples/BasicExample.java
Normal file
28
src/test/java/examples/BasicExample.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package examples;
|
||||
|
||||
public class BasicExample {
|
||||
|
||||
private boolean method1() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void method2(
|
||||
String string1,
|
||||
String string2,
|
||||
String string3,
|
||||
String string4,
|
||||
String string5,
|
||||
String string6) {
|
||||
|
||||
Object innerObject =
|
||||
new Object() {
|
||||
void method2() {}
|
||||
};
|
||||
|
||||
class InnerClass {
|
||||
void method3() {}
|
||||
|
||||
private void method4() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
214
src/test/java/notest/Test.java
Normal file
214
src/test/java/notest/Test.java
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* NoTest - Simple testing utilities
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2025 Naofal Helal
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
package notest;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.ProcessBuilder.Redirect;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.IntSummaryStatistics;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/** Marks a method as a test method */
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Test {
|
||||
|
||||
/** Utility methods for performing tests */
|
||||
public static class Util {
|
||||
public static Path javaHome = Paths.get(System.getProperty("java.home"));
|
||||
public static String javaClassPath = System.getProperty("java.class.path");
|
||||
public static String javaBin = javaHome.resolve("bin", "java").toString();
|
||||
|
||||
/** Runs all tests in {@code testClass} */
|
||||
public static boolean runTests(Class<?> testClass) {
|
||||
IntSummaryStatistics stats =
|
||||
Arrays.stream(testClass.getDeclaredMethods())
|
||||
.filter(it -> it.isAnnotationPresent(Test.class))
|
||||
.map(
|
||||
it -> {
|
||||
try {
|
||||
System.err.println(
|
||||
"╭" + center(" Test " + it.getName() + " ", 48, '─') + "╮");
|
||||
it.setAccessible(true);
|
||||
it.invoke(null);
|
||||
System.err.println(
|
||||
"╰" + center(" " + it.getName() + ": SUCCESS ", 48, '─') + "╯");
|
||||
System.err.println();
|
||||
return 1;
|
||||
} catch (IllegalAccessException ex) {
|
||||
System.err.println(
|
||||
"╰"
|
||||
+ center(" ERROR: could not run " + it.getName() + " ", 48, '─')
|
||||
+ "╯");
|
||||
System.err.println(ex.toString());
|
||||
System.err.println();
|
||||
return 0;
|
||||
} catch (InvocationTargetException ex) {
|
||||
System.err.println(
|
||||
"├"
|
||||
+ center(
|
||||
" " + ex.getTargetException().getClass().getSimpleName() + " ",
|
||||
48,
|
||||
'─')
|
||||
+ "┤");
|
||||
Optional.ofNullable(ex.getTargetException().getMessage())
|
||||
.orElse("")
|
||||
.lines()
|
||||
.forEach(line -> System.err.println("│ " + line));
|
||||
System.err.println(
|
||||
"╰" + center(" " + it.getName() + ": FAIL ", 48, '─') + "╯");
|
||||
System.err.println();
|
||||
return 0;
|
||||
}
|
||||
})
|
||||
.collect(Collectors.summarizingInt(it -> it));
|
||||
|
||||
System.err.printf(
|
||||
"Ran %d tests; %d Succeeded, %d Failed%n"
|
||||
.formatted(stats.getCount(), stats.getSum(), stats.getCount() - stats.getSum()));
|
||||
|
||||
return stats.getCount() == stats.getSum();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a Java class
|
||||
*
|
||||
* @param mainClass The fully qualified class name, e.g. {@code com.example.MyClass$MySubClass}
|
||||
* @param args Arguments to pass to the main method
|
||||
* @return Exit status code
|
||||
*/
|
||||
public static int runJava(String mainClass, String... args) {
|
||||
return runJava(new String[0], new String[0], mainClass, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a Java class
|
||||
*
|
||||
* @param additionalClassPaths Additional class paths to pass to the compiler
|
||||
* @param javaArguments Arguments to pass to the java binary
|
||||
* @param mainClass The fully qualified class name, e.g. {@code com.example.MyClass$MySubClass}
|
||||
* @param args Arguments to pass to the main method
|
||||
* @return Exit status code
|
||||
*/
|
||||
public static int runJava(
|
||||
String[] additionalClassPaths, String[] javaArguments, String mainClass, String... args) {
|
||||
String pathSeparator = System.getProperty("path.separator");
|
||||
|
||||
String classPaths =
|
||||
String.join(
|
||||
pathSeparator,
|
||||
Stream.concat(Stream.of(javaClassPath), Arrays.stream(additionalClassPaths))
|
||||
.toArray(String[]::new));
|
||||
|
||||
Stream<String> commandLineStream =
|
||||
Stream.of(
|
||||
Stream.of(javaBin.toString(), "-cp", classPaths),
|
||||
Arrays.stream(javaArguments),
|
||||
Stream.of(mainClass),
|
||||
Arrays.stream(args))
|
||||
.flatMap(it -> it);
|
||||
|
||||
return command(commandLineStream.toArray(String[]::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a shell command. May throw an {@code IOException}
|
||||
*
|
||||
* @return Exit status code
|
||||
* @throws Exception
|
||||
*/
|
||||
public static int command(String... command) {
|
||||
ProcessBuilder pb =
|
||||
new ProcessBuilder(command)
|
||||
.redirectInput(Redirect.INHERIT)
|
||||
.redirectOutput(Redirect.INHERIT)
|
||||
.redirectError(Redirect.INHERIT);
|
||||
Process process;
|
||||
try {
|
||||
process = pb.start();
|
||||
process.waitFor();
|
||||
return process.exitValue();
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns file paths that match a {@code globPattern}
|
||||
*
|
||||
* @see java.nio.file.FileSystem#getPathMatcher(String) getPathMatcher
|
||||
*/
|
||||
public static String[] glob(String globPattern) {
|
||||
Path cwd = Paths.get(".");
|
||||
PathMatcher pathMatcher =
|
||||
FileSystems.getDefault()
|
||||
.getPathMatcher(
|
||||
String.join("", "glob:", cwd.toString(), File.separator, globPattern));
|
||||
try (@SuppressWarnings("unused")
|
||||
Stream<Path> paths =
|
||||
Files.find(
|
||||
cwd,
|
||||
Integer.MAX_VALUE,
|
||||
(path, basicFileAttributes) -> pathMatcher.matches(path))) {
|
||||
return paths.map(Path::toString).toArray(String[]::new);
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException("Error during glob pattern matching", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads string to center.
|
||||
*
|
||||
* <p>From <a href="https://stackoverflow.com/a/8155547">stackoverflow.com/a/8155547</a>
|
||||
*/
|
||||
private static String center(String s, int size, char pad) {
|
||||
if (s == null || size <= s.length()) return s;
|
||||
|
||||
StringBuilder sb = new StringBuilder(size);
|
||||
for (int i = 0; i < (size - s.length()) / 2; i++) {
|
||||
sb.append(pad);
|
||||
}
|
||||
sb.append(s);
|
||||
while (sb.length() < size) {
|
||||
sb.append(pad);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user