diff --git a/.gitignore b/.gitignore
index 050076ad..bbe0c190 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,8 +2,10 @@
**/*.class
**/*.zip
**/*.jar
-development.conf
+**/*.bloop
+conf/development.conf
conf/production.conf
+conf/staging.conf
production.env
antbuild
project/project
@@ -17,6 +19,7 @@ target
.cache-main
.bsp
.metals
+.vscode
application-log*
gcloud
deploy
@@ -24,4 +27,5 @@ checkall
testdiff
/bin
checker/docker/build
+comrun/build
todo.txt
diff --git a/README.md b/README.md
index a3fd1811..24e5ca4a 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,6 @@
-CodeCheck^®^
+CodeCheck®
============
+* This project has been superseded by [codecheck3](https://github.com/cayhorstmann/codecheck3)
* [Description](https://codecheck.io)
* [Build Instructions](https://github.com/cayhorstmann/codecheck2/blob/main/build-instructions.md)
diff --git a/app/com/horstmann/codecheck/Annotations.java b/app/com/horstmann/codecheck/Annotations.java
index e4887a89..a68f29d8 100644
--- a/app/com/horstmann/codecheck/Annotations.java
+++ b/app/com/horstmann/codecheck/Annotations.java
@@ -5,6 +5,8 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -12,10 +14,36 @@
import java.util.regex.Pattern;
public class Annotations {
- public static final Set VALID_ANNOTATIONS = Set.of(
- "HIDE", "SHOW", "EDIT", "SOLUTION", "CALL", "SUB", "ID", "SAMPLE", "ARGS",
- "IN", "OUT", "TIMEOUT", "TOLERANCE", "IGNORECASE", "IGNORESPACE", "MAXOUTPUTLEN",
- "REQUIRED", "FORBIDDEN", "SCORING", "INTERLEAVE", "TILE", "FIXED", "OR", "PSEUDO");
+ public static final Set VALID_ANNOTATIONS = new LinkedHashSet<>(List.of(
+ "ARGS",
+ "CALL HIDDEN",
+ "CALL",
+ "HIDDEN",
+ "HIDE",
+ "EDIT",
+ "FIXED",
+ "FORBIDDEN",
+ "ID",
+ "SAMPLE",
+ "SUB",
+ "IGNORECASE",
+ "IGNORESPACE",
+ "INTERLEAVE",
+ "IN HIDDEN",
+ "IN",
+ "MAXOUTPUTLEN",
+ "OR",
+ "OUT",
+ "PSEUDO",
+ "REQUIRED",
+ "SCORING",
+ "SHOW",
+ "SOLUTION",
+ "TILE",
+ "TIMEOUT",
+ "TOLERANCE"
+ ));
+ // TODO SAMPLE, SCORING legacy
public static final Set NON_BLANK_BEFORE_OK = Set.of("SUB", "PSEUDO");
public static class Annotation {
@@ -27,35 +55,48 @@ public static class Annotation {
public String next = "";
}
+ /**
+ @param line the line of code to parse
+ @param start the starting comment delimiter
+ @param end the ending comment delimiter
+ @return the parsed annotation, or an empty annotation if none found
+ */
public static Annotation parse(String line, String start, String end) {
Annotation ann = new Annotation();
int i = line.indexOf(start);
if (i < 0) return ann;
String before = line.substring(0, i);
i += start.length();
- String line1 = line.stripTrailing();
- if (!line1.endsWith(end)) return ann;
- int j = line1.length() - end.length();
- int k = i;
- while (k < j && Character.isAlphabetic(line.charAt(k))) k++;
- if (k < j && !Character.isWhitespace(line.charAt(k))) return ann;
- String key = line.substring(i, k);
- if (!VALID_ANNOTATIONS.contains(key)) return ann;
+ String line1 = line.substring(i).stripTrailing();
+ if (end.length() > 0) {
+ if (!line1.endsWith(end)) return ann;
+ line1 = line1.substring(0, line1.length() - end.length()).stripTrailing();
+ }
+ boolean found = false;
+ Iterator iter = VALID_ANNOTATIONS.iterator();
+ String key = "";
+ while (!found && iter.hasNext()) {
+ key = iter.next();
+ if (line1.startsWith(key)) found = true;
+ }
+ if (!found) return ann;
+ if (key.length() < line1.length() && !Character.isWhitespace(line1.charAt(key.length()))) return ann;
// Only a few annotations can have non-blank before
if (!before.isBlank() && !NON_BLANK_BEFORE_OK.contains(key)) return ann;
ann.isValid = true;
ann.before = before;
ann.key = key;
- ann.args = line.substring(k, j).strip();
+ ann.args = line1.substring(key.length()).strip();
return ann;
}
-
private Language language;
private List annotations = new ArrayList<>();
private Set keys = new HashSet<>();
private Set solutions = new TreeSet<>();
private Set hidden = new TreeSet<>();
+ private Set hiddenCallFiles = new TreeSet<>();
+ private Set hiddenTestFiles = new TreeSet<>();
public Annotations(Language language) {
this.language = language;
@@ -88,6 +129,12 @@ private void read(Path p, byte[] contents) {
solutions.add(p);
if (a.key.equals("HIDE"))
hidden.add(p);
+ if (a.key.equals("CALL HIDDEN"))
+ hiddenCallFiles.add(p);
+ if (a.key.equals("HIDDEN")) {
+ hiddenTestFiles.add(p);
+ hidden.add(p);
+ }
a.path = p;
annotations.add(a);
}
@@ -102,7 +149,15 @@ public Set getHidden() {
return Collections.unmodifiableSet(hidden);
}
- public String findUniqueKey(String key) {
+ public Set getHiddenCallFiles() {
+ return Collections.unmodifiableSet(hiddenCallFiles);
+ }
+
+ public Set getHiddenTestFiles() {
+ return Collections.unmodifiableSet(hiddenTestFiles);
+ }
+
+ public String findUnique(String key) {
Annotation match = null;
for (Annotation a : annotations) {
if (a.key.equals(key)) {
@@ -123,7 +178,7 @@ else if (!match.args.equals(a.args)) {
return match == null ? null : match.args;
}
- public List findKeys(String key) {
+ public List findAll(String key) {
List result = new ArrayList<>();
for (Annotation a : annotations)
if (a.key.equals(key))
@@ -152,27 +207,24 @@ public double findUniqueDoubleKey(String key, double defaultValue) {
public boolean checkConditions(Map submissionFiles, Report report) {
+ String[] delims = language.pseudoCommentDelimiters();
for (Annotation a : annotations) {
boolean forbidden = a.key.equals("FORBIDDEN");
if (a.key.equals("REQUIRED") || forbidden) {
+ String nextLine = a.next;
+ String message = null;
+ if (nextLine.startsWith(delims[0]) && nextLine.endsWith(delims[1]))
+ message = nextLine.substring(delims[0].length(), nextLine.length() - delims[1].length()).trim();
StringBuilder contents = new StringBuilder();
for (String line : Util.lines(submissionFiles.get(a.path))) {
- // TODO: Removing comments like this is language specific
- contents.append(line.replaceAll("//.*$", ""));
+ String commentPattern = Pattern.quote(delims[0]) + ".*" + Pattern.quote(delims[1]);
+ contents.append(line.replaceAll(commentPattern, ""));
contents.append(" ");
}
boolean found = Pattern.compile(a.args).matcher(contents).find();
- if (found == forbidden) { // found && forbidden || !found && required
- String nextLine = a.next;
- String[] delims = language.pseudoCommentDelimiters();
- String message;
- if (nextLine.startsWith(delims[0]) && nextLine.endsWith(delims[1]))
- message = nextLine.substring(delims[0].length(), nextLine.length() - delims[1].length()).trim();
- else
- message = (forbidden ? "Found " : "Did not find ") + a.args;
- report.error(a.path + ": " + message);
- return false;
- }
+ boolean passed = found != forbidden; // found and required or not found and forbidden
+ report.condition(passed, forbidden, a.path, a.args, message);
+ if (!passed) return false;
}
}
return true;
@@ -182,6 +234,10 @@ public boolean has(String key) {
return keys.contains(key);
}
+ /*
+ * TODO: It would be more robust if substitutions could only be
+ * in non-editable lines.
+ */
public Substitution findSubstitution() {
Substitution sub = new Substitution(language);
for (Annotation a : annotations) {
@@ -191,6 +247,7 @@ public Substitution findSubstitution() {
return sub;
}
+ // TODO: SAMPLE legacy?
/**
* Checks if a path is a sample
* @param p the path without student/solution directory
@@ -205,9 +262,17 @@ public boolean isSample(Path p) {
public Calls findCalls() {
Calls calls = new Calls(language);
- for (Annotation a : annotations) {
- if (a.key.equals("CALL"))
- calls.addCall(a.path, a.args, a.next);
+ int callNum = 0;
+ for (int a=0; a[A-Za-z][A-Za-z0-9_]*)=(?.*)$");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_PATTERN; }
+
+ private static Pattern FUNCTION_PATTERN = Pattern.compile("^\\s*function\\s+(?[A-Za-z][A-Za-z0-9_]*).*");
+ @Override
+ public String functionName(String declaration) {
+ Matcher matcher = FUNCTION_PATTERN.matcher(declaration);
+ if (matcher.matches()) return matcher.group("name");
+ else return null;
+ }
+
+ @Override
+ public Map writeTester(Path file, String contents, List calls, ResourceLoader resourceLoader) {
+
+ String moduleName = moduleOf(file);
+ String result = contents + "\n";
+ result += "case $1 in\n";
+ for (int k = 0; k < calls.size(); k++) {
+ Calls.Call call = calls.get(k);
+ result += " " + (k + 1) + ")\n " + call.name + " " + call.args + "\n ;;\n";
+ }
+ result += "esac\n";
+
+ Map paths = new HashMap<>();
+ paths.put(pathOf(moduleName + "CodeCheck"), result);
+ return paths;
+ }
+}
diff --git a/app/com/horstmann/codecheck/CLanguage.java b/app/com/horstmann/codecheck/CLanguage.java
index f5415ba0..915734f1 100644
--- a/app/com/horstmann/codecheck/CLanguage.java
+++ b/app/com/horstmann/codecheck/CLanguage.java
@@ -22,7 +22,7 @@ public boolean isSource(Path p) {
private static Pattern MAIN_PATTERN = Pattern.compile("\\s*((int|void)\\s+)?main\\s*\\([^)]*\\)\\s*(\\{\\s*)?");
@Override public Pattern mainPattern() { return MAIN_PATTERN; }
- private static Pattern VARIABLE_PATTERN = Pattern.compile(".*\\S\\s+(?[A-Za-z][A-Za-z0-9]*)(\\s*[*\\[\\]]+)?\\s*=\\s*(?[^;]+);.*");
+ private static Pattern VARIABLE_PATTERN = Pattern.compile(".*\\S\\s+(?[A-Za-z][A-Za-z0-9_]*)(\\s*[*\\[\\]]+)?\\s*=\\s*(?[^;]+);.*");
@Override public Pattern variableDeclPattern() { return VARIABLE_PATTERN; }
private static Pattern ERROR_PATTERN = Pattern.compile(".+/(?[^/]+\\.cpp):(?[0-9]+):(?[0-9]+): error: (?.+)");
diff --git a/app/com/horstmann/codecheck/CSharpLanguage.java b/app/com/horstmann/codecheck/CSharpLanguage.java
index 22add934..08c1f2b3 100644
--- a/app/com/horstmann/codecheck/CSharpLanguage.java
+++ b/app/com/horstmann/codecheck/CSharpLanguage.java
@@ -7,12 +7,8 @@ public class CSharpLanguage implements Language {
public String getExtension() {
return "cs";
}
-
- private static String patternString = ".*\\S\\s+(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\s*=\\s*(?[^;]+);.*";
- private static Pattern pattern = Pattern.compile(patternString);
-
- @Override
- public Pattern variableDeclPattern() {
- return pattern;
- }
+
+ private static Pattern VARIABLE_DECL_PATTERN = Pattern.compile(
+ ".*\\S\\s+(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\s*=\\s*(?[^;]+);");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_DECL_PATTERN; }
}
\ No newline at end of file
diff --git a/app/com/horstmann/codecheck/CallMethod.java b/app/com/horstmann/codecheck/CallMethod.java
index 21a6ac40..041f0142 100644
--- a/app/com/horstmann/codecheck/CallMethod.java
+++ b/app/com/horstmann/codecheck/CallMethod.java
@@ -174,7 +174,7 @@ public void run(Path dir, Report report, Score score) throws Exception {
} finally {
loader.close();
}
- report.runTable(null, new String[] { "Arguments" }, args, actual, expected, outcomes);
+ report.runTable(null, new String[] { "Arguments" }, args, actual, expected, outcomes, null, null);
}
private boolean compare(Object a, Object b) {
diff --git a/app/com/horstmann/codecheck/Calls.java b/app/com/horstmann/codecheck/Calls.java
index e0f05fc3..25c43902 100644
--- a/app/com/horstmann/codecheck/Calls.java
+++ b/app/com/horstmann/codecheck/Calls.java
@@ -1,10 +1,8 @@
package com.horstmann.codecheck;
-import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
public class Calls {
@@ -12,6 +10,16 @@ public class Call {
String name;
String args;
List modifiers;
+ private boolean hidden = false;
+
+ public boolean isHidden() {
+ return hidden;
+ }
+
+ public void setHidden(boolean value)
+ {
+ hidden = value;
+ }
}
private Language language;
@@ -34,6 +42,12 @@ public int getSize() {
public Call getCall(int i) {
return calls.get(i);
}
+
+ public List getCalls() {
+ if (lastGroup < calls.size() - 1)
+ throw new CodeCheckException("No function below CALL in " + file + "\n");
+ return calls;
+ }
public void addCall(Path file, String args, String next) {
if (this.file == null)
@@ -57,12 +71,4 @@ else if (!this.file.equals(file))
lastGroup = calls.size() - 1;
}
}
-
- public Map writeTester(Problem problem, ResourceLoader resourceLoader) throws IOException {
- if (lastGroup < calls.size() - 1)
- throw new CodeCheckException("No function below CALL in " + file + "\n");
- String contents = Util.getString(problem.getSolutionFiles(), file);
- if (contents.isEmpty()) contents = Util.getString(problem.getUseFiles(), file);
- return language.writeTester(file, contents, calls, resourceLoader);
- }
}
diff --git a/app/com/horstmann/codecheck/CompareImages.java b/app/com/horstmann/codecheck/CompareImages.java
index d68d3bc0..82ba396b 100644
--- a/app/com/horstmann/codecheck/CompareImages.java
+++ b/app/com/horstmann/codecheck/CompareImages.java
@@ -17,23 +17,28 @@ public static boolean isImage(String name) {
return Arrays.asList(ImageIO.getReaderFileSuffixes()).contains(extension);
}
- public CompareImages(byte[] firstImage) throws IOException {
- image1 = readImage(firstImage);
+ public CompareImages(byte[] firstImage) {
+ try {
+ image1 = readImage(firstImage);
+ } catch (IOException ex) {
+ image1 = null;
+ }
}
public void setOtherImage(byte[] p) throws IOException {
- image2 = readImage(p);
+ try {
+ image2 = readImage(p);
+ } catch (IOException ex) {
+ image2 = null;
+ }
}
public BufferedImage first() { return image1; }
public BufferedImage other() { return image2; }
private static BufferedImage readImage(byte[] bytes) throws IOException {
- try {
- return ImageIO.read(new ByteArrayInputStream(bytes));
- } catch (Exception ex) {
- throw new IOException("Image not readable");
- }
+ if (bytes == null) throw new IOException("null data");
+ return ImageIO.read(new ByteArrayInputStream(bytes));
}
public boolean getOutcome() {
diff --git a/app/com/horstmann/codecheck/Comparison.java b/app/com/horstmann/codecheck/Comparison.java
index 6595af00..a12ab554 100644
--- a/app/com/horstmann/codecheck/Comparison.java
+++ b/app/com/horstmann/codecheck/Comparison.java
@@ -26,12 +26,10 @@ public boolean execute(String input, String actual, String expected, Report repo
matches.add(m);
}
if (outcome) {
- if (filename != null) {
+ if (filename != null)
report.file(filename, actual);
- }
- else {
- report.output(actual);
- }
+ else
+ report.output(actual);
}
else {
// Print inputs which are getting replaced
diff --git a/app/com/horstmann/codecheck/CppLanguage.java b/app/com/horstmann/codecheck/CppLanguage.java
index 1bd77fb3..19640976 100644
--- a/app/com/horstmann/codecheck/CppLanguage.java
+++ b/app/com/horstmann/codecheck/CppLanguage.java
@@ -4,6 +4,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -33,74 +34,47 @@ public boolean isSource(Path p) {
@Override
public Map writeTester(Path file, String contents, List calls, ResourceLoader resourceLoader) throws IOException {
- // function in solution needs to be in separate namespace
- // function of student needs be externed;
// imports on top
// Look at line in solution file following //CALL
// Remove any trailing {, add ;
// add solution wrapped in namespace solution { ... }
String moduleName = moduleOf(file);
- List lines = Util.lines(contents);
- int i = 0;
- boolean done = false;
- while (!done) {
- if (i == lines.size()) done = true;
- else {
- String line = lines.get(i).trim();
- if (line.length() == 0 || line.startsWith("#include") || line.startsWith("using ") || line.startsWith("//")) i++;
- else done = true;
- }
- }
-
- lines.add(i++, "#include \"codecheck.h\"");
+ List lines = new ArrayList<>();
+ contents
+ .lines()
+ .filter(l -> l.trim().startsWith("#include "))
+ .forEach(l -> lines.add(l));
+ lines.add("#include \"codecheck.h\"");
+ lines.add("#include ");
Set externs = new LinkedHashSet<>();
for (Calls.Call c : calls)
externs.add(c.modifiers.get(0) + " " + c.modifiers.get(1) + ";");
- lines.add(i++, "namespace solution {");
- for (String extern : externs) {
- lines.add(i++, extern); // extern function from solution
- }
- lines.add(i++, "}");
- lines.add(i++, "int main(int argc, char *argv[]) {");
- // We declare the student functions locally in main so that they don't conflict with
- // solution functions
for (String extern : externs) {
- lines.add(i++, extern); // extern function from student
+ lines.add(extern); // extern function from student
}
+ lines.add("int main(int argc, char *argv[]) {");
+ lines.add(" int arg = std::atoi(argv[1]);");
for (int k = 0; k < calls.size(); k++) {
Calls.Call call = calls.get(k);
- lines.add(i++,
- " if (codecheck::eq(argv[1], \"" + (k + 1) + "\")) {");
- lines.add(i++,
- " codecheck::compare(solution::" + call.name + "(" + call.args + "), " + call.name + "(" + call.args + "));");
- // compare expected and actual
- lines.add(i++, "}");
+ lines.add(" if (arg == " + (k + 1) + ") {");
+ lines.add(" codecheck::print(" + call.name + "(" + call.args + "));");
+ lines.add("}");
}
- lines.add(i++, " return 0;");
- lines.add(i++, "}");
- lines.add(i++, "namespace solution {");
+ lines.add(" return 0;");
lines.add("}");
Map paths = new HashMap<>();
paths.put(pathOf(moduleName + "CodeCheck"), Util.join(lines, "\n"));
+ // TODO: Include these in the same file
paths.put(Paths.get("codecheck.cpp"), resourceLoader.loadResourceAsString("codecheck.cpp"));
paths.put(Paths.get("codecheck.h"), resourceLoader.loadResourceAsString("codecheck.h"));
return paths;
}
- private static String patternString = ".*\\S\\s+(?[A-Za-z_][A-Za-z0-9_]*)(\\s*[*\\[\\]]+)?\\s*=\\s*(?[^;]+);.*";
- private static Pattern pattern = Pattern.compile(patternString);
-
- /*
- * (non-Javadoc)
- *
- * @see com.horstmann.codecheck.Language#variablePattern()
- */
- @Override
- public Pattern variableDeclPattern() {
- return pattern;
- }
+ private static Pattern VARIABLE_DECL_PATTERN = Pattern.compile(
+ ".*\\S\\s+(?[A-Za-z_][A-Za-z0-9_]*)(\\s*[*\\[\\]]+)?\\s*=\\s*(?[^;]+);");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_DECL_PATTERN; }
@Override
public List modifiers(String declaration) {
diff --git a/app/com/horstmann/codecheck/DartLanguage.java b/app/com/horstmann/codecheck/DartLanguage.java
index 0554b1e2..3915153d 100644
--- a/app/com/horstmann/codecheck/DartLanguage.java
+++ b/app/com/horstmann/codecheck/DartLanguage.java
@@ -5,6 +5,8 @@
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
+import java.util.ArrayList;
+
public class DartLanguage implements Language {
@@ -14,51 +16,31 @@ public class DartLanguage implements Language {
private static Pattern MAIN_PATTERN = Pattern.compile("\\s*((int|void)\\s+)?main\\s*\\([^)]*\\)\\s*(\\{\\s*)?");
@Override public Pattern mainPattern() { return MAIN_PATTERN; }
- private static Pattern VARIABLE_PATTERN = Pattern.compile(".*\\S\\s+(?[A-Za-z][A-Za-z0-9]*)(\\s*[*\\[\\]]+)?\\s*=\\s*(?[^;]+);.*");
- @Override public Pattern variableDeclPattern() { return VARIABLE_PATTERN; }
+ private static Pattern VARIABLE_DECL_PATTERN
+ = Pattern.compile(".*\\S\\s+(?[A-Za-z][A-Za-z0-9_]*)(\\s*[*\\[\\]]+)?\\s*=\\s*(?[^;]+);");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_DECL_PATTERN; }
private static Pattern ERROR_PATTERN = Pattern.compile(".+/(?[^/]+\\.cpp):(?[0-9]+):(?[0-9]+): error: (?.+)");
@Override public Pattern errorPattern() { return ERROR_PATTERN; }
- // TODO: Implement this
+ // TODO Add test case to samples
@Override
public Map writeTester(Path file, String contents,
List calls,
ResourceLoader resourceLoader) {
-
String moduleName = moduleOf(file);
- String classname = moduleOf(file);
- //List lines = Util.readLines(sourceDir.resolve(file));
-
- List lines = Util.lines(contents);
- int i = 0;
- lines.add(i++, "import '" + moduleName + ".dart';");
- lines.add(i++, "main() {");
+ List lines = new ArrayList<>();
+ lines.add("import '" + moduleName + ".dart';");
+ lines.add("main() {");
for (int k = 0; k < calls.size(); k++) {
-
Calls.Call call = calls.get(k);
- lines.add(i++, " var expected = "
- + call.name + "(" + call.args
- + ");");
- lines.add(i++, " print(expected);");
- lines.add(i++, " var actual = "
- + moduleName + "()." + call.name + "("
-// + call.name + "("
-
- + call.args + ");");
- lines.add(i++, " print(actual);");
- lines.add(i++, " if (expected == actual) ");
- lines.add(i++, " print(\"true\"); ");
- lines.add(i++, " else ");
- lines.add(i++, " print(\"false\"); ");
+ lines.add(" var result = " + moduleName + "()." + call.name + "("
+ + call.args + ");");
+ lines.add(" print(result);");
}
- lines.add(i++, "}");
- //lines.add("main();");
-
- Map paths = new HashMap<>();
- paths.put(pathOf(moduleName + "CodeCheck"), "");
+ lines.add("}");
- Path p = pathOf(classname + "CodeCheck");
+ Path p = pathOf(moduleName + "CodeCheck");
Map testFiles = new HashMap<>();
testFiles.put(p, Util.join(lines, "\n"));
return testFiles;
diff --git a/app/com/horstmann/codecheck/HTMLReport.java b/app/com/horstmann/codecheck/HTMLReport.java
index 87501756..716752d6 100644
--- a/app/com/horstmann/codecheck/HTMLReport.java
+++ b/app/com/horstmann/codecheck/HTMLReport.java
@@ -3,8 +3,9 @@
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
@@ -16,7 +17,19 @@ public class HTMLReport implements Report {
protected StringBuilder builder;
private List footnotes = new ArrayList<>();
private int metaOffset;
+ private boolean hidden;
+ private static boolean isImage(String filename) {
+ return Set.of("png", "gif", "jpg", "jpeg", "bmp").contains(Util.extension(filename));
+ }
+
+ private static String imageData(String filename, byte[] contents) {
+ String extension = Util.extension(filename);
+ if (extension.equals("jpg")) extension = "jpeg";
+ return "data:image/" + extension + ";base64," + Base64.getEncoder().encodeToString(contents);
+ }
+
+
// TODO: Directory
public HTMLReport(String title) {
builder = new StringBuilder();
@@ -62,12 +75,15 @@ public HTMLReport header(String section, String text) {
escape(text);
builder.append("
\n");
}
+ hidden = false;
return this;
}
@Override
- public HTMLReport run(String text) {
+ public HTMLReport run(String text, String mainclass, boolean hidden) {
+ this.hidden = hidden;
+ caption(mainclass);
if (text != null && !text.trim().equals("")) {
builder.append("");
escape(text);
@@ -95,7 +111,10 @@ public HTMLReport output(CharSequence text) {
if (text == null || text.equals(""))
return this;
builder.append("
");
- escape(text);
+ if (hidden)
+ builder.append("[Hidden]");
+ else
+ escape(text);
builder.append("\n");
return this;
}
@@ -121,15 +140,19 @@ public Report footnote(String text) {
public HTMLReport output(List lines, Set matches,
Set mismatches) {
builder.append("");
- for (int i = 0; i < lines.size(); i++) {
- String line = lines.get(i);
- if (matches.contains(i))
- passSpan(line);
- else if (mismatches.contains(i))
- failSpan(line);
- else
- escape(line);
- builder.append("\n");
+ if (hidden) {
+ builder.append("[Hidden]");
+ } else {
+ for (int i = 0; i < lines.size(); i++) {
+ String line = lines.get(i);
+ if (matches.contains(i))
+ passSpan(line);
+ else if (mismatches.contains(i))
+ failSpan(line);
+ else
+ escape(line);
+ builder.append("\n");
+ }
}
builder.append("\n");
return this;
@@ -166,33 +189,22 @@ public HTMLReport systemError(Throwable t) {
* @see com.horstmann.codecheck.Report#image(java.lang.String, byte[])
*/
@Override
- public HTMLReport image(String captionText, BufferedImage img) throws IOException {
+ public HTMLReport image(String captionText, String filename, BufferedImage img) {
if (img == null)
return this;
caption(captionText);
- image(img);
- return this;
- }
-
- /*
- * (non-Javadoc)
- *
- * @see com.horstmann.codecheck.Report#image(byte[])
- */
- @Override
- public HTMLReport image(BufferedImage img) throws IOException {
- if (img == null)
- return this;
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- ImageIO.write(img, "PNG", out);
- out.close();
- byte[] pngBytes = out.toByteArray();
- String data = Base64.getEncoder().encodeToString(pngBytes);
- builder.append("");
- builder.append("
");
- builder.append("
\n");
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ImageIO.write(img, "PNG", out);
+ out.close();
+ builder.append("");
+ builder.append("
");
+ builder.append("
\n");
+ } catch (IOException ex) {
+ builder.append("Cannot display image
");
+ }
return this;
}
@@ -203,6 +215,25 @@ public HTMLReport file(String file, String contents) {
return this;
}
+ public HTMLReport file(String fileName, byte[] contents, boolean hidden) {
+ caption(fileName);
+ if (hidden) {
+ output("[Hidden]");
+ } else if (isImage(fileName)) {
+ builder.append(";)
\n");
+ } else {
+ try {
+ output(StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(contents)).toString());
+ } catch (CharacterCodingException e) {
+ output("[Binary]");
+ }
+ }
+ return this;
+ }
+
+
private HTMLReport escape(CharSequence s) {
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
@@ -280,19 +311,7 @@ protected void addFootnotes() {
builder.append("\n");
}
}
-
- /*
- * (non-Javadoc)
- *
- * @see com.horstmann.codecheck.Report#save(java.nio.file.Path)
- */
- @Override
- public HTMLReport save(Path dir, String out) throws IOException {
- Path outPath = dir.resolve(out + ".html");
- Files.write(outPath, builder.toString().getBytes());
- return this;
- }
-
+
@Override
public void close() {
addFootnotes();
@@ -301,6 +320,9 @@ public void close() {
@Override
public String getText() { return builder.toString(); }
+
+ @Override
+ public String extension() { return "html"; }
private HTMLReport tableStart(String klass) {
builder.append(" matchData) {
@Override
public HTMLReport runTable(String[] methodNames, String[] argNames, String[][] args,
- String[] actual, String[] expected, boolean[] outcomes) {
+ String[] actual, String[] expected, boolean[] outcomes, boolean[] hidden, String mainclass) {
tableStart("run").rowStart();
headerCell("");
if (methodNames != null)
@@ -441,12 +463,20 @@ public HTMLReport runTable(String[] methodNames, String[] argNames, String[][] a
for (int i = 0; i < args.length; i++) {
rowStart();
cellStart().pass(outcomes[i]).cellEnd();
- if (methodNames != null)
- codeCell(methodNames[i]);
- for (String a : args[i])
- codeCell(a.trim());
- codeCell(actual[i]);
- codeCell(expected[i]);
+ if (hidden != null && hidden[i]) {
+ codeCell("[Hidden]");
+ for (String a : args[i])
+ codeCell("[Hidden]");
+ codeCell("[Hidden]");
+ codeCell("[Hidden]");
+ } else {
+ if (methodNames != null)
+ codeCell(methodNames[i]);
+ for (String a : args[i])
+ codeCell(a.trim());
+ codeCell(actual[i]);
+ codeCell(expected[i]);
+ }
rowEnd();
}
tableEnd();
diff --git a/app/com/horstmann/codecheck/HaskellLanguage.java b/app/com/horstmann/codecheck/HaskellLanguage.java
index 9b126c5b..83241d0e 100644
--- a/app/com/horstmann/codecheck/HaskellLanguage.java
+++ b/app/com/horstmann/codecheck/HaskellLanguage.java
@@ -36,27 +36,11 @@ public String functionName(String declaration) {
@Override public Map writeTester(Path file, String contents, List calls, ResourceLoader resourceLoader) {
Map result = new HashMap<>();
-
- // Rewrite solution in module CodeCheckSolution
String moduleName = moduleOf(file);
- Path solutionModuleFile = Paths.get("CodeCheckSolution.hs");
- StringBuilder out = new StringBuilder();
- List in = Util.lines(contents);
- out.append("module CodeCheckSolution where\n");
- for (int i = 0; i < in.size(); i++) {
- String line = in.get(i);
- if (!line.trim().startsWith("module ")) {
- out.append(line);
- out.append("\n");
- }
- }
- result.put(solutionModuleFile, out.toString());
-
// Generate testCodeCheck.hs
Path testFile = Paths.get("testCodeCheck.hs");
- out = new StringBuilder();
+ StringBuilder out = new StringBuilder();
out.append("import " + moduleName + "\n");
- out.append("import CodeCheckSolution\n");
out.append("import Control.Exception\n");
out.append("import System.Environment\n");
out.append("main :: IO ()\n");
@@ -64,50 +48,28 @@ public String functionName(String declaration) {
out.append(" args <- getArgs\n");
for (int k = 0; k < calls.size(); k++) {
Calls.Call call = calls.get(k);
- if (k == 0) out.append(" if (head args) == \"1\" then ");
+ if (k == 0 && calls.size() > 1) out.append(" if (head args) == \"1\" then ");
else if (k < calls.size() - 1) out.append(" else if (head args) == \"" + (k + 1) + "\" then ");
- else out.append(" else ");
- out.append(moduleName + "." + call.name + " " + call.args + " `comp` CodeCheckSolution."
- + call.name + " " + call.args + "\n");
+ else if (calls.size() > 1) out.append(" else ");
+ else out.append(" ");
+ out.append("pr $ " + call.name + " " + call.args + "\n");
}
out.append(" where\n");
out.append(" exec expr = (do x <- evaluate expr ; return $ Just x)\n");
out.append(" `catch` (\\(SomeException x) -> return Nothing)\n");
- out.append(" comp expr1 expr2 = do\n");
- out.append(" actual <- exec expr1\n");
- out.append(" expected <- exec expr2\n");
- out.append(" case (actual, expected) of\n");
- out.append(" (Nothing, Nothing) -> putStrLn \"error\\nerror\\ntrue\"\n");
- out.append(" (Just a, Just b) -> putStrLn$ (show a) ++ \"\\n\" ++ (show b) ++ (if a==b then \"\\ntrue\" else \"\\nfalse\")\n");
- out.append(" (Just a, Nothing) -> putStrLn $ (show a) ++ \"\\nerror\\nfalse\"\n");
- out.append(" (Nothing, Just b) -> putStrLn $ \"error\\n\" ++ (show b) ++ \"\\nfalse\"\n");
+ out.append(" pr expr = do\n");
+ out.append(" res <- exec expr\n");
+ out.append(" case res of\n");
+ out.append(" Nothing -> putStrLn \"error\"\n");
+ out.append(" Just a -> putStrLn $ show a\n");
result.put(testFile, out.toString());
return result;
}
- /*
-
-
-
- Student.maxNum [] `comp` CodeCheckSolution.maxNum [] -- function name and args
- Student.maxNum [1] `comp` CodeCheckSolution.maxNum [1]
- Student.maxNum [1,2,3] `comp` CodeCheckSolution.maxNum [1,2,3]
-*/
-
-
- private static String variablePatternString = "\\s*let\\s+(?\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\s*=\\s*(?.+)";
- private static Pattern variablePattern = Pattern.compile(variablePatternString);
-
- /*
- * (non-Javadoc)
- *
- * @see com.horstmann.codecheck.Language#variablePattern()
- */
- @Override
- public Pattern variableDeclPattern() {
- return variablePattern;
- }
+ private static Pattern VARIABLE_DECL_PATTERN = Pattern.compile(
+ "\\s*let\\s+(?\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\s*=\\s*(?.+)");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_DECL_PATTERN; }
private static Pattern ERROR_PATTERN = Pattern.compile("(?[^/]+\\.rkt):(?[0-9]+):(?[0-9]+): (?.+)");
@Override public Pattern errorPattern() { return ERROR_PATTERN; }
diff --git a/app/com/horstmann/codecheck/Input.java b/app/com/horstmann/codecheck/Input.java
new file mode 100644
index 00000000..fa88aad8
--- /dev/null
+++ b/app/com/horstmann/codecheck/Input.java
@@ -0,0 +1,27 @@
+package com.horstmann.codecheck;
+
+
+public class Input{
+ private String key;
+ private String value;
+ private boolean hidden;
+
+
+ public Input(String key, String value, boolean hidden) {
+ this.key = key;
+ this.value = value;
+ this.hidden = hidden;
+ }
+
+ public boolean getHidden(){
+ return hidden;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
\ No newline at end of file
diff --git a/app/com/horstmann/codecheck/JSONReport.java b/app/com/horstmann/codecheck/JSONReport.java
index 27b68da1..56d927bd 100644
--- a/app/com/horstmann/codecheck/JSONReport.java
+++ b/app/com/horstmann/codecheck/JSONReport.java
@@ -3,9 +3,10 @@
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.nio.file.Path;
+import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Base64;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -13,36 +14,58 @@
import javax.imageio.ImageIO;
-import com.fasterxml.jackson.annotation.JsonInclude.Include;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
public class JSONReport implements Report {
public static class Item {
public Item() {}
public Item(String name, String contents) {
+ this(name, contents, false);
+ }
+ public Item(String name, String contents, boolean hidden) {
this.name = name;
- this.value = contents;
+ this.value = contents;
+ this.hidden = hidden;
}
public String name;
public String value;
+ public boolean hidden;
}
-
+
+ public static class ImageItem {
+ public ImageItem() {}
+ public ImageItem(String caption, String filename, BufferedImage image) {
+ this.caption = caption;
+ name = filename;
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ImageIO.write(image, "PNG", out);
+ out.close();
+ data = Base64.getEncoder().encodeToString(out.toByteArray());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ public String caption;
+ public String name;
+ public String data;
+ }
+
public static class Run {
public String caption;
+ public String mainclass;
public List- args;
public String input;
public String output;
+ public boolean hidden;
public List matchedOutput;
- public List
- files = new ArrayList<>();
- public List
- images = new ArrayList<>();
+ public Map files = new HashMap<>();
+ public List images = new ArrayList<>();
public String errors;
public List errorData = new ArrayList<>();
public String html;
public Boolean passed;
}
- public static class Section {
+ public static class Section {
public String type;
public String errors;
public List errorData = new ArrayList<>();
@@ -51,23 +74,15 @@ public static class Section {
public static class ReportData {
public String errors;
- //public List
- studentFiles = new ArrayList<>();
- //public List
- providedFiles = new ArrayList<>();
public List sections = new ArrayList<>();
public Map metaData = new LinkedHashMap<>();
public String score; // TODO: Score each item
}
- private ReportData data = new ReportData();
+ protected ReportData data = new ReportData();
private Section section;
private Run run;
- /* TODO:
- * testMethod:
- * call:
- * sub:
- */
-
public JSONReport(String title) {
}
@@ -82,8 +97,10 @@ public JSONReport header(String sectionType, String text) {
}
@Override
- public JSONReport run(String caption) {
+ public JSONReport run(String caption, String mainclass, boolean hidden) {
run = new Run();
+ run.mainclass = mainclass;
+ run.hidden = hidden;
run.passed = true;
if (section.runs == null) section.runs = new ArrayList<>();
section.runs.add(run);
@@ -101,7 +118,10 @@ public JSONReport output(CharSequence text) {
if (run.html != null) builder.append(run.html);
builder.append("
Output:
");
builder.append("");
- builder.append(HTMLReport.htmlEscape(text));
+ if (run.hidden)
+ builder.append("[Hidden]");
+ else
+ builder.append(HTMLReport.htmlEscape(text));
builder.append("");
run.html = builder.toString();
@@ -143,8 +163,7 @@ public JSONReport systemError(Throwable t) {
@Override
public JSONReport args(String args) {
- // TODO: Would like to skip if no args
- // if (args == null || args.trim().length() == 0) return this;
+ if (args == null || args.trim().length() == 0) return this;
run.args = new ArrayList<>();
run.args.add(new Item("Command line arguments", args));
return this;
@@ -156,37 +175,28 @@ public JSONReport input(String input) {
StringBuilder builder = new StringBuilder();
if (run.html != null) builder.append(run.html);
if (run.input != null) {
- builder.append("Input:
");
- builder.append(HTMLReport.htmlEscape(run.input));
- builder.append("");
+ if (run.hidden) {
+ builder.append("[Hidden]
");
+ } else {
+ builder.append("Input:
");
+ builder.append(HTMLReport.htmlEscape(run.input));
+ builder.append("");
+ }
}
run.html = builder.toString();
return this;
}
@Override
- public JSONReport image(String caption, BufferedImage image) throws IOException {
- if (image == null) return this;
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- ImageIO.write(image, "PNG", out);
- out.close();
- byte[] pngBytes = out.toByteArray();
- String data = Base64.getEncoder().encodeToString(pngBytes);
- run.images.add(new Item(caption, data));
+ public JSONReport image(String caption, String filename, BufferedImage image) {
+ if (image != null) run.images.add(new ImageItem(caption, filename, image));
return this;
}
- @Override
- public JSONReport image(BufferedImage image) throws IOException {
- image("", image);
- return this;
- }
-
@Override
public JSONReport file(String file, String contents) {
- Item item = new Item(file, contents);
- if (!"studentFiles".equals(section.type) && !"providedFiles".equals(section.type)) {
- run.files.add(item);
+ if (!"studentFiles".equals(section.type)) {
+ run.files.put(file, contents);
StringBuilder builder = new StringBuilder();
if (run.html != null) builder.append(run.html);
@@ -199,6 +209,11 @@ public JSONReport file(String file, String contents) {
}
return this;
}
+
+ public JSONReport file(String fileName, byte[] contents, boolean hidden) {
+ // Not reporting provided files
+ return this;
+ }
@Override
public JSONReport add(Score score) {
@@ -207,24 +222,11 @@ public JSONReport add(Score score) {
}
@Override
- public JSONReport save(Path dir, String out) throws IOException {
- Path outPath = dir.resolve(out + ".json");
- ObjectMapper mapper = new ObjectMapper();
- mapper.setSerializationInclusion(Include.NON_DEFAULT);
- mapper.writeValue(outPath.toFile(), data);
- // JSON.std.write(data, outPath.toFile());
- return this;
- }
-
+ public String extension() { return "json"; }
+
@Override
public String getText() {
- ObjectMapper mapper = new ObjectMapper();
- mapper.setSerializationInclusion(Include.NON_DEFAULT);
- try {
- return mapper.writeValueAsString(data);
- } catch (JsonProcessingException e) {
- return null;
- }
+ return Util.toJsonString(data);
}
@Override
@@ -282,21 +284,25 @@ public JSONReport output(List lines, Set matches,
StringBuilder builder = new StringBuilder();
if (run.html != null) builder.append(run.html);
builder.append("");
- for (int i = 0; i < lines.size(); i++) {
- StringBuilder line = HTMLReport.htmlEscape(lines.get(i));
- if (matches.contains(i)) {
- builder.append("");
- builder.append(line);
- builder.append("");
- }
- else if (mismatches.contains(i)) {
- builder.append("");
- builder.append(line);
- builder.append("");
- }
- else
- builder.append(line);
- builder.append("\n");
+ if (run.hidden) {
+ for (int i = 0; i < lines.size(); i++) {
+ StringBuilder line = HTMLReport.htmlEscape(lines.get(i));
+ if (matches.contains(i)) {
+ builder.append("");
+ builder.append(line);
+ builder.append("");
+ }
+ else if (mismatches.contains(i)) {
+ builder.append("");
+ builder.append(line);
+ builder.append("");
+ }
+ else
+ builder.append(line);
+ builder.append("\n");
+ }
+ } else {
+ builder.append("[Hidden]");
}
builder.append("\n");
run.html = builder.toString();
@@ -306,11 +312,13 @@ else if (mismatches.contains(i)) {
@Override
public JSONReport runTable(String[] methodNames, String[] argNames, String[][] args, String[] actual,
- String[] expected, boolean[] outcomes) {
+ String[] expected, boolean[] outcomes, boolean[] hidden, String mainclass) {
if (section.runs == null) section.runs = new ArrayList<>();
for (int i = 0; i < actual.length; i++)
{
Run run = new Run();
+ run.mainclass = mainclass;
+ run.hidden = hidden != null && hidden[i];
run.passed = true;
if (methodNames != null) run.caption = methodNames[i];
section.runs.add(run);
@@ -372,8 +380,7 @@ public JSONReport footnote(String text) {
}
@Override
- public JSONReport errors(List errorData)
- {
+ public JSONReport errors(List errorData) {
if (section != null)
section.errorData.addAll(errorData);
return this;
diff --git a/app/com/horstmann/codecheck/JavaLanguage.java b/app/com/horstmann/codecheck/JavaLanguage.java
index fda40a3a..8566d712 100644
--- a/app/com/horstmann/codecheck/JavaLanguage.java
+++ b/app/com/horstmann/codecheck/JavaLanguage.java
@@ -7,13 +7,14 @@
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
+import java.util.ArrayList;
public class JavaLanguage implements Language {
public String getExtension() { return "java"; };
@Override
public boolean isUnitTest(Path fileName) {
- return fileName.toString().matches(".*Test[0-9]*.java");
+ return fileName.toString().matches(".*(T|_t)est[0-9]*.java");
}
private static Pattern mainPattern = Pattern
@@ -39,83 +40,50 @@ public Path pathOf(String moduleName) {
@Override
public Map writeTester(Path file, String contents, List calls, ResourceLoader resourceLoader) {
String className = moduleOf(file);
- List lines = Util.lines(contents);
- int i = 0;
- // TODO: Could be enum or record
- while (i < lines.size() && !lines.get(i).contains("class " + className))
- i++;
- if (i == lines.size())
- throw new CodeCheckException("Can't find class " + className
- + " for inserting CALL in " + file);
- lines.set(i, lines.get(i).replace("class " + className,
- "class " + className + "CodeCheck { static class Solution { static class " + className));
- i = lines.size() - 1;
- while (i >= 0 && !lines.get(i).trim().equals("}"))
- i--;
- if (i == -1)
- throw new CodeCheckException("Can't find } for inserting CALL in "
- + file);
- lines.add(i++, " }");
- lines.add(i++, " }");
- lines.add(i++, " public static void main(String[] args) throws Exception");
- lines.add(i++, " {");
- for (int k = 0; k < calls.size(); k++) {
- Calls.Call call = calls.get(k);
- boolean isStatic = call.modifiers.contains("static");
- lines.add(i++, " if (args[0].equals(\"" + (k + 1) + "\"))");
- lines.add(i++, " {");
- if (!isStatic) {
- lines.add(i++, " " + className + " obj1 = new " + className
+ List lines = new ArrayList<>();
+ contents
+ .lines()
+ .filter(l -> l.trim().startsWith("import "))
+ .forEach(l -> lines.add(l));
+ lines.add("public class " + className + "CodeCheck {");
+ lines.add(" public static void main(String[] args) throws Exception");
+ lines.add(" {");
+ for (int k = 0; k < calls.size(); k++) {
+ Calls.Call call = calls.get(k);
+ boolean isStatic = call.modifiers.contains("static");
+ lines.add(" if (args[0].equals(\"" + (k + 1) + "\"))");
+ lines.add(" {");
+ if (!isStatic) {
+ lines.add(" " + className + " obj1 = new " + className
+ "();");
- lines.add(i++, " Solution." + className + " obj2 = new Solution." + className + "();");
- }
- lines.add(i++, " Object expected = "
- + (isStatic ? "Solution." + className : "obj2") + "." + call.name + "(" + call.args
- + ");");
- lines.add(i++,
- " System.out.println(_toString(expected));");
- lines.add(i++, " Object actual = "
+ }
+ lines.add(" Object result = "
+ (isStatic ? className : "obj1") + "." + call.name + "("
+ call.args + ");");
- lines.add(i++, " System.out.println(_toString(actual));");
- lines.add(
- i++,
- " System.out.println(java.util.Objects.deepEquals(actual, expected));");
- lines.add(i++, " }");
+ lines.add(" System.out.println(_toString(result));");
+ lines.add(" }");
}
- lines.add(i++, " }");
- lines.add(i++, " private static String _toString(Object obj)");
- lines.add(i++, " {");
- lines.add(i++, " if (obj == null) return \"null\";");
- lines.add(i++, " if (obj instanceof String) return \"\\\"\" + ((String) obj).replace(\"\\\\\", \"\\\\\\\\\").replace(\"\\\"\", \"\\\\\\\"\").replace(\"\\n\", \"\\\\n\") + \"\\\"\";");
- lines.add(i++, " if (obj instanceof Object[])");
- lines.add(i++,
- " return java.util.Arrays.deepToString((Object[]) obj);");
- lines.add(i++, " if (obj.getClass().isArray())");
- lines.add(
- i++,
- " try { return (String) java.util.Arrays.class.getMethod(\"toString\", obj.getClass()).invoke(null, obj); }");
- lines.add(i++, " catch (Exception ex) {}");
- lines.add(i++, " return obj.toString();");
- lines.add(i++, " }");
-
- // expected == null ? null : expected instanceof Object[] ?
- // java.util.Arrays.deepToString((Object[]) expected) :
- // expected.getClass().isArray() ? java.util.Arrays.toString(expected) :
- // expected
+ lines.add(" }");
+ lines.add(" private static String _toString(Object obj)");
+ lines.add(" {");
+ lines.add(" if (obj == null) return \"null\";");
+ lines.add(" if (obj instanceof Object[])");
+ lines.add(" return java.util.Arrays.deepToString((Object[]) obj);");
+ lines.add(" if (obj.getClass().isArray())");
+ lines.add(" try { return (String) java.util.Arrays.class.getMethod(\"toString\", obj.getClass()).invoke(null, obj); }");
+ lines.add(" catch (Exception ex) {}");
+ lines.add(" return obj.toString();");
+ lines.add(" }");
+ lines.add("}");
Path p = pathOf(className + "CodeCheck");
Map testFiles = new HashMap<>();
testFiles.put(p, Util.join(lines, "\n"));
return testFiles;
}
- private static String patternString = "\\s*((public|static|final|private|protected)\\s+)*[A-Za-z0-9_<>\\[\\]]+\\s+(?\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\s*=\\s*(?[^;]+);.*";
- private static Pattern pattern = Pattern.compile(patternString);
-
- @Override
- public Pattern variableDeclPattern() {
- return pattern;
- }
+ private static Pattern VARIABLE_DECL_PATTERN = Pattern.compile(
+ "((public|static|final|private|protected)\\s+)*[A-Za-z0-9_<>\\[\\]]+\\s+(?\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\s*=\\s*(?[^;]+);");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_DECL_PATTERN; }
@Override
public String process(Path file, Map submissionFiles) {
diff --git a/app/com/horstmann/codecheck/JavaScriptLanguage.java b/app/com/horstmann/codecheck/JavaScriptLanguage.java
index 89a4547a..36ea55fd 100644
--- a/app/com/horstmann/codecheck/JavaScriptLanguage.java
+++ b/app/com/horstmann/codecheck/JavaScriptLanguage.java
@@ -2,7 +2,6 @@
import java.io.IOException;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
@@ -46,41 +45,20 @@ public Map writeTester(Path file, String contents, List call
Set functionNames = new TreeSet<>();
for (Calls.Call call : calls) functionNames.add(call.name);
List lines = new ArrayList<>();
- lines.add("const codecheck = require('./codecheck.js')"); // TODO: Update to ECMAScript module
- lines.add("const studentFunctions = function(){");
lines.addAll(Util.lines(contents));
- lines.add("return {");
- for (String functionName : functionNames)
- lines.add(functionName + ", ");
- lines.add("}}()");
- lines.add("const solutionFunctions = function(){");
- lines.addAll(Util.lines(contents));
- lines.add("return {");
- for (String functionName : functionNames)
- lines.add(functionName + ", ");
- lines.add("}}()");
-
for (int k = 0; k < calls.size(); k++) {
Calls.Call call = calls.get(k);
lines.add("if (process.argv[process.argv.length - 1] === '" + (k + 1) + "') {");
- lines.add("const actual = studentFunctions." + call.name + "(" + call.args + ")");
- lines.add("const expected = solutionFunctions." + call.name + "(" + call.args + ")");
- lines.add("console.log(JSON.stringify(expected))");
- lines.add("console.log(JSON.stringify(actual))");
- lines.add("console.log(codecheck.deepEquals(actual, expected))");
+ lines.add("const result = " + call.name + "(" + call.args + ")");
+ lines.add("console.log(JSON.stringify(result))");
lines.add("}");
}
Map paths = new LinkedHashMap<>();
paths.put(pathOf(moduleName + "CodeCheck"), Util.join(lines, "\n"));
- paths.put(Paths.get("codecheck.js"), resourceLoader.loadResourceAsString("codecheck.js")); // TODO mjs when we support ECMAScript modules
return paths;
}
- private static String patternString = "(var|const|let)\\s+(?[A-Za-z][A-Za-z0-9]*)\\s*=(?[^;]+);?";
- private static Pattern pattern = Pattern.compile(patternString);
-
- @Override
- public Pattern variableDeclPattern() {
- return pattern;
- }
+ private static Pattern VARIABLE_DECL_PATTERN = Pattern.compile(
+ "(var|const|let)\\s+(?[A-Za-z][A-Za-z0-9_]*)\\s*=(?[^;]+);?");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_DECL_PATTERN; }
}
diff --git a/app/com/horstmann/codecheck/KotlinLanguage.java b/app/com/horstmann/codecheck/KotlinLanguage.java
new file mode 100644
index 00000000..63dc37b3
--- /dev/null
+++ b/app/com/horstmann/codecheck/KotlinLanguage.java
@@ -0,0 +1,63 @@
+package com.horstmann.codecheck;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class KotlinLanguage implements Language {
+
+ @Override
+ public String getExtension() {
+ return "kt";
+ }
+
+ private static Pattern mainPattern1 = Pattern
+ .compile("fun\\s+main\\s*\\((\\s*\\S+\\s*:\\s*Array<\\s*String\\s*>\\s*)?\\)");
+
+ @Override
+ public boolean isMain(Path p, String contents) {
+ return mainPattern1.matcher(contents).find();
+ }
+
+ @Override
+ public Map writeTester(Path file, String contents, List calls, ResourceLoader resourceLoader) {
+ // Requirement: File name must equal to object containing called method
+ String objectName = moduleOf(file);
+ List lines = new ArrayList<>();
+ lines.add("fun main(args: Array) {");
+ for (int k = 0; k < calls.size(); k++) {
+ Calls.Call call = calls.get(k);
+ String submissionFun = call.name;
+ lines.add("if (args[0] == \"" + (k + 1) + "\") {");
+ lines.add(" val result = " + submissionFun + "(" + call.args + ")");
+ lines.add(" println(result)");
+ lines.add("}");
+ }
+ lines.add("}");
+ Path p = Paths.get(objectName + "CodeCheck.kt");
+ Map testModules = new HashMap<>();
+ testModules.put(p, Util.join(lines, "\n"));
+ return testModules;
+ }
+
+ private static Pattern functionPattern = Pattern.compile(
+ "^\\s*fun\\s+(?\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\s*(.*).*$");
+
+ @Override public String functionName(String declaration) {
+ Matcher matcher = functionPattern.matcher(declaration);
+ if (matcher.matches()) return matcher.group("name");
+ else return null;
+ }
+
+ private static Pattern VARIABLE_DECL_PATTERN = Pattern.compile(
+ "(val|var)\\s+(?\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)(\\s*:[^=]+\\s*)?\\s*=\\s*(?[^;]+)");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_DECL_PATTERN; }
+
+ private static Pattern ERROR_PATTERN = Pattern.compile("(.+/)?(?[^/]+\\.kt):(?[0-9]+):(?[0-9]+): error: (?.+)");
+ @Override public Pattern errorPattern() { return ERROR_PATTERN; }
+}
diff --git a/app/com/horstmann/codecheck/Language.java b/app/com/horstmann/codecheck/Language.java
index 420bccba..ea4323c2 100644
--- a/app/com/horstmann/codecheck/Language.java
+++ b/app/com/horstmann/codecheck/Language.java
@@ -25,7 +25,11 @@ public interface Language {
new CSharpLanguage(),
new HaskellLanguage(),
new SMLLanguage(),
- new DartLanguage()
+ new DartLanguage(),
+ new RustLanguage(),
+ new BashLanguage(),
+ new KotlinLanguage(),
+ new PHPLanguage()
};
static Language languageFor(Set files) {
@@ -116,7 +120,7 @@ default boolean isTester(Path fileName) {
if (fileName == null) return false;
String moduleName = moduleOf(fileName);
if (moduleName == null) return false;
- return moduleName.matches(".*Tester[0-9]*");
+ return moduleName.matches(".*(T|_t)ester[0-9]*");
}
/**
@@ -143,7 +147,7 @@ default boolean isMain(Path fileName, String contents) {
if (!fileName.toString().endsWith("." + extension)) return false;
Pattern p = mainPattern();
if (p == null)
- return moduleOf(fileName).matches(".*(Runn|Test)er[0-9]*");
+ return moduleOf(fileName).matches(".*(Runn|_runn|Test|_test)er[0-9]*");
else
return p.matcher(contents).find();
}
@@ -180,16 +184,16 @@ default Map writeTester(Path file, String contents, List submissionFiles, Substitution sub) throws Exception {
@@ -109,65 +113,63 @@ private void doSubstitutions(Map submissionFiles, Substitution sub
actual[i] = Util.truncate(actual[i], expected[i].length() + MUCH_LONGER);
score.pass(outcomes[i], null); // Pass/fail shown in run table
}
- report.runTable(null, argNames, args, actual, expected, outcomes);
+ report.runTable(null, argNames, args, actual, expected, outcomes, null, mainFile.toString());
});
}
+
+ private void doCalls(Map submissionFiles, Calls calls, ResourceLoader resourceLoader) throws Exception {
+ Path file = calls.getFile();
+
+ String submissionContents = submissionFiles.get(file);
+ if (submissionContents.isEmpty()) submissionContents = Util.getString(problem.getUseFiles(), file);
+ Map submissionTesterFiles = problem.getLanguage().writeTester(file, submissionContents, calls.getCalls(), resourceLoader);
+ Path submissionBase = Paths.get("submissioncallfiles");
+ for (Map.Entry entry : submissionTesterFiles.entrySet())
+ plan.addFile(submissionBase.resolve(entry.getKey()), entry.getValue());
+ List submissionSources = new ArrayList(submissionTesterFiles.keySet());
- private void doCalls(Calls calls, ResourceLoader resourceLoader) throws Exception {
- Map testerFiles = calls.writeTester(problem, resourceLoader);
- Path base = Paths.get("callfiles");
- for (Map.Entry entry : testerFiles.entrySet())
- plan.addFile(base.resolve(entry.getKey()), entry.getValue());
+ String solutionContents = Util.getString(problem.getSolutionFiles(), file);
+ if (solutionContents.isEmpty()) solutionContents = Util.getString(problem.getUseFiles(), file);
+ Map solutionTesterFiles = problem.getLanguage().writeTester(file, solutionContents, calls.getCalls(), resourceLoader);
+ Path solutionBase = Paths.get("solutioncallfiles");
+ for (Map.Entry entry : solutionTesterFiles.entrySet())
+ plan.addFile(solutionBase.resolve(entry.getKey()), entry.getValue());
+ List solutionSources = new ArrayList(solutionTesterFiles.keySet());
+
+ // TODO: This is the only place with a list of sources.
+ // If the multiple sources in the C++ tester are removed, do we still need them?
+ plan.compile("submissioncall", "submission submissioncallfiles", submissionSources, dependentSourcePaths);
+ plan.compile("solutioncall", "solution solutioncallfiles", solutionSources, dependentSourcePaths);
- String[] names = new String[calls.getSize()];
- String[][] args = new String[calls.getSize()][1];
- String[] actual = new String[calls.getSize()];
- String[] expected = new String[calls.getSize()];
- boolean[] outcomes = new boolean[calls.getSize()];
-
int timeout = timeoutMillis / calls.getSize();
int maxOutput = maxOutputLen / calls.getSize();
- List sources = new ArrayList(testerFiles.keySet());
- plan.compile("call", "submission callfiles", sources, dependentSourcePaths); // TODO: This is the only place with a list of sources. Why???
for (int i = 0; i < calls.getSize(); i++) {
- Path mainFile = sources.get(0);
- // TODO: Solution code not isolated from student. It would be more secure to
- // change call strategy to generate output.
- plan.run("call", "call", "call" + i, mainFile, "", "" + (i + 1), timeout, maxOutput, false);
+ plan.run("submissioncall", "submissioncall", "submissioncall" + i, submissionSources.get(0), "", "" + (i + 1), timeout, maxOutput, false);
+ plan.run("solutioncall", "solutioncall", "solutioncall" + i, solutionSources.get(0), "", "" + (i + 1), timeout, maxOutput, false);
}
plan.addTask(() -> {
- report.header("call", "Calling with Arguments");
- if (!plan.checkCompiled("call", report, score)) return;
+ report.header("call", "Calling with arguments");
+ if (!plan.checkCompiled("submissioncall", report, score)) return;
+ if (!plan.checkCompiled("solutioncall", report, score)) return;
+ String[] names = new String[calls.getSize()];
+ String[][] args = new String[calls.getSize()][1];
+ String[] actual = new String[calls.getSize()];
+ String[] expected = new String[calls.getSize()];
+ boolean[] outcomes = new boolean[calls.getSize()];
+ boolean[] hidden = new boolean[calls.getSize()];
+
for (int i = 0; i < calls.getSize(); i++) {
- String output = plan.outerr("call" + i);
- List lines = Util.lines(output);
+ actual[i] = plan.outerr("submissioncall" + i);
+ expected[i] = plan.outerr("solutioncall" + i);
+ outcomes[i] = actual[i].equals(expected[i]);
Calls.Call call = calls.getCall(i);
names[i] = call.name;
args[i][0] = call.args;
- if (lines.size() == 3 && Arrays.asList("true", "false").contains(lines.get(2))) {
- expected[i] = lines.get(0);
- actual[i] = Util.truncate(lines.get(1), expected[i].length() + MUCH_LONGER);
- outcomes[i] = lines.get(2).equals("true");
- } else {
- // Error in compilation or execution
- // We assume that the solution correctly produces a single line
- // Most likely, the rest is an exception report, and the true/false never came
- StringBuilder msg = new StringBuilder();
- for (int j = 1; j < lines.size(); j++) {
- String line = lines.get(j);
- if (j < lines.size() - 1 || !(line.equals("true") || line.equals("false")))
- msg.append(line); msg.append('\n');
- }
- String message = msg.toString();
-
- expected[i] = lines.size() > 0 ? lines.get(0) : "";
- actual[i] = message;
- outcomes[i] = false;
- }
+ hidden[i] = call.isHidden();
score.pass(outcomes[i], null /* no report--it's in the table */);
}
- report.runTable(names, new String[] { "Arguments" }, args, actual, expected, outcomes);
+ report.runTable(names, new String[] { "Arguments" }, args, actual, expected, outcomes, hidden, submissionSources.get(0).toString());
});
}
@@ -178,16 +180,17 @@ private void runUnitTests() {
unitTests.add(p);
}
if (unitTests.size() > 0) {
- report.header("unitTest", "Unit Tests");
+ plan.addTask(() -> report.header("unitTest", "Unit tests"));
for (Path p: unitTests) {
String id = plan.nextID("test");
plan.unitTest(id, p, dependentSourcePaths, timeoutMillis / unitTests.size(), maxOutputLen / unitTests.size());
plan.addTask(() -> {
- report.run(p.toString());
+ boolean hidden = problem.getAnnotations().getHiddenTestFiles().contains(p);
+ report.run(p.toString(), p.toString(), hidden);
if (!plan.checkCompiled(id, report, score)) return;
String outerr = plan.outerr(id);
- problem.getLanguage().reportUnitTest(outerr, report, score);
+ problem.getLanguage().reportUnitTest(outerr, report, score);
});
}
@@ -199,7 +202,8 @@ private void runTester(Path mainFile, int timeout, int maxOutputLen) throws Exce
plan.compile(compileID, "submission", mainFile, dependentSourcePaths);
plan.run(compileID, compileID, mainFile, "", null, timeout, maxOutputLen, false);
plan.addTask(() -> {
- report.run("Running " + mainFile);
+ boolean hidden = problem.getAnnotations().getHiddenTestFiles().contains(mainFile);
+ report.run(mainFile.toString(), mainFile.toString(), hidden);
if (!plan.checkCompiled(compileID, report, score)) return;
String outerr = plan.outerr(compileID);
AsExpected cond = new AsExpected(comp);
@@ -210,56 +214,60 @@ private void runTester(Path mainFile, int timeout, int maxOutputLen) throws Exce
});
}
- private void testInputs(Map inputs, Path mainFile, boolean okToInterleave) throws Exception {
+ private void testInputs(List inputs, Path mainFile, boolean okToInterleave) throws Exception {
/*
* If there are no inputs, we feed in one empty input to execute the program.
*/
if (inputs.size() == 0)
- inputs.put("", "");
+ inputs.add(new Input("", "", false));
plan.compile("submissionrun", "submission", mainFile, dependentSourcePaths);
- boolean runSolution = !problem.getInputMode() && !problem.getAnnotations().isSample(mainFile);
- if (runSolution)
+ boolean runSolution = !problem.getInputMode() && !problem.getAnnotations().isSample(mainFile); // TODO: SAMPLE legacy?
+ if (runSolution) {
plan.compile("solutionrun", "solution", mainFile, dependentSourcePaths);
+ }
plan.addTask(() -> {
- report.header("run", problem.getInputMode() ? "Output" : "Testing " + mainFile);
if (runSolution)
plan.checkSolutionCompiled("solutionrun", report, score);
plan.checkCompiled("submissionrun", report, score);
});
- for (String test : inputs.keySet()) {
- String input = inputs.get(test);
- testInput(mainFile, runSolution, test, input, timeoutMillis / inputs.size(), maxOutputLen / inputs.size(), okToInterleave);
+ for (int i = 0; i < inputs.size(); i++) {
+ String test = inputs.get(i).getKey();
+ String input = inputs.get(i).getValue();
+ boolean hidden = inputs.get(i).getHidden();
+ testInput(mainFile, runSolution, test, input, timeoutMillis / inputs.size(), maxOutputLen / inputs.size(), okToInterleave, hidden);
}
}
private void testInput(Path mainFile,
- boolean runSolution, String test, String input, int timeout, int maxOutput, boolean okToInterleave)
+ boolean runSolution, String test, String input, int timeout, int maxOutput, boolean okToInterleave, boolean hidden)
throws Exception {
- List runargs = problem.getAnnotations().findKeys("ARGS");
+ List runargs = problem.getAnnotations().findAll("ARGS");
if (runargs.size() == 0) runargs.add("");
- String out = problem.getAnnotations().findUniqueKey("OUT");
+ String out = problem.getAnnotations().findUnique("OUT");
List outFiles = out == null ? Collections.emptyList() : Arrays.asList(out.trim().split("\\s+"));
String runNumber = test.replace("test", "").trim();
- plan.addTask(() -> {
- if (!plan.compiled("submissionrun")) return;
- report.run(!test.equals("Input") && runNumber.length() > 0 ? "Test " + runNumber : null);
- });
+ String title = !test.equals("Input") && runNumber.length() > 0 ? "Test " + runNumber : null;
boolean interleaveio = okToInterleave && (problem.getLanguage().echoesStdin() == Language.Interleave.ALWAYS ||
problem.getLanguage().echoesStdin() == Language.Interleave.UNGRADED && test.equals("Input"));
if (input == null || input.isBlank()) interleaveio = false;
else if (!input.endsWith("\n")) input += "\n";
// TODO: Language settings
+ // TODO: Pass runSolution to report?
for (String args : runargs) {
- testInput(mainFile, runSolution, test, input, args, outFiles, timeout / runargs.size(), maxOutput / runargs.size(), interleaveio);
+ plan.addTask(() -> {
+ if (!plan.compiled("submissionrun")) return;
+ report.run(title, mainFile.toString(), hidden);
+ });
+ testInput(mainFile, runSolution, test, input, args, outFiles, timeout / runargs.size(), maxOutput / runargs.size(), interleaveio, hidden) ;
}
}
private void testInput(Path mainFile,
- boolean runSolution, String test, String input, String runargs, List outFiles, int timeout, int maxOutput, boolean interleaveio)
+ boolean runSolution, String test, String input, String runargs, List outFiles, int timeout, int maxOutput, boolean interleaveio, boolean hidden)
throws Exception {
String submissionRunID = plan.nextID("submissionrun");
plan.run("submissionrun", submissionRunID, mainFile, runargs, input, outFiles, timeout, maxOutput, interleaveio);
@@ -272,71 +280,60 @@ private void testInput(Path mainFile,
report.args(runargs);
if (!interleaveio && !test.equals("Input")) report.input(input);
-
- List contents = new ArrayList<>();
- List imageComp = new ArrayList<>();
+
String outerr = plan.outerr(submissionRunID);
+ String expectedOuterr = runSolution ? plan.outerr(solutionRunID) : null;
+ if (expectedOuterr != null && expectedOuterr.trim().length() > 0 && outFiles.size() == 0) {
+ boolean outcome = comp.execute(input, outerr, expectedOuterr, report, null);
+ score.pass(outcome, report);
+ } else {
+ // Not scoring output if there are outFiles, but showing in case there is an exception
+ report.output(outerr);
+ }
+
+ Map contents = new HashMap<>();
+ Map imageComp = new HashMap<>();
for (String f : outFiles) {
if (CompareImages.isImage(f)) {
- try {
- imageComp.add(new CompareImages(plan.getOutputBytes(submissionRunID, f)));
- } catch (IOException ex) {
- report.output(outerr);
- report.error(ex.getMessage());
- }
+ imageComp.put(f, new CompareImages(plan.getOutputBytes(submissionRunID, f)));
}
else
- contents.add(plan.getOutputString(submissionRunID, f));
+ contents.put(f, plan.getOutputString(submissionRunID, f));
}
if (!runSolution) {
- report.output(outerr);
for (String f : outFiles) {
if (CompareImages.isImage(f)) {
- try {
- report.image("Image", imageComp.remove(0).first());
- } catch (IOException ex) {
- report.error(ex.getMessage());
- }
+ CompareImages ci = imageComp.get(f);
+ report.image(null, f, ci.first());
}
else
- report.file(f, contents.remove(0));
+ report.file(f, contents.get(f));
}
// No score
return;
}
- String expectedOuterr = plan.outerr(solutionRunID);
-
- if (expectedOuterr != null && expectedOuterr.trim().length() > 0) {
- if (outFiles.size() > 0) {
- // Report output but don't grade it
- report.output(outerr);
- } else {
- boolean outcome = comp.execute(input, outerr, expectedOuterr, report, null);
- score.pass(outcome, report);
- }
- }
-
for (String f : outFiles) {
if (CompareImages.isImage(f)) {
- CompareImages ic = imageComp.remove(0);
+ CompareImages ic = imageComp.get(f);
try {
ic.setOtherImage(plan.getOutputBytes(solutionRunID, f));
boolean outcome = ic.getOutcome();
- report.image("Image", ic.first());
- if (!outcome) {
- report.image("Expected", ic.other());
- report.image("Mismatched pixels", ic.diff());
+ if (outcome) {
+ report.image(outFiles.size() > 1 ? f : null, f, ic.first());
+ } else {
+ report.image(outFiles.size() > 1 ? f : "Actual", f, ic.first());
+ report.image("Expected", null, ic.other());
+ report.image("Mismatched pixels", null, ic.diff());
}
score.pass(outcome, report);
} catch (IOException ex) {
report.error(ex.getMessage());
}
- } else {
+ } else { // TODO: Should we support binary files???
String expectedContents = plan.getOutputString(solutionRunID, f);
- boolean outcome = comp.execute(input, contents.remove(0),
- expectedContents, report, f);
+ boolean outcome = comp.execute(input, contents.get(f), expectedContents, report, f);
score.pass(outcome, report);
}
}
@@ -400,10 +397,11 @@ public void reportComments(Properties metadata) {
report.comment(entries.getKey().toString(), entries.getValue().toString());
}
- public Report run(Map submissionFiles, Map problemFiles,
+ public Plan run(Map submissionFiles, Map problemFiles,
String reportType, Properties metadata, ResourceLoader resourceLoader) throws IOException {
long startTime = System.currentTimeMillis();
- boolean scoring = true;
+ boolean okToInterleave = true;
+ boolean scoring = true; // TODO: Legacy?
try {
// Set up report first in case anything else throws an exception
@@ -413,45 +411,54 @@ else if ("JSON".equals(reportType))
report = new JSONReport("Report");
else if ("NJS".equals(reportType))
report = new NJSReport("Report");
+ else if ("Setup".equals(reportType)) {
+ report = new SetupReport("Report");
+ // okToInterleave = false; // TODO
+ }
else
report = new HTMLReport("Report");
+ plan = new Plan(resourceLoader.getProperty("com.horstmann.codecheck.debug") != null);
+ plan.setReport(report);
+ plan.readSolutionOutputs(problemFiles);
+
problem = new Problem(problemFiles);
+ if (report instanceof SetupReport) ((SetupReport) report).setProblem(problem);
+ plan.setLanguage(problem.getLanguage());
- plan = new Plan(problem.getLanguage(), resourceLoader.getProperty("com.horstmann.codecheck.debug") != null);
+ Set printFiles = Util.filterNot(problem.getUseFiles().keySet(), "*.jar", "*.pdf"); // TODO pdf???
+ printFiles.removeAll(problem.getSolutionFiles().keySet());
timeoutMillis = (int) problem.getAnnotations().findUniqueDoubleKey("TIMEOUT", DEFAULT_TIMEOUT_MILLIS);
maxOutputLen = (int) problem.getAnnotations().findUniqueDoubleKey("MAXOUTPUTLEN", DEFAULT_MAX_OUTPUT_LEN);
double tolerance = problem.getAnnotations().findUniqueDoubleKey("TOLERANCE", DEFAULT_TOLERANCE);
- boolean ignoreCase = !"false".equalsIgnoreCase(problem.getAnnotations().findUniqueKey("IGNORECASE"));
- boolean ignoreSpace = !"false".equalsIgnoreCase(problem.getAnnotations().findUniqueKey("IGNORESPACE"));
- boolean okToInterleave = !"false".equalsIgnoreCase(problem.getAnnotations().findUniqueKey("INTERLEAVE"));
- scoring = !"false".equalsIgnoreCase(problem.getAnnotations().findUniqueKey("SCORING"));
+ boolean ignoreCase = !"false".equalsIgnoreCase(problem.getAnnotations().findUnique("IGNORECASE"));
+ boolean ignoreSpace = !"false".equalsIgnoreCase(problem.getAnnotations().findUnique("IGNORESPACE"));
+ if ("false".equalsIgnoreCase(problem.getAnnotations().findUnique("SCORING"))) scoring = false; // TODO: Legacy?
+ if ("false".equalsIgnoreCase(problem.getAnnotations().findUnique("INTERLEAVE"))) okToInterleave = false;
comp.setTolerance(tolerance);
comp.setIgnoreCase(ignoreCase);
- comp.setIgnoreSpace(ignoreSpace);
-
+ comp.setIgnoreSpace(ignoreSpace);
+
+ if (tolerance != DEFAULT_TOLERANCE) report.attribute("tolerance", tolerance);
+ if (timeoutMillis != DEFAULT_TIMEOUT_MILLIS) report.attribute("timeout", timeoutMillis);
+ if (maxOutputLen != DEFAULT_MAX_OUTPUT_LEN) report.attribute("maxOutputLen", maxOutputLen);
+ if (ignoreCase == false) report.attribute("ignoreCase", ignoreCase);
+ if (ignoreSpace == false) report.attribute("ignoreSpace", ignoreSpace);
+ if (okToInterleave == false) report.attribute("interleave", okToInterleave);
+
getMainAndDependentSourceFiles();
reportComments(metadata);
copyFilesToPlan(submissionFiles);
-
- // TODO: This would be nice to have in Problem, except that one might later need to remove checkstyle.xml
- // the use files that the students are entitled to see
- Set printFiles = Util.filterNot(problem.getUseFiles().keySet(),
- "*.png", "*.PNG", "*.gif", "*.GIF", "*.jpg", "*.jpeg", "*.JPG", "*.bmp", "*.BMP",
- "*.jar", "*.pdf");
- printFiles.removeAll(problem.getAnnotations().getHidden());
- printFiles.removeAll(problem.getSolutionFiles().keySet());
-
if (problem.getAnnotations().checkConditions(submissionFiles, report)) {
- if (problem.getAnnotations().has("CALL")) {
+ if (problem.getAnnotations().has("CALL") || problem.getAnnotations().has("CALL HIDDEN")) {
Calls calls = problem.getAnnotations().findCalls();
mainSourcePaths.remove(calls.getFile());
dependentSourcePaths.add(calls.getFile());
- doCalls(calls, resourceLoader);
+ doCalls(submissionFiles, calls, resourceLoader);
}
if (problem.getAnnotations().has("SUB")) {
Substitution sub = problem.getAnnotations().findSubstitution();
@@ -460,10 +467,10 @@ else if ("NJS".equals(reportType))
doSubstitutions(submissionFiles, sub);
}
- Map inputs = new TreeMap<>();
+ List inputs = new ArrayList();
for (String i : new String[] { "", "1", "2", "3", "4", "5", "6", "7", "8", "9" }) {
String key = "test" + i + ".in";
-
+
Path p = Paths.get(key);
byte[] contents = null;
if (problemFiles.containsKey(p))
@@ -474,15 +481,18 @@ else if ("NJS".equals(reportType))
contents = problemFiles.get(p);
}
if (contents != null)
- inputs.put("test" + i, new String(contents, StandardCharsets.UTF_8));
+ inputs.add(new Input("test" + i, new String(contents, StandardCharsets.UTF_8), false));
}
int inIndex = inputs.size();
- for (String s : problem.getAnnotations().findKeys("IN")) {
- inputs.put("test" + ++inIndex, Util.unescapeJava(s));
+ for (String s : problem.getAnnotations().findAll("IN")) {
+ inputs.add(new Input("test" + ++inIndex, Util.unescapeJava(s), false));
+ }
+ for (String s : problem.getAnnotations().findAll("IN HIDDEN")) {
+ inputs.add(new Input("test" + ++inIndex, Util.unescapeJava(s), true));
}
if (problem.getInputMode()) {
Path p = Paths.get("Input");
- inputs.put("Input", submissionFiles.get(p));
+ inputs.add(new Input("Input", submissionFiles.get(p), false));
}
runUnitTests();
@@ -491,19 +501,20 @@ else if ("NJS".equals(reportType))
List runFiles = new ArrayList<>();
for (Path mainSourceFile : mainSourcePaths) {
if (problem.getLanguage().isTester(mainSourceFile) && !problem.getSolutionFiles().keySet().contains(mainSourceFile)
- && !problem.getAnnotations().isSample(mainSourceFile) && !problem.getInputMode())
+ && !problem.getAnnotations().isSample(mainSourceFile) && !problem.getInputMode()) // TODO: SAMPLE legacy?
testerFiles.add(mainSourceFile);
else
runFiles.add(mainSourceFile);
}
if (testerFiles.size() > 0) {
- report.header("tester", "Testers");
+ plan.addTask(() -> report.header("tester", "Running test"));
for (Path testerFile : testerFiles)
runTester(testerFile, timeoutMillis / testerFiles.size(), maxOutputLen / testerFiles.size());
}
if (runFiles.size() > 0) {
+ plan.addTask(() -> report.header("run", "Running program"));
for (Path runFile : runFiles)
testInputs(inputs, runFile, okToInterleave);
}
@@ -520,11 +531,11 @@ else if ("NJS".equals(reportType))
});
}
}
+ String remoteURL = resourceLoader.getProperty("com.horstmann.codecheck.comrun.remote");
+ String scriptCommand = resourceLoader.getProperty("com.horstmann.codecheck.comrun.local");
+ if (remoteURL == null && scriptCommand == null) scriptCommand = "/opt/codecheck/comrun";
+ plan.execute(report, remoteURL, scriptCommand);
}
- String remoteURL = resourceLoader.getProperty("com.horstmann.codecheck.comrun.remote");
- String scriptCommand = resourceLoader.getProperty("com.horstmann.codecheck.comrun.local");
- if (remoteURL == null && scriptCommand == null) scriptCommand = "/opt/codecheck/comrun";
- plan.execute(report, remoteURL, scriptCommand);
if (!problem.getInputMode()) { // Don't print submitted or provided files for run-only mode
report.header("studentFiles", "Submitted files");
@@ -537,8 +548,11 @@ else if ("NJS".equals(reportType))
if (printFiles.size() > 0) {
report.header("providedFiles", "Provided files");
- for (Path p : printFiles)
- report.file(p.toString(), new String(problem.getUseFiles().get(p), StandardCharsets.UTF_8));
+ for (Path p : printFiles) {
+ boolean hidden = problem.getAnnotations().getHidden().contains(p)
+ || problem.getAnnotations().getHiddenTestFiles().contains(p);
+ report.file(p.toString(), problem.getUseFiles().get(p), hidden);
+ }
}
}
} catch (Throwable t) {
@@ -554,6 +568,6 @@ else if ("NJS".equals(reportType))
}
else System.err.println("report is null");
}
- return report;
+ return plan;
}
}
diff --git a/app/com/horstmann/codecheck/MatlabLanguage.java b/app/com/horstmann/codecheck/MatlabLanguage.java
index bc7a31c2..8fc1f49a 100644
--- a/app/com/horstmann/codecheck/MatlabLanguage.java
+++ b/app/com/horstmann/codecheck/MatlabLanguage.java
@@ -47,27 +47,9 @@ public Map writeTester(Path file, String contents, List[A-Za-z][A-Za-z0-9_]*)\\s*=\\s*(?.+)");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_DECL_PATTERN; }
}
diff --git a/app/com/horstmann/codecheck/NJSReport.java b/app/com/horstmann/codecheck/NJSReport.java
index 9f8a5f0c..7072e2d0 100644
--- a/app/com/horstmann/codecheck/NJSReport.java
+++ b/app/com/horstmann/codecheck/NJSReport.java
@@ -1,16 +1,10 @@
package com.horstmann.codecheck;
-import java.io.IOException;
-import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import com.fasterxml.jackson.annotation.JsonInclude.Include;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
public class NJSReport extends HTMLReport {
public static class ReportData {
public List errors = new ArrayList<>();
@@ -58,26 +52,14 @@ public NJSReport errors(List errors) {
return this;
}
- @Override
- public NJSReport save(Path dir, String out) throws IOException {
- Path outPath = dir.resolve(out + ".json");
- ObjectMapper mapper = new ObjectMapper();
- mapper.setSerializationInclusion(Include.NON_DEFAULT);
- mapper.writeValue(outPath.toFile(), data);
- return this;
- }
-
@Override
public String getText() {
- ObjectMapper mapper = new ObjectMapper();
- mapper.setSerializationInclusion(Include.NON_DEFAULT);
- try {
- return mapper.writeValueAsString(data);
- } catch (JsonProcessingException e) {
- return null;
- }
+ return Util.toJsonString(data);
}
+ @Override
+ public String extension() { return "json"; }
+
@Override
public void close() {
if (sectionType != null) builder.append("\n");
diff --git a/app/com/horstmann/codecheck/PHPLanguage.java b/app/com/horstmann/codecheck/PHPLanguage.java
new file mode 100644
index 00000000..88b7648d
--- /dev/null
+++ b/app/com/horstmann/codecheck/PHPLanguage.java
@@ -0,0 +1,58 @@
+package com.horstmann.codecheck;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+
+import com.horstmann.codecheck.Calls.Call;
+
+public class PHPLanguage implements Language {
+ @Override
+ public String getExtension() {
+ return "php";
+ }
+
+ @Override
+ public Map writeTester(Path file, String contents, List calls, ResourceLoader resourceLoader) throws IOException {
+ String moduleName = moduleOf(file);
+ Set functionNames = new TreeSet<>();
+ for (Calls.Call call : calls) functionNames.add(call.name);
+ List lines = new ArrayList<>();
+ lines.addAll(Util.lines(contents));
+ for (int i = lines.size() - 1; i >= 0; i--) {
+ if (lines.get(i).strip().equals("?>")) {
+ lines.set(i, "");
+ i = 0;
+ }
+ }
+ for (int k = 0; k < calls.size(); k++) {
+ Calls.Call call = calls.get(k);
+ lines.add("if ($argv[1] === '" + (k + 1) + "') {");
+ lines.add("$result = " + call.name + "(" + call.args + ");");
+ lines.add("var_export($result);"); // TODO Or var_dump, print_r
+ lines.add("}");
+ }
+ lines.add("?>");
+ Map paths = new LinkedHashMap<>();
+ paths.put(pathOf(moduleName + "CodeCheck"), Util.join(lines, "\n"));
+ return paths;
+ }
+
+ private static Pattern VARIABLE_DECL_PATTERN = Pattern.compile(
+ "^\\s*(?\\$[A-Za-z][A-Za-z0-9_]*)\\s*=(?[^;]+);");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_DECL_PATTERN; }
+
+ public boolean isUnitTest(Path fileName) { return fileName.toString().matches(".*(T|_t)est[0-9]*.php"); }
+
+ private static final Pattern successPattern = Pattern.compile("OK \\([0-9]+ tests?, (?[0-9.]+) assertions\\)");
+ private static final Pattern failurePattern = Pattern.compile("Tests: [0-9]+, Assertions: (?[0-9]+), Failures: (?[0-9]+)\\.");
+ @Override public Pattern unitTestSuccessPattern() { return successPattern; }
+ @Override public Pattern unitTestFailurePattern() { return failurePattern; }
+
+}
diff --git a/app/com/horstmann/codecheck/Plan.java b/app/com/horstmann/codecheck/Plan.java
index 5f64c256..ebe43ec7 100644
--- a/app/com/horstmann/codecheck/Plan.java
+++ b/app/com/horstmann/codecheck/Plan.java
@@ -10,9 +10,11 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.Set;
public class Plan {
private Language language;
@@ -23,13 +25,57 @@ public class Plan {
private int nextID = 0;
private static int MIN_TIMEOUT = 3; // TODO: Maybe better to switch interleaveio and timeout?
private boolean debug;
+ private Report report;
- public Plan(Language language, boolean debug) throws IOException {
- this.language = language;
+ public Plan(boolean debug) throws IOException {
this.debug = debug;
if (debug) addScript("debug");
}
+
+ public void setLanguage(Language language) {
+ this.language = language;
+ }
+ public void setReport(Report report) {
+ this.report = report;
+ }
+
+ public Report getReport() {
+ return this.report;
+ }
+
+ public void writeSolutionOutputs(Map filesToSave) {
+ for (Map.Entry entry : outputs.entrySet()) {
+ Path p = entry.getKey();
+ if (p.getName(p.getNameCount() - 1).toString().equals("_errors"))
+ return;
+ }
+ for (Map.Entry entry : outputs.entrySet()) {
+ Path p = entry.getKey();
+ if (p.getName(0).toString().startsWith("solution")) {
+ filesToSave.put(Path.of("_outputs").resolve(p), entry.getValue());
+ }
+ }
+ }
+
+ public void readSolutionOutputs(Map savedFiles) {
+ boolean removeOnly = false;
+ for (Map.Entry entry : savedFiles.entrySet()) {
+ Path p = entry.getKey();
+ if (p.getName(p.getNameCount() - 1).toString().equals("_errors"))
+ removeOnly = true;
+ }
+ Set toRemove = new HashSet<>();
+ for (Map.Entry entry : savedFiles.entrySet()) {
+ Path p = entry.getKey();
+ if (p.getName(0).toString().equals("_outputs")) {
+ if (!removeOnly) outputs.put(p.subpath(1, p.getNameCount()), entry.getValue());
+ toRemove.add(p);
+ }
+ }
+ savedFiles.keySet().removeAll(toRemove);
+ }
+
public String nextID(String prefix) {
nextID++;
return prefix + nextID;
@@ -65,7 +111,7 @@ public boolean checkSolutionCompiled(String compileDir, Report report, Score sco
String errorReport = getOutputString(compileDir, "_errors");
if (errorReport == null) return true;
if (errorReport.trim().equals(""))
- report.systemError("Compilation of solution ailed");
+ report.systemError("Compilation of solution failed");
else
report.systemError(errorReport);
score.setInvalid();
@@ -120,7 +166,8 @@ public void compile(String compileDir, String sourceDirs, List sourceFiles
allSourceFiles.addAll(sourceFiles);
allSourceFiles.addAll(dependentSourceFiles);
addScript("prepare " + compileDir + " use " + sourceDirs);
- addScript("compile " + compileDir + " " + language.getLanguage() + " " + Util.join(allSourceFiles, " "));
+ if (!outputs.containsKey(Paths.get(compileDir).resolve("_compile")))
+ addScript("compile " + compileDir + " " + language.getLanguage() + " " + Util.join(allSourceFiles, " "));
}
// TODO maxOutputLen
@@ -133,10 +180,12 @@ public void run(String compileDir, String runDir, String runID, Path mainFile, S
if (!compileDir.equals(runDir))
addScript("prepare " + runDir + " " + compileDir);
addFile(Paths.get("in").resolve(runID), input == null ? "" : input);
- addScript("run " + runDir + " " + runID + " " + Math.max(MIN_TIMEOUT, (timeout + 500) / 1000) + " " + maxOutputLen + " " + interleaveIO + " " + language.getLanguage() + " " + mainFile + (args == null ? "" : " " + args));
+ if (!outputs.containsKey(Paths.get(runID).resolve("_run")))
+ addScript("run " + runDir + " " + runID + " " + Math.max(MIN_TIMEOUT, (timeout + 500) / 1000) + " " + maxOutputLen + " " + interleaveIO + " " + language.getLanguage() + " " + mainFile + (args == null ? "" : " " + args));
}
public void run(String compileDir, String runDir, Path mainFile, String args, String input, Collection outfiles, int timeout, int maxOutputLen, boolean interleaveIO) {
+ if (outputs.containsKey(Paths.get(runDir).resolve("_run"))) return;
run(compileDir, runDir, mainFile, input, args, timeout, maxOutputLen, interleaveIO);
if (outfiles.size() > 0)
addScript("collect " + runDir + " " + Util.join(outfiles, " "));
@@ -179,7 +228,7 @@ private void executeLocally(String scriptCommand, Report report)
responseZip = Paths.get(lines[n]);
if (!Files.exists(responseZip))
throw new CodeCheckException("comrun failed.\n" + result);
- outputs = Util.unzip(Files.readAllBytes(responseZip));
+ outputs.putAll(Util.unzip(Files.readAllBytes(responseZip)));
} finally {
if (!debug) {
Files.deleteIfExists(requestZip);
@@ -195,7 +244,9 @@ private void executeRemotely(String remoteURL, Report report)
while (retries > 0) {
try {
byte[] responseZip = Util.fileUpload(remoteURL, "job", "job.zip", requestZip);
- outputs = Util.unzip(responseZip);
+ if (responseZip.length < 2 || !(responseZip[0] == 0x50 && responseZip[1] == 0x4b))
+ throw new IOException("Remote result not a zip file");
+ outputs.putAll(Util.unzip(responseZip));
if (debug) {
Path temp = Files.createTempFile("codecheck-request", ".zip");
System.out.println("Remote request at " + temp);
diff --git a/app/com/horstmann/codecheck/Problem.java b/app/com/horstmann/codecheck/Problem.java
index dc908c55..1ee448d4 100644
--- a/app/com/horstmann/codecheck/Problem.java
+++ b/app/com/horstmann/codecheck/Problem.java
@@ -61,7 +61,7 @@ public static class DisplayData {
public Map useFiles = new LinkedHashMap<>();
public String description;
}
-
+
private Map problemFiles;
private Map useFiles = new Util.FileMap();
// the files (sources and inputs) from problemFiles that must be copied to the directory
@@ -79,11 +79,15 @@ public static class DisplayData {
private static final Pattern IMG_PATTERN = Pattern
.compile("[<]\\s*[iI][mM][gG]\\s*[sS][rR][cC]\\s*[=]\\s*['\"]([^'\"]*)['\"][^>]*[>]");
+ private static final Pattern LINK_START = Pattern
+ .compile("<\\s*[aA]\\s+[^>]*[hH][rR][eE][fF]\\s*=\\s*['\"]([^'\"]+)['\"][^>]*>");
+ private static final Pattern LINK_END = Pattern.compile("<\\s*/\\s*[aA]\\s*>");
public Problem(Map problemFiles) throws IOException {
- this.problemFiles = problemFiles;
language = Language.languageFor(problemFiles.keySet());
if (language == null) throw new CodeCheckException("Cannot find language from " + problemFiles.keySet());
+
+ this.problemFiles = problemFiles;
annotations = new Annotations(language);
String[] delims = language.pseudoCommentDelimiters();
start = delims[0];
@@ -135,9 +139,9 @@ else if (initial.equals("solution")) {
}
}
inputFiles.put(inputPath, problemFiles.get(inputPath));
- for (String runargs : annotations.findKeys("ARGS"))
+ for (String runargs : annotations.findAll("ARGS"))
for (String arg : runargs.split("\\s+"))
- if (isTextFile(arg)) {
+ if (isTextFile(arg)) { // TODO: would also make sense to display used image files
Path argPath = Paths.get(arg);
if (useFiles.containsKey(argPath)) {
inputFiles.put(argPath, useFiles.get(argPath));
@@ -201,6 +205,26 @@ private String readDescription(String descriptionFile) {
if (start != -1)
result.replace(0, start, "");
+ // Check if links are relative or not, if relative links, change it to normal text
+ Matcher linkMatcherStart = LINK_START.matcher(result);
+ Matcher linkMatcherEnd = LINK_END.matcher(result);
+ int startLink = 0;
+ int endLink = 0;
+ while (linkMatcherStart.find(startLink) && linkMatcherEnd.find(startLink)) {
+ startLink = linkMatcherStart.start();
+ endLink = linkMatcherEnd.end();
+ String hrefLink = result.substring(linkMatcherStart.start(1), linkMatcherStart.end(1)).toLowerCase();
+ if (!(hrefLink.startsWith("http://") || hrefLink.startsWith("https://"))) {
+ int startContent = linkMatcherStart.end();
+ int endContent = linkMatcherEnd.start();
+ String contentOfLink = result.substring(startContent, endContent);
+ result.replace(startLink, endLink, contentOfLink);
+ startLink += contentOfLink.length();
+ }
+ else
+ startLink = endLink;
+ }
+
Matcher matcher = IMG_PATTERN.matcher(result);
start = 0;
while (matcher.find(start)) {
@@ -284,13 +308,15 @@ private EditorState processHideShow(String contents) {
boolean hasEdit = false;
boolean hasShow = false;
boolean hasTile = false;
+ boolean firstHide = false;
for (int i = 0; i < lines.length && !hasTile && !hasEdit; i++) {
- Annotations.Annotation ann = Annotations.parse(lines[i], start, end);
- if (ann.key.equals("EDIT")) hasEdit = true;
+ Annotations.Annotation ann = Annotations.parse(lines[i], start, end);
+ if (i == 0 && (ann.key.equals("HIDE") || ann.key.equals("HIDDEN"))) firstHide = true;
+ else if (ann.key.equals("EDIT")) hasEdit = true;
else if (ann.key.equals("SHOW")) hasShow = true;
else if (ann.key.equals("TILE")) hasTile = true;
}
- if (lines.length == 0 || Annotations.parse(lines[0], start, end).key.equals("HIDE") && !hasShow && !hasEdit) {
+ if (lines.length == 0 || firstHide && !hasShow && !hasEdit) {
EditorState state = new EditorState();
state.editors = new ArrayList(); // Empty list means file is hidden
return state;
@@ -420,17 +446,6 @@ private EditorState hideEditMode(String[] lines) {
hiding = true;
if (hiding)
lines[i] = null;
- if (ann.key.equals("SHOW")) {
- hiding = false;
- String showString = start + "SHOW";
- int n1 = lines[i].indexOf(showString);
- int n2 = showString.length();
- int n3 = lines[i].lastIndexOf(end);
- if (n1 + n2 < n3)
- lines[i] = lines[i].substring(0, n1) + lines[i].substring(n1 + n2 + 1, n3);
- else
- lines[i] = null;
- }
}
}
// Emit final section
@@ -677,6 +692,7 @@ private EditorState tileMode(String[] lines) {
return state;
}
+ // TODO: This could be more liberal--see HTMLReport for identifying text vs. binary
private static boolean isTextFile(String p) { return p.toLowerCase().endsWith(".txt"); }
private static boolean isTextFile(Path p) { return isTextFile(p.toString()); }
@@ -721,7 +737,7 @@ public Problem.DisplayData getProblemData() {
}
public String getId() {
- String problemId = annotations.findUniqueKey("ID");
+ String problemId = annotations.findUnique("ID");
if (problemId == null) { // TODO: Move to Problem
problemId = Util.removeExtension(solutionFiles.keySet().iterator().next().getFileName());
}
diff --git a/app/com/horstmann/codecheck/PythonLanguage.java b/app/com/horstmann/codecheck/PythonLanguage.java
index bfa7aba2..93673bcb 100644
--- a/app/com/horstmann/codecheck/PythonLanguage.java
+++ b/app/com/horstmann/codecheck/PythonLanguage.java
@@ -5,6 +5,7 @@
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
+import java.util.ArrayList;
public class PythonLanguage implements Language {
@@ -51,7 +52,7 @@ public boolean isMain(Path p, String contents) {
@Override
public Map writeTester(Path file, String contents, List calls, ResourceLoader resourceLoader) {
String moduleName = moduleOf(file);
- List lines = Util.lines(contents);
+ List lines = new ArrayList<>();
int i = 0;
lines.add(i++, "from sys import argv");
lines.add(i++, "import " + moduleName);
@@ -61,27 +62,9 @@ public Map writeTester(Path file, String contents, List[A-Za-z][A-Za-z0-9]*)\\s*=\\s*(?.+)");
+ private static Pattern varPattern = Pattern.compile("(?[A-Za-z][A-Za-z0-9_]*)\\s*=\\s*(?.+)");
@Override public Pattern variableDeclPattern() { return varPattern; }
private static Pattern errPattern = Pattern.compile("(?.*)[\"(](?[A-Za-z0-9_]+\\.py)\"?, line (?[0-9]+).*");
@Override public Pattern errorPattern() { return errPattern; }
- public boolean isUnitTest(Path fileName) { return fileName.toString().matches(".*Test[0-9]*.py"); }
+ public boolean isUnitTest(Path fileName) { return fileName.toString().matches(".*(T|_t)est[0-9]*.py"); }
private static final Pattern successPattern = Pattern.compile("Ran (?[0-9]+) tests? in [0-9.]+s\\s+OK");
private static final Pattern failurePattern = Pattern.compile("Ran (?[0-9]+) tests? in [0-9.]+s\\s+FAILED \\([^=]+=(?[0-9]+)\\)");
diff --git a/app/com/horstmann/codecheck/RacketLanguage.java b/app/com/horstmann/codecheck/RacketLanguage.java
index aabb67fa..012a8fc5 100644
--- a/app/com/horstmann/codecheck/RacketLanguage.java
+++ b/app/com/horstmann/codecheck/RacketLanguage.java
@@ -46,12 +46,10 @@ public boolean isMain(Path p, String contents) {
@Override
public Map writeTester(Path file, String contents, List calls, ResourceLoader resourceLoader) {
String moduleName = moduleOf(file);
- // Copy source/module.rkt to target/module-solution.rkt
StringBuilder out = new StringBuilder();
out.append("#lang racket\n");
- out.append("(require (prefix-in solution:: \"" + moduleName + "-solution.rkt\"))\n");
- out.append("(require (prefix-in student:: \"" + moduleName + ".rkt\"))\n");
+ out.append("(require (prefix-in submission:: \"" + moduleName + ".rkt\"))\n");
out.append("(provide main)\n");
out.append("(define (main . args)\n");
out.append("(let ((arg (car args))) (cond\n");
@@ -61,29 +59,27 @@ public Map writeTester(Path file, String contents, List testerFiles = new HashMap<>();
testerFiles.put(Paths.get(moduleName + "CodeCheck.rkt"), out.toString());
- testerFiles.put(Paths.get(moduleName+ "-solution.rkt"), contents);
return testerFiles;
}
-
- private static String patternString = ".*\\S\\s+(?\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\s*=\\s*(?[^;]+);.*";
- private static Pattern pattern = Pattern.compile(patternString);
- @Override public Pattern variableDeclPattern() { return pattern; }
+ // CAUTION: Because the rhs is likely to contain (), there must be a space
+ // before the final ), e.g. (define l '(1 2 3) )
+ private static Pattern VARIABLE_DECL_PATTERN = Pattern.compile(
+ "\\(\\s*define\\s+(?[A-Za-z][A-Za-z0-9_]*)\\s+(?.+)\\s+\\)");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_DECL_PATTERN; }
+
@Override
public boolean isUnitTest(Path fileName) {
- return fileName.toString().matches(".*Test[0-9]*.rkt");
+ return fileName.toString().matches(".*(T|_t)est[0-9]*.rkt");
}
private static final Pattern successPattern = Pattern.compile("All (?[0-9]+) tests passed");
diff --git a/app/com/horstmann/codecheck/Report.java b/app/com/horstmann/codecheck/Report.java
index 387711f6..c88a4419 100644
--- a/app/com/horstmann/codecheck/Report.java
+++ b/app/com/horstmann/codecheck/Report.java
@@ -1,7 +1,6 @@
package com.horstmann.codecheck;
import java.awt.image.BufferedImage;
-import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
@@ -18,7 +17,7 @@ static class Match
Report header(String section, String text);
- Report run(String caption);
+ Report run(String caption, String mainclass, boolean hidden);
Report output(CharSequence text);
@@ -27,33 +26,43 @@ static class Match
Report systemError(String message);
Report systemError(Throwable t);
+
+ default Report condition(boolean passed, boolean forbidden, Path path, String regex, String message) {
+ if (!passed) {
+ if (message == null) message = (forbidden ? "Found " : "Did not find ") + regex;
+ error(path + ": " + message);
+ }
+ return this;
+ }
- Report image(String caption, BufferedImage image) throws IOException;
-
- Report image(BufferedImage image) throws IOException;
-
+ Report image(String caption, String filename, BufferedImage image);
+
Report file(String file, String contents);
+ Report file(String file, byte[] contents, boolean hidden);
Report args(String args);
Report input(String input);
Report add(Score score);
-
- Report save(Path dir, String out) throws IOException;
Report pass(boolean b);
Report compareTokens(String filename, List matches);
Report output(List lines, Set matches, Set mismatches);
- Report runTable(String[] functionNames, String[] argNames, String[][] args, String[] actual, String[] expected, boolean[] outcomes);
- Report comment(String key, String value);
+ // TODO: record for RunTableRow
+ Report runTable(String[] functionNames, String[] argNames, String[][] args, String[] actual, String[] expected, boolean[] outcomes, boolean[] hidden, String mainclass);
+ Report comment(String key, String value);
+ default Report attribute(String key, Object value) { return this; }
+
Report footnote(String text);
default void close() {}
String getText();
default Report errors(List errors) { return this; }
+
+ String extension();
}
\ No newline at end of file
diff --git a/app/com/horstmann/codecheck/RustLanguage.java b/app/com/horstmann/codecheck/RustLanguage.java
new file mode 100644
index 00000000..02b2d4dc
--- /dev/null
+++ b/app/com/horstmann/codecheck/RustLanguage.java
@@ -0,0 +1,58 @@
+package com.horstmann.codecheck;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+public class RustLanguage implements Language {
+ public String getExtension() { return "rs"; };
+
+ private static Pattern mainPattern = Pattern.compile("fn\\s+main\\s*\\([^)]*\\)\\s*(\\{\\s*)?");
+ @Override public Pattern mainPattern() { return mainPattern; }
+
+ private static Pattern VARIABLE_DECL_PATTERN =Pattern.compile("let\\s*(mut\\s*)?(?[A-Za-z][A-Za-z0-9_]*)\\s*(:\\s*(((i|u)(8|16|32|64|128|size))|(f32|f64|bool|char)|(\\[(((i|u)(8|16|32|64|128|size))|(f32|f64|bool|char))\\s*;\\s*[0-9]*\\s*\\]))\\s*)?=\\s*(?.+);\\s*");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_DECL_PATTERN; }
+
+ @Override
+ public boolean isUnitTest(Path fileName) {
+ return fileName.toString().matches(".*(T|_t)est[0-9]*.rs");
+ }
+
+ @Override
+ public Map writeTester(Path file, String contents, List calls, ResourceLoader resourceLoader) {
+ String moduleName = moduleOf(file);
+ List lines = new ArrayList<>();
+ lines.add("use std::env;");
+ lines.add("mod " + moduleName + ";");
+ lines.add("fn main() {");
+ lines.add(" let args: Vec = env::args().collect();");
+ for (int k = 0; k < calls.size(); k++) {
+ Calls.Call call = calls.get(k);
+ lines.add(" if args[1] == \"" + (k + 1) + "\" {");
+ lines.add(" let result = " + moduleName + "::" + call.name + "(" + call.args + ");");
+ lines.add(" println!(\"{:?}\", result);");
+ lines.add(" }");
+ }
+ lines.add("}");
+ Path p = pathOf(moduleName + "CodeCheck");
+ Map testFiles = new HashMap<>();
+ testFiles.put(p, Util.join(lines, "\n"));
+ return testFiles;
+ }
+
+ private static final Pattern successPattern = Pattern.compile("running (?[0-9]+) tests?[\\s\\S]*test result: ok. [0-9]+ passed; (?[0-9]+) failed;");
+ @Override public Pattern unitTestSuccessPattern() { return successPattern; }
+ private static final Pattern failurePattern = Pattern.compile("running (?[0-9]+) tests?[\\s\\S]*test result: FAILED. [0-9]+ passed; (?[0-9]+) failed;");
+ @Override public Pattern unitTestFailurePattern() { return failurePattern; }
+
+ // TODO: Change regex to include error message
+ private static Pattern ERROR_PATTERN = Pattern.compile("--> (?[^\\s\\S]+\\.rs):(?[0-9]+)");
+ @Override public Pattern errorPattern() { return ERROR_PATTERN; }
+
+
+
+
+}
diff --git a/app/com/horstmann/codecheck/SMLLanguage.java b/app/com/horstmann/codecheck/SMLLanguage.java
index 9f306d43..e9622e1d 100644
--- a/app/com/horstmann/codecheck/SMLLanguage.java
+++ b/app/com/horstmann/codecheck/SMLLanguage.java
@@ -40,50 +40,24 @@ public Map writeTester(Path file, String contents, List NONE ;\n");
- out.append("fun comp expr1 expr2 = let\n");
- out.append(" val actual = eval expr1\n");
- out.append(" val expected = eval expr2\n");
- out.append(" in\n");
- out.append(" (case (actual, expected) of\n");
- out.append(" (NONE, NONE) => \"exception\\nexception\\ntrue\\n\" |\n");
- out.append(" (NONE, SOME y) => \"exception\\n\" ^ (PolyML.makestring y) ^ \"\\nfalse\\n\" |\n");
- out.append(" (SOME x, NONE) => (PolyML.makestring x) ^ \"\\nexception\\nfalse\\n\" |\n");
- out.append(" (SOME x, SOME y) => (PolyML.makestring x) ^ \"\\n\" ^ (PolyML.makestring y) ^ \"\\n\" ^ (PolyML.makestring (x = y)) ^ \"\\n\")\n");
- out.append("end;\n");
- out.append("fun main() = print (case hd(CommandLine.arguments()) of \n");
+ out.append("fun main() = print (PolyML.makestring (case hd(CommandLine.arguments()) of \n");
for (int k = 0; k < calls.size(); k++) {
Calls.Call call = calls.get(k);
if (k < calls.size() - 1)
- out.append(" \"" + (k + 1) + "\" => comp (fn () => (Solution." +
- call.name + " " + call.args + ")) (fn () => (" +
- call.name + " " + call.args + ")) |\n");
+ out.append(" \"" + (k + 1) + "\" => (" +
+ call.name + " " + call.args + ") |\n");
else
- out.append(" _ => comp (fn () => (Solution." +
- call.name + " " + call.args + ")) (fn () => (" +
- call.name + " " + call.args + ")));\n");
+ out.append(" _ => (" +
+ call.name + " " + call.args + ")) handle exn => \"exception\");\n");
}
Map result = new HashMap<>();
result.put(testFile, out.toString());
return result;
}
- private static String variablePatternString = "\\s*val\\s+(?\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\s*=\\s*(?.+)";
- private static Pattern variablePattern = Pattern.compile(variablePatternString);
-
- /*
- * (non-Javadoc)
- *
- * @see com.horstmann.codecheck.Language#variablePattern()
- */
- @Override
- public Pattern variableDeclPattern() {
- return variablePattern;
- }
+ private static Pattern VARIABLE_DECL_PATTERN = Pattern.compile(
+ "val\\s+(?\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\s*=\\s*(?[^;]+);?");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_DECL_PATTERN; }
private static Pattern ERROR_PATTERN = Pattern.compile("(?[^/]+\\.sml):(?[0-9]+):(?[0-9]+): (?.+)");
@Override public Pattern errorPattern() { return ERROR_PATTERN; }
diff --git a/app/com/horstmann/codecheck/ScalaLanguage.java b/app/com/horstmann/codecheck/ScalaLanguage.java
index d113e654..9e7e4c70 100644
--- a/app/com/horstmann/codecheck/ScalaLanguage.java
+++ b/app/com/horstmann/codecheck/ScalaLanguage.java
@@ -3,6 +3,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
@@ -28,36 +29,19 @@ public boolean isMain(Path p, String contents) {
@Override
public Map writeTester(Path file, String contents, List calls, ResourceLoader resourceLoader) {
- List lines = Util.lines(contents);
-
- // Find class name
- int i = 0;
- while (i < lines.size() && !lines.get(i).contains("//CALL")) i++;
- if (i == lines.size())
- throw new CodeCheckException("Can't find object in " + file);
- while (i >= 0 && !lines.get(i).trim().startsWith("object")) i--;
- if (i < 0)
- throw new CodeCheckException("Can't find object in " + file);
- String[] tokens = lines.get(i).split("\\P{javaJavaIdentifierPart}+");
- int j = 0;
- if (tokens[0].length() == 0) j++;
- if (j + 1 >= tokens.length) throw new CodeCheckException("Can't find object name in " + file);
- String objectName = tokens[j + 1];
- lines.add(0, "object " + objectName + "CodeCheck extends App {");
- lines.add(1, "object Solution {");
- lines.add("}}");
- i = 0;
+ // Requirement: File name must equal to object containing called method
+ String objectName = moduleOf(file);
+ List lines = new ArrayList<>();
+ lines.add("object " + objectName + "CodeCheck extends App {");
for (int k = 0; k < calls.size(); k++) {
Calls.Call call = calls.get(k);
String submissionFun = objectName + "." + call.name;
- String solutionFun = "Solution." + submissionFun;
- lines.add(++i, "if (args(0) == \"" + (k + 1) + "\") {");
- lines.add(++i, "val actual = " + submissionFun + "(" + call.args + ")");
- lines.add(++i, "val expected = " + solutionFun + "(" + call.args + ")");
- lines.add(++i, "println(runtime.ScalaRunTime.stringOf(expected))");
- lines.add(++i, "println(runtime.ScalaRunTime.stringOf(actual))");
- lines.add(++i, "println(actual == expected) }");
- }
+ lines.add("if (args(0) == \"" + (k + 1) + "\") {");
+ lines.add(" val result = " + submissionFun + "(" + call.args + ")");
+ lines.add(" println(runtime.ScalaRunTime.stringOf(result))");
+ lines.add("}");
+ }
+ lines.add("}");
Path p = Paths.get(objectName + "CodeCheck.scala");
Map testModules = new HashMap<>();
testModules.put(p, Util.join(lines, "\n"));
@@ -73,9 +57,7 @@ public Map writeTester(Path file, String contents, List\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)(\\s*:[^=]+\\s*)?\\s*=\\s*(?[^;]+);.*");
-
- @Override
- public Pattern variableDeclPattern() { return variablePattern; }
+ private static Pattern VARIABLE_DECL_PATTERN = Pattern.compile(
+ "(val|var)\\s+(?\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)(\\s*:[^=]+\\s*)?\\s*=\\s*(?[^;]+);");
+ @Override public Pattern variableDeclPattern() { return VARIABLE_DECL_PATTERN; }
}
diff --git a/app/com/horstmann/codecheck/SetupReport.java b/app/com/horstmann/codecheck/SetupReport.java
new file mode 100644
index 00000000..2415f023
--- /dev/null
+++ b/app/com/horstmann/codecheck/SetupReport.java
@@ -0,0 +1,107 @@
+package com.horstmann.codecheck;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+public class SetupReport extends JSONReport {
+ private Problem problem;
+ private Map attributes = new HashMap<>();
+ private List conditions = new ArrayList<>();
+
+ static class Condition {
+ public boolean forbidden;
+ public String path;
+ public String regex;
+ public String message;
+ }
+
+ public SetupReport(String title) {
+ super(title);
+ }
+
+ public void setProblem(Problem problem) {
+ this.problem = problem;
+ }
+
+ @Override public JSONReport attribute(String key, Object value) {
+ attributes.put(key, value);
+ return this;
+ }
+
+ @Override public JSONReport condition(boolean passed, boolean forbidden, Path path, String regex, String message) {
+ Condition c = new Condition();
+ c.forbidden = forbidden;
+ c.path = path.toString();
+ c.regex = regex;
+ c.message = message;
+ conditions.add(c);
+ return this;
+ }
+
+ @Override public String getText() {
+ for (Section section : data.sections) {
+ for (Run run : section.runs) {
+ if (run.output == null || run.output == "") {
+ StringBuilder output = new StringBuilder();
+ for (Match match : run.matchedOutput) {
+ output.append(match.actual);
+ output.append("\n");
+ }
+ run.matchedOutput = null;
+ run.output = output.toString();
+ }
+ run.passed = null;
+ run.html = null;
+ }
+ }
+ data.metaData.clear();
+ data.score = null;
+ ObjectNode dataNode = Util.toJson(data);
+ if (problem != null) {
+ Problem.DisplayData displayData = problem.getProblemData();
+ ObjectNode problemNode = Util.toJson(displayData);
+ Iterator> fields = problemNode.fields();
+ while (fields.hasNext()) {
+ Map.Entry entry = fields.next();
+ dataNode.set(entry.getKey(), entry.getValue());
+ }
+ Map useFiles = problem.getUseFiles();
+ ObjectNode hiddenFiles = JsonNodeFactory.instance.objectNode();
+ for (Map.Entry entry: useFiles.entrySet() ) {
+ String name = entry.getKey().toString();
+ byte[] contents = entry.getValue();
+ if (!displayData.useFiles.containsKey(name)) {
+ try {
+ hiddenFiles.put(name, StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(contents)).toString());
+ } catch (CharacterCodingException e) {
+ ObjectNode node = JsonNodeFactory.instance.objectNode();
+ node.put("data", Base64.getEncoder().encodeToString(contents));
+ hiddenFiles.set(name, node);
+ }
+ }
+ }
+ if (hiddenFiles.size() > 0) {
+ dataNode.set("hiddenFiles", hiddenFiles);
+ }
+ if (!attributes.isEmpty()) {
+ dataNode.set("attributes", Util.toJson(attributes));
+ }
+ if (!conditions.isEmpty()) {
+ dataNode.set("conditions", Util.toJson(conditions));
+ }
+ }
+ return dataNode.toString();
+ }
+}
diff --git a/app/com/horstmann/codecheck/Substitution.java b/app/com/horstmann/codecheck/Substitution.java
index a7edc984..433ea662 100644
--- a/app/com/horstmann/codecheck/Substitution.java
+++ b/app/com/horstmann/codecheck/Substitution.java
@@ -3,9 +3,11 @@
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -18,20 +20,22 @@ public class Substitution {
public Substitution(Language language) {
this.language = language;
}
-
+
public void addVariable(Path file, String decl, String args) {
if (this.file == null)
this.file = file;
else if (!this.file.equals(file))
throw new CodeCheckException("SUB in " + this.file + " and " + file);
Pattern pattern = language.variableDeclPattern();
- Matcher matcher = pattern.matcher(decl);
+ Matcher matcher = pattern.matcher(decl.trim());
if (matcher.matches()) {
String name = matcher.group("name").trim();
ArrayList values = new ArrayList<>();
+ if (subs.containsKey(name))
+ throw new CodeCheckException("More than one SUB in " + file + " for " + name);
subs.put(name, values);
values.add(matcher.group("rhs"));
- for (String v : args.split(language.substitutionSeparator()))
+ for (String v : language.substitutionSeparator().split(args))
if (v.trim().length() > 0)
values.add(v);
if (size == 0)
@@ -60,19 +64,38 @@ public List values(int i) {
for (String n : subs.keySet()) r.add(subs.get(n).get(i));
return r;
}
-
+
+ public static String removeComment(String line, String start, String end) {
+ int i = line.indexOf(start);
+ if (i < 0) return line;
+ if (end.isBlank()) return line.substring(0, i);
+ int j = line.lastIndexOf(end);
+ if (j < 0) return line;
+ return line.substring(0, i) + line.substring(j + end.length());
+ }
+
public String substitute(String contents, int n) throws IOException {
Pattern pattern = language.variableDeclPattern();
List lines = Util.lines(contents);
StringBuilder out = new StringBuilder();
- for (String line : lines) {
- Matcher matcher = pattern.matcher(line);
+ String[] delims = language.pseudoCommentDelimiters();
+ String start = delims[0];
+ String end = delims[1];
+ Set alreadyUsed = new HashSet();
+ for (String l : lines) {
+ String line = removeComment(l, start, end);
+ int i = 0;
+ while (i < line.length() && Character.isWhitespace(line.charAt(i))) i++;
+ int j = line.length() - 1;
+ while (j >= i && Character.isWhitespace(line.charAt(j))) j--;
+ Matcher matcher = pattern.matcher(line.substring(i, j + 1));
if (matcher.matches()) {
String name = matcher.group("name");
- if (subs.containsKey(name)) {
- out.append(line.substring(0, matcher.start("rhs")));
+ if (subs.containsKey(name) && !alreadyUsed.contains(name)) {
+ alreadyUsed.add(name);
+ out.append(line.substring(0, i + matcher.start("rhs")));
out.append(subs.get(name).get(n));
- out.append(line.substring(matcher.end("rhs")));
+ out.append(line.substring(i + matcher.end("rhs")));
out.append("\n");
} else {
out.append(line);
diff --git a/app/com/horstmann/codecheck/TextReport.java b/app/com/horstmann/codecheck/TextReport.java
index c215e933..05302672 100644
--- a/app/com/horstmann/codecheck/TextReport.java
+++ b/app/com/horstmann/codecheck/TextReport.java
@@ -1,12 +1,10 @@
package com.horstmann.codecheck;
import java.awt.image.BufferedImage;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
+import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
@@ -15,21 +13,28 @@ public class TextReport implements Report {
private int sections;
private String section;
private List footnotes = new ArrayList<>();
+ private boolean hidden;
public TextReport(String title) {
+ // TODO This version is only built by the client-side Ant script
builder = new StringBuilder();
- builder.append("codecheck version ");
- builder.append(ResourceBundle.getBundle(
- "com.horstmann.codecheck.codecheck").getString("version"));
- builder.append(" ");
- builder.append(" started ");
- builder.append(new Date());
- builder.append("\n\n");
+ try {
+ builder.append("codecheck version ");
+ builder.append(ResourceBundle.getBundle(
+ "com.horstmann.codecheck.codecheck").getString("version"));
+ builder.append(" ");
+ builder.append(" started ");
+ builder.append(new Date());
+ builder.append("\n\n");
+ } catch (MissingResourceException e) {
+ builder = new StringBuilder();
+ }
}
private TextReport add(CharSequence s) {
+ if (s == null) return this;
builder.append(s);
- if (s != null && s.length() > 0 && s.charAt(s.length() - 1) != '\n')
+ if (s.length() > 0 && s.charAt(s.length() - 1) != '\n')
builder.append("\n");
return this;
}
@@ -42,6 +47,7 @@ private TextReport add(CharSequence s) {
@Override
public TextReport header(String section, String text) {
this.section = section;
+ hidden = false;
if ("studentFiles".equals(section) || "providedFiles".equals(section)) return this;
if (sections > 0)
@@ -70,20 +76,20 @@ private TextReport caption(String text) {
*/
@Override
public TextReport output(CharSequence text) {
- output(null, text);
+ if (hidden) add("[Hidden]"); else add(text);
return this;
}
@Override
public TextReport args(String args) {
if (args != null && args.trim().length() > 0)
- output("Command line arguments: " + args);
+ add("Command line arguments: " + args);
return this;
}
@Override
public TextReport input(String input) {
- output("Input", input);
+ addCaptioned("Input", hidden ? "[Hidden]" : input);
return this;
}
@@ -93,7 +99,7 @@ public Report footnote(String text) {
return this;
}
- private TextReport output(String captionText, CharSequence text) {
+ private TextReport addCaptioned(String captionText, CharSequence text) {
if (text == null || text.equals(""))
return this;
caption(captionText);
@@ -104,22 +110,26 @@ private TextReport output(String captionText, CharSequence text) {
@Override
public TextReport output(List lines, Set matches,
Set mismatches) {
- for (int i = 0; i < lines.size(); i++) {
- String line = lines.get(i);
- if (matches.contains(i))
- builder.append("+ ");
- else if (mismatches.contains(i))
- builder.append("- ");
- else
- builder.append(" ");
- add(line);
- }
+ if (hidden) {
+ add("[Hidden]");
+ } else {
+ for (int i = 0; i < lines.size(); i++) {
+ String line = lines.get(i);
+ if (matches.contains(i))
+ builder.append("+ ");
+ else if (mismatches.contains(i))
+ builder.append("- ");
+ else
+ builder.append(" ");
+ add(line);
+ }
+ }
return this;
}
private TextReport error(String captionText, String message) {
caption(captionText);
- output(message);
+ add(message);
return this;
}
@@ -141,8 +151,7 @@ public TextReport error(String message) {
*/
@Override
public TextReport systemError(String message) {
- add("System Error:");
- output(message);
+ error("System Error", message);
return this;
}
@@ -157,36 +166,27 @@ public TextReport systemError(Throwable t) {
* @see com.horstmann.codecheck.Report#image(java.lang.String, byte[])
*/
@Override
- public TextReport image(String captionText, BufferedImage img) {
- image(img);
- return this;
- }
-
- @Override
- public TextReport image(BufferedImage image) {
- /*
- try {
- imageCount++;
- Path out = dir.resolve("report" + imageCount + ".png");
- ImageIO.write(image, "PNG", out.toFile());
- } catch (IOException ex) {
- error("No image");
- }
- */
+ public TextReport image(String captionText, String filename, BufferedImage img) {
return this;
}
@Override
public TextReport file(String file, String contents) {
- if ("studentFiles".equals(section) || "providedFiles".equals(section)) return this;
+ if ("studentFiles".equals(section)) return this;
caption(file);
- output(contents); // TODO: Line numbers?
+ add(contents); // TODO: Line numbers?
return this;
}
+
+ public TextReport file(String fileName, byte[] contents, boolean hidden) {
+ // Only happens in providedFiles section
+ return this;
+ }
@Override
- public TextReport run(String caption) {
+ public TextReport run(String caption, String mainclass, boolean hidden) {
caption(caption);
+ this.hidden = hidden;
return this;
}
@@ -205,20 +205,11 @@ public TextReport add(Score score) {
return this;
}
- /*
- * (non-Javadoc)
- *
- * @see com.horstmann.codecheck.Report#save(java.nio.file.Path)
- */
@Override
- public TextReport save(Path dir, String out) throws IOException {
- Path outPath = dir.resolve(out + ".txt");
- Files.write(outPath, builder.toString().getBytes());
- return this;
- }
+ public String getText() { return builder.toString(); }
@Override
- public String getText() { return builder.toString(); }
+ public String extension() { return "txt"; }
@Override
public TextReport pass(boolean b) {
@@ -288,7 +279,7 @@ public TextReport compareTokens(String filename, List matchData) {
@Override
public TextReport runTable(String[] methodNames, String[] argNames,
String[][] args, String[] actual, String[] expected,
- boolean[] outcomes) {
+ boolean[] outcomes, boolean[] hidden, String mainclass) {
int cols0 = 0;
if (methodNames != null) {
@@ -315,19 +306,27 @@ public TextReport runTable(String[] methodNames, String[] argNames,
add("------");
for (int i = 0; i < args.length; i++) {
- if (methodNames != null)
- pad(methodNames[i], cols0);
- for (int j = 0; j < n; j++)
- pad(args[i][j], cols[j]);
- pad(actual[i], cols[n]);
- pad(expected[i], cols[n + 1]);
+ if (hidden != null && hidden[i]) {
+ pad("?", cols0);
+ for (int j = 0; j < n; j++)
+ pad("?", cols[j]);
+ pad("?", cols[n]);
+ pad("?", cols[n + 1]);
+ } else {
+ if (methodNames != null)
+ pad(methodNames[i], cols0);
+ for (int j = 0; j < n; j++)
+ pad(args[i][j], cols[j]);
+ pad(actual[i], cols[n]);
+ pad(expected[i], cols[n + 1]);
+ }
pass(outcomes[i]);
}
return this;
}
public TextReport comment(String key, String value) {
- if (builder.charAt(builder.length() - 1) != '\n')
+ if (builder.length() > 0 && builder.charAt(builder.length() - 1) != '\n')
builder.append('\n');
builder.append("# ");
builder.append(key);
diff --git a/app/com/horstmann/codecheck/Util.java b/app/com/horstmann/codecheck/Util.java
index 6d34f8f0..ff5bda12 100644
--- a/app/com/horstmann/codecheck/Util.java
+++ b/app/com/horstmann/codecheck/Util.java
@@ -45,6 +45,12 @@
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
public class Util {
private static Random generator = new Random();
@@ -105,7 +111,10 @@ public static Path tail(Path p) {
}
public static String extension(Path path) {
- String name = path.toString();
+ return extension(path.toString());
+ }
+
+ public static String extension(String name) {
int n = name.lastIndexOf('.');
if (n == -1)
return "";
@@ -391,7 +400,7 @@ public FileVisitResult visitFile(Path file,
public static byte[] fileUpload(String urlString, String fieldName, String fileName, byte[] bytes) throws IOException {
final int TIMEOUT = 90000; // 90 seconds
String boundary = "===" + createPrivateUID() + "===";
- URL url = new URL(urlString);
+ URL url = URI.create(urlString).toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(TIMEOUT);
conn.setReadTimeout(TIMEOUT);
@@ -441,7 +450,7 @@ public static byte[] fileUpload(String urlString, String fieldName, String fileN
public static String httpPost(String urlString, String content, String contentType) {
StringBuilder result = new StringBuilder();
try {
- URL url = new URL(urlString);
+ URL url = URI.create(urlString).toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestProperty("Content-Type", contentType);
connection.setDoOutput(true);
@@ -521,10 +530,11 @@ public static Map getParams(String url)
return params;
}
+ // TODO: What about redirects?
public static boolean exists(String url) {
boolean result = false;
try {
- HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+ HttpURLConnection conn = (HttpURLConnection) URI.create(url).toURL().openConnection();
try {
conn.connect();
result = conn.getHeaderField(null).contains("200");
@@ -538,6 +548,7 @@ public static boolean exists(String url) {
// UIDs
+ // TODO Consider using https://github.com/scru128/spec
public static String createPrivateUID() {
return new BigInteger(128, generator).toString(36).toUpperCase();
}
@@ -554,21 +565,62 @@ private static String datePrefix() {
public static String createPronouncableUID() {
StringBuilder result = new StringBuilder();
- int len = 16;
- int b = Util.generator.nextInt(2);
+ int len = 4;
for (int i = 0; i < len; i++) {
- String s = i % 2 == b ? Util.consonants : vowels;
- int n = Util.generator.nextInt(s.length());
- result.append(s.charAt(n));
- if (i % 4 == 3 && i < len - 1) {
- result.append('-');
- b = Util.generator.nextInt(2);
- }
+ if (i > 0) result.append("-");
+ result.append(generatePronounceableWord());
}
return result.toString();
}
+
+ // generates a non-bad four-letter pronounceable word
+ // of the form vcvc or cvcv (c = consonant, v = vowel)
+ private static StringBuilder generatePronounceableWord() {
+ StringBuilder word;
+ int len = 4;
+ int b = Util.generator.nextInt(2);
+ do {
+ word = new StringBuilder();
+ for (int i = 0; i < len; i++) {
+ String s = i % 2 == b ? Util.consonants : vowels;
+ int n = Util.generator.nextInt(s.length());
+ word.append(s.charAt(n));
+ }
+ } while (isBadWord(word.toString())); // generate a word until we get a non bad word
+
+ return word;
+ }
+ private static boolean isBadWord(String word){
+ String[] filteredOutWords = {"anal", "anus", "anil", "anes", "anis", "babe", "bozo", "coky", "dick", "dike", "dyke", "homo", "lube", "nude", "oral", "rape", "sexy", "titi", "wily"};
+ return Arrays.asList(filteredOutWords).contains(word);
+ }
public static boolean isPronouncableUID(String s) {
return s.matches("(([aeiouy][bcdfghjklmnpqrstvwxz]){2}|([bcdfghjklmnpqrstvwxz][aeiouy]){2})(-(([aeiouy][bcdfghjklmnpqrstvwxz]){2}|([bcdfghjklmnpqrstvwxz][aeiouy]){2})){3}");
}
+
+ private static ObjectMapper mapper = new ObjectMapper();
+ static {
+ mapper.setSerializationInclusion(Include.NON_DEFAULT);
+ }
+
+ public static ObjectNode toJson(Object obj) {
+ return (ObjectNode) mapper.convertValue(obj, JsonNode.class);
+ }
+
+ public static String toJsonString(Object obj) {
+ try {
+ return mapper.writeValueAsString(obj);
+ } catch (JsonProcessingException e) {
+ return null;
+ }
+ }
+
+ public static ObjectNode fromJsonString(String jsonString) throws JsonProcessingException, IOException {
+ return (ObjectNode) mapper.readTree(jsonString);
+ }
+
+ public static ObjectNode fromJsonString(byte[] jsonStringBytes) throws JsonProcessingException, IOException {
+ return (ObjectNode) mapper.readTree(jsonStringBytes);
+ }
}
diff --git a/app/controllers/Application.java b/app/controllers/Application.java
index 82eccec8..6c35c741 100644
--- a/app/controllers/Application.java
+++ b/app/controllers/Application.java
@@ -13,15 +13,16 @@ public class Application extends Controller {
public Result health(Http.Request request) {
try {
String df = Util.runProcess("/bin/df /", 1000);
- String mp = Util.runProcess("/usr/bin/mpstat 2 1", 3000);
Matcher matcher = dfPattern.matcher(df);
if (matcher.matches()) {
String percent = matcher.group("percent");
if (Integer.parseInt(percent) > 95)
return internalServerError("disk " + percent + "% full");
+ Runtime rt = Runtime.getRuntime();
+ double mem = rt.freeMemory() * 100.0 / rt.totalMemory();
// TODO: Analyze output
// http://stackoverflow.com/questions/9229333/how-to-get-overall-cpu-usage-e-g-57-on-linux
- return ok("CodeCheck\n" + df + "\n" + mp);
+ return ok("CodeCheck\n" + df + "\n" + mem + "% JVM memory free");
}
else return ok("df output doesn't match pattern: " + df);
} catch (Throwable ex) {
diff --git a/app/controllers/Assignment.java b/app/controllers/Assignment.java
index d1c1f4e9..7d9006d6 100644
--- a/app/controllers/Assignment.java
+++ b/app/controllers/Assignment.java
@@ -1,210 +1,38 @@
package controllers;
-/*
- An assignment is made up of problems. A problem is provided in a URL
- that is displayed in an iframe. (In the future, maybe friendly problems
- could coexist on a page or shared iframe for efficiency.) An assignment
- weighs its problems.
-
- The "problem key" is normally the problem URL. However, for interactive or CodeCheck
- problems in the textbook repo, it is the qid of the single question in the problem.
-
- CodeCheckWork is a map from problem keys to scores and states. It only stores the most recent version.
- CodeCheckSubmissions is an append-only log of all submissions of a single problem.
-
- Tables:
-
- CodeCheckAssignment
- assignmentID [primary key]
- deadline (an ISO 8601 string like "2020-12-01T23:59:59Z")
- editKey // LTI: tool consumer ID + user ID
- problems
- array of // One per group
- array of { URL, qid?, weight } // qid for book repo
-
- CodeCheckLTIResources (Legacy)
- resourceID [primary key] // LTI tool consumer ID + course ID + resource ID
- assignmentID
-
- CodeCheckWork
- assignmentID [partition key] // non-LTI: courseID? + assignmentID, LTI: toolConsumerID/courseID + assignment ID, Legacy tool consumer ID/course ID/resource ID
- workID [sort key] // non-LTI: ccid/editKey, LTI: userID
- problems
- map from URL/qids to { state, score }
- submittedAt
- tab
-
- CodeCheckSubmissions
- submissionID [partition key] // non-LTI: courseID? + assignmentID + problemKey + ccid/editKey , LTI: toolConsumerID/courseID + assignmentID + problemID + userID
- // either way, that's resource ID + workID + problem key
- submittedAt [sort key]
- state: as string, not JSON
- score
-
- with global secondary index
- problemID
- submitterID
-
- CodeCheckLTICredentials
- oauth_consumer_key [primary key]
- shared_secret
-
-
- Assignment parsing format:
-
- Groups separated by 3 or more -
- Each line:
- urlOrQid (weight%)? title
-
- Cookies
- ccid (student only)
- cckey (student only)
- PLAY_SESSION
-*/
-
import java.io.IOException;
-import java.net.URLEncoder;
+import java.lang.System.Logger;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.format.DateTimeParseException;
-import java.util.Map;
import java.util.Optional;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import javax.inject.Inject;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
-
-import models.S3Connection;
import com.horstmann.codecheck.Util;
-import play.Logger;
+
import play.mvc.Controller;
import play.mvc.Http;
import play.mvc.Result;
+import services.ServiceException;
public class Assignment extends Controller {
- @Inject private S3Connection s3conn;
- private static Logger.ALogger logger = Logger.of("com.horstmann.codecheck");
+ @Inject private services.Assignment assignmentService;
+ // TODO Do we need to log, or does internalServerError do it already?
+ private static Logger logger = System.getLogger("com.horstmann.codecheck");
- public static ArrayNode parseAssignment(String assignment) {
- if (assignment == null || assignment.trim().isEmpty())
- throw new IllegalArgumentException("No assignments");
- ArrayNode groupsNode = JsonNodeFactory.instance.arrayNode();
- Pattern problemPattern = Pattern.compile("\\s*(\\S+)(\\s+[0-9.]+%)?(.*)");
- String[] groups = assignment.split("\\s+-{3,}\\s+");
- for (int problemGroup = 0; problemGroup < groups.length; problemGroup++) {
- String[] lines = groups[problemGroup].split("\\n+");
- if (lines.length == 0) throw new IllegalArgumentException("No problems given");
- ArrayNode group = JsonNodeFactory.instance.arrayNode();
- for (int i = 0; i < lines.length; i++) {
- ObjectNode problem = JsonNodeFactory.instance.objectNode();
- Matcher matcher = problemPattern.matcher(lines[i]);
- if (!matcher.matches())
- throw new IllegalArgumentException("Bad input " + lines[i]);
- String problemDescriptor = matcher.group(1); // URL or qid
- String problemURL;
- String qid = null;
- boolean checked = false;
- if (problemDescriptor.startsWith("https")) problemURL = problemDescriptor;
- else if (problemDescriptor.startsWith("http")) problemURL = "https" + problemDescriptor.substring(4);
- else if (problemDescriptor.matches("[a-zA-Z0-9_]+(-[a-zA-Z0-9_]+)*")) {
- qid = problemDescriptor;
- problemURL = "https://www.interactivities.ws/" + problemDescriptor + ".xhtml";
- if (com.horstmann.codecheck.Util.exists(problemURL))
- checked = true;
- else
- problemURL = "https://codecheck.it/files?repo=wiley&problem=" + problemDescriptor;
- }
- else throw new IllegalArgumentException("Bad problem: " + problemDescriptor);
- if (!checked && !com.horstmann.codecheck.Util.exists(problemURL))
- throw new IllegalArgumentException("Cannot find " + problemDescriptor);
- problem.put("URL", problemURL);
- if (qid != null) problem.put("qid", qid);
-
- String weight = matcher.group(2);
- if (weight == null) weight = "100";
- else weight = weight.trim().replace("%", "");
- problem.put("weight", Double.parseDouble(weight) / 100);
-
- String title = matcher.group(3);
- if (title != null) {
- title = title.trim();
- if (!title.isEmpty())
- problem.put("title", title);
- }
- group.add(problem);
- }
- groupsNode.add(group);
+ public Result edit(Http.Request request, String assignmentID, String editKey) throws IOException {
+ try {
+ String result = assignmentService.edit(assignmentID, editKey);
+ return ok(result).as("text/html");
}
- return groupsNode;
- }
-
- private static boolean isProblemKeyFor(String key, ObjectNode problem) {
- // Textbook repo
- if (problem.has("qid")) return problem.get("qid").asText().equals(key);
- String problemURL = problem.get("URL").asText();
- // Some legacy CodeCheck questions have butchered keys such as 0101407088y6iesgt3rs6k7h0w45haxajn
- return problemURL.endsWith(key);
- }
-
- public static double score(ObjectNode assignment, ObjectNode work) {
- ArrayNode groups = (ArrayNode) assignment.get("problems");
- String workID = work.get("workID").asText();
- ArrayNode problems = (ArrayNode) groups.get(workID.hashCode() % groups.size());
- ObjectNode submissions = (ObjectNode) work.get("problems");
- double result = 0;
- double sum = 0;
- for (JsonNode p : problems) {
- ObjectNode problem = (ObjectNode) p;
- double weight = problem.get("weight").asDouble();
- sum += weight;
- for (String key : com.horstmann.codecheck.Util.iterable(submissions.fieldNames())) {
- if (isProblemKeyFor(key, problem)) {
- ObjectNode submission = (ObjectNode) submissions.get(key);
- result += weight * submission.get("score").asDouble();
- }
- }
+ catch (ServiceException ex) {
+ return badRequest(ex.getMessage());
+ }
+ catch (Exception ex) {
+ logger.log(Logger.Level.ERROR, Util.getStackTrace(ex));
+ return internalServerError(Util.getStackTrace(ex));
}
- return sum == 0 ? 0 : result / sum;
- }
-
- private static boolean editKeyValid(String suppliedEditKey, ObjectNode assignmentNode) {
- String storedEditKey = assignmentNode.get("editKey").asText();
- return suppliedEditKey.equals(storedEditKey) && !suppliedEditKey.contains("/");
- // Otherwise it's an LTI edit key (tool consumer ID + user ID)
- }
-
- /*
- * assignmentID == null: new assignment
- * assignmentID != null, editKey != null: edit assignment
- * assignmentID != null, editKey == null: clone assignment
- */
- public Result edit(Http.Request request, String assignmentID, String editKey) throws IOException {
- ObjectNode assignmentNode;
- if (assignmentID == null) {
- assignmentNode = JsonNodeFactory.instance.objectNode();
- } else {
- assignmentNode = s3conn.readJsonObjectFromDynamoDB("CodeCheckAssignments", "assignmentID", assignmentID);
- if (assignmentNode == null) return badRequest("Assignment not found");
-
- if (editKey == null) { // Clone
- assignmentNode.remove("editKey");
- assignmentNode.remove("assignmentID");
- }
- else { // Edit existing assignment
- if (!editKeyValid(editKey, assignmentNode))
- // In the latter case, it is an LTI toolConsumerID + userID
- return badRequest("editKey " + editKey + " does not match");
- }
- }
- assignmentNode.put("saveURL", "/saveAssignment");
- return ok(views.html.editAssignment.render(assignmentNode.toString(), true));
}
/*
@@ -218,113 +46,64 @@ public Result edit(Http.Request request, String assignmentID, String editKey) th
public Result work(Http.Request request, String assignmentID, String ccid, String editKey,
boolean isStudent)
throws IOException, GeneralSecurityException {
- String prefix = models.Util.prefix(request);
- String workID = "";
- boolean editKeySaved = true;
-
- ObjectNode assignmentNode = s3conn.readJsonObjectFromDynamoDB("CodeCheckAssignments", "assignmentID", assignmentID);
- if (assignmentNode == null) return badRequest("Assignment not found");
-
- assignmentNode.put("isStudent", isStudent);
- if (isStudent) {
- if (ccid == null) {
- Optional ccidCookie = request.getCookie("ccid");
- if (ccidCookie.isPresent()) {
- ccid = ccidCookie.get().value();
- Optional editKeyCookie = request.getCookie("cckey");
- if (editKeyCookie.isPresent())
- editKey = editKeyCookie.get().value();
- else { // This shouldn't happen, but if it does, clear ID
- ccid = com.horstmann.codecheck.Util.createPronouncableUID();
- editKey = Util.createPrivateUID();
- editKeySaved = false;
- }
- } else { // First time on this browser
- ccid = com.horstmann.codecheck.Util.createPronouncableUID();
- editKey = Util.createPrivateUID();
- editKeySaved = false;
- }
- } else if (editKey == null) { // Clear ID request
- ccid = com.horstmann.codecheck.Util.createPronouncableUID();
- editKey = Util.createPrivateUID();
- editKeySaved = false;
- }
- assignmentNode.put("clearIDURL", "/assignment/" + assignmentID + "/" + ccid);
- workID = ccid + "/" + editKey;
- } else { // Instructor
- if (ccid == null && editKey != null && !editKeyValid(editKey, assignmentNode))
- throw new IllegalArgumentException("Edit key does not match");
- if (ccid != null && editKey != null) // Instructor viewing student submission
- workID = ccid + "/" + editKey;
+ try {
+ String prefix = controllers.Util.prefix(request);
+
+ if (isStudent) {
+ boolean editKeySaved = true;
+ if (ccid == null) {
+ Optional ccidCookie = request.getCookie("ccid");
+ if (ccidCookie.isPresent()) {
+ ccid = ccidCookie.get().value();
+ Optional editKeyCookie = request.getCookie("cckey");
+ if (editKeyCookie.isPresent())
+ editKey = editKeyCookie.get().value();
+ else { // This shouldn't happen, but if it does, clear ID
+ ccid = Util.createPronouncableUID();
+ editKey = Util.createPrivateUID();
+ editKeySaved = false;
+ }
+ } else { // First time on this browser
+ ccid = Util.createPronouncableUID();
+ editKey = Util.createPrivateUID();
+ editKeySaved = false;
+ }
+ } else if (editKey == null) { // Clear ID request
+ ccid = Util.createPronouncableUID();
+ editKey = Util.createPrivateUID();
+ editKeySaved = false;
+ }
+ Http.Cookie newCookie1 = controllers.Util.buildCookie("ccid", ccid);
+ Http.Cookie newCookie2 = controllers.Util.buildCookie("cckey", editKey);
+ String result = assignmentService.work(prefix, assignmentID, ccid, editKey, true /* studet */, editKeySaved);
+ return ok(result).withCookies(newCookie1, newCookie2).as("text/html");
+ } else { // Instructor
+ String result = assignmentService.work(prefix, assignmentID, ccid, editKey, false /* student */, false /* editKeySaved */);
+ return ok(result).as("text/html");
+ }
}
- assignmentNode.remove("editKey");
- ArrayNode groups = (ArrayNode) assignmentNode.get("problems");
- assignmentNode.set("problems", groups.get(Math.abs(workID.hashCode()) % groups.size()));
-
- String work = null;
- if (!workID.equals(""))
- work = s3conn.readJsonStringFromDynamoDB("CodeCheckWork", "assignmentID", assignmentID, "workID", workID);
- if (work == null)
- work = "{ assignmentID: \"" + assignmentID + "\", workID: \""
- + workID + "\", problems: {} }";
-
- String lti = "undefined";
- if (isStudent) {
- String returnToWorkURL = prefix + "/private/resume/" + assignmentID + "/" + ccid + "/" + editKey;
- assignmentNode.put("returnToWorkURL", returnToWorkURL);
- assignmentNode.put("editKeySaved", editKeySaved);
- assignmentNode.put("sentAt", Instant.now().toString());
- Http.Cookie newCookie1 = Http.Cookie.builder("ccid", ccid).withPath("/").withMaxAge(Duration.ofDays(180)).build();
- Http.Cookie newCookie2 = Http.Cookie.builder("cckey", editKey).withPath("/").withMaxAge(Duration.ofDays(180)).build();
- return ok(views.html.workAssignment.render(assignmentNode.toString(), work, ccid, lti)).withCookies(newCookie1, newCookie2);
- }
- else { // Instructor
- if (ccid == null) {
- if (editKey != null) { // Instructor viewing for editing/submissions
- // TODO: Check if there are any submissions?
- assignmentNode.put("viewSubmissionsURL", "/private/viewSubmissions/" + assignmentID + "/" + editKey);
- String publicURL = prefix + "/assignment/" + assignmentID;
- String privateURL = prefix + "/private/assignment/" + assignmentID + "/" + editKey;
- String editAssignmentURL = prefix + "/private/editAssignment/" + assignmentID + "/" + editKey;
- assignmentNode.put("editAssignmentURL", editAssignmentURL);
- assignmentNode.put("privateURL", privateURL);
- assignmentNode.put("publicURL", publicURL);
- }
- String cloneURL = prefix + "/copyAssignment/" + assignmentID;
- assignmentNode.put("cloneURL", cloneURL);
- }
-
- return ok(views.html.workAssignment.render(assignmentNode.toString(), work, ccid, lti));
+ catch (ServiceException ex) {
+ return badRequest(ex.getMessage());
+ }
+ catch (Exception ex) {
+ logger.log(Logger.Level.ERROR, Util.getStackTrace(ex));
+ return internalServerError(Util.getStackTrace(ex));
}
}
public Result viewSubmissions(Http.Request request, String assignmentID, String editKey)
throws IOException {
- ObjectNode assignmentNode = s3conn.readJsonObjectFromDynamoDB("CodeCheckAssignments", "assignmentID", assignmentID);
- if (assignmentNode == null) return badRequest("Assignment not found");
-
- if (!editKeyValid(editKey, assignmentNode))
- throw new IllegalArgumentException("Edit key does not match");
-
- ArrayNode submissions = JsonNodeFactory.instance.arrayNode();
-
- Map itemMap = s3conn.readJsonObjectsFromDynamoDB("CodeCheckWork", "assignmentID", assignmentID, "workID");
-
- for (String submissionKey : itemMap.keySet()) {
- String[] parts = submissionKey.split("/");
- String ccid = parts[0];
- String submissionEditKey = parts[1];
-
- ObjectNode work = itemMap.get(submissionKey);
- ObjectNode submissionData = JsonNodeFactory.instance.objectNode();
- submissionData.put("opaqueID", ccid);
- submissionData.put("score", Assignment.score(assignmentNode, work));
- submissionData.set("submittedAt", work.get("submittedAt"));
- submissionData.put("viewURL", "/private/submission/" + assignmentID + "/" + ccid + "/" + submissionEditKey);
- submissions.add(submissionData);
+ try {
+ String result = assignmentService.viewSubmissions(assignmentID, editKey);
+ return ok(result).as("text/html");
+ }
+ catch (ServiceException ex) {
+ return badRequest(ex.getMessage());
+ }
+ catch (Exception ex) {
+ logger.log(Logger.Level.ERROR, Util.getStackTrace(ex));
+ return internalServerError(Util.getStackTrace(ex));
}
- String allSubmissionsURL = "/lti/allSubmissions?resourceID=" + URLEncoder.encode(assignmentID, "UTF-8");
- return ok(views.html.viewSubmissions.render(allSubmissionsURL, submissions.toString()));
}
/*
@@ -332,84 +111,48 @@ public Result viewSubmissions(Http.Request request, String assignmentID, String
* New or cloned: Neither request.assignmentID nor request.editKey exist
*/
public Result saveAssignment(Http.Request request) throws IOException {
- ObjectNode params = (ObjectNode) request.body().asJson();
-
- try {
- params.set("problems", parseAssignment(params.get("problems").asText()));
- } catch (IllegalArgumentException e) {
- return badRequest(e.getMessage());
+ try {
+ String prefix = controllers.Util.prefix(request);
+ ObjectNode params = (ObjectNode) request.body().asJson();
+ ObjectNode result = assignmentService.saveAssignment(prefix, params);
+ return ok(result);
}
- String assignmentID;
- String editKey;
- ObjectNode assignmentNode;
- if (params.has("assignmentID")) {
- assignmentID = params.get("assignmentID").asText();
- assignmentNode = s3conn.readJsonObjectFromDynamoDB("CodeCheckAssignments", "assignmentID", assignmentID);
- if (assignmentNode == null) return badRequest("Assignment not found");
-
- if (!params.has("editKey")) return badRequest("Missing edit key");
- editKey = params.get("editKey").asText();
- if (!editKeyValid(editKey, assignmentNode))
- return badRequest("Edit key does not match");
+ catch (ServiceException ex) {
+ return badRequest(ex.getMessage());
+ }
+ catch (Exception ex) {
+ logger.log(Logger.Level.ERROR, Util.getStackTrace(ex));
+ return internalServerError(Util.getStackTrace(ex));
}
- else { // New assignment or clone
- assignmentID = com.horstmann.codecheck.Util.createPublicUID();
- params.put("assignmentID", assignmentID);
- if (params.has("editKey"))
- editKey = params.get("editKey").asText();
- else { // LTI assignments have an edit key
- editKey = Util.createPrivateUID();
- params.put("editKey", editKey);
- }
- assignmentNode = null;
- }
- s3conn.writeJsonObjectToDynamoDB("CodeCheckAssignments", params);
-
- String prefix = models.Util.prefix(request);
- String assignmentURL = prefix + "/private/assignment/" + assignmentID + "/" + editKey;
- params.put("viewAssignmentURL", assignmentURL);
-
- return ok(params);
}
public Result saveWork(Http.Request request) throws IOException, NoSuchAlgorithmException {
- try {
- ObjectNode requestNode = (ObjectNode) request.body().asJson();
- ObjectNode result = JsonNodeFactory.instance.objectNode();
-
- Instant now = Instant.now();
- String assignmentID = requestNode.get("assignmentID").asText();
- ObjectNode assignmentNode = s3conn.readJsonObjectFromDynamoDB("CodeCheckAssignments", "assignmentID", assignmentID);
- if (assignmentNode == null) return badRequest("Assignment not found");
- String workID = requestNode.get("workID").asText();
- String problemID = requestNode.get("tab").asText();
- ObjectNode problemsNode = (ObjectNode) requestNode.get("problems");
-
- String submissionID = assignmentID + " " + workID + " " + problemID;
- ObjectNode submissionNode = JsonNodeFactory.instance.objectNode();
- submissionNode.put("submissionID", submissionID);
- submissionNode.put("submittedAt", now.toString());
- // TODO: NPE in logs for the line below
- submissionNode.put("state", problemsNode.get(problemID).get("state").toString());
- submissionNode.put("score", problemsNode.get(problemID).get("score").asDouble());
- s3conn.writeJsonObjectToDynamoDB("CodeCheckSubmissions", submissionNode);
-
- if (assignmentNode.has("deadline")) {
- try {
- Instant deadline = Instant.parse(assignmentNode.get("deadline").asText());
- if (now.isAfter(deadline))
- return badRequest("After deadline of " + deadline);
- } catch(DateTimeParseException e) { // TODO: This should never happen, but it did
- logger.error(Util.getStackTrace(e));
- }
- }
- result.put("submittedAt", now.toString());
+ try {
+ ObjectNode params = (ObjectNode) request.body().asJson();
+ ObjectNode result = assignmentService.saveWork(params);
+ return ok(result);
+ }
+ catch (ServiceException ex) {
+ return badRequest(ex.getMessage());
+ }
+ catch (Exception ex) {
+ logger.log(Logger.Level.ERROR, Util.getStackTrace(ex));
+ return internalServerError(Util.getStackTrace(ex));
+ }
+ }
- s3conn.writeNewerJsonObjectToDynamoDB("CodeCheckWork", requestNode, "assignmentID", "submittedAt");
- return ok(result);
- } catch (Exception e) {
- logger.error(Util.getStackTrace(e));
- return badRequest(e.getMessage());
- }
- }
+ public Result saveComment(Http.Request request) throws IOException {
+ try {
+ ObjectNode params = (ObjectNode) request.body().asJson();
+ ObjectNode result = assignmentService.saveComment(params);
+ return ok(result);
+ }
+ catch (ServiceException ex) {
+ return badRequest(ex.getMessage());
+ }
+ catch (Exception ex) {
+ logger.log(Logger.Level.ERROR, Util.getStackTrace(ex));
+ return internalServerError(Util.getStackTrace(ex));
+ }
+ }
}
diff --git a/app/controllers/Check.java b/app/controllers/Check.java
index 129ae249..8961bd91 100644
--- a/app/controllers/Check.java
+++ b/app/controllers/Check.java
@@ -1,15 +1,9 @@
package controllers;
import java.io.IOException;
-import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.time.Duration;
-import java.util.Base64;
-import java.util.HashMap;
-import java.util.Iterator;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
@@ -18,13 +12,10 @@
import javax.inject.Inject;
import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.horstmann.codecheck.Util;
-import models.CodeCheck;
-import play.Logger;
-import play.libs.Json;
+import play.libs.Files.TemporaryFile;
import play.libs.concurrent.HttpExecution;
import play.mvc.Controller;
import play.mvc.Http;
@@ -32,139 +23,66 @@
public class Check extends Controller {
private CodecheckExecutionContext ccec;
- @Inject private CodeCheck codeCheck;
+ @Inject private services.Check checkService;
-
- // Classic HTML report, used in Core Java for the Impatient 2e
- public CompletableFuture checkHTML(Http.Request request) throws IOException, InterruptedException {
- Map params = request.body().asFormUrlEncoded();
+ public CompletableFuture run(Http.Request request) throws IOException, InterruptedException {
return CompletableFuture.supplyAsync(() -> {
try {
- String ccid = null;
- String repo = "ext";
- String problem = "";
- Map submissionFiles = new TreeMap<>();
-
- for (String key : params.keySet()) {
- String value = params.get(key)[0];
- if (key.equals("repo"))
- repo = value;
- else if (key.equals("problem"))
- problem = value;
- else if (key.equals("ccid")) // TODO: For testing of randomization?
- ccid = value;
- else
+ Map params;
+ Map submissionFiles = new TreeMap<>();
+ String contentType = request.contentType().orElse("");
+ if ("application/x-www-form-urlencoded".equals(contentType)) {
+ params = request.body().asFormUrlEncoded();
+ for (String key : params.keySet()) {
+ String value = params.get(key)[0];
submissionFiles.put(Paths.get(key), value);
- }
- if (ccid == null) {
- Optional ccidCookie = request.getCookie("ccid");
- ccid = ccidCookie.map(Http.Cookie::value).orElse(com.horstmann.codecheck.Util.createPronouncableUID());
- }
- long startTime = System.nanoTime();
- String report = codeCheck.run("html", repo, problem, ccid, submissionFiles).getText();
- double elapsed = (System.nanoTime() - startTime) / 1000000000.0;
- if (report == null || report.length() == 0) {
- report = String.format("Timed out after %5.0f seconds\n", elapsed);
- }
-
- Http.Cookie newCookie = Http.Cookie.builder("ccid", ccid).withMaxAge(Duration.ofDays(180)).build();
- return ok(report).withCookies(newCookie).as("text/html");
- }
- catch (Exception ex) {
- return internalServerError(Util.getStackTrace(ex));
- }
- }, HttpExecution.fromThread((Executor) ccec) /* ec.current() */);
+ }
+ return ok(checkService.run(submissionFiles)).as("text/plain");
+ } else if ("multipart/form-data".equals(contentType)) {
+ play.mvc.Http.MultipartFormData body = request.body().asMultipartFormData();
+ for (var f : body.getFiles()) {
+ String name = f.getFilename();
+ TemporaryFile tempZipFile = f.getRef();
+ Path savedPath = tempZipFile.path();
+ String contents = Util.read(savedPath);
+ submissionFiles.put(Paths.get(name), contents);
+ }
+ return ok(checkService.run(submissionFiles)).as("text/plain");
+ } else if ("application/json".equals(contentType)) {
+ return ok(checkService.runJSON(request.body().asJson())).as("application/json");
+ }
+ else return internalServerError("Bad content type");
+ } catch (Exception ex) {
+ return internalServerError(Util.getStackTrace(ex));
+ }
+ }, HttpExecution.fromThread((Executor) ccec) /* ec.current() */);
}
-
- // From JS UI
+
public CompletableFuture checkNJS(Http.Request request) throws IOException, InterruptedException {
- Map params;
- if ("application/x-www-form-urlencoded".equals(request.contentType().orElse("")))
- params = request.body().asFormUrlEncoded();
- else if ("application/json".equals(request.contentType().orElse(""))) {
- params = new HashMap<>();
- JsonNode json = request.body().asJson();
- Iterator> iter = json.fields();
- while (iter.hasNext()) {
- Entry entry = iter.next();
- params.put(entry.getKey(), new String[] { entry.getValue().asText() });
- };
- }
- else
- params = request.queryString();
-
return CompletableFuture.supplyAsync(() -> {
try {
- String ccid = null;
- String repo = "ext";
- String problem = null;
- String reportType = "NJS";
- String scoreCallback = null;
- StringBuilder requestParams = new StringBuilder();
- ObjectNode studentWork = JsonNodeFactory.instance.objectNode();
- Map submissionFiles = new TreeMap<>();
- Map reportZipFiles = new TreeMap<>();
- for (String key : params.keySet()) {
- String value = params.get(key)[0];
-
- if (requestParams.length() > 0) requestParams.append(", ");
- requestParams.append(key);
- requestParams.append("=");
- int nl = value.indexOf('\n');
- if (nl >= 0) {
- requestParams.append(value.substring(0, nl));
- requestParams.append("...");
- }
- else requestParams.append(value);
-
- if ("repo".equals(key)) repo = value;
- else if ("problem".equals(key)) problem = value;
- else if ("scoreCallback".equals(key)) scoreCallback = value;
- else if ("ccid".equals(key)) ccid = value; // TODO: For testing of randomization?
- else {
- Path p = Paths.get(key);
- submissionFiles.put(p, value);
- reportZipFiles.put(p, value.getBytes(StandardCharsets.UTF_8));
- studentWork.put(key, value);
- }
- }
- if (ccid == null) {
- Optional ccidCookie = request.getCookie("ccid");
- ccid = ccidCookie.map(Http.Cookie::value).orElse(com.horstmann.codecheck.Util.createPronouncableUID());
- };
- //Logger.of("com.horstmann.codecheck.check").info("checkNJS: " + requestParams);
- //TODO last param should be submissionDir
- String report = codeCheck.run(reportType, repo, problem, ccid, submissionFiles).getText();
- ObjectNode result = (ObjectNode) Json.parse(report);
- String reportHTML = result.get("report").asText();
- reportZipFiles.put(Paths.get("report.html"), reportHTML.getBytes(StandardCharsets.UTF_8));
-
- byte[] reportZipBytes = codeCheck.signZip(reportZipFiles);
-
- // TODO Need to sign
- String reportZip = Base64.getEncoder().encodeToString(reportZipBytes);
-
- //TODO: Score callback no longer used from LTIHub. Does Engage use it?
- if (scoreCallback != null) {
- if (scoreCallback.startsWith("https://"))
- scoreCallback = "http://" + scoreCallback.substring("https://".length()); // TODO: Fix
-
- //TODO: Add to result the student submissions
- ObjectNode augmentedResult = result.deepCopy();
- augmentedResult.set("studentWork", studentWork);
-
- String resultText = Json.stringify(augmentedResult);
- Logger.of("com.horstmann.codecheck.lti").info("Request: " + scoreCallback + " " + resultText);
- String response = com.horstmann.codecheck.Util.httpPost(scoreCallback, resultText, "application/json");
- Logger.of("com.horstmann.codecheck.lti").info("Response: " + response);
- }
-
- result.put("zip", reportZip);
- Http.Cookie newCookie = Http.Cookie.builder("ccid", ccid).withMaxAge(Duration.ofDays(180)).build();
+ JsonNode json = request.body().asJson();
+ Optional ccidCookie = request.getCookie("ccid");
+ String ccid = ccidCookie.map(Http.Cookie::value).orElse(Util.createPronouncableUID());
+ ObjectNode result = checkService.checkNJS(json, ccid);
+ Http.Cookie newCookie = controllers.Util.buildCookie("ccid", ccid);
return ok(result).withCookies(newCookie).as("application/json");
} catch (Exception ex) {
return internalServerError(Util.getStackTrace(ex));
}
}, HttpExecution.fromThread((Executor) ccec) /* ec.current() */);
- }
+ }
+
+ public CompletableFuture setupReport(Http.Request request, String repo, String problem) throws IOException, InterruptedException {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ String ccid = Util.createPronouncableUID();
+ String report = checkService.setupReport(repo, problem, ccid);
+ return ok(report).as("application/json");
+ }
+ catch (Exception ex) {
+ return internalServerError(Util.getStackTrace(ex));
+ }
+ }, HttpExecution.fromThread((Executor) ccec) /* ec.current() */);
+ }
}
diff --git a/app/controllers/Config.java b/app/controllers/Config.java
new file mode 100644
index 00000000..f57e98ed
--- /dev/null
+++ b/app/controllers/Config.java
@@ -0,0 +1,41 @@
+package controllers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.Connection;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.horstmann.codecheck.ResourceLoader;
+
+import play.db.Database;
+import play.db.Databases;
+
+@Singleton public class Config implements ResourceLoader {
+ @Inject private com.typesafe.config.Config config;
+ @Inject private play.api.Environment playEnv;
+ private Database db;
+
+ public InputStream loadResource(String path) throws IOException {
+ return playEnv.classLoader().getResourceAsStream("public/resources/" + path);
+ }
+ public String getProperty(String key) {
+ return config.hasPath(key) ? config.getString(key) : null;
+ }
+ public String getString(String key) { // TODO Why do we need both getString and getProperty?
+ return getProperty(key);
+ }
+ public boolean hasPath(String key) {
+ return getProperty(key) != null;
+ }
+
+ // TODO: Thread safety
+ public Connection getDatabaseConnection() {
+ if (db == null) {
+ String driver = config.getString("com.horstmann.corejava.sql.driver");
+ String url = config.getString("com.horstmann.corejava.sql.url");
+ db = Databases.createFrom(driver, url);
+ }
+ return db.getConnection();
+ }
+}
\ No newline at end of file
diff --git a/app/controllers/Files.java b/app/controllers/Files.java
index 4ae57e80..a942bbf0 100644
--- a/app/controllers/Files.java
+++ b/app/controllers/Files.java
@@ -1,310 +1,63 @@
package controllers;
import java.io.IOException;
-import java.net.URL;
-import java.nio.file.Path;
-import java.text.MessageFormat;
-import java.time.Duration;
-import java.util.List;
-import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
import javax.script.ScriptException;
import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.horstmann.codecheck.Problem;
import com.horstmann.codecheck.Util;
-import com.typesafe.config.Config;
-import models.CodeCheck;
-import play.Logger;
import play.mvc.Controller;
import play.mvc.Http;
import play.mvc.Result;
+import services.ServiceException;
public class Files extends Controller {
- @Inject private CodeCheck codeCheck;
- private static Logger.ALogger logger = Logger.of("com.horstmann.codecheck");
- @Inject private Config config;
+ @Inject private services.Files filesService;
- String start2 = "\n\n"
- + "CodeCheck"
- + "\n"
- + "\n"
- + "\n"
- + "\n"
- + "\n"
- + "\n"
- + "\n"
- + "\n"
- + "\n"
- + "\n";
- String mid2 = "\n"
- + "\n"
- + "";
-
- public Result filesHTML2(Http.Request request, String repo, String problemName, String ccid)
+ public Result filesHTML2(Http.Request request, String repo, String problemName)
throws IOException, NoSuchMethodException, ScriptException {
- if (ccid == null) {
- Optional ccidCookie = request.getCookie("ccid");
- ccid = ccidCookie.map(Http.Cookie::value).orElse(Util.createPronouncableUID());
- }
- Map problemFiles;
- try {
- problemFiles = codeCheck.loadProblem(repo, problemName, ccid);
- } catch (Exception e) {
- logger.error("filesHTML2: Cannot load problem " + repo + "/" + " " + problemName + e.getMessage());
- return badRequest("Cannot load problem");
- }
- Problem problem = new Problem(problemFiles);
- ObjectNode data = models.Util.toJson(problem.getProblemData());
- data.put("url", models.Util.prefix(request) + "/checkNJS");
- data.put("repo", repo);
- data.put("problem", problemName);
- String description = "";
- if (data.has("description")) {
- description = data.get("description").asText();
- data.remove("description");
- }
- StringBuilder result = new StringBuilder();
- result.append(start2);
- result.append(description);
- result.append(mid2);
- result.append(data.toString());
- result.append(end2);
- wakeupChecker();
- Http.Cookie newCookie = Http.Cookie.builder("ccid", ccid).withMaxAge(Duration.ofDays(180)).build();
- return ok(result.toString()).withCookies(newCookie).as("text/html");
+ try {
+ Optional ccidCookie = request.getCookie("ccid");
+ String ccid = ccidCookie.map(Http.Cookie::value).orElse(Util.createPronouncableUID());
+ String result = filesService.filesHTML2(controllers.Util.prefix(request), repo, problemName, ccid);
+ Http.Cookie newCookie = controllers.Util.buildCookie("ccid", ccid);
+ return ok(result).withCookies(newCookie).as("text/html");
+ }
+ catch (ServiceException ex) {
+ return badRequest(ex.getMessage());
+ }
}
- private void wakeupChecker() {
- // Wake up the checker
- String path = "com.horstmann.codecheck.comrun.remote";
- if (!config.hasPath(path)) return;
- String remoteURL = config.getString(path);
- if (remoteURL.isBlank()) return;
- new Thread(() -> { try {
- URL checkerWakeupURL = new URL(remoteURL + "/api/health");
- checkerWakeupURL.openStream().readAllBytes();
- } catch (IOException e) {
- e.printStackTrace();
- } }).start();
- }
-
- private static String tracerStart = "\n"
- + "\n"
- + "\n"
- + " \n"
- + " "
- + " CodeCheck Tracer\n"
- + " \n"
- + "\n"
- + "\n";
- private static String tracerScriptStart = " \n"
- + " \n"
- + "
\n"
- + "\n"
- + "";
-
- public Result tracer(Http.Request request, String repo, String problemName, String ccid)
+ public Result tracer(Http.Request request, String repo, String problemName)
throws IOException {
- if (ccid == null) {
- Optional ccidCookie = request.getCookie("ccid");
- ccid = ccidCookie.map(Http.Cookie::value).orElse(Util.createPronouncableUID());
- }
- Map problemFiles;
- try {
- problemFiles = codeCheck.loadProblem(repo, problemName, ccid);
- } catch (Exception e) {
- logger.error("filesHTML: Cannot load problem " + repo + "/" + problemName + " " + e.getMessage());
- return badRequest("Cannot load problem " + repo + "/" + problemName);
- }
- Problem problem = new Problem(problemFiles);
- Problem.DisplayData data = problem.getProblemData();
- StringBuilder result = new StringBuilder();
- result.append(tracerStart);
- if (data.description != null)
- result.append(data.description);
- result.append(tracerScriptStart);
- result.append(Util.getString(problemFiles, Path.of("tracer.js")));
- result.append(tracerEnd);
-
- Http.Cookie newCookie = Http.Cookie.builder("ccid", ccid).withMaxAge(Duration.ofDays(180)).build();
- return ok(result.toString()).withCookies(newCookie).as("text/html");
+ try {
+ Optional ccidCookie = request.getCookie("ccid");
+ String ccid = ccidCookie.map(Http.Cookie::value).orElse(Util.createPronouncableUID());
+ String result = filesService.tracer(repo, problemName, ccid);
+ Http.Cookie newCookie = controllers.Util.buildCookie("ccid", ccid);
+ return ok(result).withCookies(newCookie).as("text/html");
+ }
+ catch (ServiceException ex) {
+ return badRequest(ex.getMessage());
+ }
}
// TODO: Caution--this won't do the right thing with param.js randomness when
// used to prebuild UI like in ebook, Udacity
- public Result fileData(Http.Request request, String repo, String problemName, String ccid)
+ public Result fileData(Http.Request request, String repo, String problemName)
throws IOException, NoSuchMethodException, ScriptException {
- if (ccid == null) {
- Optional ccidCookie = request.getCookie("ccid");
- ccid = ccidCookie.map(Http.Cookie::value).orElse(Util.createPronouncableUID());
- }
- Map problemFiles;
- try {
- problemFiles = codeCheck.loadProblem(repo, problemName, ccid);
- } catch (Exception e) {
- logger.error("fileData: Cannot load problem " + repo + "/" + problemName + " " + e.getMessage());
- return badRequest("Cannot load problem");
- }
-
- Problem problem = new Problem(problemFiles);
- Http.Cookie newCookie = Http.Cookie.builder("ccid", ccid).withMaxAge(Duration.ofDays(180)).build();
- return ok(models.Util.toJson(problem.getProblemData())).withCookies(newCookie);
+ try {
+ Optional ccidCookie = request.getCookie("ccid");
+ String ccid = ccidCookie.map(Http.Cookie::value).orElse(Util.createPronouncableUID());
+ ObjectNode result = filesService.fileData(repo, problemName, ccid);
+ Http.Cookie newCookie = controllers.Util.buildCookie("ccid", ccid);
+ return ok(result).withCookies(newCookie);
+ }
+ catch (ServiceException ex) {
+ return badRequest(ex.getMessage());
+ }
}
-
- // TODO: Legacy, also codecheck.js
- private static String start = "\n\n"
- + "\n"
- + "\n"
- + "\n"
- + "\n"
- + "\n"
- + "\n";
- private static String before = "