diff --git a/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/RExerciseDescParser.java b/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/RExerciseDescParser.java new file mode 100644 index 000000000..d49a052a8 --- /dev/null +++ b/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/RExerciseDescParser.java @@ -0,0 +1,44 @@ + +package fi.helsinki.cs.tmc.langs.r; + +import fi.helsinki.cs.tmc.langs.domain.TestDesc; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + + + +class RExerciseDescParser { + + private static Path RESULT_FILE = Paths.get(".available_points.json"); + private static final TypeReference>> MAP_TYPE_REFERENCE = + new TypeReference>>() {}; + private Path path; + private ObjectMapper mapper; + + public RExerciseDescParser(Path path) { + this.path = path; + this.mapper = new ObjectMapper(); + } + + public ImmutableList parse() throws IOException { + List testDescs = new ArrayList<>(); + byte[] json = Files.readAllBytes(path.resolve(RESULT_FILE)); + Map> parse = mapper.readValue(json, MAP_TYPE_REFERENCE); + for (String name : parse.keySet()) { + ImmutableList points = ImmutableList.copyOf(parse.get(name)); + testDescs.add(new TestDesc(name, points)); + } + return ImmutableList.copyOf(testDescs); + } + +} diff --git a/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/RPlugin.java b/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/RPlugin.java index fc2bfb89f..8b93a00c5 100644 --- a/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/RPlugin.java +++ b/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/RPlugin.java @@ -1,21 +1,23 @@ package fi.helsinki.cs.tmc.langs.r; + import fi.helsinki.cs.tmc.langs.AbstractLanguagePlugin; import fi.helsinki.cs.tmc.langs.abstraction.ValidationResult; import fi.helsinki.cs.tmc.langs.domain.ExerciseBuilder; import fi.helsinki.cs.tmc.langs.domain.ExerciseDesc; import fi.helsinki.cs.tmc.langs.domain.RunResult; +import fi.helsinki.cs.tmc.langs.domain.TestDesc; import fi.helsinki.cs.tmc.langs.io.StudentFilePolicy; import fi.helsinki.cs.tmc.langs.io.sandbox.StudentFileAwareSubmissionProcessor; -import fi.helsinki.cs.tmc.langs.io.sandbox.SubmissionProcessor; import fi.helsinki.cs.tmc.langs.io.zip.StudentFileAwareUnzipper; import fi.helsinki.cs.tmc.langs.io.zip.StudentFileAwareZipper; -import fi.helsinki.cs.tmc.langs.io.zip.Unzipper; -import fi.helsinki.cs.tmc.langs.io.zip.Zipper; -import fi.helsinki.cs.tmc.langs.python3.Python3TestResultParser; + import fi.helsinki.cs.tmc.langs.utils.ProcessRunner; + import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; + import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.SystemUtils; @@ -24,17 +26,32 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Locale; public final class RPlugin extends AbstractLanguagePlugin { + // Various static final Path-variables for filepaths + // to various folders and files in a R exercise project here + private static final Path R_FOLDER_PATH = Paths.get("R"); + private static final Path TEST_FOLDER_PATH = Paths.get("tests"); + private static final Path TESTTHAT_FOLDER_PATH = Paths.get("testthat"); + private static final Path TMC_FOLDER_PATH = Paths.get("tmc"); + private static final Path DESCRIPTION_PATH = Paths.get("DESCRIPTION"); + private static final Path RHISTORY_PATH = Paths.get(".RHistory"); + private static final Path RESULT_R_PATH = Paths.get("result.R"); + private static final String CANNOT_RUN_TESTS_MESSAGE = "Failed to run tests."; private static final String CANNOT_PARSE_TEST_RESULTS_MESSAGE = "Failed to read test results."; private static final String CANNOT_SCAN_EXERCISE_MESSAGE = "Failed to scan exercise."; private static final String CANNOT_PARSE_EXERCISE_DESCRIPTION_MESSAGE = "Failed to parse exercise description."; + // Various static final String-variables for + // error messages related to parsing and running R tests here + private static Logger log = LoggerFactory.getLogger(RPlugin.class); public RPlugin() { @@ -47,22 +64,57 @@ public RPlugin() { @Override public boolean isExerciseTypeCorrect(Path path) { - return false; + return Files.exists(path.resolve(R_FOLDER_PATH)) + || Files.exists(path.resolve(TEST_FOLDER_PATH).resolve(TESTTHAT_FOLDER_PATH)) + || Files.exists(path.resolve(DESCRIPTION_PATH)) + || Files.exists(path.resolve(RHISTORY_PATH)) + || Files.exists(path.resolve(TMC_FOLDER_PATH).resolve(RESULT_R_PATH)); + /* + R folder contains the actual R files used in the + project/package. It is automatically included when creating a + R package but now when making a regular project in RStudio. + + test/testthat folder contains the unit testing + files which use the testThat library for the R project. + + DESCRIPTION file contains package information. + Included automatically when making a new package, but not + included when making a regular project in RStudio. + + .RHistory file contains the history of executed code on + the R terminal. Generated after running code on the R + terminal for the first time. + + tmc/result.R contains the call to tmcRtestrunner's runTests function. + */ } @Override protected StudentFilePolicy getStudentFilePolicy(Path projectPath) { - return null; + return new RStudentFilePolicy(projectPath); } @Override public String getPluginName() { - return null; + return "r"; } @Override public Optional scanExercise(Path path, String exerciseName) { - return null; + ProcessRunner runner = new ProcessRunner(getAvailablePointsCommand(), path); + try { + runner.call(); + } catch (Exception e) { + System.out.println(e); + log.error(CANNOT_SCAN_EXERCISE_MESSAGE, e); + } + try { + ImmutableList testDescs = new RExerciseDescParser(path).parse(); + return Optional.of(new ExerciseDesc(exerciseName, testDescs)); + } catch (IOException e) { + log.error(CANNOT_PARSE_EXERCISE_DESCRIPTION_MESSAGE, e); + } + return Optional.absent(); } @Override @@ -98,9 +150,16 @@ private String[] getTestCommand() { } return ArrayUtils.addAll(rscr, command); } - + + private String[] getAvailablePointsCommand() { + String[] rscr = new String[] {"Rscript", "-e"}; + String[] command = new String[] {"\"library(tmcRtestrunner);" + + "getAvailablePoints(\"$PWD\")\""}; + return ArrayUtils.addAll(rscr, command); + } + @Override public void clean(Path path) { - + // TO DO } } diff --git a/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/RStudentFilePolicy.java b/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/RStudentFilePolicy.java new file mode 100644 index 000000000..fc2f92284 --- /dev/null +++ b/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/RStudentFilePolicy.java @@ -0,0 +1,25 @@ +package fi.helsinki.cs.tmc.langs.r; + +import fi.helsinki.cs.tmc.langs.io.ConfigurableStudentFilePolicy; + +import java.nio.file.Path; +import java.nio.file.Paths; + +public class RStudentFilePolicy extends ConfigurableStudentFilePolicy { + public RStudentFilePolicy(Path configFileParent) { + super(configFileParent); + } + + /** + * Returns {@code True} for all files in the projectRoot/R/ directory and other + * files required for building the project. + * + *

Will NOT return {@code True} for any test files. If test file modification are part + * of the exercise, those test files are whitelisted as ExtraStudentFiles and the + * decision to include them is made by {@link ConfigurableStudentFilePolicy}. + */ + @Override + public boolean isStudentSourceFile(Path path, Path projectRootPath) { + return path.startsWith(Paths.get("R")); + } +} diff --git a/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/TestMain.java b/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/TestMain.java index 249a726c0..82b00e175 100644 --- a/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/TestMain.java +++ b/tmc-langs-r/src/main/java/fi/helsinki/cs/tmc/langs/r/TestMain.java @@ -23,20 +23,12 @@ public static void main(String[] args) { //For now, add the path you want to test here fully, //for example: pathToGithubFolder/tmc-r/example_projects/example_project1 - String exampleProjectLocation = "/tmc-r/example_projects/example_project1"; + String exampleProjectLocation = "/example_projects/example_project1"; Path path = Paths.get(exampleProjectLocation); RunResult runRes = runTests(path); printTestResult(runRes); RunResult rr; -/* try { - rr = new RTestResultParser(path).parse(); - for (TestResult tr : rr.testResults) { - System.out.println(tr.toString()); - } - } catch (IOException e) { - System.out.println("Something wrong: " + e.getMessage()); - }*/ } public static void printTestResult(RunResult rr) { diff --git a/tmc-langs-r/src/test/java/fi/helsinki/cs/tmc/langs/r/RExerciseDescParserTest.java b/tmc-langs-r/src/test/java/fi/helsinki/cs/tmc/langs/r/RExerciseDescParserTest.java new file mode 100644 index 000000000..7e8b84953 --- /dev/null +++ b/tmc-langs-r/src/test/java/fi/helsinki/cs/tmc/langs/r/RExerciseDescParserTest.java @@ -0,0 +1,47 @@ + +package fi.helsinki.cs.tmc.langs.r; + +import static org.junit.Assert.assertEquals; + +import fi.helsinki.cs.tmc.langs.domain.TestDesc; +import fi.helsinki.cs.tmc.langs.utils.TestUtils; + +import com.google.common.collect.ImmutableList; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Path; + + + +public class RExerciseDescParserTest { + private ImmutableList re; + private Path jsonDir; + + public RExerciseDescParserTest() { + jsonDir = TestUtils.getPath(getClass(), "example_json"); + try { + re = new RExerciseDescParser(jsonDir).parse(); + } catch (IOException e) { + System.out.println("Something wrong: " + e.getMessage()); + } + } + + @Test + public void testThatParseSeemsToWorkOnExampleJson() { + assertEquals(re.size(),6); + assertEquals(re.get(0).points.size(),2); + assertEquals(re.get(0).name,"Addition works"); + assertEquals(re.get(1).points.size(),2); + assertEquals(re.get(1).name,"Multiplication works"); + assertEquals(re.get(2).points.size(),1); + assertEquals(re.get(2).name,"Subtraction works"); + assertEquals(re.get(3).points.size(),1); + assertEquals(re.get(3).name,"Division works"); + assertEquals(re.get(4).points.size(),0); + assertEquals(re.get(4).name, "Test with no points"); + assertEquals(re.get(5).points.size(),0); + assertEquals(re.get(5).name, "Dummy test set to fail"); + } +} diff --git a/tmc-langs-r/src/test/java/fi/helsinki/cs/tmc/langs/r/RPluginTest.java b/tmc-langs-r/src/test/java/fi/helsinki/cs/tmc/langs/r/RPluginTest.java new file mode 100644 index 000000000..4a0882319 --- /dev/null +++ b/tmc-langs-r/src/test/java/fi/helsinki/cs/tmc/langs/r/RPluginTest.java @@ -0,0 +1,14 @@ + +package fi.helsinki.cs.tmc.langs.r; + +import org.junit.Test; + +public class RPluginTest { + + @Test + public void testGetAvailablePointsCommand(){ + + } + + +} diff --git a/tmc-langs-r/src/test/java/fi/helsinki/cs/tmc/langs/r/RTestResultParserTest.java b/tmc-langs-r/src/test/java/fi/helsinki/cs/tmc/langs/r/RTestResultParserTest.java index f59fd828e..0ce59ca87 100644 --- a/tmc-langs-r/src/test/java/fi/helsinki/cs/tmc/langs/r/RTestResultParserTest.java +++ b/tmc-langs-r/src/test/java/fi/helsinki/cs/tmc/langs/r/RTestResultParserTest.java @@ -43,4 +43,5 @@ public void testThatParseSeemsToWorkOnExampleJson() { } } } + } diff --git a/tmc-langs-r/src/test/resources/example_json/.available_points.json b/tmc-langs-r/src/test/resources/example_json/.available_points.json new file mode 100644 index 000000000..25862cfca --- /dev/null +++ b/tmc-langs-r/src/test/resources/example_json/.available_points.json @@ -0,0 +1,8 @@ +{"Addition works": ["r1.1","r1.2"], + "Multiplication works":["r1.3", "r1.4"], + "Subtraction works":["r1.5"], + "Division works":["r1.6"], + "Test with no points":[], + "Dummy test set to fail":[] +} + diff --git a/tmc-langs-r/src/test/resources/example_json/.results.json b/tmc-langs-r/src/test/resources/example_json/.results.json index 5d0cb9f6d..c35b74613 100644 --- a/tmc-langs-r/src/test/resources/example_json/.results.json +++ b/tmc-langs-r/src/test/resources/example_json/.results.json @@ -1,6 +1,6 @@ [ { - "status": "passed", + "status": "pass", "name": "Addition works", "message": "", "backtrace": "", @@ -10,7 +10,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Multiplication works", "message": "", "backtrace": "", @@ -20,7 +20,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Subtraction works", "message": "", "backtrace": "", @@ -29,7 +29,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Division works", "message": "", "backtrace": "", @@ -38,7 +38,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Test with no points", "message": "", "backtrace": "", @@ -47,7 +47,7 @@ ] }, { - "status": "failed", + "status": "fail", "name": "Dummy test set to fail", "message": "Failed with call: expect_true, FALSE\nFALSE isn't true.\nFailed with call: expect_equal, 1, 2\n1 not equal to 2.\n1/1 mismatches\n[1] 1 - 2 == -1\n", "backtrace": "", @@ -56,7 +56,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Matrix transpose with [[1,2]] works", "message": "", "backtrace": "", @@ -65,7 +65,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Matrix transpose with [[1,2],[3,4]] works", "message": "", "backtrace": "", @@ -74,7 +74,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Constant string works", "message": "", "backtrace": "", @@ -83,7 +83,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Exercise 1 is correct", "message": "", "backtrace": "", @@ -92,7 +92,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Exercise 2 is correct", "message": "", "backtrace": "", @@ -101,7 +101,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Exercise 3 is correct", "message": "", "backtrace": "", @@ -111,7 +111,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Exercise 4 is correct", "message": "", "backtrace": "", @@ -120,7 +120,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Exercise 5 is correct", "message": "", "backtrace": "", @@ -129,7 +129,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Exercise 6 is correct", "message": "", "backtrace": "", @@ -138,7 +138,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Exercise 7 is correct", "message": "", "backtrace": "", @@ -147,7 +147,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Exercise 8 is correct", "message": "", "backtrace": "", @@ -156,7 +156,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Exercise 9 is correct", "message": "", "backtrace": "", @@ -165,7 +165,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Exercise 10 is correct", "message": "", "backtrace": "", @@ -174,7 +174,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Exercise 11 is correct", "message": "", "backtrace": "", @@ -183,7 +183,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Exercise 12 is correct", "message": "", "backtrace": "", @@ -192,7 +192,7 @@ ] }, { - "status": "passed", + "status": "pass", "name": "Exercise 13 is correct", "message": "", "backtrace": "", @@ -201,4 +201,3 @@ ] } ] -