From 2c958c2b57fff0351d6656add7112e3e63eacd0d Mon Sep 17 00:00:00 2001 From: Valentina Kiryushkina Date: Thu, 28 Apr 2016 15:17:24 +0300 Subject: [PATCH] Add tests from samples provided for step and show test result in separate tool window --- .../student/resources/META-INF/plugin.xml | 1 + .../learning/StudyBasePluginConfigurator.java | 1 + .../edu/learning/checker/StudyCheckTask.java | 88 +++++++------ .../edu/learning/checker/StudyCheckUtils.java | 53 +++++--- .../checker/StudyTestsOutputParser.java | 4 +- .../courseGeneration/StudyGenerator.java | 59 +++++---- .../stepic/EduAdaptiveStepicConnector.java | 121 ++++++++++++------ .../edu/learning/stepic/StepicWrappers.java | 2 + .../edu/learning/ui/StudyBrowserWindow.java | 2 +- .../learning/ui/StudyTestResultsToolWindow.kt | 20 +++ .../ui/StudyTestResultsToolWindowFactory.kt | 18 +++ .../edu/learning/ui/StudyToolWindow.java | 2 +- .../learning/ui/StudyToolWindowFactory.java | 3 +- .../fileTemplates/internal/test_helper.py.ft | 33 ++++- .../edu/learning/PyStudyCheckAction.java | 56 ++++---- 15 files changed, 303 insertions(+), 160 deletions(-) create mode 100644 python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyTestResultsToolWindow.kt create mode 100644 python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyTestResultsToolWindowFactory.kt diff --git a/python/educational-core/student/resources/META-INF/plugin.xml b/python/educational-core/student/resources/META-INF/plugin.xml index 2d5b272bd2bb..5652e347f6d6 100644 --- a/python/educational-core/student/resources/META-INF/plugin.xml +++ b/python/educational-core/student/resources/META-INF/plugin.xml @@ -85,6 +85,7 @@ + diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/StudyBasePluginConfigurator.java b/python/educational-core/student/src/com/jetbrains/edu/learning/StudyBasePluginConfigurator.java index 74410d74ce84..07a3b31e8514 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/StudyBasePluginConfigurator.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/StudyBasePluginConfigurator.java @@ -69,6 +69,7 @@ public abstract class StudyBasePluginConfigurator implements StudyPluginConfigur Task task = getTask(file); setTaskText(task, file.getParent()); } + toolWindow.setBottomComponent(null); } @Nullable diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckTask.java b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckTask.java index 29a4011c9306..61b7f3718596 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckTask.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckTask.java @@ -25,6 +25,7 @@ import com.jetbrains.edu.learning.courseFormat.Task; import com.jetbrains.edu.learning.stepic.EduAdaptiveStepicConnector; import com.jetbrains.edu.learning.stepic.EduStepicConnector; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public class StudyCheckTask extends com.intellij.openapi.progress.Task.Backgroundable { @@ -75,21 +76,36 @@ public class StudyCheckTask extends com.intellij.openapi.progress.Task.Backgroun public void run(@NotNull ProgressIndicator indicator) { final Course course = StudyTaskManager.getInstance(myProject).getCourse(); if (course != null && course.isAdaptive()) { - checkAdaptiveCourse(indicator); + checkForAdaptiveCourse(indicator); } else { - if (checkCourse(indicator)) return; + checkForEduCourse(indicator); } - runAfterTaskSolvedActions(); } - private boolean checkCourse(@NotNull ProgressIndicator indicator) { + private void checkForEduCourse(@NotNull ProgressIndicator indicator) { + final StudyTestsOutputParser.TestsOutput testsOutput = getTestOutput(indicator); + + postAttemptToStepic(testsOutput); + + if (testsOutput != null) { + if (testsOutput.isSuccess()) { + onTaskSolved(testsOutput.getMessage()); + } + else { + onTaskFailed(testsOutput.getMessage()); + } + runAfterTaskCheckedActions(); + } + } + + @Nullable + private StudyTestsOutputParser.TestsOutput getTestOutput(@NotNull ProgressIndicator indicator) { final CapturingProcessHandler handler = new CapturingProcessHandler(myTestProcess, null, myCommandLine); final ProcessOutput output = handler.runProcessWithProgressIndicator(indicator); if (indicator.isCanceled()) { ApplicationManager.getApplication().invokeLater( () -> StudyCheckUtils.showTestResultPopUp("Check cancelled", MessageType.WARNING.getPopupBackground(), myProject)); - return true; } @@ -102,64 +118,64 @@ public class StudyCheckTask extends com.intellij.openapi.progress.Task.Backgroun myProject)); //log error output of tests LOG.info("#educational " + stderr); - return true; + return null; } - - postAttemptToStepic(testsOutput); - - - if (testsOutput.isSuccess()) { - onTaskSolved(testsOutput.getMessage()); - } - else { - onTaskFailed(testsOutput.getMessage()); - } - return false; + return testsOutput; } - private void checkAdaptiveCourse(ProgressIndicator indicator) { + private void checkForAdaptiveCourse(ProgressIndicator indicator) { ProgressManager.getInstance().runProcessWithProgressAsynchronously(new Backgroundable(myProject, "Checking Task") { @Override public void run(@NotNull ProgressIndicator indicator) { - final Pair pair = EduAdaptiveStepicConnector.checkTask(myProject, myTask); - if (pair != null) { - final String checkMessage = pair.getSecond(); - if (pair.getFirst()) { - onTaskSolved(checkMessage); + final StudyTestsOutputParser.TestsOutput testOutput = getTestOutput(indicator); + if (testOutput != null) { + if (testOutput.isSuccess()) { + final Pair pair = EduAdaptiveStepicConnector.checkTask(myProject, myTask); + if (pair != null && !pair.getSecond().isEmpty()) { + final String checkMessage = pair.getSecond(); + if (pair.getFirst()) { + onTaskSolved(checkMessage); + } + else { + onTaskFailed(checkMessage); + } + runAfterTaskCheckedActions(); + } + else { + ApplicationManager.getApplication().invokeLater(() -> + StudyCheckUtils.showTestResultPopUp("Failed to launch checking", + MessageType.WARNING + .getPopupBackground(), + myProject)); + } } else { - onTaskFailed(checkMessage); + onTaskFailed(testOutput.getMessage()); } } - else { - ApplicationManager.getApplication().invokeLater(() -> - StudyCheckUtils.showTestResultPopUp("Failed to launch checking", - MessageType.WARNING.getPopupBackground(), - myProject)); - } } }, indicator); - } - protected void onTaskFailed( String message) { + protected void onTaskFailed(String message) { myTaskManger.setStatus(myTask, StudyStatus.Failed); ApplicationManager.getApplication().invokeLater( - () -> StudyCheckUtils.showTestResultPopUp(message, MessageType.ERROR.getPopupBackground(), myProject)); + () -> StudyCheckUtils.showTestResults(myProject, message)); } protected void onTaskSolved(String message) { myTaskManger.setStatus(myTask, StudyStatus.Solved); + ApplicationManager.getApplication().invokeLater( - () -> StudyCheckUtils.showTestResultPopUp(message, MessageType.INFO.getPopupBackground(), myProject)); + () -> StudyCheckUtils.showTestResults(myProject, message)); } - private void runAfterTaskSolvedActions() { + private void runAfterTaskCheckedActions() { StudyPluginConfigurator configurator = StudyUtils.getConfigurator(myProject); if (configurator != null) { StudyAfterCheckAction[] checkActions = configurator.getAfterCheckActions(); if (checkActions != null) { - for (StudyAfterCheckAction action: checkActions) { + for (StudyAfterCheckAction action : checkActions) { action.run(myProject, myTask, myStatusBeforeCheck); } } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckUtils.java b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckUtils.java index 9fe0ae84ae6b..c719388b61ac 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckUtils.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckUtils.java @@ -15,23 +15,25 @@ import com.intellij.openapi.ui.popup.BalloonBuilder; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.wm.IdeFocusManager; -import com.intellij.openapi.wm.IdeFrame; -import com.intellij.openapi.wm.WindowManager; +import com.intellij.openapi.wm.*; import com.intellij.openapi.wm.ex.StatusBarEx; import com.intellij.openapi.wm.ex.WindowManagerEx; +import com.intellij.ui.content.Content; +import com.jetbrains.edu.learning.StudyState; +import com.jetbrains.edu.learning.StudyTaskManager; +import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.core.EduDocumentListener; import com.jetbrains.edu.learning.core.EduUtils; import com.jetbrains.edu.learning.courseFormat.AnswerPlaceholder; import com.jetbrains.edu.learning.courseFormat.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; -import com.jetbrains.edu.learning.StudyState; -import com.jetbrains.edu.learning.StudyTaskManager; -import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.editor.StudyEditor; import com.jetbrains.edu.learning.navigation.StudyNavigator; +import com.jetbrains.edu.learning.ui.StudyTestResultsToolWindow; +import com.jetbrains.edu.learning.ui.StudyTestResultsToolWindowFactory; import org.jetbrains.annotations.NotNull; +import javax.swing.*; import java.awt.*; import java.io.IOException; import java.util.List; @@ -61,9 +63,9 @@ public class StudyCheckUtils { } public static void navigateToFailedPlaceholder(@NotNull final StudyState studyState, - @NotNull final Task task, - @NotNull final VirtualFile taskDir, - @NotNull final Project project) { + @NotNull final Task task, + @NotNull final VirtualFile taskDir, + @NotNull final Project project) { TaskFile selectedTaskFile = studyState.getTaskFile(); Editor editor = studyState.getEditor(); TaskFile taskFileToNavigate = selectedTaskFile; @@ -109,10 +111,10 @@ public class StudyCheckUtils { public static void runSmartTestProcess(@NotNull final VirtualFile taskDir, - @NotNull final StudyTestRunner testRunner, - final String taskFileName, - @NotNull final TaskFile taskFile, - @NotNull final Project project) { + @NotNull final StudyTestRunner testRunner, + final String taskFileName, + @NotNull final TaskFile taskFile, + @NotNull final Project project) { final TaskFile answerTaskFile = new TaskFile(); answerTaskFile.name = taskFileName; final VirtualFile virtualFile = taskDir.findChild(taskFileName); @@ -135,11 +137,10 @@ public class StudyCheckUtils { } - private static VirtualFile getCopyWithAnswers(@NotNull final VirtualFile taskDir, - @NotNull final VirtualFile file, - @NotNull final TaskFile source, - @NotNull final TaskFile target) { + @NotNull final VirtualFile file, + @NotNull final TaskFile source, + @NotNull final TaskFile target) { VirtualFile copy = null; try { @@ -193,4 +194,22 @@ public class StudyCheckUtils { EduUtils.flushWindows(taskFile, virtualFile, true); } } + + public static void showTestResults(@NotNull final Project project, @NotNull final String message) { + final ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project); + ToolWindow window = toolWindowManager.getToolWindow("Test Results"); + if (window == null) { + toolWindowManager.registerToolWindow("Test Results", true, ToolWindowAnchor.BOTTOM); + window = toolWindowManager.getToolWindow("Test Results"); + new StudyTestResultsToolWindowFactory().createToolWindowContent(project, window); + } + final Content[] contents = window.getContentManager().getContents(); + for (Content content : contents) { + final JComponent component = content.getComponent(); + if (component instanceof StudyTestResultsToolWindow) { + ((StudyTestResultsToolWindow)component).setText(message); + component.setVisible(true); + } + } + } } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyTestsOutputParser.java b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyTestsOutputParser.java index 55bfe7273bac..921ee4cba82c 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyTestsOutputParser.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyTestsOutputParser.java @@ -4,7 +4,7 @@ import com.intellij.execution.process.ProcessOutput; import org.jetbrains.annotations.NotNull; public class StudyTestsOutputParser { - private static final String ourStudyPrefix = "#educational_plugin"; + private static final String STUDY_PREFIX = "#educational_plugin"; public static final String TEST_OK = "test OK"; private static final String TEST_FAILED = "FAILED + "; private static final String CONGRATS_MESSAGE = "CONGRATS_MESSAGE "; @@ -32,7 +32,7 @@ public class StudyTestsOutputParser { public static TestsOutput getTestsOutput(@NotNull final ProcessOutput processOutput) { String congratulations = CONGRATULATIONS; for (String line : processOutput.getStdoutLines()) { - if (line.startsWith(ourStudyPrefix)) { + if (line.startsWith(STUDY_PREFIX)) { if (line.contains(TEST_OK)) { continue; } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/courseGeneration/StudyGenerator.java b/python/educational-core/student/src/com/jetbrains/edu/learning/courseGeneration/StudyGenerator.java index 9af4dc7af1a5..fe5cd4e65c40 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/courseGeneration/StudyGenerator.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/courseGeneration/StudyGenerator.java @@ -4,13 +4,13 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.VirtualFile; +import com.jetbrains.edu.learning.StudyTaskManager; +import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.courseFormat.Course; import com.jetbrains.edu.learning.courseFormat.Lesson; import com.jetbrains.edu.learning.courseFormat.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; -import com.jetbrains.edu.learning.StudyTaskManager; -import com.jetbrains.edu.learning.StudyUtils; import org.jetbrains.annotations.NotNull; import java.io.File; @@ -23,6 +23,7 @@ public class StudyGenerator { private StudyGenerator() { } + private static final Logger LOG = Logger.getInstance(StudyGenerator.class.getName()); /** @@ -107,34 +108,32 @@ public class StudyGenerator { public static void createCourse(@NotNull final Course course, @NotNull final VirtualFile baseDir, @NotNull final File resourceRoot, @NotNull final Project project) { - try { - final List lessons = course.getLessons(); - for (int i = 1; i <= lessons.size(); i++) { - Lesson lesson = lessons.get(i - 1); - lesson.setIndex(i); - createLesson(lesson, baseDir, resourceRoot, project); - } - baseDir.createChildDirectory(project, EduNames.SANDBOX_DIR); - File[] files = resourceRoot.listFiles(new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return !name.contains(EduNames.LESSON) && !name.equals(EduNames.COURSE_META_FILE) && !name.equals(EduNames.HINTS); - } - }); - for (File file : files) { - File dir = new File(baseDir.getPath(), file.getName()); - if (file.isDirectory()) { - FileUtil.copyDir(file, dir); - continue; - } + try { + final List lessons = course.getLessons(); + for (int i = 1; i <= lessons.size(); i++) { + Lesson lesson = lessons.get(i - 1); + lesson.setIndex(i); + createLesson(lesson, baseDir, resourceRoot, project); + } + baseDir.createChildDirectory(project, EduNames.SANDBOX_DIR); + File[] files = resourceRoot.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return !name.contains(EduNames.LESSON) && !name.equals(EduNames.COURSE_META_FILE) && !name.equals(EduNames.HINTS); + } + }); + for (File file : files) { + File dir = new File(baseDir.getPath(), file.getName()); + if (file.isDirectory()) { + FileUtil.copyDir(file, dir); + continue; + } - FileUtil.copy(file, dir); - - } - } - catch (IOException e) { - LOG.error(e); - } + FileUtil.copy(file, dir); + } + } + catch (IOException e) { + LOG.error(e); + } } - } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java index ee80ac733d98..cad89ec26265 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java @@ -9,6 +9,7 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.jetbrains.edu.learning.StudyTaskManager; @@ -42,6 +43,7 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; +import java.util.List; import java.util.concurrent.TimeUnit; import static com.jetbrains.edu.learning.stepic.EduStepicConnector.getHttpClient; @@ -57,6 +59,8 @@ public class EduAdaptiveStepicConnector { private static final String RECOMMENDATION_REACTIONS_URL = "recommendation-reactions"; private static final String ATTEMPTS_URL = "attempts"; private static final String SUBMISSION_URL = "submissions"; + private static final String PYTHON2 = "python2"; + private static final String PYTHON3 = "python3"; @Nullable public static Task getNextRecommendation(@NotNull final Project project, @NotNull Course course) { @@ -88,9 +92,11 @@ public class EduAdaptiveStepicConnector { for (int stepId : realLesson.steps) { final StepicWrappers.Step step = EduStepicConnector.getStep(stepId); if (step.name.equals("code")) { - return getTaskFromStep(realLesson.getName(), stepId, step); + return getTaskFromStep(project, stepId, step, realLesson.getName()); } } + + LOG.warn("Got a lesson without code part as a recommendation"); } } else { @@ -106,8 +112,8 @@ public class EduAdaptiveStepicConnector { return null; } - public static boolean postRecommendationReaction(@NotNull final Project project, @NotNull final String lessonId, - @NotNull final String user,int reaction) { + public static boolean postRecommendationReaction(@NotNull final Project project, @NotNull final String lessonId, + @NotNull final String user, int reaction) { final HttpPost post = new HttpPost(STEPIC_API_URL + RECOMMENDATION_REACTIONS_URL); final String json = new Gson() @@ -132,7 +138,7 @@ public class EduAdaptiveStepicConnector { // TODO: get user from settings final StepicUser user = StudyTaskManager.getInstance(project).getUser(); if (user != null && - postRecommendationReaction(project, String.valueOf(editor.getTaskFile().getTask().getLesson().id), + postRecommendationReaction(project, String.valueOf(editor.getTaskFile().getTask().getLesson().id), String.valueOf(user.id), reaction)) { final Task task = getNextRecommendation(project, course); @@ -188,16 +194,37 @@ public class EduAdaptiveStepicConnector { } @NotNull - private static Task getTaskFromStep(@NotNull String name, int lessonID, @NotNull final StepicWrappers.Step step) { + private static Task getTaskFromStep(Project project, + int lessonID, + @NotNull final StepicWrappers.Step step, @NotNull String name) { final Task task = new Task(); task.setName(name); task.setStepicId(lessonID); task.setText(step.text); + if (step.options.samples != null) { + final StringBuilder builder = new StringBuilder(); + for (List sample : step.options.samples) { + if (sample.size() == 2) { + builder.append("Sample Input:
"); + builder.append(sample.get(0)); + builder.append("
"); + builder.append("Sample Output:
"); + builder.append(sample.get(1)); + builder.append("

"); + } + } + task.setText(task.getText() + "
" + builder.toString()); + } if (step.options.test != null) { for (StepicWrappers.TestFileWrapper wrapper : step.options.test) { task.addTestsTexts(wrapper.name, wrapper.text); } } + else { + if (step.options.samples != null) { + createTestFileFromSamples(project, task, step.options.samples); + } + } task.taskFiles = new HashMap(); // TODO: it looks like we don't need taskFiles as map anymore if (step.options.files != null) { @@ -218,37 +245,39 @@ public class EduAdaptiveStepicConnector { public static Pair checkTask(@NotNull final Project project, @NotNull final Task task) { try { final int attemptId = getAttemptId(project, task, ATTEMPTS_URL); - final Editor editor = StudyUtils.getSelectedEditor(project); - String language = getLanguageString(task, project); - if (editor != null && language != null) { - final StepicWrappers.SubmissionToPostWrapper submissionToPostWrapper = - new StepicWrappers.SubmissionToPostWrapper(String.valueOf(attemptId), "python3", editor.getDocument().getText()); - final HttpPost httpPost = new HttpPost(STEPIC_API_URL + SUBMISSION_URL); - httpPost.setEntity(new StringEntity(new Gson().toJson(submissionToPostWrapper))); - final CloseableHttpClient client = getHttpClient(project); - setHeaders(httpPost, CONTENT_TYPE_APPL_JSON); - final CloseableHttpResponse execute = client.execute(httpPost); - StepicWrappers.ResultSubmissionWrapper wrapper = - new Gson().fromJson(EntityUtils.toString(execute.getEntity()), StepicWrappers.ResultSubmissionWrapper.class); + if (attemptId != -1) { + final Editor editor = StudyUtils.getSelectedEditor(project); + String language = getLanguageString(task, project); + if (editor != null && language != null) { + final StepicWrappers.SubmissionToPostWrapper submissionToPostWrapper = + new StepicWrappers.SubmissionToPostWrapper(String.valueOf(attemptId), language, editor.getDocument().getText()); + final HttpPost httpPost = new HttpPost(STEPIC_API_URL + SUBMISSION_URL); + httpPost.setEntity(new StringEntity(new Gson().toJson(submissionToPostWrapper))); + final CloseableHttpClient client = getHttpClient(project); + setHeaders(httpPost, CONTENT_TYPE_APPL_JSON); + final CloseableHttpResponse execute = client.execute(httpPost); + StepicWrappers.ResultSubmissionWrapper wrapper = + new Gson().fromJson(EntityUtils.toString(execute.getEntity()), StepicWrappers.ResultSubmissionWrapper.class); - final StepicUser user = StudyTaskManager.getInstance(project).getUser(); - if (user != null) { - final int id = user.getId(); - while (wrapper.submissions.length == 1 && wrapper.submissions[0].status.equals("evaluation")) { - TimeUnit.MILLISECONDS.sleep(500); - final URI submissionURI = new URIBuilder(STEPIC_API_URL + SUBMISSION_URL) - .addParameter("attempt", String.valueOf(attemptId)) - .addParameter("order", "desc") - .addParameter("user", String.valueOf(id)) - .build(); - final HttpGet httpGet = new HttpGet(submissionURI); - setHeaders(httpGet, CONTENT_TYPE_APPL_JSON); - final CloseableHttpResponse httpResponse = client.execute(httpGet); - wrapper = new Gson().fromJson(EntityUtils.toString(httpResponse.getEntity()), StepicWrappers.ResultSubmissionWrapper.class); - } - if (wrapper.submissions.length == 1) { - final boolean isSolved = !wrapper.submissions[0].status.equals("wrong"); - return Pair.create(isSolved, wrapper.submissions[0].hint); + final StepicUser user = StudyTaskManager.getInstance(project).getUser(); + if (user != null) { + final int id = user.getId(); + while (wrapper.submissions.length == 1 && wrapper.submissions[0].status.equals("evaluation")) { + TimeUnit.MILLISECONDS.sleep(500); + final URI submissionURI = new URIBuilder(STEPIC_API_URL + SUBMISSION_URL) + .addParameter("attempt", String.valueOf(attemptId)) + .addParameter("order", "desc") + .addParameter("user", String.valueOf(id)) + .build(); + final HttpGet httpGet = new HttpGet(submissionURI); + setHeaders(httpGet, CONTENT_TYPE_APPL_JSON); + final CloseableHttpResponse httpResponse = client.execute(httpGet); + wrapper = new Gson().fromJson(EntityUtils.toString(httpResponse.getEntity()), StepicWrappers.ResultSubmissionWrapper.class); + } + if (wrapper.submissions.length == 1) { + final boolean isSolved = !wrapper.submissions[0].status.equals("wrong"); + return Pair.create(isSolved, wrapper.submissions[0].hint); + } } } } @@ -272,11 +301,12 @@ public class EduAdaptiveStepicConnector { private static String getLanguageString(@NotNull Task task, @NotNull Project project) { final Sdk sdk = StudyUtils.findSdk(task, project); if (sdk != null) { - if (sdk.getVersionString() != null && sdk.getVersionString().startsWith("3")) { - return "python3"; - } - else { - return "python2"; + final String versionString = sdk.getVersionString(); + if (versionString != null ) { + final List versionStringParts = StringUtil.split(versionString, " "); + if (versionStringParts.size() == 2) { + return versionStringParts.get(1).startsWith("2") ? PYTHON2 : PYTHON3; + } } } else { @@ -296,6 +326,15 @@ public class EduAdaptiveStepicConnector { final CloseableHttpResponse httpResponse = client.execute(post); final StepicWrappers.AttemptContainer container = new Gson().fromJson(EntityUtils.toString(httpResponse.getEntity()), StepicWrappers.AttemptContainer.class); - return container.attempts.get(0).id; + return (container.attempts != null && !container.attempts.isEmpty()) ? container.attempts.get(0).id : -1; + } + + private static void createTestFileFromSamples(@NotNull final Project project, + @NotNull final Task task, + @NotNull final List> samples) { + String testText = "from test_helper import check_samples\n\n" + + "if __name__ == '__main__':\n" + + " check_samples(samples=" + new GsonBuilder().create().toJson(samples) + ")"; + task.addTestsTexts("tests.py", testText); } } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java index 8a02678c182c..69f1ea7d514a 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java @@ -47,6 +47,8 @@ public class StepicWrappers { @Expose String title; @Expose List files; @Expose String text; + @Expose List> samples; + public static StepOptions fromTask(final Project project, @NotNull final Task task) { final StepOptions source = new StepOptions(); diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyBrowserWindow.java b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyBrowserWindow.java index fd7a12a5e01e..75d6146c48c9 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyBrowserWindow.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyBrowserWindow.java @@ -35,7 +35,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; -class StudyBrowserWindow extends JFrame { +public class StudyBrowserWindow extends JFrame { private static final Logger LOG = Logger.getInstance(StudyToolWindow.class); private static final String EVENT_TYPE_CLICK = "click"; private JFXPanel myPanel; diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyTestResultsToolWindow.kt b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyTestResultsToolWindow.kt new file mode 100644 index 000000000000..17ec0d915507 --- /dev/null +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyTestResultsToolWindow.kt @@ -0,0 +1,20 @@ +package com.jetbrains.edu.learning.ui + +import com.intellij.openapi.ui.SimpleToolWindowPanel +import java.awt.BorderLayout +import javax.swing.JPanel + + +class StudyTestResultsToolWindow: SimpleToolWindowPanel(false) { + private val studyBrowserWindow = StudyBrowserWindow(false, false) + fun init() { + val panel = JPanel(BorderLayout()) + studyBrowserWindow.loadContent("", null) + panel.add(studyBrowserWindow.panel, BorderLayout.CENTER) + setContent(panel) + } + + fun setText(text: String) { + studyBrowserWindow.loadContent(text, null) + } +} \ No newline at end of file diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyTestResultsToolWindowFactory.kt b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyTestResultsToolWindowFactory.kt new file mode 100644 index 000000000000..53d073ae4bbc --- /dev/null +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyTestResultsToolWindowFactory.kt @@ -0,0 +1,18 @@ +package com.jetbrains.edu.learning.ui + +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindow + + +class StudyTestResultsToolWindowFactory: StudyToolWindowFactory() { + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + val testResultsToolWindow = StudyTestResultsToolWindow() + testResultsToolWindow.init() + + val contentManager = toolWindow.contentManager + val content = contentManager.factory.createContent(testResultsToolWindow, null, false) + contentManager.addContent(content) + } +} + + diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindow.java b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindow.java index 054b2d0a35ab..d8261baece92 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindow.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindow.java @@ -219,5 +219,5 @@ public abstract class StudyToolWindow extends SimpleToolWindowPanel implements D } } - public abstract void setTaskText(String text) ; + public abstract void setTaskText(String text); } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindowFactory.java b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindowFactory.java index db026cea6bad..23385fd077ce 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindowFactory.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindowFactory.java @@ -7,9 +7,9 @@ import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowFactory; import com.intellij.ui.content.Content; import com.intellij.ui.content.ContentManager; -import com.jetbrains.edu.learning.courseFormat.Course; import com.jetbrains.edu.learning.StudyProjectComponent; import com.jetbrains.edu.learning.StudyTaskManager; +import com.jetbrains.edu.learning.courseFormat.Course; import icons.InteractiveLearningIcons; import org.jetbrains.annotations.NotNull; @@ -23,7 +23,6 @@ public class StudyToolWindowFactory implements ToolWindowFactory, DumbAware { StudyTaskManager taskManager = StudyTaskManager.getInstance(project); final Course course = taskManager.getCourse(); if (course != null) { - final StudyToolWindow studyToolWindow; if (StudyProjectComponent.getInstance(project).useJavaFx()) { studyToolWindow = new StudyJavaFxToolWindow(); diff --git a/python/educational-python/resources/fileTemplates/internal/test_helper.py.ft b/python/educational-python/resources/fileTemplates/internal/test_helper.py.ft index d27866c23f01..a0ee26851527 100644 --- a/python/educational-python/resources/fileTemplates/internal/test_helper.py.ft +++ b/python/educational-python/resources/fileTemplates/internal/test_helper.py.ft @@ -9,7 +9,7 @@ def get_file_text(path): return text -def get_file_output(encoding="utf-8", path=sys.argv[-1]): +def get_file_output(encoding="utf-8", path=sys.argv[-1], arg_string=""): """ Returns answer file output :param encoding: to decode output in python3 @@ -18,7 +18,13 @@ def get_file_output(encoding="utf-8", path=sys.argv[-1]): """ import subprocess - proc = subprocess.Popen([sys.executable, path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + proc = subprocess.Popen([sys.executable, path], stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + if arg_string: + for arg in arg_string.split("\n"): + proc.stdin.write(bytearray(str(arg) + "\n", encoding)) + proc.stdin.flush() + return list(map(lambda x: str(x.decode(encoding)), proc.communicate()[0].splitlines())) @@ -31,7 +37,8 @@ def test_file_importable(): parent = os.path.abspath(os.path.join(path, os.pardir)) python_files = [f for f in os.listdir(parent) if os.path.isfile(os.path.join(parent, f)) and f.endswith(".py")] for python_file in python_files: - if python_file == "tests.py": continue + if python_file == "tests.py": + continue check_importable_path(os.path.join(parent, python_file)) return check_importable_path(path) @@ -187,7 +194,25 @@ def get_answer_placeholders(): return windows -def run_common_tests(error_text="Please, reload file and try again"): +def check_samples(samples=()): + """ + Check script output for all samples. Sample is a two element list, where the first is input and + the second is output. + """ + for sample in samples: + if len(sample) == 2: + output = get_file_output(arg_string=str(sample[0])) + if "\n".join(output) != sample[1]: + failed( + "Input:
{}

Expected:
{}

Your result:
{}".format( + sample[0].replace("\n", "
"), sample[1].replace("\n", "
"), "\n".join(output))) + return + set_congratulation_message("All test from samples passed. Now we are checking your solution on Stepic server.") + + passed() + + +def run_common_tests(): test_is_initial_text() test_is_not_empty() test_answer_placeholders_text_deleted() diff --git a/python/educational-python/student-python/src/com/jetbrains/edu/learning/PyStudyCheckAction.java b/python/educational-python/student-python/src/com/jetbrains/edu/learning/PyStudyCheckAction.java index 1b238004fd57..34e59c4027b1 100644 --- a/python/educational-python/student-python/src/com/jetbrains/edu/learning/PyStudyCheckAction.java +++ b/python/educational-python/student-python/src/com/jetbrains/edu/learning/PyStudyCheckAction.java @@ -7,7 +7,6 @@ import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.IdeFocusManager; import com.jetbrains.edu.learning.actions.StudyCheckAction; @@ -15,10 +14,12 @@ import com.jetbrains.edu.learning.actions.StudyRunAction; import com.jetbrains.edu.learning.checker.StudyCheckTask; import com.jetbrains.edu.learning.checker.StudyCheckUtils; import com.jetbrains.edu.learning.checker.StudyTestRunner; +import com.jetbrains.edu.learning.courseFormat.Course; import com.jetbrains.edu.learning.courseFormat.StudyStatus; import com.jetbrains.edu.learning.courseFormat.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; import com.jetbrains.edu.learning.editor.StudyEditor; +import com.jetbrains.edu.learning.ui.StudyToolWindow; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -40,8 +41,8 @@ public class PyStudyCheckAction extends StudyCheckAction { } if (StudyCheckUtils.hasBackgroundProcesses(project)) return; - - if (!runTask(project)) return; + final Course course = StudyTaskManager.getInstance(project).getCourse(); + if (course != null && !course.isAdaptive() && !runTask(project)) return; final Task task = studyState.getTask(); final VirtualFile taskDir = studyState.getTaskDir(); @@ -90,34 +91,37 @@ public class PyStudyCheckAction extends StudyCheckAction { final Process testProcess, final String commandLine) { return new StudyCheckTask(project, studyState, myCheckInProgress, testProcess, commandLine) { - @Override - protected void onTaskFailed(String message) { - ApplicationManager.getApplication().invokeLater(() -> { - if (myTaskDir == null) return; - myTaskManger.setStatus(myTask, StudyStatus.Failed); - for (Map.Entry entry : myTask.getTaskFiles().entrySet()) { - final String name = entry.getKey(); - final TaskFile taskFile = entry.getValue(); - if (taskFile.getAnswerPlaceholders().size() < 2) { - myTaskManger.setStatus(taskFile, StudyStatus.Failed); - continue; - } - CommandProcessor.getInstance().runUndoTransparentAction(() -> ApplicationManager.getApplication().runWriteAction(() -> { - StudyCheckUtils.runSmartTestProcess(myTaskDir, testRunner, name, taskFile, project); - })); - } - StudyCheckUtils.showTestResultPopUp(message, MessageType.ERROR.getPopupBackground(), project); - StudyCheckUtils.navigateToFailedPlaceholder(myStudyState, myTask, myTaskDir, project); - }); + @Override + protected void onTaskFailed(String message) { + ApplicationManager.getApplication().invokeLater(() -> { + if (myTaskDir == null) return; + myTaskManger.setStatus(myTask, StudyStatus.Failed); + for (Map.Entry entry : myTask.getTaskFiles().entrySet()) { + final String name = entry.getKey(); + final TaskFile taskFile = entry.getValue(); + if (taskFile.getAnswerPlaceholders().size() < 2) { + myTaskManger.setStatus(taskFile, StudyStatus.Failed); + continue; } - }; + CommandProcessor.getInstance().runUndoTransparentAction(() -> ApplicationManager.getApplication().runWriteAction(() -> { + StudyCheckUtils.runSmartTestProcess(myTaskDir, testRunner, name, taskFile, project); + })); + } + final StudyToolWindow toolWindow = StudyUtils.getStudyToolWindow(project); + if (toolWindow != null) { + StudyCheckUtils.showTestResults(project, message); + StudyCheckUtils.navigateToFailedPlaceholder(myStudyState, myTask, myTaskDir, project); + } + }); + } + }; } @Nullable private static VirtualFile getTaskVirtualFile(@NotNull final StudyState studyState, - @NotNull final Task task, - @NotNull final VirtualFile taskDir) { + @NotNull final Task task, + @NotNull final VirtualFile taskDir) { VirtualFile taskVirtualFile = studyState.getVirtualFile(); for (Map.Entry entry : task.getTaskFiles().entrySet()) { String name = entry.getKey(); @@ -131,7 +135,7 @@ public class PyStudyCheckAction extends StudyCheckAction { } return taskVirtualFile; } - + @NotNull @Override public String getActionId() {