Java packages

Java packages

by Peter Sander -
Number of replies: 12

Hello,

I'm having trouble getting the following to work.

I have a java_class question for students to develop. My sample answer looks like this:

package foobar;
public class Foo {
    private Bar bar;
    public String toString() {return "whatever";}
}

The class Foo is in a support file called Bar.java containing:

package foobar;
public class Bar {}

Not very exciting I'll admit.

In the question preview with the above sample answer submitted as the student answer, Check gives:

Debug: source code from all test runs

Run 1

package foobar;

class Foo {
    private Bar bar;
    public String toString() {
        return "whatever";
    }
}

public class __Tester__ {

    public static void main(String[] args) {
        __Tester__ main = new __Tester__();
        main.runTests();
    }

    public void runTests() {
        System.out.println(new Foo());
    }
}

which doesn't look unreasonable. However, the compiler seems unable to find the class Bar even though it's declared in the same package.

Syntax Error(s)

__Tester__.java:4: error: cannot find symbol
    private Bar bar;
            ^
  symbol:   class Bar
  location: class Foo
1 error

Take out the package declarations and the code works as expected. I've tried diddling the template, eg. putting a package declaration at the top, with no great success. 

Take out the variable declaration Bar bar and removing the support file makes the class compile, but Check now gives a runtime error:

 

TestExpectedGot
IncorrectSystem.out.println(new Foo())
whatever
***Runtime error***
Exception in thread "main" java.lang.NoClassDefFoundError: __Tester__ (wrong name: foobar/__Tester__)
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:800)
        at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:449)
        at java.net.URLClassLoader.access$100(URLClassLoader.java:71)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
        at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:482)

Has anyone gotten questions with Java classes in packages to work?

I should mention that I can strip out the package declaration from the student answer with

{{ STUDENT_ANSWER | replace({'package foobar;': ''}) | replace({'public class ': 'class ' }) }}

in the template, but that just seems awkward.


In reply to Peter Sander

Re: Java packages

by Richard Lobb -

Java packages are somewhat incompatible with the usual CodeRunner/Jobe model of a single directory in which the Twig output file plus any support files are placed prior to execution. To get around this you need to build the required directory hierarchy yourself. [And if you thought your solution was awkward, I'm not sure what you'll think of mine!] I always script tricky stuff like this in Python. Here's a solution that works for your particular problem:

  1. In Advanced customisation, set the main language to Python3, the Ace language to Java and set the memory limit to 0 (as the Java VM tries to reserve ludicrous amounts of memory).
  2. Set the test code to
    Foo foo = new Foo();
    System.out.println(foo);
    
  3. Load Bar.java as a support file
  4. Set the template to the following:
    import sys, os, shutil, subprocess
    
    __student_answer__ = """{{ STUDENT_ANSWER | e('py') }}"""
    
    # Make foobar directory, put Bar.java and Foo.java into it
    os.mkdir('foobar')
    shutil.move("Bar.java", "foobar/")
    with open("foobar/Foo.java", "w") as f:
        print(__student_answer__, file=f)
        
    # Build test class in foobar directory too
    tester = """package foobar;
    public class __Tester__ {
        public static void main(String[] args) {
            {{TEST.testcode}}
        }
    }
    """
    with open("foobar/__Tester__.java", "w") as f:
        print(tester, file=f)
    
    # Compile and run
    if (subprocess.call(["javac", "foobar/__Tester__.java"]) != 0 or
       subprocess.call(["java", "foobar/__Tester__"]) != 0):
        print("** Further testing aborted **", file=sys.stderr)
    
Proof it works:


Although that's pretty complex, if you have many questions involving packages, you could abstract this into a separate question type in which the support files specify the required package hierarchy (e.g. you might have a zip archive to be unzipped plus an additional config file).

One shortcoming worth noting: CodeRunner classifies any output to stderr as a "Runtime error" and aborts the running of further testcases. Thus compilation errors in the student's code are tagged as "Runtime errors", although the usual compiler diagnostic messages still appear in the output. You could prevent this misclassification by catching stderr output from the subprocess.call but you would then continue to run all other test cases, generating the same compile error output. There's no easy way round this, though you could use a template grader if it really worried you. 

In reply to Richard Lobb

Re: Java packages

by Peter Sander -

Wow, that was fast - nice answer, thanks!

Yes, seems it'd make more sense to turn this into a specific question type. There will be lots of Java questions and by design all classes will be in packages. I'll post back here when I get something cobbled together...


In reply to Peter Sander

Re: Java packages

by Peter Sander -

Ok, here's a slightly modified template to make it more generic. I've left in two template parameters for the name of the package and the student answer class, eg, {"package": "foobar", "class": "Foo"}. Basically I bailed on extracting this information from the student answer by trying to anticipate how students might construct their Java code file. Maybe later.

There's a limitation in that the student answer and supporting files must all be in the same package

import sys, os, shutil, subprocess
__student_answer__ = """{{ STUDENT_ANSWER | e('py') }}"""
# Make package directory, put support files into it
os.mkdir("{{ QUESTION.parameters.package }}")
[shutil.move(f, "{{ QUESTION.parameters.package }}") for f in os.listdir() if f.endswith(".java")]
# Put student answer class into package directory
with open("{{ QUESTION.parameters.package }}/{{ QUESTION.parameters.class}}.java", "w") as f:
    print(__student_answer__, file=f)
    
# Build test class in package directory too
tester = """package {{ QUESTION.parameters.package }};
public class __Tester__ {
    public static void main(String[] args) {
        {{TEST.testcode}}
    }
}
"""
with open("{{ QUESTION.parameters.package }}/__Tester__.java", "w") as f:
    print(tester, file=f)
# Compile and run
if (subprocess.call(["javac", "{{ QUESTION.parameters.package }}/__Tester__.java"]) != 0 or
   subprocess.call(["java", "{{ QUESTION.parameters.package }}/__Tester__"]) != 0):
    print("** Further testing aborted **", file=sys.stderr)
In reply to Peter Sander

Re: Java packages

by Richard Lobb -

Nice one! Thanks Peter. Are you happy for me to include that question type in the next release (with acknowledgements, of course)?

Richard

In reply to Richard Lobb

Re: Java packages

by Peter Sander -
In reply to Peter Sander

Re: Java packages

by Peter Sander -

I've tweaked my java-in-a-packagetemplate to permit package names including a dot. So, eg, the following should work for the class foo.bar.Toto:

import sys, os, shutil, subprocess
__student_answer__ = """{{ STUDENT_ANSWER | e('py') }}"""
ncoding = '{{ QUESTION.parameters.encoding }}' or 'utf-8'
# Make package directory, put support files into it
package_dir = '{{ QUESTION.parameters.package }}'.replace('.', '/')
os.makedirs(package_dir)
[shutil.move(f, package_dir) for f in os.listdir() if f.endswith('.java')]
# Put student answer class into package directory
with open(package_dir + '/{{ QUESTION.parameters.class}}.java', mode='w', encoding=ncoding) as fd:
    print(__student_answer__, file=fd)
    fd.close()
    
# Build test class in package directory too
tester = '''package {{ QUESTION.parameters.package }};
public class __Tester__ {
    public static void main(String[] args) {
        {{TEST.testcode}}
    }
}
'''
with open(package_dir + '/__Tester__.java', mode='w', encoding=ncoding) as fd:
    print(tester, file=fd)
    fd.close()
# Compile and run
if (subprocess.call(['javac', '-encoding', ncoding, package_dir + '/__Tester__.java']) != 0 or
   subprocess.call(['java', package_dir + '/__Tester__']) != 0):
    print('** Further testing aborted **', file=sys.stderr)
In reply to Peter Sander

Re: Java packages

by Peter Sander -

Right, now that I've been using CodeRunner in anger in a Java-based course, I've had another go at making it workable. Instead of customizing templates, which quickly became unmanageable, I've put the code into modules which are then loaded as support files. This reduces the amount of customization of templates to an import statement and a couple of function invocations. For example, to test student answer functionality the customized template becomes:

from java_code_tester import *

student_answer = """{{ STUDENT_ANSWER | e('py') }}"""

# manipulates student answer
student_answer = assemble_student_answer(student_answer)

# loads and filters support files
support_files = assemble_support_files()

# assembles and saves test file from student answer and support files
assemble_tester(student_answer, support_files, '''{{TEST.testcode}}''')

# take it for a spin
compile_and_run()

I've worked this into a LOCAL_PROTOTYPE_java_tester to use as a question prototype and to be customize further as needed. Pre-loading the module file java_code_tester.py into the prototype as a support file means not having to remember to load it into every question; it's already available.

The java_code_tester.py file is attached below.

In reply to Peter Sander

Re: Java packages

by Peter Sander -

Moving right along...

I also noticing from letting students loose on CodeRunner led to occasional unfortunate behaviour. For example, having given them code specs where they were to use enums in a switch, they might be tempted to just go with if-then-else-if-then-else... To CodeRunner's tester the output looks like what's wanted and the test case passes. Not what I wanted.

So I've cobbled up a few code-checker modules to actually do some static code analysis (extremely basic, it's not FindBugs but the students don't know that  :^)  ) to check whether their code is vaguely in-spec. If the code is out-of-spec CodeRunner pops an informative exception and they get to try again.

This works along the same principles as my last post - there's a module file java_code_checker.py included in the LOCAL_PROTOTYPE_java_checker question prototype which does the work of code-analysis.

For the enum checker, the template looks like:

from java_code_checker import *
from java_code_tester import *

student_answer = """{{ STUDENT_ANSWER | e('py') }}"""

student_answer = assemble_student_answer(student_answer, '{{QUESTION.parameters.enum}}')

# checks if enum used
check_for_enum(student_answer, '{{QUESTION.parameters.enum}}')

# loads and filters support files
support_files = assemble_support_files()

# assembles and saves test file from student answer and support files
assemble_tester(student_answer, support_files, '''{{TEST.testcode}}''')

# take it for a spin
compile_and_run()

These checks need a bit more configuration via the template parameters. For example, for the enums example I need to configure the name of the enum via

{"enum": "DayOfWeekend"}
because I need the name of the enum in order to include the possibility of a static import of its identifiers. One day I'll regex that bit of cruft away. One day...

This is nowhere near serious static code analysis, but it seems to keep the students a bit more honest.
In reply to Peter Sander

Re: Java packages

by Richard Lobb -

Thanks for those two postings, Peter. I've just built the first of your java_code_test.py versions into a java_general question type to be included in a file 3rd_party_PROTOYPES.xml, which will be part of the next release. The question text includes all the explanatory comment at the start of your support file. I assume you're happy with this (it includes your Copyright note)?

I also assume that this new question type is intended to replace the earlier one you submitted, so I won't be including that.

That's an interesting approach, to move most of the template code into the support file. The disadvantage is that it's harder to make use of template parameters but certainly it makes it easier to edit and manage the code. 

It seems many teachers take the path of including code to do some sort of static checking of a student's submission before running it. In our first year course we run all code through the pylint Python style checker, with tight constraints on things like maximum function length, maximum line length, use of global variables, etc. We also have our own extra code to disallow or to require the use of certain constructs, as you've done in your enum example.

In my TODO list is a plan to include pre-run checking as a separate phase of the execution sequence so that you don't get the pre-run check failures coming up as Runtime Errors. That will however add a bit of overhead, as it will yet another Jobe submission at the start.

Thanks for contributing

Richard

In reply to Richard Lobb

Re: Java packages

by Peter Sander -

Sorry this is coming in in bits-and-pieces but it's very much a work-in-progress.

I've now got FindBugs working in a CR template. This does pretty comprehensive static code analysis on the student answers. For instance, 

Preview question: Check switch with findbugs

Question 1

Incorrect
Mark 0.00 out of 1.00

Question text

Demonstrate the switch statement.

The sample answer has a couple of issues to show what FindBugs complains about. Fix the problems and FindBugs will shut up.

For example:

TestResult
System.out.println();Code looks clean
Answer:
public class Buggy { public void theMethod(String key) { switch(key) { case "continue": System.out.println("Carrying on"); case "quit": System.out.println("Outta here"); } } }



Feedback

where FindBugs complains that the case statements are missing breaks, and that there's no default: case. When the code is cleaned up:

Still got some issues to work out, such as the line numbers of problems being off.

In reply to Peter Sander

Re: Java packages

by Peter Sander -

Here's a slightly more finished version of what I've been cobbling up for Java code questions. The java_code_checkr.py is downloadble from here: https://github.com/sanderator/CodeRunner-prototypes.git.


Prototype template for static and / or dynamic checking of student answer java code specifications.

The file java_code_checkr.py should be included as a CodeRunner Support file in a prototype, eg, LOCAL_PROTOTYPE_java_checkr. Java code questions then use this prototype as their Question type.

The support file java_code_checkr.py has the functions documented below:



compile_and_run
compile_and_run(student_answer, testcode, import_static=None, xception=None, ncoding='utf-8')

Assembles code (student answer, support files, tester class.
Then compiles and (hopefully) runs the tester code.

Example of use in template:

from java_code_checkr import compile_and_run
compile_and_run("""{{STUDENT_ANSWER | e('py')}}""", '''{{TEST.testcode}}''')

 When the student answer is expected to throw a given exception there is an additional argument:

from java_code_checkr import compile_and_run
compile_and_run("""{{STUDENT_ANSWER | e('py')}}""", '''{{TEST.testcode}}''',
    xception='''{{QUESTION.parameters.exception}}''')

where the CodeRunner question type / Template params look like:

{"exception": "IllegalArgumentException"}


compile_and_findbugs
compile_and_findbugs(student_answer, testcode, import_static=None, xception=None, ncoding='utf-8')

Compiles the tester code and runs FindBugs on the bytecode.

Example of use in template:

from java_code import compile_and_findbugs
compile_and_findbugs("""{{ STUDENT_ANSWER | e('py') }}""", '''{{TEST.testcode}}''')



check_for_author
check_for_author(student_answer, existing_author=None)

Checks for an author tag in the javadoc comments. With an existing
author argument, checks that another author has been added.
This is in case the student was to modify existing code with an existing author.
If there's no additional author, then raises an error and stops further testing.

Example of use in template:

from java_code_checkr import check_for_author
check_for_author("""{{ STUDENT_ANSWER | e('py') }}""", '''{{QUESTION.parameters.existing_author}}''')

where the CodeRunner question type / Template params look like:

{"existing_author": "J. Random Author"}



check_for_enum
check_for_enum(student_answer, enum)

Verifies that the appropriate enum is declared.
If that's not the case, then raises an error and stops further testing.

Example of use in template:

from java_code_checkr import check_for_enum
check_for_enum("""{{ STUDENT_ANSWER | e('py') }}""", '''{{QUESTION.parameters.enum}}''')

where the CodeRunner question type / Template params look like:

{"enum": "DayOfWE"}



check_for_enum_in_switch
check_for_enum_in_switch(student_answer, enum_const)

Verifies that the appropriate enum constant is used in a switch case statement.
If that's not the case, then raises an error and stops further testing.

Example of use in template:

from java_code_checkr import check_for_enum_in_switch
check_for_author("""{{ STUDENT_ANSWER | e('py') }}""", '''{{QUESTION.parameters.enum_const}}''')

where the CodeRunner question type / Template params look like:

{{"enum": "DayOfWE", "enum_const": "SATURDAY"}}



check_for_extends
check_for_extends(student_answer, subclass, superclass)

Checks for 'class subclass extends superclass'.
If that's not the case, then raises an error and,stops further testing.

Example of use in template:

from java_code_checkr import check_for_extends
check_for_extends("""{{ STUDENT_ANSWER | e('py') }}""", '''{{QUESTION.parameters.subclass}}''', '''{{QUESTION.parameters.superclass}}''')

where the CodeRunner question type / Template params look like:

{"subclass": "Sub", "superclass":"Souper"}
In reply to Peter Sander

Re: Java packages

by Richard Lobb -

Thanks for posting, Peter. That looks like it should be very useful to other Java users.

Richard