diff --git a/java-src/net/sf/webcat/plugins/javatddplugin/ValidationMessageFormatter.java b/java-src/net/sf/webcat/plugins/javatddplugin/ValidationMessageFormatter.java
new file mode 100644
index 0000000..87649eb
--- /dev/null
+++ b/java-src/net/sf/webcat/plugins/javatddplugin/ValidationMessageFormatter.java
@@ -0,0 +1,76 @@
+package net.sf.webcat.plugins.javatddplugin;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ValidationMessageFormatter {
+
+ public static String formatMessage(Throwable error) {
+ if (error == null) {
+ return null;
+ }
+
+ if (error instanceof NoSuchMethodError) {
+ return "Your test has called a method that does not exist in the interface. "
+ + "The reference implementation is not guaranteed to implement this method "
+ + "and as such cannot validate this test case. "
+ + "The method is: " + error.getMessage();
+ }
+ else if (error instanceof AssertionError) {
+ String original = error.getMessage();
+ String msg;
+
+ if (original != null) {
+ Pattern p1 = Pattern.compile(
+ ".*expected:([^>]*)>? but was:([^>]*)>?");
+ Matcher m1 = p1.matcher(original);
+ if (m1.matches()) {
+ String expected = m1.group(1).trim();
+ String actual = m1.group(2).trim();
+ msg = "Your test expected: <" + expected + "> "
+ + "while the reference implementation returned: <"
+ + actual + ">";
+ }
+ else if (original.contains("expected null, but was:")) {
+ Pattern p2 = Pattern.compile(
+ ".*expected null, but was:([^>]*)>?");
+ Matcher m2 = p2.matcher(original);
+ if (m2.matches()) {
+ String actual = m2.group(1).trim();
+ msg = "Your test expected: "
+ + "while the reference implementation returned: <"
+ + actual + ">";
+ }
+ else {
+ msg = original;
+ }
+ }
+ else if (original.contains("expected same:")) {
+ Pattern p3 = Pattern.compile(
+ ".*expected same:([^>]*)>? was not:([^>]*)>?");
+ Matcher m3 = p3.matcher(original);
+ if (m3.matches()) {
+ String expected = m3.group(1).trim();
+ String actual = m3.group(2).trim();
+ msg = "Your test expected the *same* object: <"
+ + expected + "> "
+ + "while the reference implementation returned a different object: <"
+ + actual + ">";
+ }
+ else {
+ msg = original;
+ }
+ }
+ else {
+ msg = original;
+ }
+ }
+ else {
+ msg = original;
+ }
+ return msg;
+ }
+
+ return null;
+ }
+}
diff --git a/java-src/net/sf/webcat/plugins/javatddplugin/ValidationPlistJUnitResultFormatter.java b/java-src/net/sf/webcat/plugins/javatddplugin/ValidationPlistJUnitResultFormatter.java
new file mode 100644
index 0000000..dd43649
--- /dev/null
+++ b/java-src/net/sf/webcat/plugins/javatddplugin/ValidationPlistJUnitResultFormatter.java
@@ -0,0 +1,22 @@
+package net.sf.webcat.plugins.javatddplugin;
+
+import junit.framework.Test;
+
+public class ValidationPlistJUnitResultFormatter
+ extends PlistJUnitResultFormatter {
+ @Override
+ protected TestResultDescriptor describe(Test test, Throwable error) {
+ String customMsg = ValidationMessageFormatter.formatMessage(error);
+
+ if (customMsg != null) {
+ int code = codeOf(error);
+ int level = levelOf(code);
+ String stackTrace = stackTraceMessage(error, false);
+ return new TestResultDescriptor(currentSuite, test, error, code,
+ level, customMsg, stackTrace);
+ }
+ else {
+ return super.describe(test, error);
+ }
+ }
+}
diff --git a/java-src/net/sf/webcat/plugins/javatddplugin/ValidationTextJUnitResultFormatter.java b/java-src/net/sf/webcat/plugins/javatddplugin/ValidationTextJUnitResultFormatter.java
new file mode 100644
index 0000000..4863c5d
--- /dev/null
+++ b/java-src/net/sf/webcat/plugins/javatddplugin/ValidationTextJUnitResultFormatter.java
@@ -0,0 +1,61 @@
+package net.sf.webcat.plugins.javatddplugin;
+
+import junit.framework.Test;
+import junit.framework.AssertionFailedError;
+
+public class ValidationTextJUnitResultFormatter
+ extends BasicJUnitResultFormatter {
+
+ private static class FormattedThrowable extends Throwable {
+ private static final long serialVersionUID = 1L;
+ private final Throwable original;
+ private final String message;
+
+ public FormattedThrowable(String message, Throwable original) {
+ super(message, original);
+ this.message = message;
+ this.original = original;
+ this.setStackTrace(original.getStackTrace());
+ }
+
+
+ @Override
+ public String toString() {
+ return original.getClass().getName() + ": " + message;
+ }
+ }
+
+ @Override
+ public void addError(Test test, Throwable error) {
+ String msg = formatMessage(error);
+ Throwable newError = new FormattedThrowable(msg, error);
+ super.addError(test, newError);
+ }
+
+
+ @Override
+ public void addFailure(Test test, AssertionFailedError error) {
+ String msg = formatMessage(error);
+ Throwable newFailure = new FormattedThrowable(msg, error);
+ super.addFailure(test, newFailure);
+ }
+
+
+ private String formatMessage(Throwable error) {
+ String customMsg = ValidationMessageFormatter.formatMessage(error);
+
+ if (customMsg != null) {
+ return customMsg;
+ }
+
+ if (error != null) {
+ if (error.getMessage() != null) {
+ return error.getMessage();
+ }
+ else {
+ return error.toString();
+ }
+ }
+ return "";
+ }
+}
diff --git a/src/build.xml b/src/build.xml
index 5451e39..bcb7e22 100644
--- a/src/build.xml
+++ b/src/build.xml
@@ -9,6 +9,8 @@
+
+
@@ -54,6 +56,16 @@
-->
+
+
+
+
+
+
+
+
+
+
@@ -117,11 +129,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -179,6 +226,8 @@ PATH = ${printable.path}
timeout = ${exec.timeout} (for each of two test runs)
assignmentClassFiles = ${assignmentClassFiles}
assignmentClassDir = ${assignmentClassDir}
+referenceImplementationClassFiles = ${referenceImplementationClassFiles}
+referenceImplementationClassDir = ${referenceImplementationClassDir}
instructorClassFiles = ${instructorClassFiles}
instructorClassDir = ${instructorClassDir}
testCasePath = ${testCasePath}
@@ -199,8 +248,9 @@ staticAnalysisSrcExclusionPattern = ${staticAnalysisSrcExclusionPattern}
-
+
+
@@ -244,6 +294,18 @@ staticAnalysisSrcExclusionPattern = ${staticAnalysisSrcExclusionPattern}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -446,7 +608,6 @@ staticAnalysisSrcExclusionPattern = ${staticAnalysisSrcExclusionPattern}
-
-->
-
@@ -613,8 +773,7 @@ staticAnalysisSrcExclusionPattern = ${staticAnalysisSrcExclusionPattern}
failonerror="false"
failOnRuleViolation="false"
>
-
-
+
+
+
+
+
@@ -817,7 +980,7 @@ staticAnalysisSrcExclusionPattern = ${staticAnalysisSrcExclusionPattern}
The main target
============================================================ -->
-
+
diff --git a/src/checkstyle-10.7.0/checkstyle-10.7.0-all.jar b/src/checkstyle-10.7.0/checkstyle-10.7.0-all.jar
new file mode 100644
index 0000000..ae1d149
Binary files /dev/null and b/src/checkstyle-10.7.0/checkstyle-10.7.0-all.jar differ
diff --git a/src/checkstyle-10.7.0/checkstyle-10.7.0.xml b/src/checkstyle-10.7.0/checkstyle-10.7.0.xml
new file mode 100644
index 0000000..a1cadb7
--- /dev/null
+++ b/src/checkstyle-10.7.0/checkstyle-10.7.0.xml
@@ -0,0 +1,492 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/config.plist b/src/config.plist
index 8c1cb99..77829d9 100644
--- a/src/config.plist
+++ b/src/config.plist
@@ -1,874 +1,1163 @@
-{
- name = "JavaTddPlugin";
- version.major = 4;
- version.minor = 1;
- version.revision = 3;
- version.date = 20240916;
- autoPublish = true;
- requires = ( ANTForPlugins, PerlForPlugins,
- PMDForPlugins, CheckstyleForPlugins );
- provider = "Virginia Tech Computer Science";
- provider.url = "http://web-cat.org/updates";
- license = "GNU Affero General Public License v.3";
- license.url = "http://www.gnu.org/licenses/agpl.html";
- copyright =
- "(c) 2006-2025 Virginia Tech Department of Computer Science";
- info.url = "http://wiki.web-cat.org/WCWiki/JavaTddPlugin";
- history.url =
- "http://wiki.web-cat.org/WCWiki/JavaTddPlugin/ChangeHistory";
- executable = execute.pl;
- interpreter.prefix = "${PerlForPlugins.perl.exe}";
- author = "Stephen Edwards (edwards@cs.vt.edu)";
- authorUid = edwards;
- languages = ( { name = Java; version = 1.14; } );
- description = "This \"all-in-one\" plug-in is designed to provide full
- processing and feedback generation for Java assignments where
- students write their own JUnit test cases.
- It includes ANT-based compilation, JUnit processing of student-written
- tests, support for instructor-written reference tests, PMD and
- Checkstyle analysis, and Clover-based tracking of code coverage
- during student testing.";
- timeoutMultiplier = 2;
- timeoutInternalPadding = 400;
- assignmentOptions = (
- {
- property = testCases;
- type = fileOrDir;
- fileTypes = ( java );
- name = "Hidden JUnit Reference Test Class(es)";
- description =
- "A Java source file (or directory of source files) containing JUnit tests
- to run against student code to assess completeness of problem coverage.
- Test outcomes are not directly visible to students, an students only see
- hints about what may be incorrect.
- If you select a single Java file, it must contain a JUnit test class
- declared in the default package. If you select a directory, it should
- contain JUnit test classes arranged in subdirectories according to their
- Java package declarations. If you make no selection, an empty set of
- instructor reference tests will be used instead.";
- },
-/* {
- property = visibleTestCases;
- type = fileOrDir;
- fileTypes = ( java );
- name = "Visible JUnit Reference Test Class(es)";
- description =
- "A Java source file (or directory of source files) containing JUnit tests
- to run against student code to assess completeness of problem coverage.
- \"Visible\" here means pass/fail information for every test is shown (not
- limited by hint controls).
- If you select a single Java file, it must contain a JUnit test class
- declared in the default package. If you select a directory, it should
- contain JUnit test classes arranged in subdirectories according to their
- Java package declarations. If you make no selection, an empty set of
- instructor reference tests will be used instead.";
- },
-*/ {
- property = assignmentJar;
- type = fileOrDir;
- fileTypes = ( jar );
- name = "Supplemental Classes for Assignment";
- description =
- "A jar file (or a directory of class files in subdirs reflecting their
- package structure, or a directory of multiple jar files) containing
- precompiled classes to add to the classpath when compiling and running
- submissions for this assignment. If you want to apply the same
- jar settings to many assignments, use the \"Supplemental Classes\" setting
- in the \"Reusable Configuration Options\" section instead. If you have
- multiple jars to provide, place them all in the same directory in your
- Web-CAT file space and then select the whole directory.";
- },
- {
- property = localFiles;
- type = fileOrDir;
- name = "Data Files for Student";
- description =
- "A file (or a directory of files) to place in the student's current working
- directory when running his/her tests and when running reference tests. The
- file you select (or the entire contents of the directory you select) will be
- copied into the current working directory during grading so that
- student-written and instructor-written test cases can read and/or write to
- the file(s). The default is to copy no files.";
- }
- );
- optionCategories = (
- "Basic Settings",
- "Advanced Settings",
- "Experimental Settings",
- "Developer Settings"
- );
- options = (
- {
- property = useAssertions;
- type = boolean;
- default = true;
- name = "Use Java Assertions";
- category = "Basic Settings";
- description =
- "Enable Java assertions during execution. When set to false, assertions
- in student or instructor-provided code will be treated as non-executable
- (no-op's).";
- },
- {
- property = useDefaultJar;
- type = boolean;
- default = true;
- name = "Use Built-in Jars";
- category = "Basic Settings";
- description =
- "Set to true to have a set of built-in jars containing Virginia Tech
- CS 1/CS 2 classes placed on the classpath for assignments. Set to
- false to omit these jars from the classpath.";
- },
- {
- property = classpathJar;
- type = fileOrDir;
- fileTypes = ( jar );
- name = "Predefined Classes";
- category = "Basic Settings";
- description =
- "A jar file (or a directory of class files in subdirs reflecting their
- package structure, or a directory of multiple jar files) containing
- precompiled classes to add to the classpath when compiling and running
- submissions. Use this setting if you'd like to share the same jar(s)
- across several assignments. If you have multiple jars to provide,
- place them all in the same directory in your Web-CAT file space and
- then select the whole directory.";
- },
- {
- property = useXvfb;
- type = boolean;
- default = false;
- name = "Enable Xvfb During Test Execution";
- category = "Advanced Settings";
- description =
- "This option is necessary for running GUI software tests on Linux servers.
- It uses an instance of Xvfb, the X virtual frame buffer server, to run
- unit tests so that GUI tests can render to a live X server. Note that
- enabling this option will slow down test execution, since it takes time to
- start up an X server, but it is required for GUI testing on a linux
- server. Also, this option requires that Xvfb is already installed on
- the server.";
- },
- {
- property = policyFile;
- advanced = true;
- type = file;
- fileTypes = ( policy );
- name = "Java Security Policy";
- category = "Advanced Settings";
- description =
- "A Java security policy file used to limit actions on student programs at
- run-time. Leave unset to use the built-in default, which plugs most
- security holes and prevents any file system access outside the subtree
- rooted at the program's working directory.";
- },
- {
- property = remote.post.url;
- advanced = true;
- type = shortText;
- size = 40;
- name = "Remotely Post Submissions";
- category = "Advanced Settings";
- description =
- "A URL to which submissions will be posted as they are processed, to
- allow for external tools to receive student submissions. If a URL is
- specified an HTML POST request will be sent to the given URL, with the
- student's user name provided in the parameter 'user', and the student's
- submission file provided in the parameter 'uploadedfile'.";
- },
- {
- property = "grader.partnerExcludePatterns";
- advanced = true;
- type = shortText;
- size = 40;
- name = "Partner Name Exclude Patterns";
- category = "Basic Settings";
- description =
- "The plug-in will automatically scan @author tags in source code
- to try to identify the user names of partners when partners are allowed
- on an assignment. Here, you can provide a comma-separated list of names
- to exclude from consideration (e.g., the names of instructors, if some
- instructor names are listed in @author lines in pre-provided code).
- Full Perl-style regular expressions can be used if desired.";
- },
- {
- property = allStudentTestsMustPass;
- type = boolean;
- default = false;
- name = "All Student Tests Must Pass";
- category = "Basic Settings";
- description =
- "If you are truly following test-driven development practices, then no code
- is ever released until all of its unit tests pass. If this option is set to
- true, students will not receive a non-zero score or receive further
- assessment feedback unless all student tests pass. If this option is not
- set, then students may continue to proceed even if some student-written
- tests fail The student's correctness/testing score is multiplied by the
- proportion of their tests that pass.";
- },
- {
- property = studentsMustSubmitTests;
- type = boolean;
- default = true;
- name = "Students Must Submit Tests";
- category = "Basic Settings";
- description =
- "When set, this option requires all students to submit test cases for their
- own code. Submissions without test cases will received feedback to that
- effect (and no more), as well as a zero score. If you unset this option,
- then student submissions will not be required to include
- student-written test cases, and only the reference test pass rate
- will be used for scoring (i.e., student code coverage and student test pass
- rate will not be included in scoring).";
- },
- {
- property = includeStudentTestsInGrading;
- type = boolean;
- default = true;
- name = "Include Student Test Results in Grading";
- category = "Basic Settings";
- description =
- "When set, if students are required to submit tests, they are also included
- in the scoring formula. When false, student test pass/fail ressults are
- not included in the score calculation.";
- },
- {
- property = coverageMetric;
- advanced = true;
- type = radioChoice;
- name = "Test Coverage Metric";
- category = "Basic Settings";
- default = 0;
- description = "Choose the criterion used to measure how thoroughly
- a student's tests cover the corresponding code.";
- choices = ( { label = "Methods executed"; value = 0; },
- { label = "Lines executed"; value = 1; },
- { label = "Methods + conditions executed";
- value = 2; },
- { label = "Lines + conditions executed";
- value = 3; },
- { label = "Methods + lines + conditions executed";
- value = 4; }
- );
- },
- {
- property = coverageGoal;
- type = double;
- name = "Test Coverage Goal";
- category = "Basic Settings";
- description =
- "If students are required to submit tests, this value is the target test
- coverage threshold that must be achieved in order for students to receive
- full credit. It should be a number between 0.0-100.0 representing the
- minimum percent coverage required for full credit. The default is 100.0,
- but a lower value may be used to allow students some slack in test coverage
- when JaCoCo test coverage measures make achieving 100% too challenging.";
- },
- {
- property = includeTestSuitesInCoverage;
- type = boolean;
- default = false;
- name = "Include Student Test Code in Coverage Measures";
- category = "Basic Settings";
- description =
- "Normally, this plug-in excludes student-written tests from all coverage
- calculations, since the goal of the testing is to test the solution, not
- to execute more tests. When this option is set, student-written tests
- will be included in code coverage measures for the purposes of scoring,
- meaning that students will have to execute all of the code in their
- test classes.";
- },
- {
- property = requireSimpleExceptionCoverage;
- type = boolean;
- default = false;
- name = "Require Coverage of Simple Catch Blocks";
- category = "Basic Settings";
- description =
- "When set, this option requires students to test all catch blocks, including
- simple try/catch statements that are provided purely for compiler compliance
- and that simply print a stack trace or re-throw the exception. If unchecked,
- catch blocks that contain only a statement to print the stack trace or
- re-throw a wrapped version of the exception are not counted in
- coverage measurements, and do not result in point deductions.";
- },
- {
- property = requireSimpleGetterSetterCoverage;
- type = boolean;
- default = false;
- name = "Require Coverage of Simple Getters and Setters";
- category = "Basic Settings";
- description =
- "When set, this option requires students to test all simple getter and
- setter methods. A simple getter is a method whose name starts with
- \"get\", and whose body simply returns a field value. A simple setter is
- a void method whose name starts with \"set\" accepting one parameter, and
- whose body simply assigns that parameter to a field. If unchecked,
- simple getters and setters are not counted in coverage measurements, and
- do not result in point deductions.";
- },
-/*
- {
- property = "clover.includes";
- type = shortText;
- size = 40;
- default = "**";
- name = "Classes to Include in Coverage Measures";
- category = "Basic Settings";
- description =
- "Specify the Java file names that should be included in Clover coverage
- analysis. Only student class files with names that match the patterns you
- list here will be processed by Clover. Patterns are
- case-insensitive. Use * as a wildcard character (ANT-style pattern
- matching is used).";
- },
- {
- property = "clover.excludes";
- type = shortText;
- size = 40;
- default = "none";
- name = "Classes to Exclude from Coverage Measures";
- category = "Basic Settings";
- description =
- "Specify Java file names that should not be processed by
- Clover. Any classes that match the \"Classes to Include in Coverage
- Measures\" above and also match the patterns you list here
- will not be processed by Clover. Patterns are
- case-insensitive. Use * as a wildcard character (ANT-style pattern
- matching is used). Use \"none\" if you do not wish to use any
- exclusion patterns.";
- },
-*/
- {
- property = studentTestInclude;
- type = shortText;
- size = 40;
- default = "*test *tests";
- name = "Students Test Class Patterns";
- category = "Basic Settings";
- description =
- "Specify the Java class names that should be treated as JUnit-style
- test cases. Only student classes with names that match the patterns you
- list here will be executed as test cases. Patterns are case-insensitive.
- Use * as a wildcard character (ANT-style pattern matching is used).";
- },
- {
- property = studentTestExclude;
- type = shortText;
- size = 40;
- default = "abstract* *$*";
- name = "Students Test Class Exclusion Patterns";
- category = "Basic Settings";
- description =
- "Specify Java class names that should not be treated as JUnit-style
- test cases. Any classes that match the \"Student Test Class Patterns\"
- above and also match the patterns you list here
- will not be treated as executable test cases. Patterns are
- case-insensitive. Use * as a wildcard character (ANT-style pattern matching
- is used). Use \"none\" if you do not wish to use any exclusion patterns.";
- },
- {
- property = refTestInclude;
- type = shortText;
- size = 40;
- default = "*";
- name = "Reference Test Class Patterns";
- category = "Basic Settings";
- description =
- "Specify the Java class names that should be treated as JUnit-style
- test cases when selected as reference tests by an instructor. This
- setting is only relevant when an instructor selects an entire directory
- or folder of Java classes. In that case, only instructor reference classes
- with names that match the patterns you list here will be executed as test
- cases. Patterns are case-insensitive. Use * as a wildcard character
- (ANT-style pattern matching is used).";
- },
- {
- property = refTestExclude;
- type = shortText;
- size = 40;
- default = "abstract* *$*";
- name = "Reference Test Class Exclusion Patterns";
- category = "Basic Settings";
- description =
- "Specify Java class names that should not be treated as JUnit-style
- test cases when selected as reference tests by an instructor. This
- setting is only relevant when an instructor selects an entire directory or
- folder of Java classes. In that case, any classes that match the
- \"Reference Test Class Patterns\" above and also match the
- patterns you list here will not be treated as executable test cases.
- Patterns are case-insensitive. Use * as a wildcard character (ANT-style
- pattern matching is used).";
- },
- {
- property = student.testingsupport.junit4.AdaptiveTimeout.ceiling;
- advanced = true;
- type = integer;
- name = "Default Test Case Time Limit (in ms)";
- category = "Basic Settings";
- description =
- "This plug-in provides built-in adaptive detection and termination of
- infinite loops that occur within individual test methods. This setting
- controls the default timeout before a single test case method is judged as
- \"taking too long\", although this amount will be gradually increased up
- to a higher maximum value if test methods terminate but come close to
- this limit. The default if unset is ten seconds (10000 ms).";
- },
- {
- property = student.testingsupport.junit4.AdaptiveTimeout.maximum;
- advanced = true;
- type = integer;
- name = "Default Test Case Maximum Time (in ms)";
- category = "Basic Settings";
- description =
- "This plug-in provides built-in adaptive detection and termination of
- infinite loops that occur within individual test methods. This setting
- controls the maximum allowable timeout before a single test case is judged
- as \"taking too long\". The default if unset is twenty seconds (20000 ms).";
- },
- {
- property = disableCheckstyle;
- type = antBoolean;
- name = "Turn Checkstyle Off";
- category = "Basic Settings";
- description =
- "Disable Checkstyle for static analysis entirely (no need to upload a
- separate configuration file to turn it off).";
- },
- {
- property = checkstyleConfig;
- advanced = true;
- type = file;
- fileTypes = ( xml );
- name = "Checkstyle Configuration";
- category = "Basic Settings";
- description =
- "An XML file containing a Checkstyle rule configuration (see the
- Checksyle
- documentation). This plug-in uses Checkstyle v5.6. If you would
- like to turn off all Checkstyle checks entirely, use the \"Turn
- Checkstyle Off\" option instead.";
- },
- {
- property = disablePmd;
- type = antBoolean;
- name = "Turn PMD Off";
- category = "Basic Settings";
- description =
- "Disable PMD for static analysis entirely (no need to upload a
- separate configuration file to turn it off).";
- },
- {
- property = pmdConfig;
- advanced = true;
- type = file;
- fileTypes = ( xml );
- name = "PMD Configuration";
- category = "Basic Settings";
- description =
- "An XML file containing a set of PMD rules (see the
- PMD
- documentation). This plug-in uses PMD v5.0.5. If you owuld like to
- turn off all PMD checks entirely, use the \"Turn PMD Off\" options instead.";
- },
- {
- property = use.comtor;
- type = antBoolean;
- name = "Run COMTOR Comment Analyzer";
- category = "Experimental Settings";
- description =
- "Set to true to run the COMTOR Comment Analyzer and include the results
- as part of the overall feedback report.";
- },
- {
- property = staticAnalysisInclude;
- type = shortText;
- size = 40;
- default = "*";
- name = "Classes to Analyze";
- category = "Basic Settings";
- description =
- "Specify the Java class names that should be included in Checkstyle and
- PMD analysis. Only student classes with names that match the patterns you
- list here will be processed by Checkstyle and PMD. Patterns are
- case-insensitive. Use * as a wildcard character (ANT-style pattern
- matching is used).";
- },
- {
- property = staticAnalysisExclude;
- type = shortText;
- size = 40;
- default = "none";
- name = "Classes to Exclude from Analysis";
- category = "Basic Settings";
- description =
- "Specify Java class names that should not be processed by
- Checkstyle or PMD. Any classes that match the \"Classes to Analyze\"
- above and also match the patterns you list here
- will not be processed by Checkstyle or PMD. Patterns are
- case-insensitive. Use * as a wildcard character (ANT-style pattern
- matching is used). Use \"none\" if you do not wish to use any
- exclusion patterns.";
- },
- {
- property = markupProperties;
- advanced = true;
- type = file;
- fileTypes = ( properties );
- name = "Static Analysis Scoring Scheme";
- category = "Advanced Settings";
- description =
- "A Java properties file containing the point deductions and limits to
- use for messages generated by Checkstyle or PMD. The point deductions
- are specified in a fairly generic way so they can be used for many
- assignments. Deductions in the default scheme are typically 1, 2, or 5
- 'points', which are really simply relative weights. Specify a scaling
- factor below to adjust how these weights are translated into point
- deductions for a student.";
- },
- {
- property = toolDeductionScaleFactor;
- advanced = true;
- type = double;
- name = "Static Analysis Deduction Scaling Factor";
- category = "Advanced Settings";
- description =
- "The Static Analysis Scoring Scheme above defines the point deductions
- and limits to use for messages generated by Checkstyle or PMD in a generic
- way, with most deductions in the default scheme being 1, 2, or 5 points.
- Deductions in the static analysis scoring scheme are multiplied by this
- factor to translate them into actual 'point deductions' shown to the
- student.";
- },
- {
- property = hintsLimit;
- type = integer;
- default = 3;
- name = "Hints Limit";
- category = "Basic Settings";
- description =
- "Maximum number of hints the student will receive from failed reference
- tests.";
- },
- {
- property = minCoverageLevel;
- type = double;
- name = "Minimum Test Coverage for Hints";
- category = "Basic Settings";
- description =
- "If students are required to submit tests, this value is a minimum test
- coverage threshold that must be achieved in order for any hints to be
- given. It should be a number between 0.0-100.0 representing the minimum
- percent coverage required to see hints.";
- },
- {
- property = junitErrorsHideHints;
- type = boolean;
- default = false;
- name = "Clean JUnit Tests Required for Hints";
- category = "Basic Settings";
- description =
- "If students are required to submit tests, this option requires all test
- case classes to be free of PMD-based JUnit style errors, such as failing
- to include at least one test method in each test case class, failing
- to include assert*() calls in each test case method, or using \"bogus\"
- assertions such as assertEquals(1, 1).";
- },
- {
- property = hideHintsWithin;
- advanced = true;
- type = integer;
- default = 0;
- name = "Hide Hints X Days Before Deadline";
- category = "Advanced Settings";
- description =
- "Suppress all hints from failed reference tests for submissions within this
- many days of the deadline (set to zero for hints to always be visible).
- This setting allows the instructor to \"hide\" hints close to the assignment
- deadline in an attempt to encourage students to start working earlier.";
- },
- {
- property = showHintsWithin;
- advanced = true;
- type = integer;
- default = 0;
- name = "Show Hints X Days Before Deadline";
- category = "Advanced Settings";
- description =
- "Show hints (up to the Hints Limit) from failed reference tests for
- submissions within this many days of the deadline (only useful when Hide
- Hints X Days Before Deadline is non-zero, to restore hints as the
- deadline approaches).";
- },
- {
- property = wantPDF;
- type = boolean;
- default = false;
- name = "Generate PDF Printouts";
- category = "Basic Settings";
- description =
- "Set to true if you wish for a single PDF file containing a pretty-printed
- source code printout to be generated from the student's code. The printout
- will be downloadable by students, and accessible by TAs during grading.
- Note: This option uses both enscript and
- ps2pdf as external commands, and requires these programs to
- be correctly installed and configured.";
- },
-/*
- {
- property = enscriptStyle;
- type = shortText;
- size = 15;
- default = "msvc";
- name = "PDF Formatting Style (for Enscript)";
- category = "Basic Settings";
- description =
- "If you are generating PDF printouts, you can specify the formatting style
- used by enscript. This name will be passed to
- enscript using its --style= parameter. See your
- enscript documentation for more information about the styles that are
- supported. Many enscript installations support the following styles:
- a2ps, emacs, emacs-verbose,
- ifh, and msvc. It is possible to add your
- own custom formatting definitions to your enscript installation and then
- use your own style name here as well.";
- },
-*/
- {
- property = wantClassDiagrams;
- type = boolean;
- default = true;
- name = "Generate Class Diagrams";
- category = "Basic Settings";
- description =
- "Set to true if you wish to generate class diagrams from students' code.
- The diagrams will be viewable by students, and accessible by TAs during
- grading. Note: This option uses both doxygen and
- dot as external commands, and requires these programs to
- be correctly installed and configured in the JavaTddPlugin's global
- configuration options.";
- },
- {
- property = debug;
- type = integer;
- advanced = true;
- default = 0;
- name = "Debug Level";
- category = "Developer Settings";
- description =
- "Set to a non-zero value for the script to produce debugging output (the
- larger the number, the greater the detail, up to about 5). Debugging output
- on each grading script run will be e-mailed to the instructor.";
- },
- {
- property = doNotDelete;
- type = antBoolean;
- advanced = true;
- name = "Preserve Derived Files";
- category = "Developer Settings";
- description =
- "Set to true to prevent the plug-in from deleting the derived files it
- creates during the build/test process for each submission. Normally, these
- files are deleted when a given submission has been completely processed.
- This setting is provided for debugging purposes, when one wishes to
- inspect the intermediate test driver source code or other derived files.";
- },
- {
- property = generateHeatmaps;
- type = antBoolean;
- advanced = true;
- name = "Generate Bug Heatmaps";
- category = "Experimental Settings";
- description =
- "Set to true to generate GZoltar-based defect heatmaps. This option is
- experimental and for research use only. Using it will slow down
- generation of student feedback.";
- },
- {
- property = useEnhancedFeedback;
- type = boolean;
- advanced = true;
- default = false;
- name = "Use Enhanced Feedback (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to true to use the new (experimental) enhanced feedback layout.";
- },
- {
- property = useIndicatorFeedback;
- type = boolean;
- advanced = true;
- default = false;
- name = "Use Growth Mindset Feedback (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to true to use the new (experimental) growth mindset progress feedback.";
- },
- {
- property = useMaria;
- type = boolean;
- advanced = true;
- default = false;
- name = "Use Maria (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to true to show Maria (experimental), the virtual teaching assistant
- chatbot.";
- },
- {
- property = useMariaExplanations;
- type = boolean;
- advanced = true;
- default = false;
- name = "Use Maria Explanations (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to true to show \"Explain...\" links (experimental) by error messages.";
- },
- {
- property = useDailyMissions;
- type = boolean;
- advanced = true;
- default = false;
- name = "Use Daily Missions (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to true to give students daily mission challenges (experimental).";
- },
- {
- property = showAllTestOutcomes;
- type = boolean;
- advanced = true;
- default = false;
- name = "Show All Test Outcomes (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to true to use the new (experimental) feature to show all test outcomes
- instead of limited hints. Students still need to meet the coverage
- requirements and other requirements to see test outcomes, but will see
- the full table of tests instead of limited hints.";
- },
- {
- property = useFindBugs;
- type = boolean;
- advanced = true;
- default = false;
- name = "Use FindBugs (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to true to add FindBugs analysis to the static analysis scoring of
- the assignment. This will cause FindBugs to be run on student submissions,
- and the results will be used to give feedback to students. Not all FindBugs
- warnings/errors will be shown--the specific list used is research-driven.
- There currently are no controls for changing the scoring scheme or enabling
- or disabling specific FindBugs checks.";
- },
- {
- property = usePit;
- type = antBoolean;
- advanced = true;
- default = false;
- name = "Use PIT Mutation Analysis (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to true to turn on mutation analysis for evaluating student-written
- test suites. This is highly experimental and not advised for production
- use except in research settings.";
- },
- {
- property = useEMRN;
- type = antBoolean;
- advanced = true;
- default = false;
- name = "Use EMRN Grading (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to true to turn on EMRN grading scale support instead of points.";
- },
- {
- property = useEMRNManual;
- type = antBoolean;
- advanced = true;
- default = false;
- name = "Use EMRN with Manual Grading (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to true when using EMRN to save the EMR distinctions for manual
- grading, or leave false for EMRN with full auto-grading.";
- },
- {
- property = emrnExcellent;
- type = integer;
- advanced = true;
- default = 100;
- name = "EMRN Point Value - Excellent (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to the value to use for EMRN (E) scores (points, not percentage)
- when not using manual grading.";
- },
- {
- property = emrnMeetsExpectations;
- type = integer;
- advanced = true;
- default = 100;
- name = "EMRN Point Value - Meets Expectations (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to the value to use for EMRN (M) scores (points, not percentage)
- when not using manual grading.";
- },
- {
- property = emrnRevisionNeeded;
- type = integer;
- advanced = true;
- default = 10;
- name = "EMRN Point Value - Revision Needed (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to the value to use for EMRN (R) scores (points, not percentage)
- when not using manual grading.";
- },
- {
- property = useJdk11;
- type = boolean;
- advanced = true;
- default = false;
- name = "Use Java 11 (Experimental)";
- category = "Experimental Settings";
- description =
- "Set to true to switch to Java 11.";
- }
- );
- globalOptions = (
- {
- property = doxygenDir;
- type = shortText;
- size = 40;
- name = "Doxygen Directory";
- description =
- "The directory on the local server that contains the Doxygen executable.
- Doxygen (and Dot, below) are used to generate class diagrams for the code
- that students submit; if you do not have a copy of Doxygen or wish to
- disable diagram generation across all assignments, you can leave this
- field blank. If you are not the user administering this Web-CAT server,
- you will need to have your system administrator install this tool and set
- this path if you wish to use it.";
- },
- {
- property = dotDir;
- type = shortText;
- size = 40;
- name = "Dot Directory";
- description =
- "The directory on the local server that contains the Dot executable (from
- the Graphviz package). Dot (and Doxygen, above) are used to generate
- class diagrams for the code that students submit; if you do not have a copy
- of Dot or wish to disable diagram generation across all assignments, you can
- leave this field blank. If you are not the user administering this Web-CAT
- server, you will need to have your system administrator install this tool
- and set this path if you wish to use it.";
- }
- );
-}
+{
+ name = "JavaTddPlugin";
+ version.major = 4;
+ version.minor = 2;
+ version.revision = 0;
+ version.date = 20260526;
+ autoPublish = true;
+ requires = ( ANTForPlugins, PerlForPlugins,
+ PMDForPlugins, CheckstyleForPlugins );
+ provider = "Virginia Tech Computer Science";
+ provider.url = "http://web-cat.org/updates";
+ license = "GNU Affero General Public License v.3";
+ license.url = "http://www.gnu.org/licenses/agpl.html";
+ copyright =
+ "(c) 2006-2026 Virginia Tech Department of Computer Science";
+ info.url = "http://wiki.web-cat.org/WCWiki/JavaTddPlugin";
+ history.url =
+ "http://wiki.web-cat.org/WCWiki/JavaTddPlugin/ChangeHistory";
+ executable = execute.pl;
+ interpreter.prefix = "${PerlForPlugins.perl.exe}";
+ author = "Stephen Edwards (edwards@cs.vt.edu)";
+ authorUid = edwards;
+ languages = ( { name = Java; version = 1.14; } );
+ description = "This \"all-in-one\" plug-in is designed to provide full
+ processing and feedback generation for Java assignments where
+ students write their own JUnit test cases.
+ It includes ANT-based compilation, JUnit processing of student-written
+ tests, support for instructor-written reference tests, PMD and
+ Checkstyle analysis, and Clover-based tracking of code coverage
+ during student testing.";
+ timeoutMultiplier = 2;
+ timeoutInternalPadding = 400;
+ assignmentOptions = (
+ {
+ property = testCases;
+ type = fileOrDir;
+ fileTypes = ( java );
+ name = "Hidden JUnit Reference Test Class(es)";
+ description =
+ "A Java source file (or directory of source files) containing JUnit tests
+ to run against student code to assess completeness of problem coverage.
+ Test outcomes are not directly visible to students, an students only see
+ hints about what may be incorrect.
+ If you select a single Java file, it must contain a JUnit test class
+ declared in the default package. If you select a directory, it should
+ contain JUnit test classes arranged in subdirectories according to their
+ Java package declarations. If you make no selection, an empty set of
+ instructor reference tests will be used instead.";
+ },
+/* {
+ property = visibleTestCases;
+ type = fileOrDir;
+ fileTypes = ( java );
+ name = "Visible JUnit Reference Test Class(es)";
+ description =
+ "A Java source file (or directory of source files) containing JUnit tests
+ to run against student code to assess completeness of problem coverage.
+ \"Visible\" here means pass/fail information for every test is shown (not
+ limited by hint controls).
+ If you select a single Java file, it must contain a JUnit test class
+ declared in the default package. If you select a directory, it should
+ contain JUnit test classes arranged in subdirectories according to their
+ Java package declarations. If you make no selection, an empty set of
+ instructor reference tests will be used instead.";
+ },
+*/ {
+ property = assignmentJar;
+ type = fileOrDir;
+ fileTypes = ( jar );
+ name = "Supplemental Classes for Assignment";
+ description =
+ "A jar file (or a directory of class files in subdirs reflecting their
+ package structure, or a directory of multiple jar files) containing
+ precompiled classes to add to the classpath when compiling and running
+ submissions for this assignment. If you want to apply the same
+ jar settings to many assignments, use the \"Supplemental Classes\" setting
+ in the \"Reusable Configuration Options\" section instead. If you have
+ multiple jars to provide, place them all in the same directory in your
+ Web-CAT file space and then select the whole directory.";
+ },
+ {
+ property = localFiles;
+ type = fileOrDir;
+ name = "Data Files for Student";
+ description =
+ "A file (or a directory of files) to place in the student's current working
+ directory when running his/her tests and when running reference tests. The
+ file you select (or the entire contents of the directory you select) will be
+ copied into the current working directory during grading so that
+ student-written and instructor-written test cases can read and/or write to
+ the file(s). The default is to copy no files.";
+ },
+ {
+ property = referenceImplementationJar;
+ type = fileOrDir;
+ fileTypes = ( jar );
+ name = "Reference Implementation Jar File";
+ description =
+ "A jar file containing a precompiled reference implementation for the
+ project to run against student tests to assess completeness of testing
+ coverage. Test outcomes are directly visible to students. This solution
+ is for this assignment only. If you have multiple jars to provide,
+ place them all in the same directory in your Web-CAT file space and then
+ select the whole directory.";
+ },
+ {
+ property = testCaseValidationFileName;
+ type = shortText;
+ size = 40;
+ default = "ProblemSpecTest";
+ name = "Test Case Validation File Name";
+ description =
+ "Specify the java class name to use for test case validation.";
+ },
+ {
+ property = coverageGoal;
+ type = double;
+ name = "Test Coverage Goal";
+ category = "Basic Settings";
+ description =
+ "If students are required to submit tests, this value is the target test
+ coverage threshold that must be achieved in order for students to receive
+ full credit. It should be a number between 0.0-100.0 representing the
+ minimum percent coverage required for full credit. The default is 100.0,
+ but a lower value may be used to allow students some slack in test coverage
+ when JaCoCo test coverage measures make achieving 100% too challenging.";
+ }
+ );
+ optionCategories = (
+ "Basic Settings",
+ "Advanced Settings",
+ "Experimental Settings",
+ "Developer Settings",
+ "Milestone Settings"
+ );
+ options = (
+ {
+ property = useAssertions;
+ type = boolean;
+ default = true;
+ name = "Use Java Assertions";
+ category = "Basic Settings";
+ description =
+ "Enable Java assertions during execution. When set to false, assertions
+ in student or instructor-provided code will be treated as non-executable
+ (no-op's).";
+ },
+ {
+ property = useDefaultJar;
+ type = boolean;
+ default = true;
+ name = "Use Built-in Jars";
+ category = "Basic Settings";
+ description =
+ "Set to true to have a set of built-in jars containing Virginia Tech
+ CS 1/CS 2 classes placed on the classpath for assignments. Set to
+ false to omit these jars from the classpath.";
+ },
+ {
+ property = classpathJar;
+ type = fileOrDir;
+ fileTypes = ( jar );
+ name = "Predefined Classes";
+ category = "Basic Settings";
+ description =
+ "A jar file (or a directory of class files in subdirs reflecting their
+ package structure, or a directory of multiple jar files) containing
+ precompiled classes to add to the classpath when compiling and running
+ submissions. Use this setting if you'd like to share the same jar(s)
+ across several assignments. If you have multiple jars to provide,
+ place them all in the same directory in your Web-CAT file space and
+ then select the whole directory.";
+ },
+ {
+ property = useXvfb;
+ type = boolean;
+ default = false;
+ name = "Enable Xvfb During Test Execution";
+ category = "Advanced Settings";
+ description =
+ "This option is necessary for running GUI software tests on Linux servers.
+ It uses an instance of Xvfb, the X virtual frame buffer server, to run
+ unit tests so that GUI tests can render to a live X server. Note that
+ enabling this option will slow down test execution, since it takes time to
+ start up an X server, but it is required for GUI testing on a linux
+ server. Also, this option requires that Xvfb is already installed on
+ the server.";
+ },
+ {
+ property = policyFile;
+ advanced = true;
+ type = file;
+ fileTypes = ( policy );
+ name = "Java Security Policy";
+ category = "Advanced Settings";
+ description =
+ "A Java security policy file used to limit actions on student programs at
+ run-time. Leave unset to use the built-in default, which plugs most
+ security holes and prevents any file system access outside the subtree
+ rooted at the program's working directory.";
+ },
+ {
+ property = remote.post.url;
+ advanced = true;
+ type = shortText;
+ size = 40;
+ name = "Remotely Post Submissions";
+ category = "Advanced Settings";
+ description =
+ "A URL to which submissions will be posted as they are processed, to
+ allow for external tools to receive student submissions. If a URL is
+ specified an HTML POST request will be sent to the given URL, with the
+ student's user name provided in the parameter 'user', and the student's
+ submission file provided in the parameter 'uploadedfile'.";
+ },
+ {
+ property = "grader.partnerExcludePatterns";
+ advanced = true;
+ type = shortText;
+ size = 40;
+ name = "Partner Name Exclude Patterns";
+ category = "Basic Settings";
+ description =
+ "The plug-in will automatically scan @author tags in source code
+ to try to identify the user names of partners when partners are allowed
+ on an assignment. Here, you can provide a comma-separated list of names
+ to exclude from consideration (e.g., the names of instructors, if some
+ instructor names are listed in @author lines in pre-provided code).
+ Full Perl-style regular expressions can be used if desired.";
+ },
+ {
+ property = allStudentTestsMustPass;
+ type = boolean;
+ default = false;
+ name = "All Student Tests Must Pass";
+ category = "Basic Settings";
+ description =
+ "If you are truly following test-driven development practices, then no code
+ is ever released until all of its unit tests pass. If this option is set to
+ true, students will not receive a non-zero score or receive further
+ assessment feedback unless all student tests pass. If this option is not
+ set, then students may continue to proceed even if some student-written
+ tests fail The student's correctness/testing score is multiplied by the
+ proportion of their tests that pass.";
+ },
+ {
+ property = studentsMustSubmitTests;
+ type = boolean;
+ default = true;
+ name = "Students Must Submit Tests";
+ category = "Basic Settings";
+ description =
+ "When set, this option requires all students to submit test cases for their
+ own code. Submissions without test cases will received feedback to that
+ effect (and no more), as well as a zero score. If you unset this option,
+ then student submissions will not be required to include
+ student-written test cases, and only the reference test pass rate
+ will be used for scoring (i.e., student code coverage and student test pass
+ rate will not be included in scoring).";
+ },
+ {
+ property = includeStudentTestsInGrading;
+ type = boolean;
+ default = true;
+ name = "Include Student Test Results in Grading";
+ category = "Basic Settings";
+ description =
+ "When set, if students are required to submit tests, they are also included
+ in the scoring formula. When false, student test pass/fail ressults are
+ not included in the score calculation.";
+ },
+ {
+ property = coverageMetric;
+ advanced = true;
+ type = radioChoice;
+ name = "Test Coverage Metric";
+ category = "Basic Settings";
+ default = 0;
+ description = "Choose the criterion used to measure how thoroughly
+ a student's tests cover the corresponding code.";
+ choices = ( { label = "Methods executed"; value = 0; },
+ { label = "Lines executed"; value = 1; },
+ { label = "Methods + conditions executed";
+ value = 2; },
+ { label = "Lines + conditions executed";
+ value = 3; },
+ { label = "Methods + lines + conditions executed";
+ value = 4; }
+ );
+ },
+ {
+ property = includeTestSuitesInCoverage;
+ type = boolean;
+ default = false;
+ name = "Include Student Test Code in Coverage Measures";
+ category = "Basic Settings";
+ description =
+ "Normally, this plug-in excludes student-written tests from all coverage
+ calculations, since the goal of the testing is to test the solution, not
+ to execute more tests. When this option is set, student-written tests
+ will be included in code coverage measures for the purposes of scoring,
+ meaning that students will have to execute all of the code in their
+ test classes.";
+ },
+ {
+ property = requireSimpleExceptionCoverage;
+ type = boolean;
+ default = false;
+ name = "Require Coverage of Simple Catch Blocks";
+ category = "Basic Settings";
+ description =
+ "When set, this option requires students to test all catch blocks, including
+ simple try/catch statements that are provided purely for compiler compliance
+ and that simply print a stack trace or re-throw the exception. If unchecked,
+ catch blocks that contain only a statement to print the stack trace or
+ re-throw a wrapped version of the exception are not counted in
+ coverage measurements, and do not result in point deductions.";
+ },
+ {
+ property = requireSimpleGetterSetterCoverage;
+ type = boolean;
+ default = false;
+ name = "Require Coverage of Simple Getters and Setters";
+ category = "Basic Settings";
+ description =
+ "When set, this option requires students to test all simple getter and
+ setter methods. A simple getter is a method whose name starts with
+ \"get\", and whose body simply returns a field value. A simple setter is
+ a void method whose name starts with \"set\" accepting one parameter, and
+ whose body simply assigns that parameter to a field. If unchecked,
+ simple getters and setters are not counted in coverage measurements, and
+ do not result in point deductions.";
+ },
+/*
+ {
+ property = "clover.includes";
+ type = shortText;
+ size = 40;
+ default = "**";
+ name = "Classes to Include in Coverage Measures";
+ category = "Basic Settings";
+ description =
+ "Specify the Java file names that should be included in Clover coverage
+ analysis. Only student class files with names that match the patterns you
+ list here will be processed by Clover. Patterns are
+ case-insensitive. Use * as a wildcard character (ANT-style pattern
+ matching is used).";
+ },
+ {
+ property = "clover.excludes";
+ type = shortText;
+ size = 40;
+ default = "none";
+ name = "Classes to Exclude from Coverage Measures";
+ category = "Basic Settings";
+ description =
+ "Specify Java file names that should not be processed by
+ Clover. Any classes that match the \"Classes to Include in Coverage
+ Measures\" above and also match the patterns you list here
+ will not be processed by Clover. Patterns are
+ case-insensitive. Use * as a wildcard character (ANT-style pattern
+ matching is used). Use \"none\" if you do not wish to use any
+ exclusion patterns.";
+ },
+*/
+ {
+ property = studentTestInclude;
+ type = shortText;
+ size = 40;
+ default = "*test *tests";
+ name = "Students Test Class Patterns";
+ category = "Basic Settings";
+ description =
+ "Specify the Java class names that should be treated as JUnit-style
+ test cases. Only student classes with names that match the patterns you
+ list here will be executed as test cases. Patterns are case-insensitive.
+ Use * as a wildcard character (ANT-style pattern matching is used).";
+ },
+ {
+ property = studentTestExclude;
+ type = shortText;
+ size = 40;
+ default = "abstract* *$*";
+ name = "Students Test Class Exclusion Patterns";
+ category = "Basic Settings";
+ description =
+ "Specify Java class names that should not be treated as JUnit-style
+ test cases. Any classes that match the \"Student Test Class Patterns\"
+ above and also match the patterns you list here
+ will not be treated as executable test cases. Patterns are
+ case-insensitive. Use * as a wildcard character (ANT-style pattern matching
+ is used). Use \"none\" if you do not wish to use any exclusion patterns.";
+ },
+ {
+ property = refTestInclude;
+ type = shortText;
+ size = 40;
+ default = "*";
+ name = "Reference Test Class Patterns";
+ category = "Basic Settings";
+ description =
+ "Specify the Java class names that should be treated as JUnit-style
+ test cases when selected as reference tests by an instructor. This
+ setting is only relevant when an instructor selects an entire directory
+ or folder of Java classes. In that case, only instructor reference classes
+ with names that match the patterns you list here will be executed as test
+ cases. Patterns are case-insensitive. Use * as a wildcard character
+ (ANT-style pattern matching is used).";
+ },
+ {
+ property = refTestExclude;
+ type = shortText;
+ size = 40;
+ default = "abstract* *$*";
+ name = "Reference Test Class Exclusion Patterns";
+ category = "Basic Settings";
+ description =
+ "Specify Java class names that should not be treated as JUnit-style
+ test cases when selected as reference tests by an instructor. This
+ setting is only relevant when an instructor selects an entire directory or
+ folder of Java classes. In that case, any classes that match the
+ \"Reference Test Class Patterns\" above and also match the
+ patterns you list here will not be treated as executable test cases.
+ Patterns are case-insensitive. Use * as a wildcard character (ANT-style
+ pattern matching is used).";
+ },
+ {
+ property = student.testingsupport.junit4.AdaptiveTimeout.ceiling;
+ advanced = true;
+ type = integer;
+ name = "Default Test Case Time Limit (in ms)";
+ category = "Basic Settings";
+ description =
+ "This plug-in provides built-in adaptive detection and termination of
+ infinite loops that occur within individual test methods. This setting
+ controls the default timeout before a single test case method is judged as
+ \"taking too long\", although this amount will be gradually increased up
+ to a higher maximum value if test methods terminate but come close to
+ this limit. The default if unset is ten seconds (10000 ms).";
+ },
+ {
+ property = student.testingsupport.junit4.AdaptiveTimeout.maximum;
+ advanced = true;
+ type = integer;
+ name = "Default Test Case Maximum Time (in ms)";
+ category = "Basic Settings";
+ description =
+ "This plug-in provides built-in adaptive detection and termination of
+ infinite loops that occur within individual test methods. This setting
+ controls the maximum allowable timeout before a single test case is judged
+ as \"taking too long\". The default if unset is twenty seconds (20000 ms).";
+ },
+ {
+ property = disableCheckstyle;
+ type = antBoolean;
+ name = "Turn Checkstyle Off";
+ category = "Basic Settings";
+ description =
+ "Disable Checkstyle for static analysis entirely (no need to upload a
+ separate configuration file to turn it off).";
+ },
+ {
+ property = checkstyleConfig;
+ advanced = true;
+ type = file;
+ fileTypes = ( xml );
+ name = "Checkstyle Configuration";
+ category = "Basic Settings";
+ description =
+ "An XML file containing a Checkstyle rule configuration (see the
+ Checksyle
+ documentation). This plug-in uses Checkstyle v5.6. If you would
+ like to turn off all Checkstyle checks entirely, use the \"Turn
+ Checkstyle Off\" option instead.";
+ },
+ {
+ property = disablePmd;
+ type = antBoolean;
+ name = "Turn PMD Off";
+ category = "Basic Settings";
+ description =
+ "Disable PMD for static analysis entirely (no need to upload a
+ separate configuration file to turn it off).";
+ },
+ {
+ property = pmdConfig;
+ advanced = true;
+ type = file;
+ fileTypes = ( xml );
+ name = "PMD Configuration";
+ category = "Basic Settings";
+ description =
+ "An XML file containing a set of PMD rules (see the
+ PMD
+ documentation). This plug-in uses PMD v5.0.5. If you owuld like to
+ turn off all PMD checks entirely, use the \"Turn PMD Off\" options instead.";
+ },
+ {
+ property = use.comtor;
+ type = antBoolean;
+ name = "Run COMTOR Comment Analyzer";
+ category = "Experimental Settings";
+ description =
+ "Set to true to run the COMTOR Comment Analyzer and include the results
+ as part of the overall feedback report.";
+ },
+ {
+ property = staticAnalysisInclude;
+ type = shortText;
+ size = 40;
+ default = "*";
+ name = "Classes to Analyze";
+ category = "Basic Settings";
+ description =
+ "Specify the Java class names that should be included in Checkstyle and
+ PMD analysis. Only student classes with names that match the patterns you
+ list here will be processed by Checkstyle and PMD. Patterns are
+ case-insensitive. Use * as a wildcard character (ANT-style pattern
+ matching is used).";
+ },
+ {
+ property = staticAnalysisExclude;
+ type = shortText;
+ size = 40;
+ default = "none";
+ name = "Classes to Exclude from Analysis";
+ category = "Basic Settings";
+ description =
+ "Specify Java class names that should not be processed by
+ Checkstyle or PMD. Any classes that match the \"Classes to Analyze\"
+ above and also match the patterns you list here
+ will not be processed by Checkstyle or PMD. Patterns are
+ case-insensitive. Use * as a wildcard character (ANT-style pattern
+ matching is used). Use \"none\" if you do not wish to use any
+ exclusion patterns.";
+ },
+ {
+ property = markupProperties;
+ advanced = true;
+ type = file;
+ fileTypes = ( properties );
+ name = "Static Analysis Scoring Scheme";
+ category = "Advanced Settings";
+ description =
+ "A Java properties file containing the point deductions and limits to
+ use for messages generated by Checkstyle or PMD. The point deductions
+ are specified in a fairly generic way so they can be used for many
+ assignments. Deductions in the default scheme are typically 1, 2, or 5
+ 'points', which are really simply relative weights. Specify a scaling
+ factor below to adjust how these weights are translated into point
+ deductions for a student.";
+ },
+ {
+ property = toolDeductionScaleFactor;
+ advanced = true;
+ type = double;
+ name = "Static Analysis Deduction Scaling Factor";
+ category = "Advanced Settings";
+ description =
+ "The Static Analysis Scoring Scheme above defines the point deductions
+ and limits to use for messages generated by Checkstyle or PMD in a generic
+ way, with most deductions in the default scheme being 1, 2, or 5 points.
+ Deductions in the static analysis scoring scheme are multiplied by this
+ factor to translate them into actual 'point deductions' shown to the
+ student.";
+ },
+ {
+ property = hintsLimit;
+ type = integer;
+ default = 3;
+ name = "Hints Limit";
+ category = "Basic Settings";
+ description =
+ "Maximum number of hints the student will receive from failed reference
+ tests.";
+ },
+ {
+ property = minCoverageLevel;
+ type = double;
+ name = "Minimum Test Coverage for Hints";
+ category = "Basic Settings";
+ description =
+ "If students are required to submit tests, this value is a minimum test
+ coverage threshold that must be achieved in order for any hints to be
+ given. It should be a number between 0.0-100.0 representing the minimum
+ percent coverage required to see hints.";
+ },
+ {
+ property = junitErrorsHideHints;
+ type = boolean;
+ default = false;
+ name = "Clean JUnit Tests Required for Hints";
+ category = "Basic Settings";
+ description =
+ "If students are required to submit tests, this option requires all test
+ case classes to be free of PMD-based JUnit style errors, such as failing
+ to include at least one test method in each test case class, failing
+ to include assert*() calls in each test case method, or using \"bogus\"
+ assertions such as assertEquals(1, 1).";
+ },
+ {
+ property = hideHintsWithin;
+ advanced = true;
+ type = integer;
+ default = 0;
+ name = "Hide Hints X Days Before Deadline";
+ category = "Advanced Settings";
+ description =
+ "Suppress all hints from failed reference tests for submissions within this
+ many days of the deadline (set to zero for hints to always be visible).
+ This setting allows the instructor to \"hide\" hints close to the assignment
+ deadline in an attempt to encourage students to start working earlier.";
+ },
+ {
+ property = showHintsWithin;
+ advanced = true;
+ type = integer;
+ default = 0;
+ name = "Show Hints X Days Before Deadline";
+ category = "Advanced Settings";
+ description =
+ "Show hints (up to the Hints Limit) from failed reference tests for
+ submissions within this many days of the deadline (only useful when Hide
+ Hints X Days Before Deadline is non-zero, to restore hints as the
+ deadline approaches).";
+ },
+ {
+ property = wantPDF;
+ type = boolean;
+ default = false;
+ name = "Generate PDF Printouts";
+ category = "Basic Settings";
+ description =
+ "Set to true if you wish for a single PDF file containing a pretty-printed
+ source code printout to be generated from the student's code. The printout
+ will be downloadable by students, and accessible by TAs during grading.
+ Note: This option uses both enscript and
+ ps2pdf as external commands, and requires these programs to
+ be correctly installed and configured.";
+ },
+/*
+ {
+ property = enscriptStyle;
+ type = shortText;
+ size = 15;
+ default = "msvc";
+ name = "PDF Formatting Style (for Enscript)";
+ category = "Basic Settings";
+ description =
+ "If you are generating PDF printouts, you can specify the formatting style
+ used by enscript. This name will be passed to
+ enscript using its --style= parameter. See your
+ enscript documentation for more information about the styles that are
+ supported. Many enscript installations support the following styles:
+ a2ps, emacs, emacs-verbose,
+ ifh, and msvc. It is possible to add your
+ own custom formatting definitions to your enscript installation and then
+ use your own style name here as well.";
+ },
+*/
+ {
+ property = wantClassDiagrams;
+ type = boolean;
+ default = true;
+ name = "Generate Class Diagrams";
+ category = "Basic Settings";
+ description =
+ "Set to true if you wish to generate class diagrams from students' code.
+ The diagrams will be viewable by students, and accessible by TAs during
+ grading. Note: This option uses both doxygen and
+ dot as external commands, and requires these programs to
+ be correctly installed and configured in the JavaTddPlugin's global
+ configuration options.";
+ },
+ {
+ property = debug;
+ type = integer;
+ advanced = true;
+ default = 0;
+ name = "Debug Level";
+ category = "Developer Settings";
+ description =
+ "Set to a non-zero value for the script to produce debugging output (the
+ larger the number, the greater the detail, up to about 5). Debugging output
+ on each grading script run will be e-mailed to the instructor.";
+ },
+ {
+ property = doNotDelete;
+ type = antBoolean;
+ advanced = true;
+ name = "Preserve Derived Files";
+ category = "Developer Settings";
+ description =
+ "Set to true to prevent the plug-in from deleting the derived files it
+ creates during the build/test process for each submission. Normally, these
+ files are deleted when a given submission has been completely processed.
+ This setting is provided for debugging purposes, when one wishes to
+ inspect the intermediate test driver source code or other derived files.";
+ },
+ {
+ property = generateHeatmaps;
+ type = antBoolean;
+ advanced = true;
+ name = "Generate Bug Heatmaps";
+ category = "Experimental Settings";
+ description =
+ "Set to true to generate GZoltar-based defect heatmaps. This option is
+ experimental and for research use only. Using it will slow down
+ generation of student feedback.";
+ },
+ {
+ property = useEnhancedFeedback;
+ type = boolean;
+ advanced = true;
+ default = false;
+ name = "Use Enhanced Feedback (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to true to use the new (experimental) enhanced feedback layout.";
+ },
+ {
+ property = useIndicatorFeedback;
+ type = boolean;
+ advanced = true;
+ default = false;
+ name = "Use Growth Mindset Feedback (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to true to use the new (experimental) growth mindset progress feedback.";
+ },
+ {
+ property = useMaria;
+ type = boolean;
+ advanced = true;
+ default = false;
+ name = "Use Maria (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to true to show Maria (experimental), the virtual teaching assistant
+ chatbot.";
+ },
+ {
+ property = useMariaExplanations;
+ type = boolean;
+ advanced = true;
+ default = false;
+ name = "Use Maria Explanations (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to true to show \"Explain...\" links (experimental) by error messages.";
+ },
+ {
+ property = useDailyMissions;
+ type = boolean;
+ advanced = true;
+ default = false;
+ name = "Use Daily Missions (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to true to give students daily mission challenges (experimental).";
+ },
+ {
+ property = showAllTestOutcomes;
+ type = boolean;
+ advanced = true;
+ default = false;
+ name = "Show All Test Outcomes (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to true to use the new (experimental) feature to show all test outcomes
+ instead of limited hints. Students still need to meet the coverage
+ requirements and other requirements to see test outcomes, but will see
+ the full table of tests instead of limited hints.";
+ },
+ {
+ property = useFindBugs;
+ type = boolean;
+ advanced = true;
+ default = false;
+ name = "Use FindBugs (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to true to add FindBugs analysis to the static analysis scoring of
+ the assignment. This will cause FindBugs to be run on student submissions,
+ and the results will be used to give feedback to students. Not all FindBugs
+ warnings/errors will be shown--the specific list used is research-driven.
+ There currently are no controls for changing the scoring scheme or enabling
+ or disabling specific FindBugs checks.";
+ },
+ {
+ property = usePit;
+ type = antBoolean;
+ advanced = true;
+ default = false;
+ name = "Use PIT Mutation Analysis (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to true to turn on mutation analysis for evaluating student-written
+ test suites. This is highly experimental and not advised for production
+ use except in research settings.";
+ },
+ {
+ property = useEMRN;
+ type = antBoolean;
+ advanced = true;
+ default = false;
+ name = "Use EMRN Grading (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to true to turn on EMRN grading scale support instead of points.";
+ },
+ {
+ property = useEMRNManual;
+ type = antBoolean;
+ advanced = true;
+ default = false;
+ name = "Use EMRN with Manual Grading (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to true when using EMRN to save the EMR distinctions for manual
+ grading, or leave false for EMRN with full auto-grading.";
+ },
+ {
+ property = emrnExcellent;
+ type = integer;
+ advanced = true;
+ default = 100;
+ name = "EMRN Point Value - Excellent (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to the value to use for EMRN (E) scores (points, not percentage)
+ when not using manual grading.";
+ },
+ {
+ property = emrnMeetsExpectations;
+ type = integer;
+ advanced = true;
+ default = 100;
+ name = "EMRN Point Value - Meets Expectations (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to the value to use for EMRN (M) scores (points, not percentage)
+ when not using manual grading.";
+ },
+ {
+ property = emrnRevisionNeeded;
+ type = integer;
+ advanced = true;
+ default = 10;
+ name = "EMRN Point Value - Revision Needed (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to the value to use for EMRN (R) scores (points, not percentage)
+ when not using manual grading.";
+ },
+ {
+ property = useJdk11;
+ type = boolean;
+ advanced = true;
+ default = false;
+ name = "Use Java 11 (Experimental)";
+ category = "Experimental Settings";
+ description =
+ "Set to true to switch to Java 11.";
+ },
+ {
+ property = useTestCaseValidation;
+ type = antBoolean;
+ advanced = true;
+ default = false;
+ name = "Run student test cases against reference implementation (Experimental)";
+ category = "Developer Settings";
+ description =
+ "Set to true to turn on test case validation for running student-written
+ test suites against reference implementations.";
+ },
+ {
+ property = showTestCaseValidation;
+ type = antBoolean;
+ advanced = true;
+ default = false;
+ name = "Show students their test cases against the reference implementation (Experimental)";
+ category = "Developer Settings";
+ description =
+ "Set to true to show students test case validation for running student-written
+ test suites against reference implementations.";
+ },
+ {
+ property = includeValidationInGrading;
+ type = antBoolean;
+ advanced = true;
+ default = false;
+ name = "Include Test Validation Results in Grading";
+ category = "Developer Settings";
+ description =
+ "When set, students grade for test case validation will be included in
+ the scoring formula. When false, student test validation results are
+ not included in the score calculation.";
+ },
+ {
+ property = maxValidationPenalty;
+ type = integer;
+ advanced = true;
+ default = 10;
+ name = "Max Validation Penalty";
+ category = "Developer Settings";
+ description =
+ "Set the maximum penalty that can be applied for test case validation failures.";
+ },
+ {
+ property = validationTestsRequired;
+ type = integer;
+ advanced = true;
+ default = 10;
+ name = "Test Cases Required for Validation";
+ category = "Developer Settings";
+ description =
+ "Set the minimum number of test cases that must be submitted for test case
+ validation.";
+ },
+ {
+ property = validationFailuresAllowed;
+ type = integer;
+ advanced = true;
+ default = 0;
+ name = "Test Case Validation Failures Allowed";
+ category = "Developer Settings";
+ description =
+ "Set the maximum number of test case validation failures allowed.";
+ },
+ {
+ property = milestonePassed.1;
+ type = boolean;
+ default = false;
+ name = "Milestone 1 Passed";
+ category = "Milestone Settings";
+ description =
+ "If milestone 1 had already been passed.";
+ },
+ {
+ property = milestoneDueDate.1;
+ type = integer;
+ default = 0;
+ size = 40;
+ name = "Milestone 1 Due Date (YYYYMMDD)";
+ category = "Milestone Settings";
+ description =
+ "The milestone check automatically activates for submissions made before
+ this date. Use ISO format YYYYMMDD. Leave blank to disable milestone checks.";
+ },
+ {
+ property = milestoneDueTime.1;
+ type = integer;
+ default = 0;
+ size = 40;
+ name = "Milestone 1 Time Due (HHMMSS)";
+ category = "Milestone Settings";
+ description =
+ "The milestone check automatically activates for submissions made before
+ this time. Use format HHMMSS.";
+ },
+ {
+ property = milestoneMinStudentTests.1;
+ type = integer;
+ default = 0;
+ name = "Minimum Student Tests for Milestone";
+ category = "Milestone Settings";
+ description =
+ "Minimum number of student-written JUnit tests required before the milestone due date.";
+ },
+ {
+ property = milestoneMinRefTests.1;
+ type = integer;
+ default = 0;
+ name = "Minimum Reference Tests for Milestone";
+ category = "Milestone Settings";
+ description =
+ "Minimum percentage of Reference tests required before the milestone due date.";
+ },
+ {
+ property = milestoneStyleMin.1;
+ type = integer;
+ default = 0;
+ name = "Minimum Style Score for Milestone";
+ category = "Milestone Settings";
+ description =
+ "Minimum passing style score required before the milestone due date.";
+ },
+ {
+ property = milestoneMinMutationCoverage.1;
+ type = integer;
+ default = 0;
+ name = "Minimum Mutation Coverage Student Tests for Milestone";
+ category = "Milestone Settings";
+ description =
+ "Minimum mutation coverage required before the milestone due date.";
+ },
+ {
+ property = milestonePassed.2;
+ type = boolean;
+ default = false;
+ name = "Milestone 2 Passed";
+ category = "Milestone Settings";
+ description =
+ "If milestone 2 has been passed.";
+ },
+ {
+ property = milestoneDueDate.2;
+ type = integer;
+ default = 0;
+ size = 40;
+ name = "Milestone 2 Due Date (YYYYMMDD)";
+ category = "Milestone Settings";
+ description =
+ "The milestone check automatically activates for submissions made before
+ this date. Use ISO format YYYYMMDD. Leave blank to disable milestone checks.";
+ },
+ {
+ property = milestoneDueTime.2;
+ type = integer;
+ default = 0;
+ size = 40;
+ name = "Milestone 1 Time Due (HHMMSS)";
+ category = "Milestone Settings";
+ description =
+ "The milestone check automatically activates for submissions made before
+ this time. Use format HHMMSS.";
+ },
+ {
+ property = milestoneMinStudentTests.2;
+ type = integer;
+ default = 0;
+ name = "Minimum Student Tests for Milestone";
+ category = "Milestone Settings";
+ description =
+ "Minimum number of student-written JUnit tests required before the milestone due date.";
+ },
+ {
+ property = milestoneMinRefTests.2;
+ type = integer;
+ default = 0;
+ name = "Minimum Reference Tests for Milestone";
+ category = "Milestone Settings";
+ description =
+ "Minimum percentage of Reference tests required before the milestone due date.";
+ },
+ {
+ property = milestoneStyleMin.2;
+ type = integer;
+ default = 0;
+ name = "Minimum Style Score for Milestone";
+ category = "Milestone Settings";
+ description =
+ "Minimum passing style score required before the milestone due date.";
+ },
+ {
+ property = milestoneMinMutationCoverage.2;
+ type = integer;
+ default = 0;
+ name = "Minimum Mutation Coverage Student Tests for Milestone";
+ category = "Milestone Settings";
+ description =
+ "Minimum mutation coverage required before the milestone due date.";
+ },
+ {
+ property = milestonePassed.3;
+ type = boolean;
+ default = false;
+ name = "Milestone 3 Passed";
+ category = "Milestone Settings";
+ description =
+ "If milestone 3 has been passed";
+ },
+ {
+ property = milestoneDueDate.3;
+ type = integer;
+ default = 0;
+ size = 40;
+ name = "Milestone 3 Due Date (YYYYMMDD)";
+ category = "Milestone Settings";
+ description =
+ "The milestone check automatically activates for submissions made before
+ this date. Use ISO format YYYYMMDD. Leave blank to disable milestone checks.";
+ },
+ {
+ property = milestoneDueTime.3;
+ type = integer;
+ default = 0;
+ size = 40;
+ name = "Milestone 1 Time Due (HHMMSS)";
+ category = "Milestone Settings";
+ description =
+ "The milestone check automatically activates for submissions made before
+ this time. Use format HHMMSS.";
+ },
+ {
+ property = milestoneMinStudentTests.3;
+ type = integer;
+ default = 0;
+ name = "Minimum Student Tests for Milestone";
+ category = "Milestone Settings";
+ description =
+ "Minimum number of student-written JUnit tests required before the milestone due date.";
+ },
+ {
+ property = milestoneMinRefTests.3;
+ type = integer;
+ default = 0;
+ name = "Minimum Reference Tests for Milestone";
+ category = "Milestone Settings";
+ description =
+ "Minimum percentage of Reference tests required before the milestone due date.";
+ },
+ {
+ property = milestoneStyleMin.3;
+ type = integer;
+ default = 0;
+ name = "Minimum Style Score for Milestone";
+ category = "Milestone Settings";
+ description =
+ "Minimum passing style score required before the milestone due date.";
+ },
+ {
+ property = milestoneMinMutationCoverage.3;
+ type = integer;
+ default = 0;
+ name = "Minimum Mutation Coverage Student Tests for Milestone";
+ category = "Milestone Settings";
+ description =
+ "Minimum mutation coverage required before the milestone due date.";
+ }
+ );
+ globalOptions = (
+ {
+ property = doxygenDir;
+ type = shortText;
+ size = 40;
+ name = "Doxygen Directory";
+ description =
+ "The directory on the local server that contains the Doxygen executable.
+ Doxygen (and Dot, below) are used to generate class diagrams for the code
+ that students submit; if you do not have a copy of Doxygen or wish to
+ disable diagram generation across all assignments, you can leave this
+ field blank. If you are not the user administering this Web-CAT server,
+ you will need to have your system administrator install this tool and set
+ this path if you wish to use it.";
+ },
+ {
+ property = dotDir;
+ type = shortText;
+ size = 40;
+ name = "Dot Directory";
+ description =
+ "The directory on the local server that contains the Dot executable (from
+ the Graphviz package). Dot (and Doxygen, above) are used to generate
+ class diagrams for the code that students submit; if you do not have a copy
+ of Dot or wish to disable diagram generation across all assignments, you can
+ leave this field blank. If you are not the user administering this Web-CAT
+ server, you will need to have your system administrator install this tool
+ and set this path if you wish to use it.";
+ }
+ );
+}
diff --git a/src/execute.pl b/src/execute.pl
index adbcced..49ffb48 100644
--- a/src/execute.pl
+++ b/src/execute.pl
@@ -23,6 +23,7 @@
use Web_CAT::JUnitResultsReader;
use XML::Smart;
use Data::Dump qw(dump);
+use Time::Local;
#=============================================================================
@@ -87,6 +88,7 @@
my $pid = $cfg->getProperty('userName');
my $workingDir = $cfg->getProperty('workingDir');
my $resultDir = $cfg->getProperty('resultDir');
+my $solutionDir= $cfg->getProperty('solutionDir');
#Using ResultDir in ErrorMapper file and we set the value here
setResultDir($resultDir);
@@ -110,6 +112,20 @@
my $usePit = $cfg->getProperty('usePit', 0);
$usePit = ($usePit =~ m/^(true|on|yes|y|1)$/i);
if ($usePit) { $cfg->setProperty('enablePit', 'true'); }
+my $useTestCaseValidation = $cfg->getProperty('useTestCaseValidation', 0);
+$useTestCaseValidation = ($useTestCaseValidation =~ m/^(true|on|yes|y|1)$/i);
+if ($useTestCaseValidation) { $cfg->setProperty('enableTestCaseValidation', 'true'); }
+my $showTestCaseValidation = $cfg->getProperty('showTestCaseValidation', 0);
+$showTestCaseValidation = ($showTestCaseValidation =~ m/^(true|on|yes|y|1)$/i);
+if ($showTestCaseValidation) { $cfg->setProperty('showTestCaseValidation', 'true'); }
+my $testCaseValidationFileName = $cfg->getProperty('testCaseValidationFileName', 'ProblemSpecTest');
+if ($useTestCaseValidation) { $cfg->setProperty('testCaseValidationFileName', $testCaseValidationFileName); }
+my $maxValidationPenalty = $cfg->getProperty('maxValidationPenalty', 10);
+if ($maxValidationPenalty) { $cfg->setProperty('maxValidationPenalty', $maxValidationPenalty); }
+my $validationTestsRequired = $cfg->getProperty('validationTestsRequired', 0);
+if ($validationTestsRequired) { $cfg->setProperty('validationTestsRequired', $validationTestsRequired); }
+my $validationFailuresAllowed = $cfg->getProperty('validationFailuresAllowed', 0);
+if ($validationFailuresAllowed) { $cfg->setProperty('validationFailuresAllowed', $validationFailuresAllowed); }
my $useEMRN = $cfg->getProperty('useEMRN', 0);
$useEMRN = ($useEMRN =~ m/^(true|on|yes|y|1)$/i);
my $useEMRNManual = $cfg->getProperty('useEMRNManual', 0);
@@ -137,6 +153,9 @@
#my $instructorCasesPassed = undef;
my $instructorCasesPercent = 0;
my $studentCasesPercent = 0;
+my $validateCasesPercent = 0;
+my $validationPenalty = 0;
+my $printableValidationPenalty = 0;
my $codeCoveragePercent = 0;
#my $studentTestMsgs;
my $hasJUnitErrors = 0;
@@ -146,6 +165,7 @@
'studentHasSrcs' => 0,
'studentTestResults' => undef,
'instrTestResults' => undef,
+ 'validateTestResults'=> undef,
'toolDeductions' => 0,
'compileMsgs' => "",
'compileErrs' => 0,
@@ -179,6 +199,7 @@
'whitespace' => 1,
'lineLength' => 1,
'other' => 1,
+ 'pointsGained' => 0,
'pointsGainedPercent' => 100
);
@@ -201,6 +222,8 @@
'problemCoveragePercent' => 100
);
+my @milestoneResults;
+
# A limit for number of errors in a subcategory in the feedback.
# Example: codingFlaws is a subcategory.
my $maxErrorsPerSubcategory = 8;
@@ -338,6 +361,117 @@
'outOfMemoryErrors' => undef
);
+#=============================================================================
+# Multiple Milestone Settings
+#=============================================================================
+
+my $MAX_MILESTONES = 3; # Maximum number of milestones to check for
+my @milestoneDueDatesTimestamps = ();
+my @milestoneMinStudentTests = ();
+my @milestoneMinRefTests = ();
+my @milestoneStyleMins = ();
+my @milestoneMinMutationCoverages = ();
+my @milestoneNumbers = ();
+my $milestoneCount = 0;
+
+sub milestoneHasConfiguredRequirements
+{
+ my ($dueDateProp, $dueTimeProp, $studentTestsProp, $refTestsProp,
+ $styleProp, $coverageProp) = @_;
+
+ return 1 if (defined $dueDateProp && $dueDateProp ne '' && $dueDateProp ne '0');
+ return 1 if (defined $dueTimeProp && $dueTimeProp ne '' && $dueTimeProp ne '0');
+ return 1 if (defined $studentTestsProp && $studentTestsProp ne '' && $studentTestsProp ne '0');
+ return 1 if (defined $refTestsProp && $refTestsProp ne '' && $refTestsProp ne '0');
+ return 1 if (defined $styleProp && $styleProp ne '' && $styleProp ne '0');
+ return 1 if (defined $coverageProp && $coverageProp ne '' && $coverageProp ne '0');
+
+ return 0;
+}
+
+sub formatTimestampForDisplay
+{
+ my ($timestampMillis) = @_;
+
+ return 'Unknown' unless defined $timestampMillis && $timestampMillis ne '';
+ return 'Unknown' unless $timestampMillis =~ /^\d+$/;
+
+ my $timestampSeconds = int($timestampMillis / 1000);
+ my @timeParts = localtime($timestampSeconds);
+ return sprintf('%04d-%02d-%02d %02d:%02d:%02d',
+ $timeParts[5] + 1900,
+ $timeParts[4] + 1,
+ $timeParts[3],
+ $timeParts[2],
+ $timeParts[1],
+ $timeParts[0]);
+}
+
+# Dynamically read properties for milestones
+for (my $i = 1; $i <= $MAX_MILESTONES; $i++)
+{
+ my $dueDateProp = $cfg->getProperty("milestoneDueDate.$i");
+ my $dueTimeProp = $cfg->getProperty("milestoneDueTime.$i");
+ my $studentTestsProp = $cfg->getProperty("milestoneMinStudentTests.$i");
+ my $refTestsProp = $cfg->getProperty("milestoneMinRefTests.$i");
+ my $styleProp = $cfg->getProperty("milestoneStyleMin.$i");
+ my $coverageProp = $cfg->getProperty("milestoneMinMutationCoverage.$i");
+
+ my $hasMilestone = milestoneHasConfiguredRequirements(
+ $dueDateProp, $dueTimeProp, $studentTestsProp,
+ $refTestsProp, $styleProp, $coverageProp);
+ next unless $hasMilestone;
+
+ my $epochSeconds = 0;
+ my $rawIntDate = defined $dueDateProp ? $dueDateProp : 0;
+ if (defined $rawIntDate && $rawIntDate =~ m/^\d{8}$/)
+ {
+ my $year = int($rawIntDate / 10000);
+ my $month = int(($rawIntDate % 10000) / 100);
+ my $day = $rawIntDate % 100;
+
+ # milestoneDueTime is HHMMSS; default to 23:59:59 when omitted/invalid.
+ my $rawDueTime = defined $dueTimeProp ? $dueTimeProp : 235959;
+ if (!defined($rawDueTime) || $rawDueTime !~ m/^\d{1,6}$/)
+ {
+ $rawDueTime = 235959;
+ }
+ $rawDueTime = sprintf("%06d", $rawDueTime);
+ my ($hour, $minute, $second) = $rawDueTime =~ m/^(\d{2})(\d{2})(\d{2})$/;
+
+ $epochSeconds = eval {
+ timelocal($second, $minute, $hour, $day, $month - 1, $year - 1900);
+ };
+ $epochSeconds = 0 if $@ || !defined($epochSeconds);
+ }
+
+ # Store milestones densely at 0-based indices for consistent access.
+ my $idx = $milestoneCount;
+ $milestoneDueDatesTimestamps[$idx] = $epochSeconds * 1000;
+ $milestoneMinStudentTests[$idx] = defined $studentTestsProp ? $studentTestsProp : 0;
+ $milestoneMinRefTests[$idx] = defined $refTestsProp ? $refTestsProp : 0;
+ $milestoneStyleMins[$idx] = defined $styleProp ? $styleProp : 0;
+ $milestoneMinMutationCoverages[$idx] = defined $coverageProp ? $coverageProp : 0;
+ $milestoneNumbers[$idx] = $i;
+
+ $milestoneCount++;
+}
+
+# Check which milestones have been passed
+my @milestoneAlreadyPassed = ();
+
+for (my $i = 0; $i < $milestoneCount; $i++) {
+ my $mNum = $milestoneNumbers[$i];
+
+ # Read property from config file
+ my $status = $cfg->getProperty("milestonePassed.$mNum", 'false');
+
+ if ($status =~ m/^(true|on|yes|y|1)$/i) {
+ $milestoneAlreadyPassed[$i] = 1; # Mark as passed in local array
+ } else {
+ $milestoneAlreadyPassed[$i] = 0;
+ }
+}
#-------------------------------------------------------
# In addition, some local definitions within this script
@@ -346,6 +480,18 @@
{
# $ENV{JAVA_HOME} = '/usr/java/jdk-11';
$ENV{JAVA_HOME} = '/usr/lib/jvm/java-11';
+
+ $cfg->setProperty('checkstyle.jar',
+ "${pluginHome}/checkstyle-10.7.0/checkstyle-10.7.0-all.jar");
+
+ $cfg->setProperty('checkstyleConfigFile',
+ "${pluginHome}/checkstyle-10.7.0/checkstyle-10.7.0.xml");
+
+ $cfg->setProperty('pmd.lib',
+ "${pluginHome}/pmd-7.21.0/lib");
+
+ $cfg->setProperty('pmdConfigFile',
+ "${pluginHome}/pmd-7.21.0/pmd.xml");
}
Web_CAT::Utilities::initFromConfig($cfg);
if (defined($ENV{JAVA_HOME}))
@@ -418,6 +564,10 @@
$cfg->getProperty('includeStudentTestsInGrading', 0);
$includeStudentTestsInGrading =
($includeStudentTestsInGrading =~ m/^(true|on|yes|y|1)$/i);
+my $includeValidationInGrading =
+ $cfg->getProperty('includeValidationInGrading', 0);
+$includeValidationInGrading =
+ ($includeValidationInGrading =~ m/^(true|on|yes|y|1)$/i);
my $studentsMustSubmitTests =
$cfg->getProperty('studentsMustSubmitTests', 0);
$studentsMustSubmitTests =
@@ -745,6 +895,19 @@ sub setClassPatternIfNeeded
}
}
+# referenceImplementationJar
+{
+ my $jarFileOrDir = $cfg->getProperty('referenceImplementationJar');
+ if (defined $jarFileOrDir && $jarFileOrDir ne "")
+ {
+ my $path = confirmExists($scriptData, $jarFileOrDir);
+ $cfg->setProperty('referenceImplementationClassFiles', $path);
+ if (-d $path)
+ {
+ $cfg->setProperty('referenceImplementationClassDir', $path);
+ }
+ }
+}
# timeout
my $timeoutForOneRun = $cfg->getProperty('timeoutForOneRun', 30);
@@ -1284,6 +1447,8 @@ sub addCannotFindSymbolStruct
new Web_CAT::JUnitResultsReader("$resultDir/student.inc");
$status{'instrTestResults'} =
new Web_CAT::JUnitResultsReader("$resultDir/instr.inc");
+ $status{'validateTestResults'} =
+ new Web_CAT::JUnitResultsReader("$resultDir/validate.inc");
foreach my $class ($status{'studentTestResults'}->suites)
{
@@ -1662,7 +1827,7 @@ sub trackMessageInstance
print $msg;
}
}
-
+
if ($group eq "suppress")
{
return;
@@ -1998,7 +2163,7 @@ sub trackMessageInstance
}
$msg = htmlEscape($msg);
- # highlight variable name, if there is one
+ # highlight variable name, if there is one
my $v = $bug->{LocalVariable}{Message};
if (!$v->null)
{
@@ -2015,7 +2180,7 @@ sub trackMessageInstance
}
}
- # highlight method name, if there is one
+ # highlight method name, if there is one
my $method = $bug->{Method}{Message};
if (!$method->null)
{
@@ -2045,13 +2210,13 @@ sub trackMessageInstance
}
my $fileName = '';
- $bug->{beginline} =
+ $bug->{beginline} =
$bug->{SourceLine}{start}->content
|| $bug->{Method}{SourceLine}{start}->content
|| $bug->{Field}{SourceLine}{start}->content
|| $bug->{Class}{SourceLine}{start}->content
|| 1;
- $bug->{endline} =
+ $bug->{endline} =
$bug->{SourceLine}{end}->content
|| $bug->{Method}{SourceLine}{end}->content
|| $bug->{Field}{SourceLine}{end}->content
@@ -2178,11 +2343,14 @@ sub trackMessageInstance
# set PointsGained in Style section color the radial bar
if ($maxToolScore > 0)
{
+ $styleSectionStatus{'pointsGained'} =
+ $maxToolScore - $status{'toolDeductions'};
$styleSectionStatus{'pointsGainedPercent'} =
(($maxToolScore - $status{'toolDeductions'})/$maxToolScore) * 100;
}
else
{
+ $styleSectionStatus{'pointsGained'} = 0;
$styleSectionStatus{'pointsGainedPercent'} = 100;
}
@@ -2232,8 +2400,7 @@ sub trackMessageInstance
}
}
$studentCasesPercent =
- $status{'studentTestResults'}->testPassRate * 100.0;
- # int($status{'studentTestResults'}->testPassRate * 100.0 + 0.5);
+ int($status{'studentTestResults'}->testPassRate * 100.0 * 10 + 0.5) / 10;
if ($status{'studentTestResults'}->testsFailed > 0
&& $studentCasesPercent == 100)
{
@@ -2851,7 +3018,7 @@ sub processStatementsUncovered
$gradedElements = 1;
$gradedElementsCovered = 1;
}
-
+
# set code markup properties
$cfg->setProperty("statElementsLabel", "Mutants Detected");
my %fileDeductionProperties = ();
@@ -3548,7 +3715,7 @@ sub processStatementsUncovered
$codeCoveragePercent =
int(($gradedElementsCovered * 1.0 / $gradedElements)
# / $coverageGoal
- * 100.0 + 0.5);
+ * 100.0 * 10 + 0.5) / 10;
if ($codeCoveragePercent > 100) { $codeCoveragePercent = 100; }
if (($gradedElementsCovered * 1.0 / $gradedElements) < $coverageGoal
&& $codeCoveragePercent == 100)
@@ -3621,6 +3788,11 @@ sub processStatementsUncovered
{
$status{'instrTestResults'}->saveToCfg($cfg, 'instructor.test');
}
+if (defined $status{'validateTestResults'}
+ && $status{'validateTestResults'}->hasResults)
+{
+ $status{'validateTestResults'}->saveToCfg($cfg, 'validate.test');
+}
if (defined $messageStats)
{
my $staticResults = '';
@@ -3672,7 +3844,7 @@ sub processStatementsUncovered
$cfg->setProperty('static.analysis.results', '(' . $staticResults . ')');
}
$cfg->setProperty('outcomeProperties',
- '("instructor.test.results", "student.test.results", '
+ '("instructor.test.results", "student.test.results", "validate.test.results, '
. '"static.analysis.results")');
@@ -3734,7 +3906,7 @@ sub markCodingSectionUsingInstrResults
else
{
$instructorCasesPercent =
- int($status{'instrTestResults'}->testPassRate * 100.0 + 0.5);
+ int($status{'instrTestResults'}->testPassRate * 100.0 * 10 + 0.5) / 10;
if ($instructorCasesPercent == 100)
{
# Don't show 100% if some cases failed
@@ -4049,118 +4221,617 @@ sub markCodingSectionUsingInstrResults
}
}
-
#=============================================================================
-# generate HTML versions of any other source files
+# generate test validation results
#=============================================================================
-
-if ($debug > 3)
-{
-foreach my $ff (keys %codeMessages)
+if (defined $status{'validateTestResults'} && $useTestCaseValidation)
{
- print "file $ff:\n";
- foreach my $line (keys %{$codeMessages{$ff}})
+ my $sectionTitle = "Detailed Test Validation Results";
+ if ($status{'validateTestResults'}->testsExecuted == 0
+ || ($studentsMustSubmitTests
+ && !$status{'studentTestResults'}->hasResults))
{
- print "file $ff: line $line:\n";
- if (defined $codeMessages{$ff}->{$line}{violations})
+ $sectionTitle .=
+ "(Unknown!)";
+ $validateCasesPercent = "unknown";
+ }
+ elsif ($status{'validateTestResults'}->allTestsPass)
+ {
+ $sectionTitle .= "(100%)";
+ $validateCasesPercent = 100;
+ }
+ else
+ {
+ $validateCasesPercent =
+ int($status{'validateTestResults'}->testPassRate * 100.0 * 10 + 0.5) / 10;
+ if ($validateCasesPercent == 100)
{
- my @comments =
- sort { $b->{line}->content <=> $a->{line}->content }
- @{ $codeMessages{$ff}->{$line}{violations} };
- print "file $ff: line $line: total comments = ",
- $#comments + 1, "\n";
- foreach my $c (@comments)
- {
- my $message = $c->{message}->content;
- if (!defined $message || $message eq '')
- {
- $message = $c->content;
- }
- # print "comment = ", $c->data(tree => $c), "\n";
- print 'group = ', $c->{group}->content, ', line = ',
- $c->{line}->content, ', message = ',
- $message, "\n";
- }
+ # Don't show 100% if some cases failed
+ $validateCasesPercent--;
}
+ $sectionTitle .= "($validateCasesPercent%)";
}
-}
-}
+ if ($showTestCaseValidation)
+ {
+ $status{'feedback'}->startFeedbackSection(
+ $sectionTitle, ++$expSectionId,
+ $useEnhancedFeedback || ($validateCasesPercent >= 100));
+ $status{'feedback'}->print("
Valid Test Percentage: ");
+ if ($validateCasesPercent == 100)
+ {
+ $status{'feedback'}->print("100%");
+ }
+ else
+ {
+ $status{'feedback'}->print(
+ "$validateCasesPercent");
+ if ($validateCasesPercent ne "unknown")
+ {
+ $status{'feedback'}->print("%");
+ }
+ $status{'feedback'}->print("");
+ }
+ $status{'feedback'}->print("
");
-# The extended error messages from config files which replace the builtin
-# messages from Checkstyle and PMD are longer.
-# Use those messages as 'enhancedMessage' and shorter messages as
-# 'errorMessage'.
-sub addShorterMessages
-{
- my $struct = shift;
- my $rule = shift;
+ if ($status{'compileErrs'}) # $validateCases == 0
+ {
+ $status{'feedback'}->print(<Your Specification Tests failed to compile correctly against
+the reference implementation.
+
This is most likely because you have not followed the correct format
+for the specification tests (using only methods defined in the interface).
+
Failure to follow these constraints will prevent the proper assessment
+of your solution and your tests.
+EOF
+ if ($status{'compileMsgs'} ne "")
+ {
+ $status{'feedback'}->print(<The following specific error(s) were discovered while compiling
+your specification tests against the reference implementation:
+
+
\n");
+ }
+ }
+ elsif ($studentsMustSubmitTests
+ && !$status{'studentTestResults'}->hasResults)
+ {
+ $status{'feedback'}->print(<You are required to write your own software tests
+for this assignment. You must provide your own tests
+to get further feedback.
+EOF
+ }
+ elsif ($status{'validateTestResults'}->allTestsFail)
+ {
+ $status{'feedback'}->print(<Your problem setup does not appear to be
+consistent with the assignment.
+EOF
+ if ($studentsMustSubmitTests)
+ {
+ $status{'feedback'}->print(<For this assignment, your test cases are being assessed by running
+your tests against the reference solution.
+EOF
+ }
+ $status{'feedback'}->print(<In this case, none of your specification tests pass on the reference
+solution, which may mean that your specification tests make incorrect
+assumptions about some aspect of the required behavior. This discrepancy prevented
+Web-CAT from properly assessing the thoroughness of your test cases.
+
Double check that you have carefully followed all initial conditions
+requested in the assignment in setting up your test cases.
+EOF
- my $shortMessage = codingStyleMessageValue($rule);
+ }
+ elsif ($status{'validateTestResults'}->allTestsPass)
+ {
+ $status{'feedback'}->print(<Your tests appear to match the expectations for this assignment since none expect
+outputs that conflict with the reference implementation.
+EOF
+ }
+ else
+ {
+ if ($studentsMustSubmitTests)
+ {
+ $status{'feedback'}->print(<For this assignment, your test cases are being assessed by running
+your tests against the reference solution.
+
Some of your tests fail when run against the reference implementation.
+
This happens when your test cases embody misconceptions of the problem
+spec by expecting different output from what the reference implementation
+generates for this that test case.
+
Your test cases contain misconceptions of the problem spec, so your
+testing is incomplete.
+EOF
+ }
+ $status{'feedback'}->print(<Double check that you have carefully followed all requirements of the
+assignment when setting up your tests.
+EOF
+ }
+ if ($hintsLimit != 0 && !$status{'compileErrs'})
+ {
+ if ($studentsMustSubmitTests
+ && $hasJUnitErrors
+ && $junitErrorsHideHints)
+ {
+ $status{'feedback'}->print(<Your JUnit test classes contain problems that must be
+fixed before you can receive any more specific feedback. Be sure that
+all of your test classes contain test methods, and that all of your test
+methods include appropriate assertions to check for expected behavior.
+You must fix these problems with your own tests to get further feedback.
+EOF
+ }
+ }
- if ($shortMessage)
{
- $struct = expandedMessage->new(
- entityName => $struct->entityName,
- lineNum => $struct->lineNum,
- errorMessage => $shortMessage,
- linesOfCode => $struct->linesOfCode,
- enhancedMessage => $struct->errorMessage,
+ if ($codingSectionStatus{'compilerErrors'} == 1)
+ {
+ # Transform the plain text JUnit results into an interactive HTML
+ # view.
+ JavaTddPlugin::transformTestResults('validate_',
+ "$resultDir/validate-results.txt",
+ "$resultDir/validate-results.html"
);
- }
+ }
- return $struct;
+ if ($codingSectionStatus{'compilerErrors'} == 1)
+ {
+ open(VALIDATERESULTS, "$resultDir/validate-results.html");
+ my @lines = ;
+ close(VALIDATERESULTS);
+ if ($#lines >= 0)
+ {
+ $status{'feedback'}->print(<The results of running your test cases are shown
+below. Click on a failed test to see the reason for the failure and an
+execution trace that shows where the error occurred.
+EOF
+ $status{'feedback'}->print(@lines);
+ }
+ unlink "$resultDir/validate-results.html";
+
+ @lines = linesFromFile("$resultDir/validate-out.txt", 75000, 4000);
+ if ($#lines >= 0)
+ {
+ $status{'feedback'}->startFeedbackSection(
+ "Output from your tests", ++$expSectionId, 1, 2,
+ "
");
+
+ if ($totalMilestones == 0)
+ {
+ $status{'feedback'}->print(<No milestone configuration was detected.
+
This assignment may not have milestones configured, or the milestone
+properties could not be found.
+EOF
+ }
+ elsif ($milestonePercentKnown && $milestonePercent >= 100)
+ {
+ $status{'feedback'}->print(<Congratulations! You have met all $totalMilestones milestones for this assignment.
+EOF
+ }
+ else
+ {
+ $status{'feedback'}->print(<You have met $milestonesPassed out of $totalMilestones milestones.
+Review the details below to see which requirements need to be addressed.
+EOF
+ }
+
+ # Display detailed milestone information
+ if ($totalMilestones > 0)
+ {
+ $status{'feedback'}->print(
+ "
The following milestones are defined for this assignment:
\n");
+
+ if (@milestoneResults)
+ {
+ printMilestoneFeedbackDetails($status{'feedback'}, \@milestoneResults);
+ }
+ else
+ {
+ # Fallback when no detailed milestone results are available: show requirements only
+ $status{'feedback'}->print("
\n");
+ for (my $i = 1; $i <= $totalMilestones; $i++)
+ {
+ my $dueDateProp = $cfg->getProperty("milestoneDueDate.$i", '0');
+ my $dueTimeProp = $cfg->getProperty("milestoneDueTime.$i", '0');
+ my $passed = $cfg->getProperty("milestonePassed.$i", 'false');
+ my $mutationCovMin = $cfg->getProperty("milestoneMinMutationCoverage.$i", '0');
+ my $studentTestMin = $cfg->getProperty("milestoneMinStudentTests.$i", '0');
+ my $refTestMin = $cfg->getProperty("milestoneMinRefTests.$i", '0');
+ my $styleMin = $cfg->getProperty("milestoneStyleMin.$i", '0');
+
+ next unless milestoneHasConfiguredRequirements(
+ $dueDateProp, $dueTimeProp, $studentTestMin,
+ $refTestMin, $styleMin, $mutationCovMin);
+
+ my $class = ($passed eq 'true' || $passed eq '1') ? 'complete' : 'incomplete';
+ my $statusText = ($passed eq 'true' || $passed eq '1') ? 'Met' : 'Not Met';
+
+ $status{'feedback'}->print("
\n";
+}
+
# Coding Section
my $incomplete = ($expandSectionId == 1) ? ' incomplete' : '';
print IMPROVEDFEEDBACKFILE <testsExecuted == 0)
{
$showTesting = 0;
@@ -6368,7 +7549,7 @@ sub computeExpandSectionId
my $rawPct = $rawScore / $maxPossible;
my $subTime = $cfg->getProperty('submissionTimestamp', 0);
my $dueTime = $cfg->getProperty('dueDateTimestamp', $subTime);
-
+
my $emrnCmt = '';
my $emrnCategory = 'Not Assessable';
# print <= 0.95
&& $runtimeScore / $maxCorrectnessScore >= 0.95
@@ -6404,7 +7585,6 @@ sub computeExpandSectionId
$emrnCmt = 'Your submission passes all auto-grader checks for this '
. 'assignment.';
}
-
$staticScore = $maxToolScore
* 1.0 / ($maxToolScore + $maxCorrectnessScore)
* $emrnExcellent;
@@ -6517,6 +7697,110 @@ sub computeExpandSectionId
}
+my @milestone_results = ();
+
+for (my $i = 0; $i < $milestoneCount; $i++) {
+ my $mNum = defined($milestoneNumbers[$i]) ? $milestoneNumbers[$i] : ($i + 1);
+ my $dueDate = $milestoneDueDatesTimestamps[$i];
+ my $status = "IN PROGRESS";
+
+ # Requirements
+ my $reqStudentTests = $milestoneMinStudentTests[$i] // 0;
+ my $reqRefTests = $milestoneMinRefTests[$i] //0;
+ my $reqCover = $milestoneMinMutationCoverages[$i] // 0;
+ my $reqStyle = $milestoneStyleMins[$i] // 0;
+
+ next unless milestoneHasConfiguredRequirements(
+ $cfg->getProperty("milestoneDueDate.$mNum"),
+ $cfg->getProperty("milestoneDueTime.$mNum"),
+ $reqStudentTests, $reqRefTests, $reqStyle, $reqCover);
+
+ my $actualStudentTests = 0;
+ my $actualRefTests = 0;
+ my $actualCoverage = 0;
+ my $actualStyle = 0;
+
+
+
+ if ($milestoneAlreadyPassed[$i]) {
+ # SKIP THE CHECK: They already passed it!
+ $status = "ACHIEVED";
+
+ $actualStudentTests = $reqStudentTests;
+ $actualRefTests = $reqRefTests;
+ $actualCoverage = $reqCover;
+ $actualStyle = $reqStyle;
+
+ } else {
+ # Actuals
+ $actualStudentTests = (defined $status{'validateTestResults'}) ? ($status{'validateTestResults'}->testsExecuted - $status{'validateTestResults'}->testsFailed) : 0;
+ $actualRefTests =
+ (defined $instructorCasesPercent
+ && $instructorCasesPercent =~ /^\d+(?:\.\d+)?$/)
+ ? $instructorCasesPercent
+ : 0;
+ $actualCoverage = $testingSectionStatus{'codeCoveragePercent'};
+ $actualStyle = defined $styleSectionStatus{'pointsGained'}
+ ? $styleSectionStatus{'pointsGained'}
+ : $styleSectionStatus{'pointsGainedPercent'};
+
+ # Determine Status
+ my $met = ($actualStudentTests >= $reqStudentTests && $actualCoverage >= $reqCover && $actualStyle >= $reqStyle && $actualRefTests >= $reqRefTests) ? 1 : 0;
+
+ if ($met) {
+ $status = "ACHIEVED";
+ $cfg->setProperty("milestonePassed.$mNum", "true");
+ } elsif (defined($dueDate)
+ && $dueDate > 0
+ && $cfg->getProperty('submissionTimestamp', 0) > $dueDate) {
+ $status = "MISSED";
+ }
+ }
+
+ # 2. Build the json for this milestone
+ my %milestone_entry = (
+ milestoneNumber => $mNum,
+ dueDate => (defined($dueDate) ? $dueDate : 0),
+ status => $status,
+ requirements => {
+ minStudentTests => int($reqStudentTests),
+ minRefTests => int($reqRefTests),
+ minCover => int($reqCover),
+ minStyle => int($reqStyle)
+ },
+ actuals => {
+ studentTests => $actualStudentTests,
+ referenceTests => $actualRefTests,
+ cover => $actualCoverage,
+ style => $actualStyle
+ }
+ );
+
+ push @milestone_results, \%milestone_entry;
+}
+
+# 3. Write to milestones.json
+my $json_output = {
+ submissionDate => $cfg->getProperty('submissionTimestamp', 0),
+ milestones => \@milestone_results
+};
+
+# Encode and write
+eval {
+ require JSON::PP;
+ my $json_text = JSON::PP->new->utf8->pretty->encode($json_output);
+ my $milestoneFilename = "$resultDir/milestones.json";
+
+ open(my $fh, '>', $milestoneFilename) or die "Could not open '$milestoneFilename' for writing: $!";
+ print $fh $json_text;
+ close $fh;
+ print "Successfully wrote milestone data to $milestoneFilename\n" if $debug;
+};
+if ($@) {
+ print "Error generating JSON: $@" if $debug;
+}
+
+
#=============================================================================
# Script log
#=============================================================================
diff --git a/src/perllib/File/Slurp.pm b/src/perllib/File/Slurp.pm
new file mode 100644
index 0000000..e5329b1
--- /dev/null
+++ b/src/perllib/File/Slurp.pm
@@ -0,0 +1,1128 @@
+package File::Slurp;
+
+use strict;
+use warnings ;
+
+our $VERSION = '9999.32';
+$VERSION = eval $VERSION;
+
+use Carp ;
+use Exporter qw(import);
+use Fcntl qw( :DEFAULT ) ;
+use File::Basename ();
+use File::Spec;
+use File::Temp qw(tempfile);
+use IO::Handle ();
+use POSIX qw( :fcntl_h ) ;
+use Errno ;
+
+my @std_export = qw(
+ read_file
+ write_file
+ overwrite_file
+ append_file
+ read_dir
+) ;
+
+my @edit_export = qw(
+ edit_file
+ edit_file_lines
+) ;
+
+my @abbrev_export = qw(
+ rf
+ wf
+ ef
+ efl
+) ;
+
+our @EXPORT_OK = (
+ @edit_export,
+ @abbrev_export,
+ qw(
+ slurp
+ prepend_file
+ ),
+) ;
+
+our %EXPORT_TAGS = (
+ 'all' => [ @std_export, @edit_export, @abbrev_export, @EXPORT_OK ],
+ 'edit' => [ @edit_export ],
+ 'std' => [ @std_export ],
+ 'abr' => [ @abbrev_export ],
+) ;
+
+our @EXPORT = @std_export ;
+
+my $max_fast_slurp_size = 1024 * 100 ;
+
+my $is_win32 = $^O =~ /win32/i ;
+
+*slurp = \&read_file ;
+*rf = \&read_file ;
+
+sub read_file {
+ my $file_name = shift;
+ my $opts = (ref $_[0] eq 'HASH') ? shift : {@_};
+ # options we care about:
+ # array_ref binmode blk_size buf_ref chomp err_mode scalar_ref
+
+ # let's see if we have a stringified object before doing anything else
+ # We then only have to deal with when we are given a file handle/globref
+ if (ref($file_name)) {
+ my $ref_result = _check_ref($file_name, $opts);
+ if (ref($ref_result)) {
+ @_ = ($opts, $ref_result);
+ goto &_error;
+ }
+ $file_name = $ref_result if $ref_result;
+ # we have now stringified $file_name if possible. if it's still a ref
+ # then we probably have a file handle
+ }
+
+ my $fh;
+ if (ref($file_name)) {
+ $fh = $file_name;
+ }
+ else {
+ # to keep with the old ways, read in :raw by default
+ unless (open $fh, "<:raw", $file_name) {
+ @_ = ($opts, "read_file '$file_name' - open: $!");
+ goto &_error;
+ }
+ # even though we set raw, let binmode take place here (busted)
+ if (my $bm = $opts->{binmode}) {
+ binmode $fh, $bm;
+ }
+ }
+
+ # we are now sure to have an open file handle. Let's slurp it in the same
+ # way that File::Slurper does.
+ my $buf;
+ my $buf_ref = $opts->{buf_ref} || \$buf;
+ ${$buf_ref} = '';
+ my $blk_size = $opts->{blk_size} || 1024 * 1024;
+ if (my $size = -f $fh && -s _) {
+ $blk_size = $size if $size < $blk_size;
+ my ($pos, $read) = 0;
+ do {
+ unless(defined($read = read $fh, ${$buf_ref}, $blk_size, $pos)) {
+ @_ = ($opts, "read_file '$file_name' - read: $!");
+ goto &_error;
+ }
+ $pos += $read;
+ } while ($read && $pos < $size);
+ }
+ else {
+ ${$buf_ref} = do { local $/; <$fh> };
+ }
+ seek($fh, $opts->{_data_tell}, SEEK_SET) if $opts->{_is_data} && $opts->{_data_tell};
+
+ # line endings if we're on Windows
+ ${$buf_ref} =~ s/\015\012/\012/g if ${$buf_ref} && $is_win32 && !$opts->{binmode};
+
+ # we now have a buffer filled with the file content. Figure out how to
+ # return it to the user
+ my $want_array = wantarray; # let's only ask for this once
+ if ($want_array || $opts->{array_ref}) {
+ use re 'taint';
+ my $sep = $/;
+ $sep = '\n\n+' if defined $sep && $sep eq '';
+ # split the buffered content into lines
+ my @lines = length(${$buf_ref}) ?
+ ${$buf_ref} =~ /(.*?$sep|.+)/sg : ();
+ chomp @lines if $opts->{chomp};
+ return \@lines if $opts->{array_ref};
+ return @lines;
+ }
+ return $buf_ref if $opts->{scalar_ref};
+ # if the function was called in scalar context, return the contents
+ return ${$buf_ref} if defined $want_array;
+ # if we were called in void context, return nothing
+ return;
+}
+
+# errors in this sub are returned as scalar refs
+# a normal IO/GLOB handle is an empty return
+# an overloaded object returns its stringified as a scalarfilename
+
+sub _check_ref {
+
+ my( $handle, $opts ) = @_ ;
+
+# check if we are reading from a handle (GLOB or IO object)
+
+ if ( eval { $handle->isa( 'GLOB' ) || $handle->isa( 'IO' ) } ) {
+
+# we have a handle. deal with seeking to it if it is DATA
+
+ my $err = _seek_data_handle( $handle, $opts ) ;
+
+# return the error string if any
+
+ return \$err if $err ;
+
+# we have good handle
+ return ;
+ }
+
+ eval { require overload } ;
+
+# return an error if we can't load the overload pragma
+# or if the object isn't overloaded
+
+ return \"Bad handle '$handle' is not a GLOB or IO object or overloaded"
+ if $@ || !overload::Overloaded( $handle ) ;
+
+# must be overloaded so return its stringified value
+
+ return "$handle" ;
+}
+
+sub _seek_data_handle {
+
+ my( $handle, $opts ) = @_ ;
+ # store some meta-data about the __DATA__ file handle
+ $opts->{_is_data} = 0;
+ $opts->{_data_tell} = 0;
+
+# DEEP DARK MAGIC. this checks the UNTAINT IO flag of a
+# glob/handle. only the DATA handle is untainted (since it is from
+# trusted data in the source file). this allows us to test if this is
+# the DATA handle and then to do a sysseek to make sure it gets
+# slurped correctly. on some systems, the buffered i/o pointer is not
+# left at the same place as the fd pointer. this sysseek makes them
+# the same so slurping with sysread will work.
+
+ eval{ require B } ;
+
+ if ( $@ ) {
+
+ return <IO->IoFLAGS & 16 ) {
+
+ # we now know we have the data handle. Let's store its original
+ # location in the file so that we can put it back after the read.
+ # this is only done for Bugwards-compatibility in some dists such as
+ # CPAN::Index::API that made use of the oddity where sysread was in use
+ # before
+ $opts->{_is_data} = 1;
+ $opts->{_data_tell} = tell($handle);
+# set the seek position to the current tell.
+
+ # unless( sysseek( $handle, tell( $handle ), SEEK_SET ) ) {
+ # return "read_file '$handle' - sysseek: $!" ;
+ # }
+ }
+
+# seek was successful, return no error string
+
+ return ;
+}
+
+*wf = \&write_file ;
+
+sub write_file {
+ my $file_name = shift;
+ my $opts = (ref $_[0] eq 'HASH') ? shift : {};
+ # options we care about:
+ # append atomic binmode buf_ref err_mode no_clobber perms
+
+ my $fh;
+ my $no_truncate = 0;
+ my $orig_filename;
+ # let's see if we have a stringified object or some sort of handle
+ # or globref before doing anything else
+ if (ref($file_name)) {
+ my $ref_result = _check_ref($file_name, $opts);
+ if (ref($ref_result)) {
+ # some error happened while checking for a ref
+ @_ = ($opts, $ref_result);
+ goto &_error;
+ }
+ if ($ref_result) {
+ # we have now stringified $file_name from the overloaded obj
+ $file_name = $ref_result;
+ }
+ else {
+ # we now have a proper handle ref
+ # make sure we don't call truncate on it
+ $fh = $file_name;
+ $no_truncate = 1;
+ # can't do atomic or permissions on a file handle
+ delete $opts->{atomic};
+ delete $opts->{perms};
+ }
+ }
+
+ # open the file for writing if we were given a filename
+ unless ($fh) {
+ $orig_filename = $file_name;
+ my $perms = defined($opts->{perms}) ? $opts->{perms} : 0666;
+ # set the mode for the sysopen
+ my $mode = O_WRONLY | O_CREAT;
+ $mode |= O_APPEND if $opts->{append};
+ $mode |= O_EXCL if $opts->{no_clobber};
+ if ($opts->{atomic}) {
+ # in an atomic write, we must open a new file in the same directory
+ # as the original to account for ACLs. We must also set the new file
+ # to the same permissions as the original unless overridden by the
+ # caller's request to set a specified permission set.
+ my $dir = File::Spec->rel2abs(File::Basename::dirname($file_name));
+ if (!defined($opts->{perms}) && -e $file_name && -f _) {
+ $perms = 07777 & (stat $file_name)[2];
+ }
+ # we must ensure we're using a good temporary filename (doesn't already
+ # exist). This is slower, but safer.
+ {
+ local $^W = 0; # AYFKM
+ (undef, $file_name) = tempfile('.tempXXXXX', DIR => $dir, OPEN => 0);
+ }
+ }
+ $fh = local *FH;
+ unless (sysopen($fh, $file_name, $mode, $perms)) {
+ @_ = ($opts, "write_file '$file_name' - sysopen: $!");
+ goto &_error;
+ }
+ }
+ # we now have an open file handle as well as data to write to that handle
+ if (my $binmode = $opts->{binmode}) {
+ binmode($fh, $binmode);
+ }
+
+ # get the data to print to the file
+ # get the buffer ref - it depends on how the data is passed in
+ # after this if/else $buf_ref will have a scalar ref to the data
+ my $buf_ref;
+ my $data_is_ref = 0;
+ if (ref($opts->{buf_ref}) eq 'SCALAR') {
+ # a scalar ref passed in %opts has the data
+ # note that the data was passed by ref
+ $buf_ref = $opts->{buf_ref};
+ $data_is_ref = 1;
+ }
+ elsif (ref($_[0]) eq 'SCALAR') {
+ # the first value in @_ is the scalar ref to the data
+ # note that the data was passed by ref
+ $buf_ref = shift;
+ $data_is_ref = 1;
+ }
+ elsif (ref($_[0]) eq 'ARRAY') {
+ # the first value in @_ is the array ref to the data so join it.
+ ${$buf_ref} = join '', @{$_[0]};
+ }
+ else {
+ # good old @_ has all the data so join it.
+ ${$buf_ref} = join '', @_;
+ }
+
+ # seek and print
+ seek($fh, 0, SEEK_END) if $opts->{append};
+ print {$fh} ${$buf_ref};
+ truncate($fh, tell($fh)) unless $no_truncate;
+ close($fh);
+
+ if ($opts->{atomic} && !rename($file_name, $orig_filename)) {
+ @_ = ($opts, "write_file '$file_name' - rename: $!");
+ goto &_error;
+ }
+
+ return 1;
+}
+
+# this is for backwards compatibility with the previous File::Slurp module.
+# write_file always overwrites an existing file
+*overwrite_file = \&write_file ;
+
+# the current write_file has an append mode so we use that. this
+# supports the same API with an optional second argument which is a
+# hash ref of options.
+
+sub append_file {
+
+# get the optional opts hash ref
+ my $opts = $_[1] ;
+ if ( ref $opts eq 'HASH' ) {
+
+# we were passed an opts ref so just mark the append mode
+
+ $opts->{append} = 1 ;
+ }
+ else {
+
+# no opts hash so insert one with the append mode
+
+ splice( @_, 1, 0, { append => 1 } ) ;
+ }
+
+# magic goto the main write_file sub. this overlays the sub without touching
+# the stack or @_
+
+ goto &write_file
+}
+
+# prepend data to the beginning of a file
+
+sub prepend_file {
+
+ my $file_name = shift ;
+
+#print "FILE $file_name\n" ;
+
+ my $opts = ( ref $_[0] eq 'HASH' ) ? shift : {} ;
+
+# delete unsupported options
+
+ my @bad_opts =
+ grep $_ ne 'err_mode' && $_ ne 'binmode', keys %{$opts} ;
+
+ delete @{$opts}{@bad_opts} ;
+
+ my $prepend_data = shift ;
+ $prepend_data = '' unless defined $prepend_data ;
+ $prepend_data = ${$prepend_data} if ref $prepend_data eq 'SCALAR' ;
+
+#print "PRE [$prepend_data]\n" ;
+
+ my $err_mode = delete $opts->{err_mode} ;
+ $opts->{ err_mode } = 'croak' ;
+ $opts->{ scalar_ref } = 1 ;
+
+ my $existing_data = eval { read_file( $file_name, $opts ) } ;
+
+ if ( $@ ) {
+
+ @_ = ( { err_mode => $err_mode },
+ "prepend_file '$file_name' - read_file: $!" ) ;
+ goto &_error ;
+ }
+
+#print "EXIST [$$existing_data]\n" ;
+
+ $opts->{atomic} = 1 ;
+ my $write_result =
+ eval { write_file( $file_name, $opts,
+ $prepend_data, $$existing_data ) ;
+ } ;
+
+ if ( $@ ) {
+
+ @_ = ( { err_mode => $err_mode },
+ "prepend_file '$file_name' - write_file: $!" ) ;
+ goto &_error ;
+ }
+
+ return $write_result ;
+}
+
+# edit a file as a scalar in $_
+
+*ef = \&edit_file ;
+
+sub edit_file(&$;$) {
+
+ my( $edit_code, $file_name, $opts ) = @_ ;
+ $opts = {} unless ref $opts eq 'HASH' ;
+
+# my $edit_code = shift ;
+# my $file_name = shift ;
+# my $opts = ( ref $_[0] eq 'HASH' ) ? shift : {} ;
+
+#print "FILE $file_name\n" ;
+
+# delete unsupported options
+
+ my @bad_opts =
+ grep $_ ne 'err_mode' && $_ ne 'binmode', keys %{$opts} ;
+
+ delete @{$opts}{@bad_opts} ;
+
+# keep the user err_mode and force croaking on internal errors
+
+ my $err_mode = delete $opts->{err_mode} ;
+ $opts->{ err_mode } = 'croak' ;
+
+# get a scalar ref for speed and slurp the file into a scalar
+
+ $opts->{ scalar_ref } = 1 ;
+ my $existing_data = eval { read_file( $file_name, $opts ) } ;
+
+ if ( $@ ) {
+
+ @_ = ( { err_mode => $err_mode },
+ "edit_file '$file_name' - read_file: $!" ) ;
+ goto &_error ;
+ }
+
+#print "EXIST [$$existing_data]\n" ;
+
+ my( $edited_data ) = map { $edit_code->(); $_ } $$existing_data ;
+
+ $opts->{atomic} = 1 ;
+ my $write_result =
+ eval { write_file( $file_name, $opts, $edited_data ) } ;
+
+ if ( $@ ) {
+
+ @_ = ( { err_mode => $err_mode },
+ "edit_file '$file_name' - write_file: $!" ) ;
+ goto &_error ;
+ }
+
+ return $write_result ;
+}
+
+*efl = \&edit_file_lines ;
+
+sub edit_file_lines(&$;$) {
+
+ my( $edit_code, $file_name, $opts ) = @_ ;
+ $opts = {} unless ref $opts eq 'HASH' ;
+
+# my $edit_code = shift ;
+# my $file_name = shift ;
+# my $opts = ( ref $_[0] eq 'HASH' ) ? shift : {} ;
+
+#print "FILE $file_name\n" ;
+
+# delete unsupported options
+
+ my @bad_opts =
+ grep $_ ne 'err_mode' && $_ ne 'binmode', keys %{$opts} ;
+
+ delete @{$opts}{@bad_opts} ;
+
+# keep the user err_mode and force croaking on internal errors
+
+ my $err_mode = delete $opts->{err_mode} ;
+ $opts->{ err_mode } = 'croak' ;
+
+# get an array ref for speed and slurp the file into lines
+
+ $opts->{ array_ref } = 1 ;
+ my $existing_data = eval { read_file( $file_name, $opts ) } ;
+
+ if ( $@ ) {
+
+ @_ = ( { err_mode => $err_mode },
+ "edit_file_lines '$file_name' - read_file: $!" ) ;
+ goto &_error ;
+ }
+
+#print "EXIST [$$existing_data]\n" ;
+
+ my @edited_data = map { $edit_code->(); $_ } @$existing_data ;
+
+ $opts->{atomic} = 1 ;
+ my $write_result =
+ eval { write_file( $file_name, $opts, @edited_data ) } ;
+
+ if ( $@ ) {
+
+ @_ = ( { err_mode => $err_mode },
+ "edit_file_lines '$file_name' - write_file: $!" ) ;
+ goto &_error ;
+ }
+
+ return $write_result ;
+}
+
+# basic wrapper around opendir/readdir
+
+sub read_dir {
+
+ my $dir = shift ;
+ my $opts = ( ref $_[0] eq 'HASH' ) ? shift : { @_ } ;
+
+# this handle will be destroyed upon return
+
+ local(*DIRH);
+
+# open the dir and handle any errors
+
+ unless ( opendir( DIRH, $dir ) ) {
+
+ @_ = ( $opts, "read_dir '$dir' - opendir: $!" ) ;
+ goto &_error ;
+ }
+
+ my @dir_entries = readdir(DIRH) ;
+
+ @dir_entries = grep( $_ ne "." && $_ ne "..", @dir_entries )
+ unless $opts->{'keep_dot_dot'} ;
+
+ if ( $opts->{'prefix'} ) {
+
+ $_ = File::Spec->catfile($dir, $_) for @dir_entries;
+ }
+
+ return @dir_entries if wantarray ;
+ return \@dir_entries ;
+}
+
+# error handling section
+#
+# all the error handling uses magic goto so the caller will get the
+# error message as if from their code and not this module. if we just
+# did a call on the error code, the carp/croak would report it from
+# this module since the error sub is one level down on the call stack
+# from read_file/write_file/read_dir.
+
+
+my %err_func = (
+ 'carp' => \&carp,
+ 'croak' => \&croak,
+) ;
+
+sub _error {
+
+ my( $opts, $err_msg ) = @_ ;
+
+# get the error function to use
+
+ my $func = $err_func{ $opts->{'err_mode'} || 'croak' } ;
+
+# if we didn't find it in our error function hash, they must have set
+# it to quiet and we don't do anything.
+
+ return unless $func ;
+
+# call the carp/croak function
+
+ $func->($err_msg) if $func ;
+
+# return a hard undef (in list context this will be a single value of
+# undef which is not a legal in-band value)
+
+ return undef ;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+File::Slurp - Simple and Efficient Reading/Writing/Modifying of Complete Files
+
+=head1 SYNOPSIS
+
+ use File::Slurp;
+
+ # read in a whole file into a scalar
+ my $text = read_file('/path/file');
+
+ # read in a whole file into an array of lines
+ my @lines = read_file('/path/file');
+
+ # write out a whole file from a scalar
+ write_file('/path/file', $text);
+
+ # write out a whole file from an array of lines
+ write_file('/path/file', @lines);
+
+ # Here is a simple and fast way to load and save a simple config file
+ # made of key=value lines.
+ my %conf = read_file('/path/file') =~ /^(\w+)=(.*)$/mg;
+ write_file('/path/file', {atomic => 1}, map "$_=$conf{$_}\n", keys %conf);
+
+ # insert text at the beginning of a file
+ prepend_file('/path/file', $text);
+
+ # in-place edit to replace all 'foo' with 'bar' in file
+ edit_file { s/foo/bar/g } '/path/file';
+
+ # in-place edit to delete all lines with 'foo' from file
+ edit_file_lines sub { $_ = '' if /foo/ }, '/path/file';
+
+ # read in a whole directory of file names (skipping . and ..)
+ my @files = read_dir('/path/to/dir');
+
+=head1 DESCRIPTION
+
+This module provides subs that allow you to read or write entire files
+with one simple call. They are designed to be simple to use, have
+flexible ways to pass in or get the file contents and to be very
+efficient. There is also a sub to read in all the files in a
+directory.
+
+=head2 WARNING - PENDING DOOM
+
+Although you technically I, do NOT use this module to work on file handles,
+pipes, sockets, standard IO, or the C handle. These are
+features implemented long ago that just really shouldn't be abused here.
+
+Be warned: this activity will lead to inaccurate encoding/decoding of data.
+
+All further mentions of actions on the above have been removed from this
+documentation and that feature set will likely be deprecated in the future.
+
+In other words, if you don't have a filename to pass, consider using the
+standard C<< do { local $/; <$fh> } >>, or
+L/L for working with C<__DATA__>.
+
+=head1 FUNCTIONS
+
+L implements the following functions.
+
+=head2 append_file
+
+ use File::Slurp qw(append_file write_file);
+ my $res = append_file('/path/file', "Some text");
+ # same as
+ my $res = write_file('/path/file', {append => 1}, "Some text");
+
+The C function is simply a synonym for the
+L function, but ensures that the C option is
+set.
+
+=head2 edit_file
+
+ use File::Slurp qw(edit_file);
+ # perl -0777 -pi -e 's/foo/bar/g' /path/file
+ edit_file { s/foo/bar/g } '/path/file';
+ edit_file sub { s/foo/bar/g }, '/path/file';
+ sub replace_foo { s/foo/bar/g }
+ edit_file \&replace_foo, '/path/file';
+
+The C function reads in a file into C<$_>, executes a code block that
+should modify C<$_>, and then writes C<$_> back to the file. The C
+function reads in the entire file and calls the code block one time. It is
+equivalent to the C<-pi> command line options of Perl but you can call it from
+inside your program and not have to fork out a process.
+
+The first argument to C is a code block or a code reference. The
+code block is not followed by a comma (as with C and C