Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

The Artemis Java Test Sandbox. A JUnit 5 Extension for Easy and Secure Artemis Java Testing

License

NotificationsYou must be signed in to change notification settings

ls1intum/Ares

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

The Artemis Java Test Sandbox

Java CIMaven CentralSonatype Nexus (Snapshots)

Ares, also known as Artemis Java Test Sandbox(abbr. AJTS), is a JUnit5 extension for easy and secure Java testing on the interactive learningplatformArtemis.

Its main features are:

  • a security manager to prevent students crashing the tests or cheating

  • more robust tests and builds due to limits on time, threads and io

  • support for public and hidden Artemis tests, where hidden ones obey a custom deadline

  • utilities for improved feedback in Artemis like processing multiline errormessages or pointing to a possible location that caused an Exception

  • utilities to test exercises using System.out and System.in comfortably

Project Status

Quality Gate StatusMaintainability RatingSecurity RatingReliability Rating

Lines of CodeCoverageTechnical DebtVulnerabilitiesBugsDuplicated Lines (%)

1. Installation

Note: Ares requires at least Java 11.

Ares is provided as Maven/Gradle dependency. To use Ares in the testenvironment of a Maven project, include

<dependency>    <groupId>de.tum.in.ase</groupId>    <artifactId>artemis-java-test-sandbox</artifactId>    <version>1.13.0</version>    <scope>test</scope></dependency>

in thedependencies section.

For Gradle projects, include

testImplementation'de.tum.in.ase:artemis-java-test-sandbox:1.13.0'

in thedependencies section.

You can now remove dependencies to JUnit 5, AssertJ and Hamcrest ifpresent because Ares already includes them. If you want to use jqwik (>=1.2.4) or JUnit 4 (JUnit 5 vintage), simply include them in thedependencies section.

2. Basic Usage

Ares provides a high level of security, which comes at the cost ofusability. Several steps need to be taken in order to make tests workproperly, and it might require some time to understand what Ares does.Please study at least this complete basic usage guide before using Aresin production.

2.1. Setup

Assume you have a Java 11 Maven project, and the inside ofpom.xmllooks like this:

<properties>    <maven.compiler.source>11</maven.compiler.source>    <maven.compiler.target>11</maven.compiler.target></properties><dependencies>    <dependency>        <groupId>de.tum.in.ase</groupId>        <artifactId>artemis-java-test-sandbox</artifactId>        <version>1.13.0</version>        <scope>test</scope>    </dependency></dependencies>

Consider the following student class that needs to be tested:

importjava.util.Objects;publicfinalclassPenguin {privatefinalStringname;publicPenguin(Stringname) {this.name =Objects.requireNonNull(name,"name must not be null");    }publicStringgetName() {returnname;    }}

And you have already written the following simple JUnit 5 test class:

importstaticorg.junit.jupiter.api.Assertions.*;importorg.junit.jupiter.api.Test;publicclassPenguinTest {@TestvoidtestPenguinPublic() {Penguinpingu =newPenguin("Julian");assertEquals("Julian",pingu.getName(),"getName() does not return the name supplied to the contructor");    }@TestvoidtestPenguinHidden() {assertThrows(NullPointerException.class, () ->newPenguin(null));    }}

In this example,

  • testPenguinPublic() is supposed to be executedafter each push and directly give the students their feedback, while

  • testPenguinHidden() should be executed only after the exercisedeadline, and the results should not be visible before the deadline.

While Artemis has a feature to mark test cases as hidden, this will notprevent the contents of the test case leaking through static variables,files and similar, be it accidentally or on purpose. To prevent that,the hidden test case must not be executed before the deadline at all.

The public test case does not need to be hidden, as its purpose is togive direct feedback. However, there are still multiple possibleproblems like crashing the Maven build bySystem.exit(0) or containingan endless loop. Both can have a negative impact on the interactivelearning experience because the students get confronted with anincomprehensible log of a failed build. Such errors can be explained,but that takes a lot of time, especially if it happens a lot (and itwill, if the number of students is sufficiently large).

It is also a security concern again, students could try to read the.java files containing the test classes.

2.2. Integrating Ares

Therefore, we will use Ares to secure the tests and avoid unintelligiblefeedback. The most basic way to do this is by using the@Public and@Hidden annotations:

importstaticorg.junit.jupiter.api.Assertions.*;importorg.junit.jupiter.api.Test;// IMPORTANT: make sure to use the "jupiter" ones (if you are not using jqwik)importde.tum.in.test.api.jupiter.Hidden;importde.tum.in.test.api.jupiter.Public;// This example won't work just like that, see below whypublicclassPenguinTest {@Public@TestvoidtestPenguinPublic() {Penguinpingu =newPenguin("Julian");assertEquals("Julian",pingu.getName(),"getName() does not return the name supplied to the contructor");    }@Hidden@TestvoidtestPenguinHidden() {assertThrows(NullPointerException.class, () ->newPenguin(null));    }}

The code above won’t work just like that, if you try to run it as is,you will get the following reported by JUnit:java.lang.annotation.AnnotationFormatError: cannot find a deadline for hidden test testPenguinHidden()

Ares needs to know what the deadline is. We tell Ares with anotherannotation:

// Format must be ISO_LOCAL_DATE(T| )ISO_LOCAL_TIME( ZONE_ID)?@Deadline("2020-06-09 03:14 Europe/Berlin")publicclassPenguinTest {// ...}

That annotation (like most of the Ares annotations) can also be placedon the test method (and nested classes), if multiple are present, theone that is closest to the test case is used.

Now, it already works! Try to play around with the deadline in theannotation. If the givenLocalDateTime lies in the past, the test caseis executed and - together with the student code presented earlier -passes. If the deadline hasn’t passed, the test case won’t pass either.It fails withorg.opentest4j.AssertionFailedError: hidden tests will be executed after the deadline.and the test was not executed, as the deadline is always checked beforeany hidden test case is executed.

You might have noticed that we specify the time zone as well. Althoughthe annotation parser permits leaving it unspecified, this bears therisk of (not) executing the tests at the correct time if the buildagent’s time zone is different from the one on your machine or what youwould expect it to be. If you run tests where the time zone is/was notset, Ares will warn you about that in the logs.

2.3. What about Security?

The hidden test case was not executed and static variables cannot leakits contents. If you changegetName() to

publicStringgetName() {System.exit(0);returnname;}

You will now with Ares get the following error message:

java.lang.SecurityException: do not use System.exit(int)/// potential problem location: Penguin.getName(Penguin.java:12) ///

As you might be able to see, Ares threw a SecurityException. But it alsoadded/// potential problem location: Penguin.getName(Penguin.java:12) ///.This is the line from the stack trace which Ares thinks is most relevantfor the student, essentially, it searches for the uppermost stack framethat is located in the student’s code. Student code is basicallyeverything that is not whitelisted.

But what is whitelisted?

  • The test class itself (in case of nested classes, the outermost classis whitelisted) and therefore,all its nested classes and methods,too.

  • A predefined set of packages, like everything that starts withjava.,sun.,com.sun.,org.junit,org.apache., …Therefore,never use such package names for student assignments!

  • Single classes whitelisted using@WhitelistClass andall classes matching@AddTrustedPackage

  • Additional package prefixes provided with by system propertyares.security.trustedpackages separated by a comma,.

Ares also grants permissions that are requested by certain actions(System.exit, File IO, Networking, Threads, …) based on whitelistedstack frames.Ares granting a permission requires all stack frames tobe whitelisted.

Another test:
Adding one of the following lines totestPenguinPublic()itself, and it will still pass using the correct student code:

Files.readString(Path.of("pom.xml"));// orFiles.readString(Path.of("src/test/java/PenguinTest.java"));// assuming default maven structure

If you instead add one of the lines to thegetName() method again, youwill get something like:
java.lang.SecurityException: access to path src\test\java\PenguinTest.java denied in line 16 in Penguin.java.
Which is exactly what you want, students should not be able to read thecode of the test classes. By default, student code has no access to anypath, not even read access.

By the way, adding@WhitelistClass(Penguin.class) to the test class ormethod will make the test run fine again becausePenguin is nowwhitelisted and can therefore access all files without problems.Sonever whitelist classes that students can edit.

2.3.1. What you need to do outside Ares

Sadly, due to the way classes are loaded and the class path works whentesting student code with maven, there are still vulnerabilities ifstudents manage to load classes that would be in trusted packages. Thisis especially problematic if they shadow library classes, such asJUnit’sAssertions.

To prevent that, you have to use the Maven Enforcer Plugin to make sureno student content lands in trusted packages:

Maven:

<plugin>    <groupId>org.apache.maven.plugins</groupId>    <artifactId>maven-enforcer-plugin</artifactId>    <version>3.1.0</version>    <executions>        <execution>            <id>enforce-no-student-code-in-trusted-packages</id>            <phase>process-classes</phase><!--(1)-->            <goals>                <goal>enforce</goal>            </goals>        </execution>    </executions>    <configuration>        <rules>            <requireFilesDontExist>                <files><!--(2)-->                    <file>${project.build.outputDirectory}/ch/qos/logback/</file>                    <file>${project.build.outputDirectory}/com/github/javaparser/</file>                    <file>${project.build.outputDirectory}/com/intellij/</file>                    <file>${project.build.outputDirectory}/com/sun/</file>                    <file>${project.build.outputDirectory}/de/tum/in/test/api/</file>                    <file>${project.build.outputDirectory}/java/</file>                    <file>${project.build.outputDirectory}/javax/</file>                    <file>${project.build.outputDirectory}/jdk/</file>                    <file>${project.build.outputDirectory}/net/jqwik/</file>                    <file>${project.build.outputDirectory}/org/apache/</file>                    <file>${project.build.outputDirectory}/org/assertj/</file>                    <file>${project.build.outputDirectory}/org/eclipse/</file>                    <file>${project.build.outputDirectory}/org/jacoco/</file>                    <file>${project.build.outputDirectory}/org/json/</file>                    <file>${project.build.outputDirectory}/org/junit/</file>                    <file>${project.build.outputDirectory}/org/opentest4j/</file>                    <file>${project.build.outputDirectory}/sun/</file>                    <file>${project.build.outputDirectory}/org/gradle/</file>                    <file>${project.build.outputDirectory}/worker/org/gradle/</file>                </files>            </requireFilesDontExist>        </rules>    </configuration></plugin>

Gradle:

def forbiddenPackageFolders= [//(2)"$studentOutputDir/ch/qos/logback/","$studentOutputDir/com/github/javaparser/","$studentOutputDir/com/intellij/","$studentOutputDir/com/sun/","$studentOutputDir/de/tum/in/test/api/","$studentOutputDir/java/","$studentOutputDir/javax/","$studentOutputDir/jdk/","$studentOutputDir/net/jqwik/","$studentOutputDir/org/assertj/","$studentOutputDir/org/apache/","$studentOutputDir/org/eclipse/","$studentOutputDir/org/gradle/","$studentOutputDir/org/jacoco/","$studentOutputDir/org/json/","$studentOutputDir/org/junit/","$studentOutputDir/org/opentest4j/","$studentOutputDir/sun/","$studentOutputDir/worker/org/gradle/"]test {    doFirst {//(1)for (String packageFolderin forbiddenPackageFolders) {assert!file(packageFolder).exists():"$packageFolder must not exist within the submission."        }    }// ...}
  1. Important: you want to enforce the non-existence of classes aftertheir generation but before testing.

  2. This is where all folders/packages go that we don’t want to existin student code. You will always find the most recent recommendationfor Ares here. If you use additional third-party libraries that needto be configured using@AddTrustedPackage, you should add thosepackages here as well. Ares will check that all entries are present.
    If you don’t want Ares to do so, set theares.maven.ignore orares.gradle.ignore system property totrue. In case you want Aresto look into a different file, you can set theares.maven.pom orares.gradle.build to a path other than the defaultpom.xml orbuild.gradle.

2.4. Further Important Options

Are we done now? With the most fundamental parts yes, but there is a bitmore you need to know about testing with Ares, as this was just a verybasic example with a single class and not much testing. Without furtherknowledge, you might not get Ares to work and consequently get ratherannoyed or even enraged. To prevent that, please read on.

2.4.1. Path Access and Class Loading

You can use@WhitelistPath and@BlacklistPath to control access topaths. By default, no access is granted, and so you need to use@WhitelistPath to give student code the permission to read and writefiles explicitly. You can specify exceptions using@BlacklistPathwhich will overpower the whitelisted paths.

The following examples will make use ofcourse1920xyz as placeholdervalue for the real Artemis exercise name/id. Replace it with the realone when borrowing code snippets, or nothing will work as expected.

Most importantly, this does not only apply to explicit file IO, but alsoto the.class files that the class loader reads, as needed. Thisalready happens if one student class requires another one, that has notbeen loaded after that. You can recognize that in the standard erroroutput:

[WARN] [main] BAD PATH ACCESS: K:\repo\course1920xyz-solution\bin\some\Thing.class (BL:false, WL:false)

This usually means the class loader could not load the class. Theparentheses show, that the problem is the missing whitelisting.Therefore, all test setups should have some whitelisting.

A number of examples how you can whitelist paths in Ares:

  • @WhitelistPath("") will grant read access to the paths in thedirectory of execution, which is usually where thepom.xml is.

  • @WhitelistPath("pom.xml") will allow students to read thepom.xml.

  • @WhitelistPath("..") will allow read access to the level above themaven project. In Eclipse, that is the level of your workspace.

  • @WhitelistPath(value = "../course1920xyz**", type = PathType.GLOB)grants read access to projects beginning with the exercise "id" usedby Artemis. Should you use the Eclipse feature "Referenced Projects"(or the analog to that in your IDE) to link the student/solution projectto the tests, you will need a setting like this.

  • @WhitelistPath(value = "data", level = PathActionLevel.DELETE) willallow students to read, write and delete files in thedata directoryand subdirectories.

  • @WhitelistPath("target") allows reading files in target (Maven output folder)

  • @BlacklistPath(value = "*Test.{java,class}", type = PathType.GLOB)prevents access to classes in source code or compiled form that containTest. If you leave away the* afterTest, nested classes are notblacklisted. Student classes should not be called something with"Test" then.

That was not everything but already quite a lot. Take a look at theJavadoc of the annotations and enums used, if you want to know more.Before you give up, here is my recommendation how to start:

@WhitelistPath(value ="../course1920xyz**",type =PathType.GLOB)// for manual assessment and development@WhitelistPath("target")// mainly for Artemis@BlacklistPath("target/test-classes")// prevent access to test-related classes and resources

Add a@BlacklistPath for other important classes, like your referenceimplementations of the solution to test against should you use thatapproach.

Note: the Artemis project starts withcourse1920xyz, but the build inBamboo (by Artemis) will happen in a directory named after the buildplan, which is in upper case and therefore, begins withCOURSE1920XYZ.Make sure that you do not build multiple student solutions in the samedirectory on the same machine using the git clone (lower case) approach.Otherwise, adjust the whitelisting to your needs.

2.4.2. Timeouts

JUnit already provides means of applying timeouts to tests. However,those arenot strict in the sense of "enforced in the strongestpossible way". What is meant by that?

There are three different ways how the timeouts can work:

  • likeorg.junit.jupiter.api.Timeout
    This timeout is not preemptive, and the test itself runs in the samethread executing the tests. It will only try to stop the test via aninterrupt. If that fails like it does for an endless loop, the testwill definitively fail. After it is finished. Which might neverhappen and the main reason not to use this when it comes to testingunknown code.

  • likeorg.junit.jupiter.api.Assertions.assertTimeoutPreemptively
    This will fail the test preemptively by executing theExecutableargument itself in a different thread than the thread executing alltests. It will only try to stop the test via an interrupt, but ifthat fails it will simply carry on. The test thread might still run,though.

  • likede.tum.in.test.api.StrictTimeout
    This uses a mechanism similar toassertTimeoutPreemptively, butwill resort to harder means if necessary.It will in the following order:

    1. wait the given duration

    2. interrupt the thread executing the test and wait no longer (likeassertTimeoutPreemptively)

    3. block the creation of new threads

    4. interrupt all threads created during the test and try to join thethreads

    5. if that fails, useThread.stop() on all remaining threadsand try to join again

    6. repeat step 5 multiple times, if required

    7. Should that fail, report a special SecurityException that not allthreads could be stopped. (see the standard error output for a detailedreport then)If that happens, no more tests can be properly executedbecause the security cannot be guaranteed and the test cases cannot beexecuted "in isolation". All following tests will fail.

Rule 1: When testing with Ares, always use@StrictTimeout fortimeouts, the others will not work reliably, especially in conjunctionwith the Ares security.

Rule 2: When writing tests for Artemis, always use@StrictTimeout.There is no reason to omit the timeout, since you do not know the codestudents will write. (And they will write code spawning millions ofthreads in endless loops, which in turn will do the same recursively.)

2.4.3. Showing Standard Output

By default, Ares will record standard and error output of each testinternally and not print it to the console. The recorded output can thenbe obtained and tested, seeIOTester The reason for this is onthe one hand to keep the console and logs short and clean and on theother hand prevent students from accidentally messing up the logs withmillions of lines. Ares also has a hard limit on the total number ofprinted chars at around 10 million.

To mirror the output recorded by Ares to the console, use the@MirrorOutput annotation on the test class or method.

It is also worth noting that Ares enforces valid UTF-8being printed and throws an appropriate exception otherwise.

2.4.4. Testing the Exercise before Release

Hidden tests will be executed by Ares only after the deadline. Thisposes the problem, how the exercise creators should work on the tasks,tests and the sample solution. One possible solution would be to use analternative deadline annotation or change the deadline temporarily. Theproblem is that it is quite likely one might forget to change it backagain, and protecting the hidden tests would fail.

Use@ActivateHiddenBefore just like@Deadline to state theLocalDateTime before which hidden tests should be executed. This dateshould, of course, be before the release of the exercise on Artemis.

2.4.5. Extending a Deadline and Disability Compensation

You can use@ExtendedDeadline together with a duration like1d or2d 12h 30m to extend the deadline by the given amount.@ExtendedDeadline("1d"), for example, extends the deadline by one day.If you use the annotation on different levels (e.g. class and method)without stating a new deadline (e.g. deadline only on class level), theextensions will be added together.

2.4.6. Threads and Concurrency

By default, Ares will not allow non-whitelisted code to use threads atall. That includes thread pools, but excludes the common pool and itsusers, like parallel streams. To allow the use of Threads, use theannotation@AllowThreads. The number of active threads is alsolimited, the default value of that is 1000, but can be changed in theannotation. Please keep in mind that this limit should not be largerthan 1000 to prevent performance and timeout chaos.

New threads are for security reasons not directly whitelisted by Aresand will not be allowed to do anything security critical. If you trust athread (at least its entry point), you can explicitly request the threadto be whitelisted usingArtemisSecurityManager.requestThreadWhitelisting(Thread). The threadcalling the method and its stack must be whitelisted, of course.

2.4.7. Testing Console Interaction

One example showing some possibilities here:

voidtestSquareCorrect(IOTestertester) {//(1)tester.provideInputLines("5");//(2)InputOutputPenguin.calculateSquare();//(3)tester.err().assertThat().isEmpty();//(4)tester.out().assertThat().isEqualTo("""                Enter Number:                Answer:                25""");//(5)}
  1. DeclareIOTester as parameter.

  2. Provide input lines before calling the student code.This content will be used for reading lines fromSystem.in.

  3. Call the student code to process the input and produce output.

  4. Assert that nothing was printed toSystem.err.

  5. Assert that the standard output (in this case excluding the finalline break) is equal to the given text block (if you use textblocks, be aware of their newline handling).

Note that Ares normalizes the line breaks to\n, andOutputTesteroffers many different approaches to checking output (e.g. single string, list of strings, …​).

If students read more lines than provided, they get the following feedback:…​java.lang.IllegalStateException: no further console input request after the last(number 1: "5") expected.…​

See alsoIOTester and for more examples, theInputOutputUsertest.

Showing Standard Output covers how the student output ismanaged and shown in the test logs.

Tip

In case the defaultIOTester from Ares does not meet your requirements,you can provide a custom implementation by applying@WithIOManager(MyCustomOne.class)to e.g. the test class or individual methods. This also allows you to registera custom parameter to control IO testing with ease inside the test method.Have a look into the test class linked above to learn more or read the documentation ofIOManager.

2.4.8. Networking

Ares allows for local network connections by using the@AllowLocalPortannotation.

There are plenty of configuration options, and the code can get complicatedquickly due to the threads required to test network connections.One issue can be that waiting network connections block threads in sucha way that they cannot be stopped (waiting in native code), so werecommend using timeouts for connections at least on one end consistently.

For examples, have a look at the testNetworkUser.

2.4.9. Locale

You can set a locale for Ares (and the rest of Java) by adding the@UseLocale JUnit extension to classes/methods, which will set the Javadefault localethat is also used by Ares. The locale is changed only for the scopewhere the annotation is applied.

Ares is currently localized in German (de_DE) and English (en_US),whereen_US is the fallback for any other locale.

See also theLocaleUsertest for more examples.

3. Additional Notes

3.1. Older Versions

For versions prior to1.0.0, a repository block had to be added to<repositories> section of thepom.xml that referenced the Mavenrepository URLhttps://gitlab.com/ajts-mvn/repo/raw/master/.

Using older Ares versions is highly discouraged, remove theserepository declarations and update to the newest Ares version if theyappear in your projects.

3.2. GitHub Packages

GitHub Packages does currently not allow unregistered, public access tothe packages. Therefore, you will need to authenticate to GitHub if youuse

<repositories>    <repository>        <id>ares</id>        <name>Ares Maven Packages</name>        <url>https://maven.pkg.github.com/ls1intum/Ares</url>    </repository></repositories>

4. License

Ares was created by Christian Femers and is licensed under theMIT License, seeLICENSE.md.


[8]ページ先頭

©2009-2025 Movatter.jp