Verified Commit 073519c3 authored by Daniel Mangold's avatar Daniel Mangold
Browse files

Removed example JUnit test and added utility class

parent a7597955
package h05;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class ExampleJUnitTest {
@Test
public void testAddition() {
assertEquals(2, 1 + 1);
}
}
package h05;
import org.opentest4j.TestAbortedException;
import java.io.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.math.BigInteger;
import java.net.URI;
import java.net.http.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.*;
import static java.lang.reflect.Modifier.*;
import static org.junit.jupiter.api.Assertions.*;
public class Utils {
public static final long SEED = new Random().nextLong();
private static final boolean CHECK_FOR_UPDATES = true, CHECK_HASHES = true, AUTO_UPDATE = true;
// -------------------------------------- //
// DO NOT CHANGE ANYTHING BELOW THIS LINE //
// -------------------------------------- //
public static final Random RANDOM = new Random(SEED);
public static final Map<Class<?>, Boolean> CLASS_CORRECT = new HashMap<>();
public static final Map<Method, Boolean> METHOD_CORRECT = new HashMap<>();
private static final String LOCAL_VERSION = "1.0";
private static final Map<TestType.Type, String> METHOD_LOOKUP = Map.of(
TestType.Type.CLASS, "checkClass",
TestType.Type.INTERFACE, "checkInterface"
);
static {
if (CHECK_FOR_UPDATES)
Updater.checkForUpdates();
System.out.println("Seed: " + SEED);
}
/**
* Returns the class object for a given name or throws an {@link TestAbortedException} if it is not found
* @param className the fully qualified name of the class
* @return the class object for the corresponding name
*/
public static Class<?> getClassForName(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
throw new TestAbortedException("Class " + e.getMessage() + " not found", e);
}
}
/**
* Creates a dependency on another class
* <br><br>
* Requires that all of the supplied class are annotated with {@link TestType}. <br>
* Invokes the method specified in {@link Utils#METHOD_LOOKUP} and puts {@code true} in
* the lookup table, or {@code false} if any exception was thrown. If a class exists in
* the {@link Utils#CLASS_CORRECT} lookup table then that value is used without
* invoking the method. <br>
* If an invocation failed or the value for a class is {@code false}, a
* {@link TestAbortedException} is thrown
* @param classes the classes that are required by the calling test class
*/
public static void requireTest(Class<?>... classes) {
for (Class<?> c : classes) {
String testClassName = c.getName(), actualClassName = testClassName.substring(0, testClassName.length() - 4);
Exception e = null;
if (CLASS_CORRECT.containsKey(c)) {
if (CLASS_CORRECT.get(c))
continue;
} else {
try {
Method method = c.getDeclaredMethod(METHOD_LOOKUP.get(c.getDeclaredAnnotation(TestType.class).value()));
method.setAccessible(true);
method.invoke(null);
CLASS_CORRECT.put(c, true);
continue;
} catch (Exception ee) {
CLASS_CORRECT.put(c, false);
e = ee;
}
}
throw new TestAbortedException(
Thread.currentThread().getStackTrace()[2].getClassName() + " requires " + actualClassName + " to be implemented correctly", e);
}
}
/**
* Creates a dependency on another method
* <br><br>
* Requires that the declaring class is annotated with {@link TestType}. <br>
* Invokes the given method and puts {@code true} in the lookup table, or {@code false}
* if any exception was thrown. If the method exists in the
* {@link Utils#METHOD_CORRECT} lookup table then that value is used without invoking
* the method. <br>
* If the invocation failed or the value for a class is {@code false}, a
* {@link TestAbortedException} is thrown
* @param method the method to be invoked
* @param args the arguments to supply on invocation
*/
public static void requireTest(Method method, Object... args) {
if (METHOD_CORRECT.containsKey(method)) {
if (METHOD_CORRECT.get(method))
return;
else
throw new TestAbortedException(Thread.currentThread().getStackTrace()[2].getMethodName() + " requires that " +
method.getName() + " executes without any exceptions");
}
try {
requireTest(method.getDeclaringClass());
} catch (TestAbortedException e) {
METHOD_CORRECT.put(method, false);
throw e;
}
try {
method.invoke(method.getDeclaringClass().getDeclaredConstructor().newInstance(), args);
METHOD_CORRECT.put(method, true);
} catch (ReflectiveOperationException e) {
METHOD_CORRECT.put(method, false);
throw new TestAbortedException(Thread.currentThread().getStackTrace()[2].getMethodName() + " requires that " +
method.getName() + " executes without any exceptions", e.getCause());
}
}
/**
* Annotation interface for determining what method should be invoked by
* {@link Utils#requireTest(Class[])}
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestType {
enum Type {CLASS, INTERFACE}
Type value();
}
/**
* Tries to invoke the given method with the given parameters and throws the actual
* Throwable that caused the InvocationTargetException
* @param method the method to invoke
* @param instance the instance to invoke the method on
* @param params the parameter to invoke the method with
* @throws Throwable the actual Throwable (Exception)
*/
public static void getActualException(Method method, Object instance, Object... params) throws Throwable {
try {
method.invoke(instance, params);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
/**
* Assert that the given member has (map value = {@code true}) or does not have
* (map value = {@code false}) the modifiers in {@code modifiers}. <br>
* Throws a {@link AssertionError} if the assertion fails
* <br><br>
* Calls {@link Utils#assertHasModifiers(Member, Map, String)} with {@code null} as last
* parameter (default error message)
* @param member the member to check the modifiers of
* @param modifiers a map of modifiers (constants in {@link Modifier}) with a
* corresponding boolean value
*/
public static void assertHasModifiers(Member member, Map<Integer, Boolean> modifiers) {
assertHasModifiers(member, modifiers, null);
}
/**
* Assert that the given member has (map value = {@code true}) or does not have
* (map value = {@code false}) the modifiers in {@code modifiers}. <br>
* Throws a {@link AssertionError} if the assertion fails
* @param member the member to check the modifiers of
* @param modifiers a map of modifiers (constants in {@link Modifier}) with a
* corresponding boolean value
* @param msg the error message to pass to {@link AssertionError}
*/
public static void assertHasModifiers(Member member, Map<Integer, Boolean> modifiers, String msg) {
Class<Modifier> modifierClass = Modifier.class;
try {
for (Integer modifier : modifiers.keySet()) {
Method method;
String msgSuffix;
switch (modifier) {
case PUBLIC:
method = modifierClass.getDeclaredMethod("isPublic", int.class);
msgSuffix = "public";
break;
case PROTECTED:
method = modifierClass.getDeclaredMethod("isProtected", int.class);
msgSuffix = "protected";
break;
case PRIVATE:
method = modifierClass.getDeclaredMethod("isPrivate", int.class);
msgSuffix = "private";
break;
case STATIC:
method = modifierClass.getDeclaredMethod("isStatic", int.class);
msgSuffix = "static";
break;
case FINAL:
method = modifierClass.getDeclaredMethod("isFinal", int.class);
msgSuffix = "final";
break;
case ABSTRACT:
method = modifierClass.getDeclaredMethod("isAbstract", int.class);
msgSuffix = "abstract";
break;
default:
throw new IllegalArgumentException(String.valueOf(modifier));
}
if (modifiers.get(modifier))
assertTrue((Boolean) method.invoke(null, member.getModifiers()),
msg != null ? msg : member.getName() + " must be " + msgSuffix);
else
assertFalse((Boolean) method.invoke(null, member.getModifiers()),
msg != null ? msg : member.getName() + " must not be " + msgSuffix);
}
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
}
private static class Updater {
private static final String REPOSITORY_URL = "https://git.rwth-aachen.de/aud-tests/AuD-2021-H05-Student/-/raw/master/";
/**
* Checks if the repository is newer than the local copy and does the following actions
* if so:
*
* <ul>
* <li>{@link Utils#CHECK_HASHES} = {@code true}: only compares the MD5 hashes of all files</li>
* <li>{@link Utils#AUTO_UPDATE} = {@code true}: compares the MD5 hashes of all files and
* re-downloads those who do not match</li>
* </ul>
*
* Messages are printed in any case to let the user know what is happening
*/
private static void checkForUpdates() {
HttpResponse<String> response = getHttpResource(".test_version");
if (response == null || response.statusCode() != 200) {
System.err.println("Unable to fetch version from repository");
return;
}
try (BufferedReader remoteReader = new BufferedReader(new StringReader(response.body()))) {
Version localVersion = new Version(LOCAL_VERSION),
remoteVersion = new Version(remoteReader.readLine());
boolean updatedLocal = false;
if (remoteVersion.isNewerThan(localVersion)) {
System.out.println("Update available! Local version: " + localVersion + " -- Remote version: " + remoteVersion);
System.out.println("Changelog: " + REPOSITORY_URL + "changelog.txt");
if (AUTO_UPDATE) {
updateLocal(".test_version", ".test_version");
updatedLocal = true;
}
} else
System.out.println("Local tests are up to date");
for (
String line = remoteReader.readLine();
(CHECK_HASHES || updatedLocal) && line != null && line.length() != 0;
line = remoteReader.readLine()
) {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
String fileName = line.split(" ")[0], expectedHash = line.split(" ")[1];
StringJoiner systemSpecificFileName = new StringJoiner(System.getProperty("file.separator"));
Arrays.stream(fileName.split("/")).forEach(systemSpecificFileName::add);
messageDigest.reset();
File file = new File(systemSpecificFileName.toString());
if (!file.exists()) {
System.err.println(systemSpecificFileName + " not found, can't compare hashes");
continue;
}
try (InputStream inputStream = new FileInputStream(file)) {
String actualHash = new BigInteger(1, messageDigest.digest(inputStream.readAllBytes())).toString(16);
actualHash = "0".repeat(32 - actualHash.length()) + actualHash;
if (!actualHash.equals(expectedHash)) {
System.out.println("Hash mismatch for file " + systemSpecificFileName);
if (AUTO_UPDATE)
updateLocal(systemSpecificFileName.toString(), fileName);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
} catch (IOException | InterruptedException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
/**
* Requests a resource / file from the repository
* @param resource the resource to get from the repository
* @return a {@link HttpResponse<String>} object
*/
private static HttpResponse<String> getHttpResource(String resource) {
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build();
HttpRequest request = HttpRequest.newBuilder(
URI.create(REPOSITORY_URL + resource)).build();
try {
return client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
e.printStackTrace();
return null;
}
}
/**
* Updates (overwrites) the specified file (first parameter) with the contents of the
* file at the repository (second parameter)
* @param fileName the relative path to the local file
* @param remoteFileName the relative path to the remote file
*/
private static void updateLocal(String fileName, String remoteFileName) throws IOException, InterruptedException {
System.out.print("Updating " + fileName + "... ");
File localFile = new File(fileName);
HttpResponse<String> response = getHttpResource(remoteFileName);
if (response != null && response.statusCode() == 200) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(localFile))) {
writer.write(response.body());
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("done");
} else
System.out.println("unable to fetch file from repository");
}
private static class Version {
private final Integer MAJOR_VERSION, MINOR_VERSION;
private Version(String version) {
String[] versions = version.split("\\.");
MAJOR_VERSION = Integer.parseInt(versions[0]);
MINOR_VERSION = Integer.parseInt(versions[1]);
}
/**
* Returns whether this Version is newer (higher version number) than the given one
* @param version the Version object to compare to
* @return {@code true} if this Version object is newer, {@code false} otherwise
*/
private boolean isNewerThan(Version version) {
return MAJOR_VERSION > version.MAJOR_VERSION || (MAJOR_VERSION.equals(version.MAJOR_VERSION) && MINOR_VERSION > version.MINOR_VERSION);
}
@Override
public String toString() {
return MAJOR_VERSION + "." + MINOR_VERSION;
}
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment