From 7e38df13ab16a17931776b2721401cea20460e0f Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Mon, 7 Apr 2025 00:25:50 +0200 Subject: [PATCH 001/105] Add HaskellDynamicClusteringTestManager view and controller - implementation missing, marked with TODO@CHW and will be similar as in docker test --- .../HaskellDynamicClusteringTestManager.java | 54 ++++++++++++++++ .../servlets/controller/TestManager.java | 3 + ...skellDynamicClusteringTestManagerView.java | 62 +++++++++++++++++++ .../view/TestManagerAddTestFormView.java | 43 +++++++++++++ 4 files changed, 162 insertions(+) create mode 100644 src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellDynamicClusteringTestManager.java create mode 100644 src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellDynamicClusteringTestManagerView.java diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellDynamicClusteringTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellDynamicClusteringTestManager.java new file mode 100644 index 000000000..c4e660fd4 --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellDynamicClusteringTestManager.java @@ -0,0 +1,54 @@ +/* + * Copyright 2025 Sven Strickroth + * Copyright 2025 Christian Wagner + * + * This file is part of the GATE. + * + * GATE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * GATE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GATE. If not, see . + */ + +package de.tuclausthal.submissioninterface.servlets.controller; + +import de.tuclausthal.submissioninterface.servlets.GATEController; +import de.tuclausthal.submissioninterface.servlets.RequestAdapter; +import de.tuclausthal.submissioninterface.servlets.view.HaskellDynamicClusteringTestManagerView; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.hibernate.Session; + +import java.io.IOException; +import java.io.Serial; + +/** + * Controller-Servlet for clustering haskell submissions based on common errors (dynamic/runtime analysis) + * + * @author Christian Wagner + */ +@GATEController +public class HaskellDynamicClusteringTestManager extends HttpServlet { + @Serial + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + Session session = RequestAdapter.getSession(request); + + // TODO@CHW missing implementation + + request.setAttribute("testattribute", "somevalue"); + getServletContext().getNamedDispatcher(HaskellDynamicClusteringTestManagerView.class.getSimpleName()).forward(request, response); + + } +} diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java index ef5b25bd7..94852c0d1 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java @@ -272,6 +272,9 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr test.setGiveDetailsToStudents(request.getParameter("giveDetailsToStudents") != null); session.getTransaction().commit(); response.sendRedirect(Util.generateRedirectURL(TaskManager.class.getSimpleName() + "?action=editTask&lecture=" + task.getTaskGroup().getLecture().getId() + "&taskid=" + task.getTaskid(), response)); + } else if ("saveNewTest".equals(request.getParameter("action")) && "haskellDynamicClustering".equals(request.getParameter("type"))) { + // TODO@CHW: implement similar as docker test + response.sendRedirect(Util.generateRedirectURL(HaskellDynamicClusteringTestManager.class.getSimpleName(), response)); } else if ("deleteTest".equals(request.getParameter("action"))) { TestDAOIf testDAO = DAOFactory.TestDAOIf(session); session.beginTransaction(); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellDynamicClusteringTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellDynamicClusteringTestManagerView.java new file mode 100644 index 000000000..980667f85 --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellDynamicClusteringTestManagerView.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 Sven Strickroth + * Copyright 2025 Christian Wagner + * + * This file is part of the GATE. + * + * GATE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * GATE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GATE. If not, see . + */ + +package de.tuclausthal.submissioninterface.servlets.view; + + +import de.tuclausthal.submissioninterface.servlets.GATEView; +import de.tuclausthal.submissioninterface.template.Template; +import de.tuclausthal.submissioninterface.template.TemplateFactory; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Serial; + +import static de.tuclausthal.submissioninterface.servlets.view.TestManagerAddTestFormView.printHaskellDynamicClusteringTestForm; + +/** + * View-Servlet for clustering haskell submissions based on common errors (dynamic/runtime analysis) + * + * @author Christian Wagner + */ +@GATEView +public class HaskellDynamicClusteringTestManagerView extends HttpServlet { + @Serial + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + Template template = TemplateFactory.getTemplate(request, response); + + template.addKeepAlive(); + template.printTemplateHeader("Haskell dynamisches Error Clustering bearbeiten"); + + PrintWriter out = response.getWriter(); + out.println("..."); + // printHaskellDynamicClusteringTestForm(); // TODO@CHW needs task as parameter, look at Docker test implementation + + // TODO@CHW implement HTML to setup the haskell dynamic clustering in detail + + template.printTemplateFooter(); + } +} diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java index 4a061f1f3..56fca8ae5 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java @@ -308,6 +308,49 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println(""); out.println(""); out.println(""); + + if (Files.isRegularFile(Path.of(DockerTest.SAFE_DOCKER_SCRIPT))) { + printHaskellDynamicClusteringTestForm(out, response, task); + } else { + out.println("

(Das dynamische Error Clustering für Haskell Abgaben ist nicht verfügbar, da /usr/local/bin/safe-docker nicht gefunden wurde.)

"); + } + template.printTemplateFooter(); } + + public static void printHaskellDynamicClusteringTestForm(PrintWriter out, HttpServletResponse response, Task task) { + out.println("

Haskell dynamisches Error Clustering

"); + + out.println("
"); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.print(""); + out.println(""); + out.println("
Titel:
Tutorentest:
# ausführbar für Studierende:
Studierenden Test-Details anzeigen:
Weitere Einstellungen auf zweiter Seite...
Abbrechen
"); + out.println("
"); + } } From 051b48ac57dd47077e72c8804316d88f9012b9ee Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 13 Apr 2025 23:49:40 +0200 Subject: [PATCH 002/105] Add new HaskellRuntimeTest in testframework and in datamodel - extends DockerTest --- .../datamodel/HaskellRuntimeTest.java | 38 +++++++++++++++++++ .../tests/impl/HaskellRuntimeTest.java | 31 +++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTest.java create mode 100644 src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellRuntimeTest.java diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTest.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTest.java new file mode 100644 index 000000000..eec49224d --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 Sven Strickroth + * Copyright 2025 Christian Wagner + * + * This file is part of the GATE. + * + * GATE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * GATE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GATE. If not, see . + */ + +package de.tuclausthal.submissioninterface.persistence.datamodel; + +import de.tuclausthal.submissioninterface.testframework.tests.AbstractTest; +import jakarta.persistence.Entity; +import jakarta.persistence.Transient; + + +/** + * Haskell runtime test, extends the DockerTest by automatically generating haskell testcases and by clustering + * @author Christian Wagner + */ +@Entity +public class HaskellRuntimeTest extends DockerTest { + @Override + @Transient + public AbstractTest getTestImpl() { + return new de.tuclausthal.submissioninterface.testframework.tests.impl.HaskellRuntimeTest(this); + } +} diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellRuntimeTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellRuntimeTest.java new file mode 100644 index 000000000..e8ed0b0bd --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellRuntimeTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2025 Sven Strickroth + * Copyright 2025 Christian Wagner + * + * This file is part of the GATE. + * + * GATE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * GATE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GATE. If not, see . + */ + +package de.tuclausthal.submissioninterface.testframework.tests.impl; + +/** + * @author Christian Wagner + */ +public class HaskellRuntimeTest extends DockerTest { + public HaskellRuntimeTest(final de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest test) { + super(test); + } + + // TODO@CHW: Override: public void performTest(final Path basePath, final Path submissionPath, final TestExecutorTestResult testResult) throws Exception { +} From bab09b991fdfc40e51872273fedfc62a0caac359 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Mon, 14 Apr 2025 00:09:37 +0200 Subject: [PATCH 003/105] Rename HaskellDynamicClusteringTest to HaskellRuntimeTest --- ...eringTestManager.java => HaskellRuntimeTestManager.java} | 6 +++--- .../servlets/controller/TestManager.java | 4 ++-- ...tManagerView.java => HaskellRuntimeTestManagerView.java} | 4 ++-- .../servlets/view/TestManagerAddTestFormView.java | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) rename src/main/java/de/tuclausthal/submissioninterface/servlets/controller/{HaskellDynamicClusteringTestManager.java => HaskellRuntimeTestManager.java} (84%) rename src/main/java/de/tuclausthal/submissioninterface/servlets/view/{HaskellDynamicClusteringTestManagerView.java => HaskellRuntimeTestManagerView.java} (94%) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellDynamicClusteringTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java similarity index 84% rename from src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellDynamicClusteringTestManager.java rename to src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index c4e660fd4..05490af55 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellDynamicClusteringTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -21,7 +21,7 @@ import de.tuclausthal.submissioninterface.servlets.GATEController; import de.tuclausthal.submissioninterface.servlets.RequestAdapter; -import de.tuclausthal.submissioninterface.servlets.view.HaskellDynamicClusteringTestManagerView; +import de.tuclausthal.submissioninterface.servlets.view.HaskellRuntimeTestManagerView; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -37,7 +37,7 @@ * @author Christian Wagner */ @GATEController -public class HaskellDynamicClusteringTestManager extends HttpServlet { +public class HaskellRuntimeTestManager extends HttpServlet { @Serial private static final long serialVersionUID = 1L; @@ -48,7 +48,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro // TODO@CHW missing implementation request.setAttribute("testattribute", "somevalue"); - getServletContext().getNamedDispatcher(HaskellDynamicClusteringTestManagerView.class.getSimpleName()).forward(request, response); + getServletContext().getNamedDispatcher(HaskellRuntimeTestManagerView.class.getSimpleName()).forward(request, response); } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java index 94852c0d1..dd9bb735a 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java @@ -272,9 +272,9 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr test.setGiveDetailsToStudents(request.getParameter("giveDetailsToStudents") != null); session.getTransaction().commit(); response.sendRedirect(Util.generateRedirectURL(TaskManager.class.getSimpleName() + "?action=editTask&lecture=" + task.getTaskGroup().getLecture().getId() + "&taskid=" + task.getTaskid(), response)); - } else if ("saveNewTest".equals(request.getParameter("action")) && "haskellDynamicClustering".equals(request.getParameter("type"))) { + } else if ("saveNewTest".equals(request.getParameter("action")) && "haskellruntime".equals(request.getParameter("type"))) { // TODO@CHW: implement similar as docker test - response.sendRedirect(Util.generateRedirectURL(HaskellDynamicClusteringTestManager.class.getSimpleName(), response)); + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName(), response)); } else if ("deleteTest".equals(request.getParameter("action"))) { TestDAOIf testDAO = DAOFactory.TestDAOIf(session); session.beginTransaction(); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellDynamicClusteringTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java similarity index 94% rename from src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellDynamicClusteringTestManagerView.java rename to src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 980667f85..27c736dbe 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellDynamicClusteringTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -32,7 +32,7 @@ import java.io.PrintWriter; import java.io.Serial; -import static de.tuclausthal.submissioninterface.servlets.view.TestManagerAddTestFormView.printHaskellDynamicClusteringTestForm; +import static de.tuclausthal.submissioninterface.servlets.view.TestManagerAddTestFormView.printHaskellRuntimeTestForm; /** * View-Servlet for clustering haskell submissions based on common errors (dynamic/runtime analysis) @@ -40,7 +40,7 @@ * @author Christian Wagner */ @GATEView -public class HaskellDynamicClusteringTestManagerView extends HttpServlet { +public class HaskellRuntimeTestManagerView extends HttpServlet { @Serial private static final long serialVersionUID = 1L; diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java index 56fca8ae5..680b83d7c 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java @@ -310,7 +310,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println(""); if (Files.isRegularFile(Path.of(DockerTest.SAFE_DOCKER_SCRIPT))) { - printHaskellDynamicClusteringTestForm(out, response, task); + printHaskellRuntimeTestForm(out, response, task); } else { out.println("

(Das dynamische Error Clustering für Haskell Abgaben ist nicht verfügbar, da /usr/local/bin/safe-docker nicht gefunden wurde.)

"); } @@ -318,13 +318,13 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro template.printTemplateFooter(); } - public static void printHaskellDynamicClusteringTestForm(PrintWriter out, HttpServletResponse response, Task task) { + public static void printHaskellRuntimeTestForm(PrintWriter out, HttpServletResponse response, Task task) { out.println("

Haskell dynamisches Error Clustering

"); out.println("
"); out.println(""); out.println(""); - out.println(""); + out.println(""); out.println(""); out.println(""); out.println(""); From 580aafc20337fcda151128544e097ac601088057 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Mon, 14 Apr 2025 01:02:05 +0200 Subject: [PATCH 004/105] User can now create haskell runtime tests --- .../persistence/dao/TestDAOIf.java | 3 +++ .../persistence/dao/impl/TestDAO.java | 10 ++++++++ .../controller/HaskellRuntimeTestManager.java | 2 +- .../servlets/controller/TestManager.java | 23 +++++++++++++++++-- .../view/TestManagerAddTestFormView.java | 16 ++++++++++++- 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/TestDAOIf.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/TestDAOIf.java index c59cd7d04..fa93a2928 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/TestDAOIf.java +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/TestDAOIf.java @@ -24,6 +24,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CommentsMetricTest; import de.tuclausthal.submissioninterface.persistence.datamodel.CompileTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JUnitTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Task; @@ -62,6 +63,8 @@ public interface TestDAOIf { DockerTest createDockerTest(Task task); + HaskellRuntimeTest createHaskellRuntimeTest(Task task); + ChecklistTest createChecklistTest(Task task); /** diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/impl/TestDAO.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/impl/TestDAO.java index b657d1f6f..d11ed639b 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/impl/TestDAO.java +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/impl/TestDAO.java @@ -35,6 +35,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CommentsMetricTest; import de.tuclausthal.submissioninterface.persistence.datamodel.CompileTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JUnitTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Task; @@ -162,6 +163,15 @@ public DockerTest createDockerTest(Task task) { return test; } + @Override + public HaskellRuntimeTest createHaskellRuntimeTest(Task task) { + Session session = getSession(); + HaskellRuntimeTest test = new HaskellRuntimeTest(); + test.setTask(task); + session.persist(test); + return test; + } + @Override public ChecklistTest createChecklistTest(Task task) { Session session = getSession(); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 05490af55..cb188c20c 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -45,7 +45,7 @@ public class HaskellRuntimeTestManager extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { Session session = RequestAdapter.getSession(request); - // TODO@CHW missing implementation + // TODO@CHW missing implementation, request redirected by TestManager request.setAttribute("testattribute", "somevalue"); getServletContext().getNamedDispatcher(HaskellRuntimeTestManagerView.class.getSimpleName()).forward(request, response); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java index dd9bb735a..ff10f13d7 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java @@ -41,6 +41,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CommentsMetricTest; import de.tuclausthal.submissioninterface.persistence.datamodel.CompileTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JUnitTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; @@ -273,8 +274,26 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr session.getTransaction().commit(); response.sendRedirect(Util.generateRedirectURL(TaskManager.class.getSimpleName() + "?action=editTask&lecture=" + task.getTaskGroup().getLecture().getId() + "&taskid=" + task.getTaskid(), response)); } else if ("saveNewTest".equals(request.getParameter("action")) && "haskellruntime".equals(request.getParameter("type"))) { - // TODO@CHW: implement similar as docker test - response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName(), response)); + // TODO@CHW: make sure all parameters accessed by request.getParameter() are defined in HaskellRuntimeTestManagerView + + session.beginTransaction(); + TestDAOIf testDAO = DAOFactory.TestDAOIf(session); + + HaskellRuntimeTest test = testDAO.createHaskellRuntimeTest(task); + test.setTimesRunnableByStudents(Util.parseInteger(request.getParameter("timesRunnableByStudents"), 0)); + test.setForTutors(request.getParameter("tutortest") != null); + test.setTestTitle(request.getParameter("title")); + test.setTestDescription(request.getParameter("description")); + test.setTimeout(Util.parseInteger(request.getParameter("timeout"), 15)); + test.setGiveDetailsToStudents(request.getParameter("giveDetailsToStudents") != null); + String preparationCode = request.getParameter("preparationcode"); + if (preparationCode == null) preparationCode = ""; + test.setPreparationShellCode( + preparationCode.replaceAll("\r\n", "\n") + ); + + session.getTransaction().commit(); + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + test.getId(), response)); } else if ("deleteTest".equals(request.getParameter("action"))) { TestDAOIf testDAO = DAOFactory.TestDAOIf(session); session.beginTransaction(); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java index 680b83d7c..8419d88d3 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java @@ -331,6 +331,10 @@ public static void printHaskellRuntimeTestForm(PrintWriter out, HttpServletRespo out.println(""); out.println(""); out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); out.println(""); out.println(""); out.println(""); @@ -339,15 +343,25 @@ public static void printHaskellRuntimeTestForm(PrintWriter out, HttpServletRespo out.println(""); out.println(""); out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); out.println(""); out.println(""); out.println(""); out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); out.println(""); out.println(""); out.println(""); out.print(""); out.println(""); out.println("
Titel:
Beschreibung:
Tutorentest:
Timeout (s):
Studierenden Test-Details anzeigen:
Preparation Code:
Weitere Einstellungen auf zweiter Seite...
Abbrechen
"); From b2d66ff6426e5a007dc6c13731caaf033e84c581 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Mon, 14 Apr 2025 08:24:30 +0200 Subject: [PATCH 005/105] Implement view and controller for haskell runtime test - view: HaskellRuntimeTestManagerView (based on DockerTestManagerOverView) - controller: HaskellRuntimeTestManager (based on DockerTestManager) --- .../controller/HaskellRuntimeTestManager.java | 112 +++++++++++- .../view/HaskellRuntimeTestManagerView.java | 160 +++++++++++++++++- 2 files changed, 265 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index cb188c20c..0bbf8a3d0 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -19,20 +19,33 @@ package de.tuclausthal.submissioninterface.servlets.controller; +import de.tuclausthal.submissioninterface.persistence.dao.DAOFactory; +import de.tuclausthal.submissioninterface.persistence.dao.ParticipationDAOIf; +import de.tuclausthal.submissioninterface.persistence.dao.TestDAOIf; +import de.tuclausthal.submissioninterface.persistence.datamodel.Test; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTestStep; +import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; +import de.tuclausthal.submissioninterface.persistence.datamodel.ParticipationRole; import de.tuclausthal.submissioninterface.servlets.GATEController; import de.tuclausthal.submissioninterface.servlets.RequestAdapter; import de.tuclausthal.submissioninterface.servlets.view.HaskellRuntimeTestManagerView; +import de.tuclausthal.submissioninterface.servlets.view.MessageView; +import de.tuclausthal.submissioninterface.util.Util; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.hibernate.Session; +import org.hibernate.Transaction; import java.io.IOException; import java.io.Serial; +import java.util.Objects; /** - * Controller-Servlet for clustering haskell submissions based on common errors (dynamic/runtime analysis) + * Controller-Servlet for clustering haskell submissions based on common errors (dynamic/runtime analysis). + * This servlet allows advisors to manage (add, edit, remove) test steps. * * @author Christian Wagner */ @@ -43,12 +56,105 @@ public class HaskellRuntimeTestManager extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + // similar code in DockerTestManager Session session = RequestAdapter.getSession(request); - // TODO@CHW missing implementation, request redirected by TestManager + TestDAOIf testDAOIf = DAOFactory.TestDAOIf(session); + Test test = testDAOIf.getTest(Util.parseInteger(request.getParameter("testid"), 0)); + if (!(test instanceof HaskellRuntimeTest haskellRuntimeTest)) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + request.setAttribute("title", "Test nicht gefunden"); + getServletContext().getNamedDispatcher(MessageView.class.getSimpleName()).forward(request, response); + return; + } - request.setAttribute("testattribute", "somevalue"); + ParticipationDAOIf participationDAO = DAOFactory.ParticipationDAOIf(session); + Participation participation = participationDAO.getParticipation(RequestAdapter.getUser(request), haskellRuntimeTest.getTask().getTaskGroup().getLecture()); + if (participation == null || participation.getRoleType() != ParticipationRole.ADVISOR) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "insufficient rights"); + return; + } + + request.setAttribute("test", haskellRuntimeTest); getServletContext().getNamedDispatcher(HaskellRuntimeTestManagerView.class.getSimpleName()).forward(request, response); + } + + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + // similar code in DockerTestManager + Session session = RequestAdapter.getSession(request); + TestDAOIf testDAOIf = DAOFactory.TestDAOIf(session); + Test test = testDAOIf.getTest(Util.parseInteger(request.getParameter("testid"), 0)); + if (!(test instanceof HaskellRuntimeTest haskellRuntimeTest)) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + request.setAttribute("title", "Test nicht gefunden"); + getServletContext().getNamedDispatcher(MessageView.class.getSimpleName()).forward(request, response); + return; + } + + ParticipationDAOIf participationDAO = DAOFactory.ParticipationDAOIf(session); + Participation participation = participationDAO.getParticipation(RequestAdapter.getUser(request), haskellRuntimeTest.getTask().getTaskGroup().getLecture()); + if (participation == null || participation.getRoleType() != ParticipationRole.ADVISOR) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "insufficient rights"); + return; + } + + if ("edittest".equals(request.getParameter("action"))) { + Transaction tx = session.beginTransaction(); + haskellRuntimeTest.setTestTitle(request.getParameter("title")); + haskellRuntimeTest.setForTutors(request.getParameter("tutortest") != null); + haskellRuntimeTest.setTimesRunnableByStudents(Util.parseInteger(request.getParameter("timesRunnableByStudents"), 0)); + haskellRuntimeTest.setGiveDetailsToStudents(request.getParameter("giveDetailsToStudents") != null); + haskellRuntimeTest.setPreparationShellCode(request.getParameter("preparationcode").replaceAll("\r\n", "\n")); // TODO@CHW: can getParameter("preparationcode") be null? + tx.commit(); + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + return; + } else if ("addNewStep".equals(request.getParameter("action"))) { + String title = request.getParameter("title"); + String testCode = request.getParameter("testcode").replaceAll("\r\n", "\n"); + String expect = request.getParameter("expect").replaceAll("\r\n", "\n"); + DockerTestStep newStep = new DockerTestStep(haskellRuntimeTest, title, testCode, expect); + Transaction tx = session.beginTransaction(); + session.persist(newStep); + tx.commit(); + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + return; + } else if ("updateStep".equals(request.getParameter("action"))) { + DockerTestStep step = null; + for (DockerTestStep istep : haskellRuntimeTest.getTestSteps()) { + if (istep.getTeststepid() == Util.parseInteger(request.getParameter("teststepid"), -1)) { + step = istep; + break; + } + } + if (step != null) { + Transaction tx = session.beginTransaction(); + String title = request.getParameter("title"); + String testCode = request.getParameter("testcode").replaceAll("\r\n", "\n"); + step.setTitle(title); + step.setTestcode(testCode); + step.setExpect(Objects.toString(request.getParameter("expect"), "").replaceAll("\r\n", "\n")); + tx.commit(); + } + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + return; + } else if ("deleteStep".equals(request.getParameter("action"))) { + DockerTestStep step = null; + for (DockerTestStep istep : haskellRuntimeTest.getTestSteps()) { + if (istep.getTeststepid() == Util.parseInteger(request.getParameter("teststepid"), -1)) { + step = istep; + break; + } + } + if (step != null) { + Transaction tx = session.beginTransaction(); + session.remove(step); + tx.commit(); + } + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + return; + } + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "invalid request"); } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 27c736dbe..9fedc58bb 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -20,9 +20,14 @@ package de.tuclausthal.submissioninterface.servlets.view; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTestStep; import de.tuclausthal.submissioninterface.servlets.GATEView; +import de.tuclausthal.submissioninterface.servlets.controller.HaskellRuntimeTestManager; +import de.tuclausthal.submissioninterface.servlets.controller.TaskManager; import de.tuclausthal.submissioninterface.template.Template; import de.tuclausthal.submissioninterface.template.TemplateFactory; +import de.tuclausthal.submissioninterface.util.Util; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -48,14 +53,161 @@ public class HaskellRuntimeTestManagerView extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { Template template = TemplateFactory.getTemplate(request, response); + HaskellRuntimeTest test = (HaskellRuntimeTest) request.getAttribute("test"); + template.addKeepAlive(); - template.printTemplateHeader("Haskell dynamisches Error Clustering bearbeiten"); + template.printEditTaskTemplateHeader("Haskell Runtime Test bearbeiten", test.getTask()); PrintWriter out = response.getWriter(); - out.println("..."); - // printHaskellDynamicClusteringTestForm(); // TODO@CHW needs task as parameter, look at Docker test implementation - // TODO@CHW implement HTML to setup the haskell dynamic clustering in detail + // similar code in TestManagerAddTestFormView + // TODO@CHW: can this code duplicate be avoided using printHaskellRuntimeTestForm()? + // TODO@CHW: compare with printHaskellRuntimeTestForm() and add remaining fields + out.println("

" + Util.escapeHTML(test.getTestTitle()) + "

"); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.print(""); + out.println(""); + out.println("
Titel:
Tutorentest: (Ergebnis wird den TutorInnen zur Korrektur angezeigt)
# ausführbar für Studierende:
Studierenden Test-Details anzeigen:
Preparation Code:
Abbrechen
"); + out.println(""); + + out.println("
"); + out.println("

Testschritte

"); + + for (DockerTestStep step : test.getTestSteps()) { + out.println("

" + Util.escapeHTML(step.getTitle()) + "

"); + out.println("
"); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println("
Titel:
Testcode:
Erwartete Ausgabe:
"); + out.print("Löschen
"); + out.println("
"); + } + + out.println("
"); + + out.println("

Neuer Test-Schritt (?)

"); + + out.println("
Hilfe:
"); + out.println("

Diese Art von Test erlaubt es beliebige einfache Ausgabe-Tests zu definieren. Mit dem Preparation-Code können vorbereitende Schritte als Bash-Skript programmiert werden. Ist dieser Schritt erfolgreich, werden die einzelnen Testschritte nacheinander aufgerufen, wobei für jeden Testschritt die Ausgabe auf STDOUT mit einem erwartetem Wert überprüft werden.

"); + out.println("

Preparation-Code z. B.:

"); + /* @formatter:off */ + out.println("

Test-Schritt-Definition z. B.:
"+ + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
Titel:
Testcode:
Erwartete Ausgabe:

"); + out.println("

Ausgabe bei Testdurchführung z. B.:
" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
TestErwartetErhaltenOK?
Eingabe \"gut\"
Geben Sie ein hochdeutsches Wort ein:\n"
+                + "\"guad\"
Geben Sie ein hochdeutsches Wort ein:\n"
+                + "\"guad\"" +
+                "
ja
Leere Eingabe
ja

"); + /* @formatter:on */ + out.println("

Die erwartete Ausgabe und tatsächliche Ausgabe wird getrimmt und hinsichtlich der Zeilenenden auf \"\\n\" normalisiert und mittels exaktem Stringvergleich verglichen. Im Testcode kann beliebiger Bash-Code verwendet werden. In der Umgebung ist per Default \"set -e\" gesetzt, so dass das Skript nach einem nicht behandelten Fehler sofort abgebrochen wird.

"); + out.println("

Wird das Bash-Skript vorzeitig beendet, erhalten die Studierenden die Ausgabe \"Nicht alle Tests wurden durchlaufen. Das Program wurde nicht ordentlich beendet.\", wobei die Tabelle alle bisherigen zzgl. den zuletzt ausgeführten Test zeigt (die Spalte \"Erhalten\" ist dann ggf. leer). Bricht das Testskript nach der Preparation-Code-Phase ab, wird dies den Studierenden als Laufzeitfehler angezeigt und der Inhalt von STDERR seit dem Beginn des Testschritts bereitgestellt. Bricht das Testskript beim Preparationcode mit dem ExitCode 15 ab, wird den Studierenden nur der Text \"Der zu testende Code ist syntaktisch nicht korrekt und kann daher nicht getestet werden.\" angezeigt, ansonsten, dass ein Syntaxfehler aufgetreten ist inkl. Inhalt von STDERR.

"); + out.println("

Diese Art von Test ist für einfache Ausgabetests ausgerichtet, es können aber auch approximative Tests oder nahezu beliebige Überprüfungen durchgefühert werden, z.B. erwartet \"True\" und Testcode \"ghci -e \"pi_approx 6 < 3.0 && pi_approx 6 > 2.99\" pi_approx.hs\".

"); + out.println("
"); + + out.println("
"); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.print(""); + out.println(""); + out.println("
Titel:
Testcode:
Erwartete Ausgabe:
Abbrechen
"); + out.println("
"); template.printTemplateFooter(); } From f34785538209cfa88cc41409bb82b5aadc521d11 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Mon, 14 Apr 2025 08:56:42 +0200 Subject: [PATCH 006/105] Add basic groupHaskellRuntimeTestResults() --- .../testanalyzer/CommonErrorAnalyzer.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java index 51e2224bb..e427f3dbf 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java @@ -34,6 +34,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CompileTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JUnitTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Test; import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; @@ -58,6 +59,8 @@ public void runAnalysis(final TestResult testResult) { groupCompilerTestResults((CompileTest) test, testResult); } else if (test instanceof JavaAdvancedIOTest) { groupJavaIOTestResults((JavaAdvancedIOTest) test, testResult); + } else if (test instanceof HaskellRuntimeTest haskellRuntimeTest) { + groupHaskellRuntimeTestResults(haskellRuntimeTest, testResult); } else if (test instanceof JUnitTest) { groupJUnitTestResults((JUnitTest) test, testResult); } @@ -151,6 +154,22 @@ private static String[] getJavaIOKeyStr(final JavaAdvancedIOTest test, final Tes return new String[] { stepsStr, keyStr }; } + private void groupHaskellRuntimeTestResults(final HaskellRuntimeTest test, final TestResult testResult) { + if (testResult.getPassedTest()) { + return; + } + + // TODO@CHW implement correctly + + final JsonObject testOutputJson = Json.createReader(new StringReader(testResult.getTestOutput())).readObject(); + + if (testOutputJson.containsKey("exitCode") && testOutputJson.getInt("exitCode") != 0) { + // TODO: maybe modify and use groupTestResultToCommonErrors() + // TODO: exitCode != 0 on compile error and on runtime error + bindCommonError(testResult, "Failed", "Failed", null); + } + } + private void groupJUnitTestResults(JUnitTest test, final TestResult testResult) { for (final TestResult otherTestResult : testResult.getSubmission().getTestResults()) { if (!otherTestResult.getPassedTest() && otherTestResult.getTest() instanceof CompileTest) { From 35d7c95a7faa657464ecb3fb9aa80717a74bf4d5 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Sat, 12 Apr 2025 22:06:33 +0200 Subject: [PATCH 007/105] implement new Test for syntax tests and implement first regex clsutering --- .../servlets/view/ShowTaskTutorView.java | 1 + .../view/TestManagerAddTestFormView.java | 41 +++++++++++++++++++ .../testanalyzer/CommonErrorAnalyzer.java | 29 +++++++++++++ 3 files changed, 71 insertions(+) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorView.java index 838472d16..7dfdefeaa 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorView.java @@ -151,6 +151,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro } out.println(""); + if (participation.getRoleType() == ParticipationRole.ADVISOR) { out.println("

"); } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java index 8419d88d3..f74648321 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java @@ -158,10 +158,48 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println(""); out.println(""); out.println(""); + + out.println("

Haskell Syntax Test

"); + out.println("
"); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.print(""); + out.println(""); + out.println("
Titel:
Tutorentest:
# ausführbar für Studierende:
Studierenden Test-Details anzeigen:
Preparation Code:
Weitere Einstellungen auf zweiter Seite...
Abbrechen
"); + out.println("
"); } else { out.println("

(Docker-Tests sind nicht verfügbar, da /usr/local/bin/safe-docker nicht gefunden wurde.)

"); } + // similar code in ChecklistTestManagerView out.println("

Checklist Test

"); out.println("
"); @@ -316,6 +354,9 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro } template.printTemplateFooter(); + + + } public static void printHaskellRuntimeTestForm(PrintWriter out, HttpServletResponse response, Task task) { diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java index e427f3dbf..7b8e59eb5 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java @@ -37,6 +37,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Test; import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; +import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; public class CommonErrorAnalyzer { //from Literatur @@ -64,6 +65,9 @@ public void runAnalysis(final TestResult testResult) { } else if (test instanceof JUnitTest) { groupJUnitTestResults((JUnitTest) test, testResult); } + else if (test instanceof DockerTest) { + groupDockerTestResults((DockerTest) test, testResult); + } } private void groupCompilerTestResults(final CompileTest test, final TestResult testResult) { @@ -293,4 +297,29 @@ private boolean assignOneTestResultToErrorTypes(final TestResult testResult, fin } return foundErrorGroup; } + + private void groupDockerTestResults(final DockerTest test, final TestResult testResult) { + if (testResult.getPassedTest()) { + return; + } + + JsonObject testOutputJson = Json.createReader(new StringReader(testResult.getTestOutput())).readObject(); + String stderr = testOutputJson.containsKey("stderr") ? testOutputJson.getString("stderr") : ""; + + String keyStr = ""; + + // Optional: zusätzliche Hinweise sammeln + if (testOutputJson.containsKey("exitCode")) { + keyStr += "ExitCode: " + testOutputJson.getInt("exitCode") + " "; + } + if (testOutputJson.containsKey("time-exceeded") && testOutputJson.getBoolean("time-exceeded")) { + keyStr += "Timeout "; + } + if (testOutputJson.containsKey("missing-tests")) { + keyStr += "Missing tests "; + } + + groupTestResultToCommonErrors(testResult, stderr, keyStr); + } + } From d8c08befeb843c3e9049d4e62b16c273cdc21d8a Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Sun, 13 Apr 2025 20:22:25 +0200 Subject: [PATCH 008/105] improve the new Haskell Syntax Test so it is like the Java Compile Test Conflicts during rebase: src/main/java/de/tuclausthal/submissioninterface/persistence/dao/TestDAOIf.java src/main/java/de/tuclausthal/submissioninterface/persistence/dao/impl/TestDAO.java src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java --- .../persistence/dao/TestDAOIf.java | 3 + .../persistence/dao/impl/TestDAO.java | 10 ++ .../datamodel/HaskellSyntaxTest.java | 35 +++++ .../servlets/controller/TestManager.java | 23 ++++ .../view/PerformStudentTestResultView.java | 3 + .../servlets/view/PerformTestResultView.java | 3 + .../view/TestManagerAddTestFormView.java | 11 +- .../tests/impl/HaskellSyntaxTest.java | 125 ++++++++++++++++++ 8 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java create mode 100644 src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/TestDAOIf.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/TestDAOIf.java index fa93a2928..0fc65e40f 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/TestDAOIf.java +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/TestDAOIf.java @@ -24,6 +24,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CommentsMetricTest; import de.tuclausthal.submissioninterface.persistence.datamodel.CompileTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JUnitTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; @@ -63,6 +64,8 @@ public interface TestDAOIf { DockerTest createDockerTest(Task task); + HaskellSyntaxTest createHaskellSyntaxTest(Task task); + HaskellRuntimeTest createHaskellRuntimeTest(Task task); ChecklistTest createChecklistTest(Task task); diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/impl/TestDAO.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/impl/TestDAO.java index d11ed639b..e3a30415a 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/impl/TestDAO.java +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/impl/TestDAO.java @@ -35,6 +35,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CommentsMetricTest; import de.tuclausthal.submissioninterface.persistence.datamodel.CompileTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JUnitTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; @@ -163,6 +164,15 @@ public DockerTest createDockerTest(Task task) { return test; } + @Override + public HaskellSyntaxTest createHaskellSyntaxTest(Task task) { + Session session = getSession(); + HaskellSyntaxTest test = new HaskellSyntaxTest(); + test.setTask(task); + session.persist(test); + return test; + } + @Override public HaskellRuntimeTest createHaskellRuntimeTest(Task task) { Session session = getSession(); diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java new file mode 100644 index 000000000..3d7eaaf3b --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java @@ -0,0 +1,35 @@ +package de.tuclausthal.submissioninterface.persistence.datamodel; + +import java.lang.invoke.MethodHandles; + +import jakarta.persistence.Entity; +import jakarta.persistence.Transient; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import de.tuclausthal.submissioninterface.testframework.tests.AbstractTest; + +@Entity +public class HaskellSyntaxTest extends Test { + private static final long serialVersionUID = 1L; + + @Override + @Transient + @JsonIgnore + public AbstractTest getTestImpl() { + return new de.tuclausthal.submissioninterface.testframework.tests.impl.HaskellSyntaxTest(this); + } + + @Override + @Transient + public boolean TutorsCanRun() { + return true; + } + + @Override + public String toString() { + return MethodHandles.lookup().lookupClass().getSimpleName() + + " (" + Integer.toHexString(hashCode()) + "): id:" + getId() + + "; testtitle:" + getTestTitle(); + } +} diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java index ff10f13d7..e5eaf08ec 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java @@ -41,6 +41,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CommentsMetricTest; import de.tuclausthal.submissioninterface.persistence.datamodel.CompileTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JUnitTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; @@ -154,6 +155,28 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr test.setPreparationShellCode(preparationcode.replaceAll("\r\n", "\n")); session.getTransaction().commit(); response.sendRedirect(Util.generateRedirectURL(DockerTestManager.class.getSimpleName() + "?testid=" + test.getId(), response)); + } else if ("saveNewTest".equals(request.getParameter("action")) && "haskellsyntax".equals(request.getParameter("type"))) { + session.beginTransaction(); + TestDAOIf testDAO = DAOFactory.TestDAOIf(session); + HaskellSyntaxTest test = testDAO.createHaskellSyntaxTest(task); + + int timesRunnableByStudents = Util.parseInteger(request.getParameter("timesRunnableByStudents"), 0); + boolean tutortest = request.getParameter("tutortest") != null; + String title = request.getParameter("title"); + String description = request.getParameter("description"); + + test.setTimesRunnableByStudents(timesRunnableByStudents); + test.setForTutors(tutortest); + test.setTestTitle(title); + test.setTestDescription(description); + test.setGiveDetailsToStudents(request.getParameter("giveDetailsToStudents") != null); + test.setTimeout(15); // falls du es trotzdem festlegen willst + session.getTransaction().commit(); + + // Zurück zur Aufgabenübersicht (wie bei CompileTest) + response.sendRedirect(Util.generateRedirectURL(TaskManager.class.getSimpleName() + + "?action=editTask&lecture=" + task.getTaskGroup().getLecture().getId() + + "&taskid=" + task.getTaskid(), response)); } else if ("saveNewTest".equals(request.getParameter("action")) && "checklist".equals(request.getParameter("type"))) { session.beginTransaction(); TestDAOIf testDAO = DAOFactory.TestDAOIf(session); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java index f50f093e8..5fafae195 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java @@ -29,6 +29,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.ChecklistTest; import de.tuclausthal.submissioninterface.persistence.datamodel.ChecklistTestCheckItem; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; import de.tuclausthal.submissioninterface.persistence.datamodel.LogEntry; import de.tuclausthal.submissioninterface.persistence.datamodel.Task; @@ -108,6 +109,8 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro ShowJavaAdvancedIOTestResult.printTestResults(out, jaiot, testResult.getTestOutput(), true, null); } else if (test instanceof DockerTest dt) { ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), true, null); + } else if (test instanceof HaskellSyntaxTest hst){ + //TODO: Add error Output } else { out.println("Ausgabe:
" + Util.escapeHTML(testResult.getTestOutput()) + "
"); } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java index a8ec8d1ad..5406530ec 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java @@ -27,6 +27,7 @@ import jakarta.servlet.http.HttpServletResponse; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; import de.tuclausthal.submissioninterface.persistence.datamodel.ParticipationRole; @@ -68,6 +69,8 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr ShowJavaAdvancedIOTestResult.printTestResults(out, jaiot, testResult.getTestOutput(), (participation == null || !participation.getRoleType().equals(ParticipationRole.ADVISOR)), null); } else if (test instanceof DockerTest dt) { ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), (participation == null || participation.getRoleType().compareTo(ParticipationRole.TUTOR) < 0), null); + } else if (test instanceof HaskellSyntaxTest hst){ + //TODO: Ein gescheiter Output muss sich hier evtl. überlegt werden } else { out.println("Ausgabe:
" + Util.escapeHTML(testResult.getTestOutput()) + "
"); } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java index f74648321..4db0cf81f 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java @@ -170,6 +170,10 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println(""); out.println(""); out.println(""); + out.println("Beschreibung:"); + out.println(""); + out.println(""); + out.println(""); out.println("Tutorentest:"); out.println(" "); out.println(""); @@ -182,13 +186,6 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println(""); out.println(""); out.println(""); - out.println("Preparation Code:"); - out.println(""); - out.println(""); - out.println(""); - out.println("Weitere Einstellungen auf zweiter Seite..."); - out.println(""); - out.println(""); out.print(" Abbrechen"); diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java new file mode 100644 index 000000000..a4235e8ce --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java @@ -0,0 +1,125 @@ +package de.tuclausthal.submissioninterface.testframework.tests.impl; + +import java.io.IOException; +import java.io.Writer; +import java.lang.invoke.MethodHandles; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Random; + +import jakarta.json.Json; +import jakarta.json.JsonObjectBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.tuclausthal.submissioninterface.testframework.executor.TestExecutorTestResult; +import de.tuclausthal.submissioninterface.util.Util; + +public class HaskellSyntaxTest extends TempDirTest { + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final String SAFE_DOCKER_SCRIPT = "/usr/local/bin/safe-docker"; + private static final Random RANDOM = new Random(); + private final String separator; + private Path tempDir; + + public HaskellSyntaxTest(de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest test) { + super(test); + this.separator = "##"; + } + + @Override + public void performTest(Path basePath, Path submissionPath, TestExecutorTestResult testResult) throws Exception { + try { + tempDir = Util.createTemporaryDirectory("test"); + if (tempDir == null) { + throw new IOException("Failed to create tempdir!"); + } + + Path adminDir = tempDir.resolve("admin"); + Path studentDir = tempDir.resolve("student"); + Files.createDirectories(adminDir); + Files.createDirectories(studentDir); + + Util.recursiveCopy(submissionPath, studentDir); + + String bashScript = """ + #!/bin/bash + set -e + echo '%s' + for file in *.hs; do + ghci -ignore-dot-ghci -v0 -ferror-spans -fdiagnostics-color=never -Wall -e ":load $file" -e ":quit" + done + """.formatted(separator); + + Path testScript = adminDir.resolve("test.sh"); + try (Writer writer = Files.newBufferedWriter(testScript)) { + writer.write(bashScript); + } + + List cmd = List.of( + "sudo", + SAFE_DOCKER_SCRIPT, + "--timeout=" + test.getTimeout(), + "--dir=" + Util.escapeCommandlineArguments(adminDir.toAbsolutePath().toString()), + "--", + "bash", + Util.escapeCommandlineArguments(testScript.toAbsolutePath().toString()) + ); + + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.directory(studentDir.toFile()); + pb.environment().keySet().removeIf(k -> !List.of("PATH", "USER", "LANG").contains(k)); + + LOG.debug("Executing HaskellSyntaxTest docker process: {}", cmd); + Process proc = pb.start(); + ProcessOutputGrabber outputGrabber = new ProcessOutputGrabber(proc); + + int exitCode = -1; + boolean aborted = false; + try { + exitCode = proc.waitFor(); + } catch (InterruptedException e) { + aborted = true; + } + outputGrabber.waitFor(); + + if (exitCode == 23 || exitCode == 24) aborted = true; + + boolean success = (exitCode == 0) && !outputGrabber.getStdErrBuffer().toString().toLowerCase().contains("error:"); + + testResult.setTestPassed(success); + testResult.setTestOutput(generateJsonResult(outputGrabber.getStdOutBuffer(), outputGrabber.getStdErrBuffer(), exitCode, success, aborted)); + + } finally { + if (tempDir != null) { + Util.recursiveDelete(tempDir); + } + } + } + + private String generateJsonResult(StringBuffer stdout, StringBuffer stderr, int exitCode, boolean success, boolean aborted) { + JsonObjectBuilder builder = Json.createObjectBuilder() + .add("stdout", stdout.toString()) + .add("separator", separator + "\n") + .add("exitCode", exitCode) + .add("exitedCleanly", success); + + if (stderr.length() > 0) { + builder.add("stderr", stderr.toString()); + } + if (aborted) { + builder.add("time-exceeded", true); + } + if (tempDir != null) { + builder.add("tmpdir", tempDir.toAbsolutePath().toString()); + } + return builder.build().toString(); + } + + @Override + protected void performTestInTempDir(Path basePath, Path tempDir, TestExecutorTestResult testResult) throws Exception { + // unused + } +} From 92641b0bee84ce423096436de086d73d87b5105c Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Mon, 14 Apr 2025 03:05:36 +0200 Subject: [PATCH 009/105] implement the clustering for the Java Syntax Test with a decent View --- .../view/ShowSubmissionStudentView.java | 10 +- .../servlets/view/ShowSubmissionView.java | 8 +- .../ShowHaskellSyntaxTestResult.java | 44 +++++++++ .../testanalyzer/CommonErrorAnalyzer.java | 18 ++++ .../haskell/HaskellErrorClassifierIf.java | 7 ++ .../haskell/RegexBasedHaskellClustering.java | 93 +++++++++++++++++++ 6 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java create mode 100644 src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/HaskellErrorClassifierIf.java create mode 100644 src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java index d7a2a451a..bb01da31a 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java @@ -23,16 +23,13 @@ import java.io.PrintWriter; import java.util.List; +import de.tuclausthal.submissioninterface.persistence.datamodel.*; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError; -import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; -import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; -import de.tuclausthal.submissioninterface.persistence.datamodel.Submission; -import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; import de.tuclausthal.submissioninterface.servlets.GATEView; import de.tuclausthal.submissioninterface.servlets.controller.ShowFile; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowDockerTestResult; @@ -82,6 +79,9 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro } else if (testResult.getTest() instanceof DockerTest) { out.println("
"); ShowDockerTestResult.printTestResults(out, (DockerTest) testResult.getTest(), testResult.getTestOutput(), true, javaScript); + } else if (testResult.getTest() instanceof HaskellSyntaxTest hst) { + out.println("
"); + ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), false, javaScript); } else { out.println("
"); } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionView.java index 2a118774b..f223a07d9 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionView.java @@ -40,6 +40,7 @@ import de.tuclausthal.submissioninterface.persistence.dao.PointGivenDAOIf; import de.tuclausthal.submissioninterface.persistence.dao.SubmissionDAOIf; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; import de.tuclausthal.submissioninterface.persistence.datamodel.MCOption; import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; @@ -64,6 +65,7 @@ import de.tuclausthal.submissioninterface.servlets.controller.ShowSubmission; import de.tuclausthal.submissioninterface.servlets.controller.ShowUser; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowDockerTestResult; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowJavaAdvancedIOTestResult; import de.tuclausthal.submissioninterface.tasktypes.ClozeTaskType; import de.tuclausthal.submissioninterface.template.Template; @@ -320,7 +322,11 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro } else if (testResult.getTest() instanceof DockerTest dt) { out.println("
"); ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), false, javaScript); - } else { + } else if (testResult.getTest() instanceof HaskellSyntaxTest hst) { + out.println("
"); + ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), false, javaScript); + } + else { out.println("
"); } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java new file mode 100644 index 000000000..008b62db8 --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java @@ -0,0 +1,44 @@ +package de.tuclausthal.submissioninterface.servlets.view.fragments; + +import java.io.PrintWriter; +import java.io.StringReader; + +import jakarta.json.Json; +import jakarta.json.JsonObject; + +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; + +public class ShowHaskellSyntaxTestResult { + /** + * @param out PrintWriter zum Ausgeben + * @param test der HaskellSyntaxTest + * @param testOutput JSON-String aus testResult.getTestOutput() + * @param isStudent true, wenn Studentensicht, false wenn Tutorsicht + * @param javaScript Hier kannst Du Code-Preview/JS-Schnipsel anfügen, wenn Du willst + */ + public static void printTestResults(PrintWriter out, HaskellSyntaxTest test, String testOutput, boolean isStudent, StringBuilder javaScript) { + // JSON auslesen: + JsonObject json = Json.createReader(new StringReader(testOutput)).readObject(); + + String stderr = json.getString("stderr", ""); + + // ErrorMessage + if (!stderr.isEmpty()) { + out.println("

Fehlerausgabe (stderr):

"); + // Du kannst Zeilen hier farblich kennzeichnen oder ab Zeilenumbrüchen splitten + out.println("
" + escapeHTML(stderr) + "
"); + } + + out.println(""); + } + + private static String escapeHTML(String str) { + if (str == null) { + return ""; + } + return str + .replace("&", "&") + .replace("<", "<") + .replace(">", ">"); + } +} \ No newline at end of file diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java index 7b8e59eb5..0bb2e54da 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java @@ -22,6 +22,7 @@ import java.io.StringReader; import java.util.List; +import de.tuclausthal.submissioninterface.testanalyzer.haskell.RegexBasedHaskellClustering; import jakarta.json.Json; import jakarta.json.JsonArray; import jakarta.json.JsonObject; @@ -38,6 +39,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.Test; import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; public class CommonErrorAnalyzer { //from Literatur @@ -68,6 +70,9 @@ public void runAnalysis(final TestResult testResult) { else if (test instanceof DockerTest) { groupDockerTestResults((DockerTest) test, testResult); } + else if (test instanceof HaskellSyntaxTest){ + groupHaskellSyntaxTestResults((HaskellSyntaxTest) test, testResult); + } } private void groupCompilerTestResults(final CompileTest test, final TestResult testResult) { @@ -322,4 +327,17 @@ private void groupDockerTestResults(final DockerTest test, final TestResult test groupTestResultToCommonErrors(testResult, stderr, keyStr); } + private void groupHaskellSyntaxTestResults(final HaskellSyntaxTest test, final TestResult testResult) { + if (testResult.getPassedTest()) { + return; + } + + JsonObject testOutputJson = Json.createReader(new StringReader(testResult.getTestOutput())).readObject(); + String stderr = testOutputJson.containsKey("stderr") ? testOutputJson.getString("stderr") : ""; + + String keyStr = "HaskellSyntax: "; + new RegexBasedHaskellClustering(session).classify(testResult, stderr, keyStr); + } + + } diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/HaskellErrorClassifierIf.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/HaskellErrorClassifierIf.java new file mode 100644 index 000000000..64e106838 --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/HaskellErrorClassifierIf.java @@ -0,0 +1,7 @@ +package de.tuclausthal.submissioninterface.testanalyzer.haskell; + +import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; + +public interface HaskellErrorClassifierIf { + void classify(TestResult testResult, String stderr, String keyStr); +} \ No newline at end of file diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java new file mode 100644 index 000000000..5965dd04b --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java @@ -0,0 +1,93 @@ +package de.tuclausthal.submissioninterface.testanalyzer.haskell; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import de.tuclausthal.submissioninterface.persistence.dao.CommonErrorDAOIf; +import de.tuclausthal.submissioninterface.persistence.dao.DAOFactory; +import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError; +import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError.Type; +import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; +import org.hibernate.Session; + +public class RegexBasedHaskellClustering implements HaskellErrorClassifierIf { + + private final Session session; + private final Map clusters; + + public RegexBasedHaskellClustering(Session session) { + this.session = session; + // HINWEIS: Deutsche Beschreibungen der Fehlerarten: + this.clusters = new LinkedHashMap<>(Map.ofEntries( + Map.entry("Parse-Fehler", Pattern.compile("\\bparse\\s+error\\b", Pattern.CASE_INSENSITIVE)), + Map.entry("Typenkonflikt", Pattern.compile("couldn'?t match (expected type|type)", Pattern.CASE_INSENSITIVE)), + Map.entry("Variable nicht im Gültigkeitsbereich", Pattern.compile("not in scope", Pattern.CASE_INSENSITIVE)), + Map.entry("Leerer do-Block", Pattern.compile("empty\\s+'do'\\s+block", Pattern.CASE_INSENSITIVE)), + Map.entry("Unendlicher Typ", Pattern.compile("occurs check:.*infinite type", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), + Map.entry("Modul nicht gefunden", Pattern.compile("could not find module", Pattern.CASE_INSENSITIVE)), + Map.entry("Fehlendes Binding", Pattern.compile("type signature.*lacks an accompanying binding", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), + Map.entry("Fehlende Instanz", Pattern.compile("no instance for", Pattern.CASE_INSENSITIVE)), + Map.entry("Abweichende Arity", Pattern.compile("equations for .* have different numbers of arguments", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), + Map.entry("Doppelte Signatur", Pattern.compile("duplicate type signatures?", Pattern.CASE_INSENSITIVE)), + Map.entry("Mehrdeutiger Bezeichner", Pattern.compile("ambiguous occurrence", Pattern.CASE_INSENSITIVE)), + Map.entry("Syntaxfehler", Pattern.compile("syntax error", Pattern.CASE_INSENSITIVE)), + Map.entry("Ungültige Typensignatur", Pattern.compile("invalid type signature", Pattern.CASE_INSENSITIVE)), + Map.entry("Mehrfache Deklarationen", Pattern.compile("multiple declarations", Pattern.CASE_INSENSITIVE)), + Map.entry("Fehlerhafter Datenkonstruktor", Pattern.compile("cannot parse data constructor in a data/newtype declaration", Pattern.CASE_INSENSITIVE)), + Map.entry("Fehlerhafter Typ-Header", Pattern.compile("malformed head of type or class declaration", Pattern.CASE_INSENSITIVE)), + Map.entry("Lexikalischer Fehler", Pattern.compile("lexical error at character", Pattern.CASE_INSENSITIVE)), + Map.entry("Kind-Konflikt", Pattern.compile("expected kind .* but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), + Map.entry("Mehrdeutiger Typ", Pattern.compile("ambiguous type variable", Pattern.CASE_INSENSITIVE)), + Map.entry("Doppelte Instanz", Pattern.compile("duplicate instance declarations", Pattern.CASE_INSENSITIVE)), + Map.entry("Fehlende Constraint", Pattern.compile("could not deduce.*\\(", Pattern.CASE_INSENSITIVE)), + Map.entry("Konfliktierende Bindings", Pattern.compile("conflicting definitions for", Pattern.CASE_INSENSITIVE)), + Map.entry("Methode nicht in Klasse", Pattern.compile("is not a \\(visible\\) method of class", Pattern.CASE_INSENSITIVE)), + Map.entry("Ungültige Instanz-Signatur", Pattern.compile("illegal type signature in instance declaration", Pattern.CASE_INSENSITIVE)), + Map.entry("Unvollständiger Typ", Pattern.compile("expecting one more argument to .*has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), + Map.entry("Constraint erwartet, aber Typ erhalten", Pattern.compile("expected a constraint, but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), + Map.entry("Pattern Binding in Instanz", Pattern.compile("pattern bindings.*not allowed in instance declaration", Pattern.CASE_INSENSITIVE)), + Map.entry("Falsche Konstruktor-Arity", Pattern.compile("the constructor ‘.*’ should have \\d+ argument[s]?, but has been given \\d+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), + Map.entry("Typed Hole", Pattern.compile("found hole: _ ::", Pattern.CASE_INSENSITIVE)), + Map.entry("Ungültige Binding-Syntax", Pattern.compile("illegal binding of built-in syntax", Pattern.CASE_INSENSITIVE)), + Map.entry("Letzte Anweisung im 'do'-Block", Pattern.compile("the last statement in a 'do' block must be an expression", Pattern.CASE_INSENSITIVE)), + Map.entry("Überlappende Instanzen", Pattern.compile("overlapping instances for", Pattern.CASE_INSENSITIVE)), + Map.entry("Ungültiges Enum-Deriving", Pattern.compile("can't make a derived instance of ['‘`]Enum", Pattern.CASE_INSENSITIVE)), + Map.entry("Ungültige Instanz-Form", Pattern.compile("illegal instance declaration.*flexibleinstances", Pattern.CASE_INSENSITIVE)), + Map.entry("Kein Datenkonstruktor", Pattern.compile("not a data constructor", Pattern.CASE_INSENSITIVE)), + Map.entry("Ungültiges Deriving", Pattern.compile("illegal deriving item", Pattern.CASE_INSENSITIVE)), + Map.entry("Flexible Kontexte benötigt", Pattern.compile("non type-variable argument in the constraint", Pattern.CASE_INSENSITIVE)), + Map.entry("Erneut ungültige Typensignatur", Pattern.compile("illegal type signature", Pattern.CASE_INSENSITIVE)), + Map.entry("Fehlende GADTs-Erweiterung", Pattern.compile("enable the GADTs extension", Pattern.CASE_INSENSITIVE)), + Map.entry("Kind-Konflikt (Constraint vs. Typ)", Pattern.compile("expected (a constraint|a type), but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), + Map.entry("Ungültiger Typ-Operator", Pattern.compile("illegal operator .* in type .*", Pattern.CASE_INSENSITIVE)), + Map.entry("Fehlende Klammern im Range-Ausdruck", Pattern.compile("a section must be enclosed in parentheses", Pattern.CASE_INSENSITIVE)), + Map.entry("Warnung", Pattern.compile("warning", Pattern.CASE_INSENSITIVE)), + Map.entry("Sonstiger Fehler", Pattern.compile(".")) + )); + } + + @Override + public void classify(TestResult testResult, String stderr, String keyStr) { + CommonErrorDAOIf commonErrorDAO = DAOFactory.CommonErrorDAOIf(session); + boolean assigned = false; + + for (var entry : clusters.entrySet()) { + if (entry.getValue().matcher(stderr).find()) { + String clusterName = entry.getKey(); + CommonError commonError = commonErrorDAO.getCommonError(keyStr + clusterName, testResult.getTest()); + if (commonError != null) { + commonError.getTestResults().add(testResult); + } else { + commonErrorDAO.newCommonError(keyStr + clusterName, clusterName, testResult, Type.CompileTimeError); + } + assigned = true; + break; + } + } + + if (!assigned) { + commonErrorDAO.newCommonError(keyStr + "Nicht klassifiziert", "Unklassifiziert", testResult, null); + } + } +} From 4a3f9b03d8241be321b4d4b6c37d432347cde724 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Mon, 14 Apr 2025 03:32:38 +0200 Subject: [PATCH 010/105] improve the clustering so that it is clustered based on pririty rather than the first error message --- .../haskell/RegexBasedHaskellClustering.java | 137 ++++++++++-------- 1 file changed, 78 insertions(+), 59 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java index 5965dd04b..a4b665fa4 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java @@ -1,7 +1,6 @@ package de.tuclausthal.submissioninterface.testanalyzer.haskell; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; import java.util.regex.Pattern; import de.tuclausthal.submissioninterface.persistence.dao.CommonErrorDAOIf; @@ -14,80 +13,100 @@ public class RegexBasedHaskellClustering implements HaskellErrorClassifierIf { private final Session session; - private final Map clusters; + private final LinkedHashMap clusters; public RegexBasedHaskellClustering(Session session) { this.session = session; - // HINWEIS: Deutsche Beschreibungen der Fehlerarten: - this.clusters = new LinkedHashMap<>(Map.ofEntries( - Map.entry("Parse-Fehler", Pattern.compile("\\bparse\\s+error\\b", Pattern.CASE_INSENSITIVE)), - Map.entry("Typenkonflikt", Pattern.compile("couldn'?t match (expected type|type)", Pattern.CASE_INSENSITIVE)), - Map.entry("Variable nicht im Gültigkeitsbereich", Pattern.compile("not in scope", Pattern.CASE_INSENSITIVE)), - Map.entry("Leerer do-Block", Pattern.compile("empty\\s+'do'\\s+block", Pattern.CASE_INSENSITIVE)), - Map.entry("Unendlicher Typ", Pattern.compile("occurs check:.*infinite type", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), - Map.entry("Modul nicht gefunden", Pattern.compile("could not find module", Pattern.CASE_INSENSITIVE)), - Map.entry("Fehlendes Binding", Pattern.compile("type signature.*lacks an accompanying binding", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), - Map.entry("Fehlende Instanz", Pattern.compile("no instance for", Pattern.CASE_INSENSITIVE)), - Map.entry("Abweichende Arity", Pattern.compile("equations for .* have different numbers of arguments", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), - Map.entry("Doppelte Signatur", Pattern.compile("duplicate type signatures?", Pattern.CASE_INSENSITIVE)), - Map.entry("Mehrdeutiger Bezeichner", Pattern.compile("ambiguous occurrence", Pattern.CASE_INSENSITIVE)), - Map.entry("Syntaxfehler", Pattern.compile("syntax error", Pattern.CASE_INSENSITIVE)), - Map.entry("Ungültige Typensignatur", Pattern.compile("invalid type signature", Pattern.CASE_INSENSITIVE)), - Map.entry("Mehrfache Deklarationen", Pattern.compile("multiple declarations", Pattern.CASE_INSENSITIVE)), - Map.entry("Fehlerhafter Datenkonstruktor", Pattern.compile("cannot parse data constructor in a data/newtype declaration", Pattern.CASE_INSENSITIVE)), - Map.entry("Fehlerhafter Typ-Header", Pattern.compile("malformed head of type or class declaration", Pattern.CASE_INSENSITIVE)), - Map.entry("Lexikalischer Fehler", Pattern.compile("lexical error at character", Pattern.CASE_INSENSITIVE)), - Map.entry("Kind-Konflikt", Pattern.compile("expected kind .* but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), - Map.entry("Mehrdeutiger Typ", Pattern.compile("ambiguous type variable", Pattern.CASE_INSENSITIVE)), - Map.entry("Doppelte Instanz", Pattern.compile("duplicate instance declarations", Pattern.CASE_INSENSITIVE)), - Map.entry("Fehlende Constraint", Pattern.compile("could not deduce.*\\(", Pattern.CASE_INSENSITIVE)), - Map.entry("Konfliktierende Bindings", Pattern.compile("conflicting definitions for", Pattern.CASE_INSENSITIVE)), - Map.entry("Methode nicht in Klasse", Pattern.compile("is not a \\(visible\\) method of class", Pattern.CASE_INSENSITIVE)), - Map.entry("Ungültige Instanz-Signatur", Pattern.compile("illegal type signature in instance declaration", Pattern.CASE_INSENSITIVE)), - Map.entry("Unvollständiger Typ", Pattern.compile("expecting one more argument to .*has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), - Map.entry("Constraint erwartet, aber Typ erhalten", Pattern.compile("expected a constraint, but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), - Map.entry("Pattern Binding in Instanz", Pattern.compile("pattern bindings.*not allowed in instance declaration", Pattern.CASE_INSENSITIVE)), - Map.entry("Falsche Konstruktor-Arity", Pattern.compile("the constructor ‘.*’ should have \\d+ argument[s]?, but has been given \\d+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), - Map.entry("Typed Hole", Pattern.compile("found hole: _ ::", Pattern.CASE_INSENSITIVE)), - Map.entry("Ungültige Binding-Syntax", Pattern.compile("illegal binding of built-in syntax", Pattern.CASE_INSENSITIVE)), - Map.entry("Letzte Anweisung im 'do'-Block", Pattern.compile("the last statement in a 'do' block must be an expression", Pattern.CASE_INSENSITIVE)), - Map.entry("Überlappende Instanzen", Pattern.compile("overlapping instances for", Pattern.CASE_INSENSITIVE)), - Map.entry("Ungültiges Enum-Deriving", Pattern.compile("can't make a derived instance of ['‘`]Enum", Pattern.CASE_INSENSITIVE)), - Map.entry("Ungültige Instanz-Form", Pattern.compile("illegal instance declaration.*flexibleinstances", Pattern.CASE_INSENSITIVE)), - Map.entry("Kein Datenkonstruktor", Pattern.compile("not a data constructor", Pattern.CASE_INSENSITIVE)), - Map.entry("Ungültiges Deriving", Pattern.compile("illegal deriving item", Pattern.CASE_INSENSITIVE)), - Map.entry("Flexible Kontexte benötigt", Pattern.compile("non type-variable argument in the constraint", Pattern.CASE_INSENSITIVE)), - Map.entry("Erneut ungültige Typensignatur", Pattern.compile("illegal type signature", Pattern.CASE_INSENSITIVE)), - Map.entry("Fehlende GADTs-Erweiterung", Pattern.compile("enable the GADTs extension", Pattern.CASE_INSENSITIVE)), - Map.entry("Kind-Konflikt (Constraint vs. Typ)", Pattern.compile("expected (a constraint|a type), but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)), - Map.entry("Ungültiger Typ-Operator", Pattern.compile("illegal operator .* in type .*", Pattern.CASE_INSENSITIVE)), - Map.entry("Fehlende Klammern im Range-Ausdruck", Pattern.compile("a section must be enclosed in parentheses", Pattern.CASE_INSENSITIVE)), - Map.entry("Warnung", Pattern.compile("warning", Pattern.CASE_INSENSITIVE)), - Map.entry("Sonstiger Fehler", Pattern.compile(".")) - )); + this.clusters = new LinkedHashMap<>(); + + // Muster in Priorisierungsreihenfolge eintragen + clusters.put("Parse-Fehler", Pattern.compile("\\bparse\\s+error\\b", Pattern.CASE_INSENSITIVE)); + clusters.put("Typenkonflikt", Pattern.compile("couldn'?t match (expected type|type)", Pattern.CASE_INSENSITIVE)); + clusters.put("Variable nicht im Gültigkeitsbereich", Pattern.compile("not in scope", Pattern.CASE_INSENSITIVE)); + clusters.put("Leerer do-Block", Pattern.compile("empty\\s+'do'\\s+block", Pattern.CASE_INSENSITIVE)); + clusters.put("Unendlicher Typ", Pattern.compile("occurs check:.*infinite type", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + clusters.put("Modul nicht gefunden", Pattern.compile("could not find module", Pattern.CASE_INSENSITIVE)); + clusters.put("Fehlendes Binding", Pattern.compile("type signature.*lacks an accompanying binding", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + clusters.put("Fehlende Instanz", Pattern.compile("no instance for", Pattern.CASE_INSENSITIVE)); + clusters.put("Abweichende Arity", Pattern.compile("equations for .* have different numbers of arguments", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + clusters.put("Doppelte Signatur", Pattern.compile("duplicate type signatures?", Pattern.CASE_INSENSITIVE)); + clusters.put("Mehrdeutiger Bezeichner", Pattern.compile("ambiguous occurrence", Pattern.CASE_INSENSITIVE)); + clusters.put("Syntaxfehler", Pattern.compile("syntax error", Pattern.CASE_INSENSITIVE)); + clusters.put("Ungültige Typensignatur", Pattern.compile("invalid type signature", Pattern.CASE_INSENSITIVE)); + clusters.put("Mehrfache Deklarationen", Pattern.compile("multiple declarations", Pattern.CASE_INSENSITIVE)); + clusters.put("Fehlerhafter Datenkonstruktor", Pattern.compile("cannot parse data constructor in a data/newtype declaration", Pattern.CASE_INSENSITIVE)); + clusters.put("Fehlerhafter Typ-Header", Pattern.compile("malformed head of type or class declaration", Pattern.CASE_INSENSITIVE)); + clusters.put("Lexikalischer Fehler", Pattern.compile("lexical error at character", Pattern.CASE_INSENSITIVE)); + clusters.put("Kind-Konflikt", Pattern.compile("expected kind .* but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + clusters.put("Mehrdeutiger Typ", Pattern.compile("ambiguous type variable", Pattern.CASE_INSENSITIVE)); + clusters.put("Doppelte Instanz", Pattern.compile("duplicate instance declarations", Pattern.CASE_INSENSITIVE)); + clusters.put("Fehlende Constraint", Pattern.compile("could not deduce.*\\(", Pattern.CASE_INSENSITIVE)); + clusters.put("Konfliktierende Bindings", Pattern.compile("conflicting definitions for", Pattern.CASE_INSENSITIVE)); + clusters.put("Methode nicht in Klasse", Pattern.compile("is not a \\(visible\\) method of class", Pattern.CASE_INSENSITIVE)); + clusters.put("Ungültige Instanz-Signatur", Pattern.compile("illegal type signature in instance declaration", Pattern.CASE_INSENSITIVE)); + clusters.put("Unvollständiger Typ", Pattern.compile("expecting one more argument to .*has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + clusters.put("Constraint erwartet, aber Typ erhalten", Pattern.compile("expected a constraint, but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + clusters.put("Pattern Binding in Instanz", Pattern.compile("pattern bindings.*not allowed in instance declaration", Pattern.CASE_INSENSITIVE)); + clusters.put("Falsche Konstruktor-Arity", Pattern.compile("the constructor ‘.*’ should have \\d+ argument[s]?, but has been given \\d+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + clusters.put("Typed Hole", Pattern.compile("found hole: _ ::", Pattern.CASE_INSENSITIVE)); + clusters.put("Ungültige Binding-Syntax", Pattern.compile("illegal binding of built-in syntax", Pattern.CASE_INSENSITIVE)); + clusters.put("Letzte Anweisung im 'do'-Block", Pattern.compile("the last statement in a 'do' block must be an expression", Pattern.CASE_INSENSITIVE)); + clusters.put("Überlappende Instanzen", Pattern.compile("overlapping instances for", Pattern.CASE_INSENSITIVE)); + clusters.put("Ungültiges Enum-Deriving", Pattern.compile("can't make a derived instance of ['‘`]Enum", Pattern.CASE_INSENSITIVE)); + clusters.put("Ungültige Instanz-Form", Pattern.compile("illegal instance declaration.*flexibleinstances", Pattern.CASE_INSENSITIVE)); + clusters.put("Kein Datenkonstruktor", Pattern.compile("not a data constructor", Pattern.CASE_INSENSITIVE)); + clusters.put("Ungültiges Deriving", Pattern.compile("illegal deriving item", Pattern.CASE_INSENSITIVE)); + clusters.put("Flexible Kontexte benötigt", Pattern.compile("non type-variable argument in the constraint", Pattern.CASE_INSENSITIVE)); + clusters.put("Erneut ungültige Typensignatur", Pattern.compile("illegal type signature", Pattern.CASE_INSENSITIVE)); + clusters.put("Fehlende GADTs-Erweiterung", Pattern.compile("enable the GADTs extension", Pattern.CASE_INSENSITIVE)); + clusters.put("Kind-Konflikt (Constraint vs. Typ)", Pattern.compile("expected (a constraint|a type), but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + clusters.put("Ungültiger Typ-Operator", Pattern.compile("illegal operator .* in type .*", Pattern.CASE_INSENSITIVE)); + clusters.put("Fehlende Klammern im Range-Ausdruck", Pattern.compile("a section must be enclosed in parentheses", Pattern.CASE_INSENSITIVE)); + clusters.put("Warnung", Pattern.compile("warning", Pattern.CASE_INSENSITIVE)); + clusters.put("Sonstiger Fehler", Pattern.compile(".*", Pattern.DOTALL)); } @Override public void classify(TestResult testResult, String stderr, String keyStr) { CommonErrorDAOIf commonErrorDAO = DAOFactory.CommonErrorDAOIf(session); - boolean assigned = false; + List matchedClusters = new ArrayList<>(); for (var entry : clusters.entrySet()) { if (entry.getValue().matcher(stderr).find()) { - String clusterName = entry.getKey(); - CommonError commonError = commonErrorDAO.getCommonError(keyStr + clusterName, testResult.getTest()); + matchedClusters.add(entry.getKey()); + } + } + + System.out.println("DEBUG: Fehlerausgabe:\n" + stderr); + System.out.println("DEBUG: Gefundene Cluster: " + matchedClusters); + + for (String preferred : clusters.keySet()) { + if (!preferred.equals("Sonstiger Fehler") && matchedClusters.contains(preferred)) { + String fullKey = keyStr + preferred; + CommonError commonError = commonErrorDAO.getCommonError(fullKey, testResult.getTest()); if (commonError != null) { commonError.getTestResults().add(testResult); } else { - commonErrorDAO.newCommonError(keyStr + clusterName, clusterName, testResult, Type.CompileTimeError); + commonErrorDAO.newCommonError(fullKey, preferred, testResult, Type.CompileTimeError); } - assigned = true; - break; + System.out.println("DEBUG: Zugewiesenes Cluster: " + preferred); + return; } } - if (!assigned) { - commonErrorDAO.newCommonError(keyStr + "Nicht klassifiziert", "Unklassifiziert", testResult, null); + if (matchedClusters.contains("Sonstiger Fehler")) { + String fallbackKey = keyStr + "Sonstiger Fehler"; + CommonError commonError = commonErrorDAO.getCommonError(fallbackKey, testResult.getTest()); + if (commonError != null) { + commonError.getTestResults().add(testResult); + } else { + commonErrorDAO.newCommonError(fallbackKey, "Sonstiger Fehler", testResult, Type.CompileTimeError); + } + System.out.println("DEBUG: Zugewiesenes Cluster: Sonstiger Fehler"); + return; } + + System.out.println("DEBUG: Kein Cluster zugewiesen -> Unklassifiziert."); + commonErrorDAO.newCommonError(keyStr + "Nicht klassifiziert", "Unklassifiziert", testResult, null); } -} +} \ No newline at end of file From 43764650191dd45b83d8f7e4b297a68b7d5b19fa Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Thu, 17 Apr 2025 02:57:12 +0200 Subject: [PATCH 011/105] add copyright declaration and remove debug console logs --- .../datamodel/HaskellSyntaxTest.java | 18 ++++++++++++ .../ShowHaskellSyntaxTestResult.java | 29 ++++++++++++------- .../haskell/HaskellErrorClassifierIf.java | 18 ++++++++++++ .../haskell/RegexBasedHaskellClustering.java | 23 +++++++++++---- .../tests/impl/HaskellSyntaxTest.java | 20 ++++++++++++- 5 files changed, 92 insertions(+), 16 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java index 3d7eaaf3b..283d6e036 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java @@ -1,3 +1,21 @@ +/* + * Copyright 2025 Sven Strickroth + * Copyright 2025 Esat Avci + * + * This file is part of the GATE. + * + * GATE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * GATE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GATE. If not, see . + */ package de.tuclausthal.submissioninterface.persistence.datamodel; import java.lang.invoke.MethodHandles; diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java index 008b62db8..e86f6f3e1 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java @@ -1,3 +1,21 @@ +/* + * Copyright 2025 Sven Strickroth + * Copyright 2025 Esat Avci + * + * This file is part of the GATE. + * + * GATE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * GATE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GATE. If not, see . + */ package de.tuclausthal.submissioninterface.servlets.view.fragments; import java.io.PrintWriter; @@ -9,23 +27,14 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; public class ShowHaskellSyntaxTestResult { - /** - * @param out PrintWriter zum Ausgeben - * @param test der HaskellSyntaxTest - * @param testOutput JSON-String aus testResult.getTestOutput() - * @param isStudent true, wenn Studentensicht, false wenn Tutorsicht - * @param javaScript Hier kannst Du Code-Preview/JS-Schnipsel anfügen, wenn Du willst - */ public static void printTestResults(PrintWriter out, HaskellSyntaxTest test, String testOutput, boolean isStudent, StringBuilder javaScript) { - // JSON auslesen: + JsonObject json = Json.createReader(new StringReader(testOutput)).readObject(); String stderr = json.getString("stderr", ""); - // ErrorMessage if (!stderr.isEmpty()) { out.println("

Fehlerausgabe (stderr):

"); - // Du kannst Zeilen hier farblich kennzeichnen oder ab Zeilenumbrüchen splitten out.println("
" + escapeHTML(stderr) + "
"); } diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/HaskellErrorClassifierIf.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/HaskellErrorClassifierIf.java index 64e106838..da5836450 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/HaskellErrorClassifierIf.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/HaskellErrorClassifierIf.java @@ -1,3 +1,21 @@ +/* + * Copyright 2025 Sven Strickroth + * Copyright 2025 Esat Avci + * + * This file is part of the GATE. + * + * GATE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * GATE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GATE. If not, see . + */ package de.tuclausthal.submissioninterface.testanalyzer.haskell; import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java index a4b665fa4..e7ade1266 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java @@ -1,3 +1,21 @@ +/* + * Copyright 2025 Sven Strickroth + * Copyright 2025 Esat Avci + * + * This file is part of the GATE. + * + * GATE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * GATE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GATE. If not, see . + */ package de.tuclausthal.submissioninterface.testanalyzer.haskell; import java.util.*; @@ -77,8 +95,6 @@ public void classify(TestResult testResult, String stderr, String keyStr) { } } - System.out.println("DEBUG: Fehlerausgabe:\n" + stderr); - System.out.println("DEBUG: Gefundene Cluster: " + matchedClusters); for (String preferred : clusters.keySet()) { if (!preferred.equals("Sonstiger Fehler") && matchedClusters.contains(preferred)) { @@ -89,7 +105,6 @@ public void classify(TestResult testResult, String stderr, String keyStr) { } else { commonErrorDAO.newCommonError(fullKey, preferred, testResult, Type.CompileTimeError); } - System.out.println("DEBUG: Zugewiesenes Cluster: " + preferred); return; } } @@ -102,11 +117,9 @@ public void classify(TestResult testResult, String stderr, String keyStr) { } else { commonErrorDAO.newCommonError(fallbackKey, "Sonstiger Fehler", testResult, Type.CompileTimeError); } - System.out.println("DEBUG: Zugewiesenes Cluster: Sonstiger Fehler"); return; } - System.out.println("DEBUG: Kein Cluster zugewiesen -> Unklassifiziert."); commonErrorDAO.newCommonError(keyStr + "Nicht klassifiziert", "Unklassifiziert", testResult, null); } } \ No newline at end of file diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java index a4235e8ce..e9e42cc84 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java @@ -1,3 +1,21 @@ +/* + * Copyright 2025 Sven Strickroth + * Copyright 2025 Esat Avci + * + * This file is part of the GATE. + * + * GATE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * GATE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GATE. If not, see . + */ package de.tuclausthal.submissioninterface.testframework.tests.impl; import java.io.IOException; @@ -120,6 +138,6 @@ private String generateJsonResult(StringBuffer stdout, StringBuffer stderr, int @Override protected void performTestInTempDir(Path basePath, Path tempDir, TestExecutorTestResult testResult) throws Exception { - // unused + //currently unused } } From 6f8a20cbcea6affdca6b3c4f4d48b37e6726af84 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Thu, 17 Apr 2025 19:51:02 +0200 Subject: [PATCH 012/105] Place Haskell Runtime Test immediately below Haskell Syntax Test --- .../view/HaskellRuntimeTestManagerView.java | 4 - .../view/TestManagerAddTestFormView.java | 112 ++++++++---------- 2 files changed, 50 insertions(+), 66 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 9fedc58bb..d55884955 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -37,8 +37,6 @@ import java.io.PrintWriter; import java.io.Serial; -import static de.tuclausthal.submissioninterface.servlets.view.TestManagerAddTestFormView.printHaskellRuntimeTestForm; - /** * View-Servlet for clustering haskell submissions based on common errors (dynamic/runtime analysis) * @@ -61,8 +59,6 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro PrintWriter out = response.getWriter(); // similar code in TestManagerAddTestFormView - // TODO@CHW: can this code duplicate be avoided using printHaskellRuntimeTestForm()? - // TODO@CHW: compare with printHaskellRuntimeTestForm() and add remaining fields out.println("

" + Util.escapeHTML(test.getTestTitle()) + "

"); out.println(""); out.println(""); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java index 4db0cf81f..0a9635c86 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java @@ -120,8 +120,8 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println(""); out.println(""); - // similar code in DockerTestManagerView if (Files.isRegularFile(Path.of(DockerTest.SAFE_DOCKER_SCRIPT))) { + // similar code in DockerTestManagerView out.println("

Docker Test

"); out.println("
"); out.println(""); @@ -192,11 +192,58 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println(""); out.println(""); out.println("
"); + + // similar code in HaskellRuntimeTestManagerView + out.println("

Haskell Runtime Test

"); + out.println("
"); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.print(""); + out.println(""); + out.println("
Titel:
Beschreibung:
Tutorentest:
# ausführbar für Studierende:
Timeout (s):
Studierenden Test-Details anzeigen:
Preparation Code:
Weitere Einstellungen auf zweiter Seite...
Abbrechen
"); + out.println("
"); } else { - out.println("

(Docker-Tests sind nicht verfügbar, da /usr/local/bin/safe-docker nicht gefunden wurde.)

"); + out.println("

(Docker-Tests, Haskell Syntax Tests und Haskell Runtime Tests sind nicht verfügbar, da /usr/local/bin/safe-docker nicht gefunden wurde.)

"); } - // similar code in ChecklistTestManagerView out.println("

Checklist Test

"); out.println("
"); @@ -344,65 +391,6 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println(""); out.println("
"); - if (Files.isRegularFile(Path.of(DockerTest.SAFE_DOCKER_SCRIPT))) { - printHaskellRuntimeTestForm(out, response, task); - } else { - out.println("

(Das dynamische Error Clustering für Haskell Abgaben ist nicht verfügbar, da /usr/local/bin/safe-docker nicht gefunden wurde.)

"); - } - template.printTemplateFooter(); - - - - } - - public static void printHaskellRuntimeTestForm(PrintWriter out, HttpServletResponse response, Task task) { - out.println("

Haskell dynamisches Error Clustering

"); - - out.println("
"); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.print(""); - out.println(""); - out.println("
Titel:
Beschreibung:
Tutorentest:
# ausführbar für Studierende:
Timeout (s):
Studierenden Test-Details anzeigen:
Preparation Code:
Weitere Einstellungen auf zweiter Seite...
Abbrechen
"); - out.println("
"); } } From 6194b25f76d85c561d3569eea5f1cc43904a6041 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Thu, 17 Apr 2025 20:06:47 +0200 Subject: [PATCH 013/105] Show correct testtypes in overview "Funktionstest der Abgaben" --- .../submissioninterface/servlets/view/TaskManagerView.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TaskManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TaskManagerView.java index 791c665a2..61f001ce6 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TaskManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TaskManagerView.java @@ -33,6 +33,8 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CommentsMetricTest; import de.tuclausthal.submissioninterface.persistence.datamodel.CompileTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JUnitTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Lecture; @@ -452,6 +454,10 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println("Kommentar-Metrik-Test
"); } else if (test instanceof JavaAdvancedIOTest) { out.println("Erweiterer Java-IO-Test
"); + } else if (test instanceof HaskellSyntaxTest) { + out.println("Haskell Syntax Test
"); + } else if (test instanceof HaskellRuntimeTest) { + out.println("Haskell Runtime Test
"); } else if (test instanceof DockerTest) { out.println("Docker
"); } else if (test instanceof ChecklistTest) { From 022f645c9a4e0dfa1c6c25615f4a36bb87736177 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Thu, 17 Apr 2025 20:09:02 +0200 Subject: [PATCH 014/105] Change default title of haskell syntax/runtime test --- .../servlets/view/TestManagerAddTestFormView.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java index 0a9635c86..795da2f79 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java @@ -167,7 +167,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println(""); out.println(""); out.println(""); - out.println(""); + out.println(""); out.println(""); out.println(""); out.println(""); @@ -202,7 +202,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println("
Titel:
Beschreibung:
"); out.println(""); out.println(""); - out.println(""); + out.println(""); out.println(""); out.println(""); out.println(""); From 762b853f9e4d7db0a1adbaaac5c3d20bde92de07 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Mon, 21 Apr 2025 15:29:24 +0200 Subject: [PATCH 015/105] Move haskell syntax classifier into own package --- .../submissioninterface/testanalyzer/CommonErrorAnalyzer.java | 2 +- .../haskell/{ => syntax}/HaskellErrorClassifierIf.java | 2 +- .../haskell/{ => syntax}/RegexBasedHaskellClustering.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/{ => syntax}/HaskellErrorClassifierIf.java (92%) rename src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/{ => syntax}/RegexBasedHaskellClustering.java (99%) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java index 0bb2e54da..56f631e07 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java @@ -22,7 +22,7 @@ import java.io.StringReader; import java.util.List; -import de.tuclausthal.submissioninterface.testanalyzer.haskell.RegexBasedHaskellClustering; +import de.tuclausthal.submissioninterface.testanalyzer.haskell.syntax.RegexBasedHaskellClustering; import jakarta.json.Json; import jakarta.json.JsonArray; import jakarta.json.JsonObject; diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/HaskellErrorClassifierIf.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/HaskellErrorClassifierIf.java similarity index 92% rename from src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/HaskellErrorClassifierIf.java rename to src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/HaskellErrorClassifierIf.java index da5836450..00d205871 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/HaskellErrorClassifierIf.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/HaskellErrorClassifierIf.java @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with GATE. If not, see . */ -package de.tuclausthal.submissioninterface.testanalyzer.haskell; +package de.tuclausthal.submissioninterface.testanalyzer.haskell.syntax; import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java similarity index 99% rename from src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java rename to src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java index e7ade1266..309fbe212 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/RegexBasedHaskellClustering.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with GATE. If not, see . */ -package de.tuclausthal.submissioninterface.testanalyzer.haskell; +package de.tuclausthal.submissioninterface.testanalyzer.haskell.syntax; import java.util.*; import java.util.regex.Pattern; From 389035c8c54f482886cbe7451c5d26a8379e62a7 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Mon, 21 Apr 2025 16:19:06 +0200 Subject: [PATCH 016/105] Small improvements: imports, formatting --- .../servlets/view/ShowSubmissionStudentView.java | 7 ++++++- .../testanalyzer/CommonErrorAnalyzer.java | 12 +++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java index bb01da31a..62569dca2 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java @@ -23,7 +23,12 @@ import java.io.PrintWriter; import java.util.List; -import de.tuclausthal.submissioninterface.persistence.datamodel.*; +import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError; +import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.Submission; +import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java index 56f631e07..b10c18b8b 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java @@ -62,16 +62,14 @@ public void runAnalysis(final TestResult testResult) { groupCompilerTestResults((CompileTest) test, testResult); } else if (test instanceof JavaAdvancedIOTest) { groupJavaIOTestResults((JavaAdvancedIOTest) test, testResult); - } else if (test instanceof HaskellRuntimeTest haskellRuntimeTest) { - groupHaskellRuntimeTestResults(haskellRuntimeTest, testResult); } else if (test instanceof JUnitTest) { groupJUnitTestResults((JUnitTest) test, testResult); - } - else if (test instanceof DockerTest) { - groupDockerTestResults((DockerTest) test, testResult); - } - else if (test instanceof HaskellSyntaxTest){ + } else if (test instanceof HaskellSyntaxTest) { groupHaskellSyntaxTestResults((HaskellSyntaxTest) test, testResult); + } else if (test instanceof HaskellRuntimeTest haskellRuntimeTest) { + groupHaskellRuntimeTestResults(haskellRuntimeTest, testResult); + } else if (test instanceof DockerTest) { + groupDockerTestResults((DockerTest) test, testResult); } } From b9865a18ea2af7103ac78cc8c1d7126d72f68a36 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Tue, 22 Apr 2025 18:29:25 +0200 Subject: [PATCH 017/105] Forward requests from HaskellRuntimeTestManager to DockerTestManager - DockerTestManager now also handles case of HaskellRuntimeTest (since HaskellRuntimeTest is a specialization of DockerTest) - HaskellRuntimeTestManager is kept for consistency (it is the corresponding controller servlet of HaskellRuntimeTestManagerView) --- .../controller/DockerTestManager.java | 19 ++- .../controller/HaskellRuntimeTestManager.java | 113 +----------------- 2 files changed, 16 insertions(+), 116 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/DockerTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/DockerTestManager.java index 28b5add0d..5a58196f5 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/DockerTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/DockerTestManager.java @@ -33,6 +33,7 @@ import de.tuclausthal.submissioninterface.persistence.dao.ParticipationDAOIf; import de.tuclausthal.submissioninterface.persistence.dao.TestDAOIf; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTestStep; import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; import de.tuclausthal.submissioninterface.persistence.datamodel.ParticipationRole; @@ -40,6 +41,7 @@ import de.tuclausthal.submissioninterface.servlets.GATEController; import de.tuclausthal.submissioninterface.servlets.RequestAdapter; import de.tuclausthal.submissioninterface.servlets.view.DockerTestManagerOverView; +import de.tuclausthal.submissioninterface.servlets.view.HaskellRuntimeTestManagerView; import de.tuclausthal.submissioninterface.servlets.view.MessageView; import de.tuclausthal.submissioninterface.util.Util; @@ -71,7 +73,11 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro } request.setAttribute("test", test); - getServletContext().getNamedDispatcher(DockerTestManagerOverView.class.getSimpleName()).forward(request, response); + + String testManagerViewClassSimpleName = test instanceof HaskellRuntimeTest ? + HaskellRuntimeTestManagerView.class.getSimpleName() : DockerTestManagerOverView.class.getSimpleName(); + + getServletContext().getNamedDispatcher(testManagerViewClassSimpleName).forward(request, response); } @Override @@ -93,6 +99,9 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr return; } + String testManagerClassSimpleName = test instanceof HaskellRuntimeTest ? + HaskellRuntimeTestManager.class.getSimpleName() : DockerTestManager.class.getSimpleName(); + if ("edittest".equals(request.getParameter("action"))) { Transaction tx = session.beginTransaction(); test.setTestTitle(request.getParameter("title")); @@ -101,7 +110,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr test.setGiveDetailsToStudents(request.getParameter("giveDetailsToStudents") != null); test.setPreparationShellCode(request.getParameter("preparationcode").replaceAll("\r\n", "\n")); tx.commit(); - response.sendRedirect(Util.generateRedirectURL(DockerTestManager.class.getSimpleName() + "?testid=" + test.getId(), response)); + response.sendRedirect(Util.generateRedirectURL(testManagerClassSimpleName + "?testid=" + test.getId(), response)); return; } else if ("addNewStep".equals(request.getParameter("action"))) { String title = request.getParameter("title"); @@ -111,7 +120,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr Transaction tx = session.beginTransaction(); session.persist(newStep); tx.commit(); - response.sendRedirect(Util.generateRedirectURL(DockerTestManager.class.getSimpleName() + "?testid=" + test.getId(), response)); + response.sendRedirect(Util.generateRedirectURL(testManagerClassSimpleName + "?testid=" + test.getId(), response)); return; } else if ("updateStep".equals(request.getParameter("action"))) { DockerTestStep step = null; @@ -130,7 +139,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr step.setExpect(Objects.toString(request.getParameter("expect"), "").replaceAll("\r\n", "\n")); tx.commit(); } - response.sendRedirect(Util.generateRedirectURL(DockerTestManager.class.getSimpleName() + "?testid=" + test.getId(), response)); + response.sendRedirect(Util.generateRedirectURL(testManagerClassSimpleName + "?testid=" + test.getId(), response)); return; } else if ("deleteStep".equals(request.getParameter("action"))) { DockerTestStep step = null; @@ -145,7 +154,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr session.remove(step); tx.commit(); } - response.sendRedirect(Util.generateRedirectURL(DockerTestManager.class.getSimpleName() + "?testid=" + test.getId(), response)); + response.sendRedirect(Util.generateRedirectURL(testManagerClassSimpleName + "?testid=" + test.getId(), response)); return; } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 0bbf8a3d0..f32771ee1 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -19,29 +19,14 @@ package de.tuclausthal.submissioninterface.servlets.controller; -import de.tuclausthal.submissioninterface.persistence.dao.DAOFactory; -import de.tuclausthal.submissioninterface.persistence.dao.ParticipationDAOIf; -import de.tuclausthal.submissioninterface.persistence.dao.TestDAOIf; -import de.tuclausthal.submissioninterface.persistence.datamodel.Test; -import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; -import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTestStep; -import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; -import de.tuclausthal.submissioninterface.persistence.datamodel.ParticipationRole; import de.tuclausthal.submissioninterface.servlets.GATEController; -import de.tuclausthal.submissioninterface.servlets.RequestAdapter; -import de.tuclausthal.submissioninterface.servlets.view.HaskellRuntimeTestManagerView; -import de.tuclausthal.submissioninterface.servlets.view.MessageView; -import de.tuclausthal.submissioninterface.util.Util; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.hibernate.Session; -import org.hibernate.Transaction; import java.io.IOException; import java.io.Serial; -import java.util.Objects; /** * Controller-Servlet for clustering haskell submissions based on common errors (dynamic/runtime analysis). @@ -56,105 +41,11 @@ public class HaskellRuntimeTestManager extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - // similar code in DockerTestManager - Session session = RequestAdapter.getSession(request); - - TestDAOIf testDAOIf = DAOFactory.TestDAOIf(session); - Test test = testDAOIf.getTest(Util.parseInteger(request.getParameter("testid"), 0)); - if (!(test instanceof HaskellRuntimeTest haskellRuntimeTest)) { - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - request.setAttribute("title", "Test nicht gefunden"); - getServletContext().getNamedDispatcher(MessageView.class.getSimpleName()).forward(request, response); - return; - } - - ParticipationDAOIf participationDAO = DAOFactory.ParticipationDAOIf(session); - Participation participation = participationDAO.getParticipation(RequestAdapter.getUser(request), haskellRuntimeTest.getTask().getTaskGroup().getLecture()); - if (participation == null || participation.getRoleType() != ParticipationRole.ADVISOR) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "insufficient rights"); - return; - } - - request.setAttribute("test", haskellRuntimeTest); - getServletContext().getNamedDispatcher(HaskellRuntimeTestManagerView.class.getSimpleName()).forward(request, response); + getServletContext().getNamedDispatcher(DockerTestManager.class.getSimpleName()).forward(request, response); } @Override public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - // similar code in DockerTestManager - Session session = RequestAdapter.getSession(request); - TestDAOIf testDAOIf = DAOFactory.TestDAOIf(session); - Test test = testDAOIf.getTest(Util.parseInteger(request.getParameter("testid"), 0)); - if (!(test instanceof HaskellRuntimeTest haskellRuntimeTest)) { - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - request.setAttribute("title", "Test nicht gefunden"); - getServletContext().getNamedDispatcher(MessageView.class.getSimpleName()).forward(request, response); - return; - } - - ParticipationDAOIf participationDAO = DAOFactory.ParticipationDAOIf(session); - Participation participation = participationDAO.getParticipation(RequestAdapter.getUser(request), haskellRuntimeTest.getTask().getTaskGroup().getLecture()); - if (participation == null || participation.getRoleType() != ParticipationRole.ADVISOR) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "insufficient rights"); - return; - } - - if ("edittest".equals(request.getParameter("action"))) { - Transaction tx = session.beginTransaction(); - haskellRuntimeTest.setTestTitle(request.getParameter("title")); - haskellRuntimeTest.setForTutors(request.getParameter("tutortest") != null); - haskellRuntimeTest.setTimesRunnableByStudents(Util.parseInteger(request.getParameter("timesRunnableByStudents"), 0)); - haskellRuntimeTest.setGiveDetailsToStudents(request.getParameter("giveDetailsToStudents") != null); - haskellRuntimeTest.setPreparationShellCode(request.getParameter("preparationcode").replaceAll("\r\n", "\n")); // TODO@CHW: can getParameter("preparationcode") be null? - tx.commit(); - response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); - return; - } else if ("addNewStep".equals(request.getParameter("action"))) { - String title = request.getParameter("title"); - String testCode = request.getParameter("testcode").replaceAll("\r\n", "\n"); - String expect = request.getParameter("expect").replaceAll("\r\n", "\n"); - DockerTestStep newStep = new DockerTestStep(haskellRuntimeTest, title, testCode, expect); - Transaction tx = session.beginTransaction(); - session.persist(newStep); - tx.commit(); - response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); - return; - } else if ("updateStep".equals(request.getParameter("action"))) { - DockerTestStep step = null; - for (DockerTestStep istep : haskellRuntimeTest.getTestSteps()) { - if (istep.getTeststepid() == Util.parseInteger(request.getParameter("teststepid"), -1)) { - step = istep; - break; - } - } - if (step != null) { - Transaction tx = session.beginTransaction(); - String title = request.getParameter("title"); - String testCode = request.getParameter("testcode").replaceAll("\r\n", "\n"); - step.setTitle(title); - step.setTestcode(testCode); - step.setExpect(Objects.toString(request.getParameter("expect"), "").replaceAll("\r\n", "\n")); - tx.commit(); - } - response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); - return; - } else if ("deleteStep".equals(request.getParameter("action"))) { - DockerTestStep step = null; - for (DockerTestStep istep : haskellRuntimeTest.getTestSteps()) { - if (istep.getTeststepid() == Util.parseInteger(request.getParameter("teststepid"), -1)) { - step = istep; - break; - } - } - if (step != null) { - Transaction tx = session.beginTransaction(); - session.remove(step); - tx.commit(); - } - response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); - return; - } - - response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "invalid request"); + getServletContext().getNamedDispatcher(DockerTestManager.class.getSimpleName()).forward(request, response); } } From bcc716a1566b113b51cedd25694c15985a9600d6 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Tue, 22 Apr 2025 19:02:57 +0200 Subject: [PATCH 018/105] Fix wrong html closing tags --- .../servlets/view/DockerTestManagerOverView.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/DockerTestManagerOverView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/DockerTestManagerOverView.java index dce171887..199b0bc13 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/DockerTestManagerOverView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/DockerTestManagerOverView.java @@ -92,7 +92,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println("

Testschritte

"); for (DockerTestStep step : test.getTestSteps()) { - out.println("

" + Util.escapeHTML(step.getTitle()) + "

"); + out.println("

" + Util.escapeHTML(step.getTitle()) + "

"); out.println("
"); out.println(""); out.println(""); @@ -125,7 +125,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println("
"); - out.println("

Neuer Test-Schritt (?)

"); + out.println("

Neuer Test-Schritt (?)

"); out.println("
Hilfe:
"); out.println("

Diese Art von Test erlaubt es beliebige einfache Ausgabe-Tests zu definieren. Mit dem Preparation-Code können vorbereitende Schritte als Bash-Skript programmiert werden. Ist dieser Schritt erfolgreich, werden die einzelnen Testschritte nacheinander aufgerufen, wobei für jeden Testschritt die Ausgabe auf STDOUT mit einem erwartetem Wert überprüft werden.

"); From 3f475744b5ab4773ff781f6e743f33dda430a77a Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Tue, 22 Apr 2025 23:58:15 +0200 Subject: [PATCH 019/105] replace code duplicate with already defined function util function --- .../view/fragments/ShowHaskellSyntaxTestResult.java | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java index e86f6f3e1..27e37be12 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java @@ -21,6 +21,7 @@ import java.io.PrintWriter; import java.io.StringReader; +import de.tuclausthal.submissioninterface.util.Util; import jakarta.json.Json; import jakarta.json.JsonObject; @@ -35,19 +36,9 @@ public static void printTestResults(PrintWriter out, HaskellSyntaxTest test, Str if (!stderr.isEmpty()) { out.println("

Fehlerausgabe (stderr):

"); - out.println("
" + escapeHTML(stderr) + "
"); + out.println("
" + Util.escapeHTML(stderr) + "
"); } out.println("
"); } - - private static String escapeHTML(String str) { - if (str == null) { - return ""; - } - return str - .replace("&", "&") - .replace("<", "<") - .replace(">", ">"); - } } \ No newline at end of file From a60dfe619c914e9d5f7eab08210d44614395d663 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Wed, 23 Apr 2025 14:58:23 +0200 Subject: [PATCH 020/105] remove code duplicates --- .../datamodel/HaskellSyntaxTest.java | 23 +--- .../view/PerformStudentTestResultView.java | 7 +- .../servlets/view/PerformTestResultView.java | 7 +- .../view/ShowSubmissionStudentView.java | 6 +- .../servlets/view/ShowSubmissionView.java | 7 +- .../servlets/view/ShowTaskStudentView.java | 15 +-- .../testframework/tests/impl/DockerTest.java | 127 ++++++++++-------- .../tests/impl/HaskellSyntaxTest.java | 110 +++------------ 8 files changed, 106 insertions(+), 196 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java index 283d6e036..8af4cfa8e 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java @@ -18,36 +18,17 @@ */ package de.tuclausthal.submissioninterface.persistence.datamodel; -import java.lang.invoke.MethodHandles; - import jakarta.persistence.Entity; import jakarta.persistence.Transient; -import com.fasterxml.jackson.annotation.JsonIgnore; import de.tuclausthal.submissioninterface.testframework.tests.AbstractTest; @Entity -public class HaskellSyntaxTest extends Test { - private static final long serialVersionUID = 1L; - +public class HaskellSyntaxTest extends DockerTest { @Override @Transient - @JsonIgnore - public AbstractTest getTestImpl() { + public AbstractTest getTestImpl() { return new de.tuclausthal.submissioninterface.testframework.tests.impl.HaskellSyntaxTest(this); } - - @Override - @Transient - public boolean TutorsCanRun() { - return true; - } - - @Override - public String toString() { - return MethodHandles.lookup().lookupClass().getSimpleName() - + " (" + Integer.toHexString(hashCode()) + "): id:" + getId() - + "; testtitle:" + getTestTitle(); - } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java index 5fafae195..ea1acd2ce 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.PrintWriter; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -107,10 +108,10 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro if (test.isGiveDetailsToStudents() && !testResult.getTestOutput().isEmpty()) { if (test instanceof JavaAdvancedIOTest jaiot) { ShowJavaAdvancedIOTestResult.printTestResults(out, jaiot, testResult.getTestOutput(), true, null); - } else if (test instanceof DockerTest dt) { - ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), true, null); } else if (test instanceof HaskellSyntaxTest hst){ - //TODO: Add error Output + ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), true, null); + } else if (test instanceof DockerTest dt) { + ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), true, null); } else { out.println("Ausgabe:
" + Util.escapeHTML(testResult.getTestOutput()) + "
"); } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java index 5406530ec..3c17f6c85 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.PrintWriter; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -67,11 +68,11 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr if (!testResult.getTestOutput().isEmpty()) { if (test instanceof JavaAdvancedIOTest jaiot) { ShowJavaAdvancedIOTestResult.printTestResults(out, jaiot, testResult.getTestOutput(), (participation == null || !participation.getRoleType().equals(ParticipationRole.ADVISOR)), null); + } else if (test instanceof HaskellSyntaxTest hst){ + ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), (participation == null || participation.getRoleType().compareTo(ParticipationRole.TUTOR) < 0), null); } else if (test instanceof DockerTest dt) { ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), (participation == null || participation.getRoleType().compareTo(ParticipationRole.TUTOR) < 0), null); - } else if (test instanceof HaskellSyntaxTest hst){ - //TODO: Ein gescheiter Output muss sich hier evtl. überlegt werden - } else { + } else { out.println("Ausgabe:
" + Util.escapeHTML(testResult.getTestOutput()) + "
"); } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java index 62569dca2..a032af9b0 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java @@ -81,12 +81,12 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro if (testResult.getTest() instanceof JavaAdvancedIOTest) { out.println("
"); ShowJavaAdvancedIOTestResult.printTestResults(out, (JavaAdvancedIOTest) testResult.getTest(), testResult.getTestOutput(), true, javaScript); + }else if (testResult.getTest() instanceof HaskellSyntaxTest hst) { + out.println("
"); + ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), false, javaScript); } else if (testResult.getTest() instanceof DockerTest) { out.println("
"); ShowDockerTestResult.printTestResults(out, (DockerTest) testResult.getTest(), testResult.getTestOutput(), true, javaScript); - } else if (testResult.getTest() instanceof HaskellSyntaxTest hst) { - out.println("
"); - ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), false, javaScript); } else { out.println("
"); } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionView.java index f223a07d9..e96a526c0 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionView.java @@ -319,13 +319,12 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro if (testResult.getTest() instanceof JavaAdvancedIOTest jaiot) { out.println("
"); ShowJavaAdvancedIOTestResult.printTestResults(out, jaiot, testResult.getTestOutput(), false, javaScript); - } else if (testResult.getTest() instanceof DockerTest dt) { - out.println("
"); - ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), false, javaScript); } else if (testResult.getTest() instanceof HaskellSyntaxTest hst) { out.println("
"); ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), false, javaScript); - } + } else if (testResult.getTest() instanceof DockerTest dt) { + out.println("
"); + ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), false, javaScript);} else { out.println("
"); } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java index 29329ed6b..5f1bb3ade 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java @@ -29,6 +29,8 @@ import java.util.List; import java.util.Random; +import de.tuclausthal.submissioninterface.persistence.datamodel.*; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -42,18 +44,7 @@ import de.tuclausthal.submissioninterface.persistence.dao.PointGivenDAOIf; import de.tuclausthal.submissioninterface.persistence.dao.TestCountDAOIf; import de.tuclausthal.submissioninterface.persistence.dao.impl.TestResultCommonErrorDAO; -import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError; -import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; -import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; -import de.tuclausthal.submissioninterface.persistence.datamodel.MCOption; -import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; -import de.tuclausthal.submissioninterface.persistence.datamodel.PointCategory; -import de.tuclausthal.submissioninterface.persistence.datamodel.PointGiven; import de.tuclausthal.submissioninterface.persistence.datamodel.Points.PointStatus; -import de.tuclausthal.submissioninterface.persistence.datamodel.Submission; -import de.tuclausthal.submissioninterface.persistence.datamodel.Task; -import de.tuclausthal.submissioninterface.persistence.datamodel.Test; -import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; import de.tuclausthal.submissioninterface.servlets.GATEView; import de.tuclausthal.submissioninterface.servlets.RequestAdapter; import de.tuclausthal.submissioninterface.servlets.controller.CloseSubmissionByStudent; @@ -385,6 +376,8 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro if (!testResult.getTestOutput().isEmpty() && testResult.getTest().isGiveDetailsToStudents()) { if (testResult.getTest() instanceof JavaAdvancedIOTest) { ShowJavaAdvancedIOTestResult.printTestResults(out, (JavaAdvancedIOTest) testResult.getTest(), testResult.getTestOutput(), true, null); + } else if (testResult.getTest() instanceof HaskellSyntaxTest) { + ShowHaskellSyntaxTestResult.printTestResults(out, (HaskellSyntaxTest) testResult.getTest(), testResult.getTestOutput(), true, null); } else if (testResult.getTest() instanceof DockerTest) { ShowDockerTestResult.printTestResults(out, (DockerTest) testResult.getTest(), testResult.getTestOutput(), true, null); } else { diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java index 14e8572ea..737e529a1 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java @@ -38,92 +38,71 @@ import de.tuclausthal.submissioninterface.testframework.executor.TestExecutorTestResult; import de.tuclausthal.submissioninterface.util.Util; -/** - * @author Sven Strickroth - */ public class DockerTest extends TempDirTest { final static private Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); final static public String SAFE_DOCKER_SCRIPT = "/usr/local/bin/safe-docker"; - private static final Random random = new Random(); - private final String separator; - private Path tempDir; + protected final String separator; + protected Path tempDir; - public DockerTest(final de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest test) { + public DockerTest(de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest test) { super(test); separator = "##"; } @Override - public void performTest(final Path basePath, final Path submissionPath, final TestExecutorTestResult testResult) throws Exception { + public final void performTest(final Path basePath, final Path submissionPath, final TestExecutorTestResult testResult) throws Exception { try { tempDir = Util.createTemporaryDirectory("test"); - //Configuration.getInstance().getDataPath() if (tempDir == null) { throw new IOException("Failed to create tempdir!"); } - final Path administrativeDir = tempDir.resolve("administrative"); - Files.createDirectories(administrativeDir); - + final Path adminDir = tempDir.resolve("administrative"); final Path studentDir = tempDir.resolve("student"); + Files.createDirectories(adminDir); Files.createDirectories(studentDir); - Util.recursiveCopy(submissionPath, studentDir); - StringBuilder testCode = new StringBuilder(); - testCode.append("#!/bin/bash\n"); - testCode.append("set -e\n"); - testCode.append(test.getPreparationShellCode()); - testCode.append("\n"); - - for (DockerTestStep testStep : test.getTestSteps()) { - testCode.append("echo '" + separator + "'\n"); - testCode.append("echo '" + separator + "' >&2\n"); - testCode.append("{\n"); - testCode.append("set -e\n"); - testCode.append(testStep.getTestcode()); - testCode.append("\n"); - testCode.append("}\n"); + String testCode = generateTestShellScript(); + Path testScript = adminDir.resolve("test.sh"); + try (Writer fw = Files.newBufferedWriter(testScript)) { + fw.write(testCode); } - final Path testDriver = administrativeDir.resolve("test.sh"); - try (Writer fw = Files.newBufferedWriter(testDriver)) { - fw.write(testCode.toString()); - } List params = new ArrayList<>(); params.add("sudo"); params.add(SAFE_DOCKER_SCRIPT); params.add("--timeout=" + test.getTimeout()); - params.add("--dir=" + Util.escapeCommandlineArguments(administrativeDir.toAbsolutePath().toString())); + params.add("--dir=" + Util.escapeCommandlineArguments(adminDir.toAbsolutePath().toString())); params.add("--"); params.add("bash"); - params.add(Util.escapeCommandlineArguments(testDriver.toAbsolutePath().toString())); + params.add(Util.escapeCommandlineArguments(testScript.toAbsolutePath().toString())); ProcessBuilder pb = new ProcessBuilder(params); pb.directory(studentDir.toFile()); - /* only forward explicitly specified environment variables to test processes */ pb.environment().keySet().removeIf(key -> !("PATH".equalsIgnoreCase(key) || "USER".equalsIgnoreCase(key) || "LANG".equalsIgnoreCase(key))); - LOG.debug("Executing external process: {} in {}", params, studentDir); + LOG.debug("Executing {} docker process: {}", this.getClass().getSimpleName(), params); + Process process = pb.start(); - ProcessOutputGrabber outputGrapper = new ProcessOutputGrabber(process); - // no need to check for timeout, we fully rely on the safe-docker script here - int exitValue = -1; + ProcessOutputGrabber outputGrabber = new ProcessOutputGrabber(process); + + + int exitCode = -1; boolean aborted = false; try { - exitValue = process.waitFor(); + exitCode = process.waitFor(); } catch (InterruptedException e) { aborted = true; } - outputGrapper.waitFor(); - if (exitValue == 23 || exitValue == 24) { // magic value of the safe-docker script (23=timeout, 24=oom) - aborted = true; - } + outputGrabber.waitFor(); + if (exitCode == 23 || exitCode == 24) aborted = true; - boolean exitedCleanly = (exitValue == 0); - testResult.setTestPassed(calculateTestResult(exitedCleanly, outputGrapper.getStdOutBuffer(), outputGrapper.getStdErrBuffer(), exitValue, aborted)); - testResult.setTestOutput(outputGrapper.getStdOutBuffer().toString()); + boolean success = isSuccessful(exitCode, outputGrabber.getStdErrBuffer()); + String outPutJSON = generateJsonResult(outputGrabber.getStdOutBuffer(), outputGrabber.getStdErrBuffer(), exitCode, success, aborted); + testResult.setTestPassed(success); + testResult.setTestOutput(outPutJSON); } finally { if (tempDir != null) { Util.recursiveDelete(tempDir); @@ -131,8 +110,48 @@ public void performTest(final Path basePath, final Path submissionPath, final Te } } - // similar code in JavaAdvancedIOTest - protected boolean calculateTestResult(boolean exitedCleanly, final StringBuffer processOutput, final StringBuffer stdErr, final int exitCode, final boolean aborted) { + protected ProcessOutputGrabber executeDockerContainer (Path adminDir, Path studentDir, Path testScript) throws Exception { + List params = new ArrayList<>(); + params.add("sudo"); + params.add(SAFE_DOCKER_SCRIPT); + params.add("--timeout=" + test.getTimeout()); + params.add("--dir=" + Util.escapeCommandlineArguments(adminDir.toAbsolutePath().toString())); + params.add("--"); + params.add("bash"); + params.add(Util.escapeCommandlineArguments(testScript.toAbsolutePath().toString())); + + ProcessBuilder pb = new ProcessBuilder(params); + pb.directory(studentDir.toFile()); + pb.environment().keySet().removeIf(key -> !("PATH".equalsIgnoreCase(key) || "USER".equalsIgnoreCase(key) || "LANG".equalsIgnoreCase(key))); + LOG.debug("Executing {} docker process: {}", this.getClass().getSimpleName(), params); + Process process = pb.start(); + ProcessOutputGrabber outputGrabber = new ProcessOutputGrabber(process); + return outputGrabber; + } + + protected boolean isSuccessful(int exitCode, StringBuffer stderr) { + return exitCode == 0; + } + + protected String generateTestShellScript() { + StringBuilder testCode = new StringBuilder(); + testCode.append("#!/bin/bash\n"); + testCode.append("set -e\n"); + testCode.append(test.getPreparationShellCode()); + testCode.append("\n"); + for (DockerTestStep testStep : test.getTestSteps()) { + testCode.append("echo '" + separator + "'\n"); + testCode.append("echo '" + separator + "' >&2\n"); + testCode.append("{\n"); + testCode.append("set -e\n"); + testCode.append(testStep.getTestcode()); + testCode.append("\n"); + testCode.append("}\n"); + } + return testCode.toString(); + } + + protected String generateJsonResult(StringBuffer processOutput, StringBuffer stdErr, int exitCode, boolean exitedCleanly, boolean aborted) { JsonObjectBuilder builder = Json.createObjectBuilder(); builder.add("stdout", processOutput.toString()); if (stdErr.length() > 0) { @@ -145,7 +164,7 @@ protected boolean calculateTestResult(boolean exitedCleanly, final StringBuffer builder.add("exitCode", exitCode); builder.add("exitedCleanly", exitedCleanly); if (aborted) { - builder.add("time-exceeded", aborted); + builder.add("time-exceeded", true); } int start = 0; @@ -164,23 +183,17 @@ protected boolean calculateTestResult(boolean exitedCleanly, final StringBuffer job.add("id", test.getTestSteps().get(i).getTeststepid()); job.add("got", outputs.get(i + 1)); job.add("expected", test.getTestSteps().get(i).getExpect()); - if (!outputs.get(i + 1).trim().equals(test.getTestSteps().get(i).getExpect().trim())) { - exitedCleanly = false; - job.add("ok", false); - } else { - job.add("ok", true); - } + job.add("ok", outputs.get(i + 1).trim().equals(test.getTestSteps().get(i).getExpect().trim())); arrb.add(job); } builder.add("steps", arrb); if (i < test.getTestSteps().size()) { builder.add("missing-tests", true); - exitedCleanly = false; } processOutput.setLength(0); processOutput.append(builder.build().toString()); - return exitedCleanly; + return builder.build().toString(); } @Override diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java index e9e42cc84..147dd3907 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java @@ -18,112 +18,39 @@ */ package de.tuclausthal.submissioninterface.testframework.tests.impl; -import java.io.IOException; -import java.io.Writer; -import java.lang.invoke.MethodHandles; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Random; - import jakarta.json.Json; import jakarta.json.JsonObjectBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import de.tuclausthal.submissioninterface.testframework.executor.TestExecutorTestResult; -import de.tuclausthal.submissioninterface.util.Util; -public class HaskellSyntaxTest extends TempDirTest { - private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private static final String SAFE_DOCKER_SCRIPT = "/usr/local/bin/safe-docker"; - private static final Random RANDOM = new Random(); - private final String separator; - private Path tempDir; +public class HaskellSyntaxTest extends DockerTest { public HaskellSyntaxTest(de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest test) { super(test); - this.separator = "##"; } - @Override - public void performTest(Path basePath, Path submissionPath, TestExecutorTestResult testResult) throws Exception { - try { - tempDir = Util.createTemporaryDirectory("test"); - if (tempDir == null) { - throw new IOException("Failed to create tempdir!"); - } - - Path adminDir = tempDir.resolve("admin"); - Path studentDir = tempDir.resolve("student"); - Files.createDirectories(adminDir); - Files.createDirectories(studentDir); - - Util.recursiveCopy(submissionPath, studentDir); - - String bashScript = """ - #!/bin/bash - set -e - echo '%s' - for file in *.hs; do - ghci -ignore-dot-ghci -v0 -ferror-spans -fdiagnostics-color=never -Wall -e ":load $file" -e ":quit" - done - """.formatted(separator); - - Path testScript = adminDir.resolve("test.sh"); - try (Writer writer = Files.newBufferedWriter(testScript)) { - writer.write(bashScript); - } - - List cmd = List.of( - "sudo", - SAFE_DOCKER_SCRIPT, - "--timeout=" + test.getTimeout(), - "--dir=" + Util.escapeCommandlineArguments(adminDir.toAbsolutePath().toString()), - "--", - "bash", - Util.escapeCommandlineArguments(testScript.toAbsolutePath().toString()) - ); - - ProcessBuilder pb = new ProcessBuilder(cmd); - pb.directory(studentDir.toFile()); - pb.environment().keySet().removeIf(k -> !List.of("PATH", "USER", "LANG").contains(k)); - - LOG.debug("Executing HaskellSyntaxTest docker process: {}", cmd); - Process proc = pb.start(); - ProcessOutputGrabber outputGrabber = new ProcessOutputGrabber(proc); - - int exitCode = -1; - boolean aborted = false; - try { - exitCode = proc.waitFor(); - } catch (InterruptedException e) { - aborted = true; - } - outputGrabber.waitFor(); - - if (exitCode == 23 || exitCode == 24) aborted = true; - - boolean success = (exitCode == 0) && !outputGrabber.getStdErrBuffer().toString().toLowerCase().contains("error:"); - - testResult.setTestPassed(success); - testResult.setTestOutput(generateJsonResult(outputGrabber.getStdOutBuffer(), outputGrabber.getStdErrBuffer(), exitCode, success, aborted)); + protected String generateTestShellScript() { + return """ + #!/bin/bash + set -e + echo '%s' + for file in *.hs; do + ghci -ignore-dot-ghci -v0 -ferror-spans -fdiagnostics-color=never -Wall -e ":load $file" -e ":quit" + done + """.formatted(separator); + } - } finally { - if (tempDir != null) { - Util.recursiveDelete(tempDir); - } - } + @Override + protected boolean isSuccessful(int exitCode, StringBuffer stderr) { + return exitCode == 0 && !stderr.toString().toLowerCase().contains("error:"); } - private String generateJsonResult(StringBuffer stdout, StringBuffer stderr, int exitCode, boolean success, boolean aborted) { + @Override + protected String generateJsonResult(StringBuffer stdout, StringBuffer stderr, int exitCode, boolean success, boolean aborted) { JsonObjectBuilder builder = Json.createObjectBuilder() .add("stdout", stdout.toString()) .add("separator", separator + "\n") .add("exitCode", exitCode) .add("exitedCleanly", success); - if (stderr.length() > 0) { builder.add("stderr", stderr.toString()); } @@ -135,9 +62,4 @@ private String generateJsonResult(StringBuffer stdout, StringBuffer stderr, int } return builder.build().toString(); } - - @Override - protected void performTestInTempDir(Path basePath, Path tempDir, TestExecutorTestResult testResult) throws Exception { - //currently unused - } } From cdf93b379e47d79a3cea9bea25ca805877148ce4 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Thu, 24 Apr 2025 01:24:21 +0200 Subject: [PATCH 021/105] reverse unnecessary code changes and focus on keeping DockerTest as it was --- .../ShowHaskellSyntaxTestResult.java | 2 +- .../testframework/tests/impl/DockerTest.java | 173 ++++++++++-------- .../tests/impl/HaskellSyntaxTest.java | 65 +++++-- 3 files changed, 149 insertions(+), 91 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java index 27e37be12..ddf8c882a 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java @@ -35,7 +35,7 @@ public static void printTestResults(PrintWriter out, HaskellSyntaxTest test, Str String stderr = json.getString("stderr", ""); if (!stderr.isEmpty()) { - out.println("

Fehlerausgabe (stderr):

"); + out.println("

Fehlerausgabe:

"); out.println("
" + Util.escapeHTML(stderr) + "
"); } diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java index 737e529a1..3894d7092 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java @@ -38,71 +38,81 @@ import de.tuclausthal.submissioninterface.testframework.executor.TestExecutorTestResult; import de.tuclausthal.submissioninterface.util.Util; +/** + * @author Sven Strickroth + */ public class DockerTest extends TempDirTest { final static private Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); final static public String SAFE_DOCKER_SCRIPT = "/usr/local/bin/safe-docker"; + private static final Random random = new Random(); - protected final String separator; - protected Path tempDir; + private final String separator; + private Path tempDir; - public DockerTest(de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest test) { + public DockerTest(final de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest test) { super(test); separator = "##"; } @Override - public final void performTest(final Path basePath, final Path submissionPath, final TestExecutorTestResult testResult) throws Exception { + public void performTest(final Path basePath, final Path submissionPath, final TestExecutorTestResult testResult) throws Exception { try { tempDir = Util.createTemporaryDirectory("test"); + //Configuration.getInstance().getDataPath() if (tempDir == null) { throw new IOException("Failed to create tempdir!"); } - final Path adminDir = tempDir.resolve("administrative"); + final Path administrativeDir = tempDir.resolve("administrative"); + Files.createDirectories(administrativeDir); + final Path studentDir = tempDir.resolve("student"); - Files.createDirectories(adminDir); Files.createDirectories(studentDir); + Util.recursiveCopy(submissionPath, studentDir); String testCode = generateTestShellScript(); - Path testScript = adminDir.resolve("test.sh"); - try (Writer fw = Files.newBufferedWriter(testScript)) { - fw.write(testCode); - } + final Path testDriver = administrativeDir.resolve("test.sh"); + try (Writer fw = Files.newBufferedWriter(testDriver)) { + fw.write(testCode.toString()); + } List params = new ArrayList<>(); params.add("sudo"); params.add(SAFE_DOCKER_SCRIPT); params.add("--timeout=" + test.getTimeout()); - params.add("--dir=" + Util.escapeCommandlineArguments(adminDir.toAbsolutePath().toString())); + params.add("--dir=" + Util.escapeCommandlineArguments(administrativeDir.toAbsolutePath().toString())); params.add("--"); params.add("bash"); - params.add(Util.escapeCommandlineArguments(testScript.toAbsolutePath().toString())); + params.add(Util.escapeCommandlineArguments(testDriver.toAbsolutePath().toString())); ProcessBuilder pb = new ProcessBuilder(params); pb.directory(studentDir.toFile()); + /* only forward explicitly specified environment variables to test processes */ pb.environment().keySet().removeIf(key -> !("PATH".equalsIgnoreCase(key) || "USER".equalsIgnoreCase(key) || "LANG".equalsIgnoreCase(key))); - LOG.debug("Executing {} docker process: {}", this.getClass().getSimpleName(), params); - - Process process = pb.start(); - ProcessOutputGrabber outputGrabber = new ProcessOutputGrabber(process); + debugLog(params, studentDir); - int exitCode = -1; + Process process = pb.start(); + ProcessOutputGrabber outputGrapper = new ProcessOutputGrabber(process); + // no need to check for timeout, we fully rely on the safe-docker script here + int exitValue = -1; boolean aborted = false; try { - exitCode = process.waitFor(); + exitValue = process.waitFor(); } catch (InterruptedException e) { aborted = true; } - outputGrabber.waitFor(); - if (exitCode == 23 || exitCode == 24) aborted = true; + outputGrapper.waitFor(); + if (exitValue == 23 || exitValue == 24) { // magic value of the safe-docker script (23=timeout, 24=oom) + aborted = true; + } - boolean success = isSuccessful(exitCode, outputGrabber.getStdErrBuffer()); - String outPutJSON = generateJsonResult(outputGrabber.getStdOutBuffer(), outputGrabber.getStdErrBuffer(), exitCode, success, aborted); - testResult.setTestPassed(success); - testResult.setTestOutput(outPutJSON); + boolean exitedCleanly = isProcessSuccessful(exitValue, outputGrapper.getStdErrBuffer()); + + // for modularization and flexibility in child classes + analyzeAndSetResult(exitedCleanly, outputGrapper.getStdOutBuffer(), outputGrapper.getStdErrBuffer(), exitValue, aborted, testResult); } finally { if (tempDir != null) { Util.recursiveDelete(tempDir); @@ -110,62 +120,20 @@ public final void performTest(final Path basePath, final Path submissionPath, fi } } - protected ProcessOutputGrabber executeDockerContainer (Path adminDir, Path studentDir, Path testScript) throws Exception { - List params = new ArrayList<>(); - params.add("sudo"); - params.add(SAFE_DOCKER_SCRIPT); - params.add("--timeout=" + test.getTimeout()); - params.add("--dir=" + Util.escapeCommandlineArguments(adminDir.toAbsolutePath().toString())); - params.add("--"); - params.add("bash"); - params.add(Util.escapeCommandlineArguments(testScript.toAbsolutePath().toString())); - - ProcessBuilder pb = new ProcessBuilder(params); - pb.directory(studentDir.toFile()); - pb.environment().keySet().removeIf(key -> !("PATH".equalsIgnoreCase(key) || "USER".equalsIgnoreCase(key) || "LANG".equalsIgnoreCase(key))); - LOG.debug("Executing {} docker process: {}", this.getClass().getSimpleName(), params); - Process process = pb.start(); - ProcessOutputGrabber outputGrabber = new ProcessOutputGrabber(process); - return outputGrabber; - } - - protected boolean isSuccessful(int exitCode, StringBuffer stderr) { + protected boolean isProcessSuccessful(int exitCode, StringBuffer stderr) { return exitCode == 0; } - protected String generateTestShellScript() { - StringBuilder testCode = new StringBuilder(); - testCode.append("#!/bin/bash\n"); - testCode.append("set -e\n"); - testCode.append(test.getPreparationShellCode()); - testCode.append("\n"); - for (DockerTestStep testStep : test.getTestSteps()) { - testCode.append("echo '" + separator + "'\n"); - testCode.append("echo '" + separator + "' >&2\n"); - testCode.append("{\n"); - testCode.append("set -e\n"); - testCode.append(testStep.getTestcode()); - testCode.append("\n"); - testCode.append("}\n"); - } - return testCode.toString(); + protected void analyzeAndSetResult(boolean exitedCleanly, StringBuffer stdout, StringBuffer stderr, int exitCode, boolean aborted, TestExecutorTestResult result) { + boolean passed = calculateTestResult(exitedCleanly, stdout, stderr, exitCode, aborted); + result.setTestPassed(passed); + result.setTestOutput(stdout.toString()); } - protected String generateJsonResult(StringBuffer processOutput, StringBuffer stdErr, int exitCode, boolean exitedCleanly, boolean aborted) { - JsonObjectBuilder builder = Json.createObjectBuilder(); - builder.add("stdout", processOutput.toString()); - if (stdErr.length() > 0) { - builder.add("stderr", stdErr.toString()); - } - builder.add("separator", separator + "\n"); - if (tempDir != null) { - builder.add("tmpdir", tempDir.toAbsolutePath().toString()); - } - builder.add("exitCode", exitCode); - builder.add("exitedCleanly", exitedCleanly); - if (aborted) { - builder.add("time-exceeded", true); - } + + // similar code in JavaAdvancedIOTest + protected boolean calculateTestResult(boolean exitedCleanly, final StringBuffer processOutput, final StringBuffer stdErr, final int exitCode, final boolean aborted) { + JsonObjectBuilder builder = createJsonBuilder(exitedCleanly, processOutput, stdErr, exitCode, aborted); int start = 0; int splitterPos; @@ -183,19 +151,70 @@ protected String generateJsonResult(StringBuffer processOutput, StringBuffer std job.add("id", test.getTestSteps().get(i).getTeststepid()); job.add("got", outputs.get(i + 1)); job.add("expected", test.getTestSteps().get(i).getExpect()); - job.add("ok", outputs.get(i + 1).trim().equals(test.getTestSteps().get(i).getExpect().trim())); + if (!outputs.get(i + 1).trim().equals(test.getTestSteps().get(i).getExpect().trim())) { + exitedCleanly = false; + job.add("ok", false); + } else { + job.add("ok", true); + } arrb.add(job); } builder.add("steps", arrb); if (i < test.getTestSteps().size()) { builder.add("missing-tests", true); + exitedCleanly = false; } processOutput.setLength(0); processOutput.append(builder.build().toString()); - return builder.build().toString(); + return exitedCleanly; } + protected JsonObjectBuilder createJsonBuilder(boolean exitedCleanly, final StringBuffer processOutput, final StringBuffer stdErr, final int exitCode, final boolean aborted) { + JsonObjectBuilder builder = Json.createObjectBuilder(); + builder.add("stdout", processOutput.toString()); + if (stdErr.length() > 0) { + builder.add("stderr", stdErr.toString()); + } + builder.add("separator", separator + "\n"); + if (tempDir != null) { + builder.add("tmpdir", tempDir.toAbsolutePath().toString()); + } + builder.add("exitCode", exitCode); + builder.add("exitedCleanly", exitedCleanly); + if (aborted) { + builder.add("time-exceeded", aborted); + } + + return builder; + + } + + protected String generateTestShellScript(){ + StringBuilder testCode = new StringBuilder(); + testCode.append("#!/bin/bash\n"); + testCode.append("set -e\n"); + testCode.append(test.getPreparationShellCode()); + testCode.append("\n"); + + for (DockerTestStep testStep : test.getTestSteps()) { + testCode.append("echo '" + separator + "'\n"); + testCode.append("echo '" + separator + "' >&2\n"); + testCode.append("{\n"); + testCode.append("set -e\n"); + testCode.append(testStep.getTestcode()); + testCode.append("\n"); + testCode.append("}\n"); + } + + return testCode.toString(); + } + + protected void debugLog(List params, Path studentDir){ + LOG.debug("Executing external process: {} in {}", params, studentDir); + } + + @Override protected void performTestInTempDir(Path basePath, Path pTempDir, TestExecutorTestResult testResult) throws Exception {} } diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java index 147dd3907..df11357f7 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java @@ -18,39 +18,56 @@ */ package de.tuclausthal.submissioninterface.testframework.tests.impl; +import java.io.IOException; +import java.io.Writer; +import java.lang.invoke.MethodHandles; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Random; + import jakarta.json.Json; import jakarta.json.JsonObjectBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.tuclausthal.submissioninterface.testframework.executor.TestExecutorTestResult; +import de.tuclausthal.submissioninterface.util.Util; public class HaskellSyntaxTest extends DockerTest { + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final String SAFE_DOCKER_SCRIPT = "/usr/local/bin/safe-docker"; + private static final Random RANDOM = new Random(); + private final String separator; + private Path tempDir; public HaskellSyntaxTest(de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest test) { super(test); - } - @Override - protected String generateTestShellScript() { - return """ - #!/bin/bash - set -e - echo '%s' - for file in *.hs; do - ghci -ignore-dot-ghci -v0 -ferror-spans -fdiagnostics-color=never -Wall -e ":load $file" -e ":quit" - done - """.formatted(separator); + this.separator = "##"; } @Override - protected boolean isSuccessful(int exitCode, StringBuffer stderr) { + protected boolean isProcessSuccessful(int exitCode, StringBuffer stderr) { return exitCode == 0 && !stderr.toString().toLowerCase().contains("error:"); } @Override - protected String generateJsonResult(StringBuffer stdout, StringBuffer stderr, int exitCode, boolean success, boolean aborted) { + protected void analyzeAndSetResult(boolean exitedCleanly, StringBuffer stdout, StringBuffer stderr, int exitCode, boolean aborted, TestExecutorTestResult result) { + result.setTestPassed(exitedCleanly); + result.setTestOutput(generateJsonResult(stdout, stderr, exitCode, exitedCleanly, aborted)); + } + + + + + private String generateJsonResult(StringBuffer stdout, StringBuffer stderr, int exitCode, boolean success, boolean aborted) { JsonObjectBuilder builder = Json.createObjectBuilder() .add("stdout", stdout.toString()) .add("separator", separator + "\n") .add("exitCode", exitCode) .add("exitedCleanly", success); + if (stderr.length() > 0) { builder.add("stderr", stderr.toString()); } @@ -62,4 +79,26 @@ protected String generateJsonResult(StringBuffer stdout, StringBuffer stderr, in } return builder.build().toString(); } + + @Override + protected void performTestInTempDir(Path basePath, Path tempDir, TestExecutorTestResult testResult) throws Exception { + //currently unused + } + + @Override + protected String generateTestShellScript(){ + return """ + #!/bin/bash + set -e + echo '%s' + for file in *.hs; do + ghci -ignore-dot-ghci -v0 -ferror-spans -fdiagnostics-color=never -Wall -e ":load $file" -e ":quit" + done + """.formatted(separator); + } + + @Override + protected void debugLog(List params, Path studentDir){ + LOG.debug("Executing HaskellSyntaxTest docker process: {} in {}", params, studentDir); + } } From ee20e7b30e88eddd4365746e4431b14235a721de Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Thu, 24 Apr 2025 23:09:01 +0200 Subject: [PATCH 022/105] remove unnecessary code fragments and modularizations --- .../testframework/tests/impl/DockerTest.java | 6 +--- .../tests/impl/HaskellSyntaxTest.java | 31 ++----------------- 2 files changed, 4 insertions(+), 33 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java index 3894d7092..bef3b5800 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java @@ -109,7 +109,7 @@ public void performTest(final Path basePath, final Path submissionPath, final Te aborted = true; } - boolean exitedCleanly = isProcessSuccessful(exitValue, outputGrapper.getStdErrBuffer()); + boolean exitedCleanly = (exitValue == 0); // for modularization and flexibility in child classes analyzeAndSetResult(exitedCleanly, outputGrapper.getStdOutBuffer(), outputGrapper.getStdErrBuffer(), exitValue, aborted, testResult); @@ -120,10 +120,6 @@ public void performTest(final Path basePath, final Path submissionPath, final Te } } - protected boolean isProcessSuccessful(int exitCode, StringBuffer stderr) { - return exitCode == 0; - } - protected void analyzeAndSetResult(boolean exitedCleanly, StringBuffer stdout, StringBuffer stderr, int exitCode, boolean aborted, TestExecutorTestResult result) { boolean passed = calculateTestResult(exitedCleanly, stdout, stderr, exitCode, aborted); result.setTestPassed(passed); diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java index df11357f7..b5c60b8c5 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java @@ -47,39 +47,14 @@ public HaskellSyntaxTest(de.tuclausthal.submissioninterface.persistence.datamode this.separator = "##"; } - @Override - protected boolean isProcessSuccessful(int exitCode, StringBuffer stderr) { - return exitCode == 0 && !stderr.toString().toLowerCase().contains("error:"); - } - @Override protected void analyzeAndSetResult(boolean exitedCleanly, StringBuffer stdout, StringBuffer stderr, int exitCode, boolean aborted, TestExecutorTestResult result) { - result.setTestPassed(exitedCleanly); - result.setTestOutput(generateJsonResult(stdout, stderr, exitCode, exitedCleanly, aborted)); + boolean success = exitedCleanly && !stderr.toString().toLowerCase().contains("error:"); + result.setTestPassed(success); + result.setTestOutput(createJsonBuilder(exitedCleanly, stdout, stderr, exitCode, aborted).toString()); } - - - private String generateJsonResult(StringBuffer stdout, StringBuffer stderr, int exitCode, boolean success, boolean aborted) { - JsonObjectBuilder builder = Json.createObjectBuilder() - .add("stdout", stdout.toString()) - .add("separator", separator + "\n") - .add("exitCode", exitCode) - .add("exitedCleanly", success); - - if (stderr.length() > 0) { - builder.add("stderr", stderr.toString()); - } - if (aborted) { - builder.add("time-exceeded", true); - } - if (tempDir != null) { - builder.add("tmpdir", tempDir.toAbsolutePath().toString()); - } - return builder.build().toString(); - } - @Override protected void performTestInTempDir(Path basePath, Path tempDir, TestExecutorTestResult testResult) throws Exception { //currently unused From 8e50e4dc6b05d67182b395e2749f7fcd0db89c13 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Thu, 24 Apr 2025 23:57:18 +0200 Subject: [PATCH 023/105] fix bug where json is saved in a wrong format --- .../testframework/tests/impl/HaskellSyntaxTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java index b5c60b8c5..df8038b05 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java @@ -51,7 +51,7 @@ public HaskellSyntaxTest(de.tuclausthal.submissioninterface.persistence.datamode protected void analyzeAndSetResult(boolean exitedCleanly, StringBuffer stdout, StringBuffer stderr, int exitCode, boolean aborted, TestExecutorTestResult result) { boolean success = exitedCleanly && !stderr.toString().toLowerCase().contains("error:"); result.setTestPassed(success); - result.setTestOutput(createJsonBuilder(exitedCleanly, stdout, stderr, exitCode, aborted).toString()); + result.setTestOutput(createJsonBuilder(success, stdout, stderr, exitCode, aborted).build().toString()); } From 76c7d6b04fce4086a8e4004e7df861697aff98cd Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Fri, 25 Apr 2025 00:43:00 +0200 Subject: [PATCH 024/105] fix bug with closing div tag and reduce doubble declaration of variables for HaskellSyntaxTest --- .../view/fragments/ShowHaskellSyntaxTestResult.java | 1 - .../testframework/tests/impl/DockerTest.java | 8 ++++---- .../testframework/tests/impl/HaskellSyntaxTest.java | 6 ------ 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java index ddf8c882a..814d98ccb 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java @@ -39,6 +39,5 @@ public static void printTestResults(PrintWriter out, HaskellSyntaxTest test, Str out.println("
" + Util.escapeHTML(stderr) + "
"); } - out.println(""); } } \ No newline at end of file diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java index bef3b5800..c02c446d2 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java @@ -42,12 +42,12 @@ * @author Sven Strickroth */ public class DockerTest extends TempDirTest { - final static private Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + final static protected Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); final static public String SAFE_DOCKER_SCRIPT = "/usr/local/bin/safe-docker"; - private static final Random random = new Random(); - private final String separator; - private Path tempDir; + protected static final Random random = new Random(); + protected final String separator; + protected Path tempDir; public DockerTest(final de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest test) { super(test); diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java index df8038b05..bbc25128f 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java @@ -36,15 +36,9 @@ import de.tuclausthal.submissioninterface.util.Util; public class HaskellSyntaxTest extends DockerTest { - private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private static final String SAFE_DOCKER_SCRIPT = "/usr/local/bin/safe-docker"; - private static final Random RANDOM = new Random(); - private final String separator; - private Path tempDir; public HaskellSyntaxTest(de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest test) { super(test); - this.separator = "##"; } @Override From 442748f28fb6bcd3b3ed2608be31475350b0149c Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Fri, 25 Apr 2025 20:11:01 +0200 Subject: [PATCH 025/105] Remove manual testcase definition from HaskellRuntimeTest --- .../view/HaskellRuntimeTestManagerView.java | 111 +----------------- 1 file changed, 1 insertion(+), 110 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index d55884955..2a27ba4a0 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -93,117 +93,8 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println(""); out.println("
"); - out.println("

Testschritte

"); - for (DockerTestStep step : test.getTestSteps()) { - out.println("

" + Util.escapeHTML(step.getTitle()) + "

"); - out.println("
"); - out.println(""); - out.println(""); - out.println(""); - out.println("
Titel:
Beschreibung:
"); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println("
Titel:
Testcode:
Erwartete Ausgabe:
"); - out.print("Löschen
"); - out.println(""); - } - - out.println("
"); - - out.println("

Neuer Test-Schritt (?)

"); - - out.println("
Hilfe:
"); - out.println("

Diese Art von Test erlaubt es beliebige einfache Ausgabe-Tests zu definieren. Mit dem Preparation-Code können vorbereitende Schritte als Bash-Skript programmiert werden. Ist dieser Schritt erfolgreich, werden die einzelnen Testschritte nacheinander aufgerufen, wobei für jeden Testschritt die Ausgabe auf STDOUT mit einem erwartetem Wert überprüft werden.

"); - out.println("

Preparation-Code z. B.:

"); - /* @formatter:off */ - out.println("

Test-Schritt-Definition z. B.:
"+ - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "
Titel:
Testcode:
Erwartete Ausgabe:

"); - out.println("

Ausgabe bei Testdurchführung z. B.:
" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "
TestErwartetErhaltenOK?
Eingabe \"gut\"
Geben Sie ein hochdeutsches Wort ein:\n"
-                + "\"guad\"
Geben Sie ein hochdeutsches Wort ein:\n"
-                + "\"guad\"" +
-                "
ja
Leere Eingabe
ja

"); - /* @formatter:on */ - out.println("

Die erwartete Ausgabe und tatsächliche Ausgabe wird getrimmt und hinsichtlich der Zeilenenden auf \"\\n\" normalisiert und mittels exaktem Stringvergleich verglichen. Im Testcode kann beliebiger Bash-Code verwendet werden. In der Umgebung ist per Default \"set -e\" gesetzt, so dass das Skript nach einem nicht behandelten Fehler sofort abgebrochen wird.

"); - out.println("

Wird das Bash-Skript vorzeitig beendet, erhalten die Studierenden die Ausgabe \"Nicht alle Tests wurden durchlaufen. Das Program wurde nicht ordentlich beendet.\", wobei die Tabelle alle bisherigen zzgl. den zuletzt ausgeführten Test zeigt (die Spalte \"Erhalten\" ist dann ggf. leer). Bricht das Testskript nach der Preparation-Code-Phase ab, wird dies den Studierenden als Laufzeitfehler angezeigt und der Inhalt von STDERR seit dem Beginn des Testschritts bereitgestellt. Bricht das Testskript beim Preparationcode mit dem ExitCode 15 ab, wird den Studierenden nur der Text \"Der zu testende Code ist syntaktisch nicht korrekt und kann daher nicht getestet werden.\" angezeigt, ansonsten, dass ein Syntaxfehler aufgetreten ist inkl. Inhalt von STDERR.

"); - out.println("

Diese Art von Test ist für einfache Ausgabetests ausgerichtet, es können aber auch approximative Tests oder nahezu beliebige Überprüfungen durchgefühert werden, z.B. erwartet \"True\" und Testcode \"ghci -e \"pi_approx 6 < 3.0 && pi_approx 6 > 2.99\" pi_approx.hs\".

"); - out.println("
"); - - out.println("
"); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.print(""); - out.println(""); - out.println("
Titel:
Testcode:
Erwartete Ausgabe:
Abbrechen
"); - out.println("
"); + out.println("

Automated testcase generation is not yet implemented.

"); template.printTemplateFooter(); } From 8716465759f31e5d8fdde0576f264560dd93d4b7 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Thu, 8 May 2025 18:18:03 +0200 Subject: [PATCH 026/105] Add new line at end of each file --- .../servlets/view/fragments/ShowHaskellSyntaxTestResult.java | 2 +- .../testanalyzer/haskell/syntax/HaskellErrorClassifierIf.java | 2 +- .../haskell/syntax/RegexBasedHaskellClustering.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java index 814d98ccb..a93658f67 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java @@ -40,4 +40,4 @@ public static void printTestResults(PrintWriter out, HaskellSyntaxTest test, Str } } -} \ No newline at end of file +} diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/HaskellErrorClassifierIf.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/HaskellErrorClassifierIf.java index 00d205871..a80ab3cbe 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/HaskellErrorClassifierIf.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/HaskellErrorClassifierIf.java @@ -22,4 +22,4 @@ public interface HaskellErrorClassifierIf { void classify(TestResult testResult, String stderr, String keyStr); -} \ No newline at end of file +} diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java index 309fbe212..e3f31dfef 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java @@ -122,4 +122,4 @@ public void classify(TestResult testResult, String stderr, String keyStr) { commonErrorDAO.newCommonError(keyStr + "Nicht klassifiziert", "Unklassifiziert", testResult, null); } -} \ No newline at end of file +} From 402f094a4df0974a8696a3e2214aeb87b4d8d919 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Thu, 8 May 2025 20:10:44 +0200 Subject: [PATCH 027/105] Add action "generateNewTestSteps" to haskell runtime test - the generator is still under development - added a placeholder instead --- .../controller/HaskellRuntimeTestManager.java | 55 ++++++++++++++++++- .../view/HaskellRuntimeTestManagerView.java | 52 +++++++++++++++++- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index f32771ee1..aa6a68bf8 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -19,18 +19,27 @@ package de.tuclausthal.submissioninterface.servlets.controller; +import de.tuclausthal.submissioninterface.persistence.dao.DAOFactory; +import de.tuclausthal.submissioninterface.persistence.dao.ParticipationDAOIf; +import de.tuclausthal.submissioninterface.persistence.dao.TestDAOIf; +import de.tuclausthal.submissioninterface.persistence.datamodel.*; import de.tuclausthal.submissioninterface.servlets.GATEController; +import de.tuclausthal.submissioninterface.servlets.RequestAdapter; +import de.tuclausthal.submissioninterface.servlets.view.MessageView; +import de.tuclausthal.submissioninterface.util.Util; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.hibernate.Session; +import org.hibernate.Transaction; import java.io.IOException; import java.io.Serial; /** * Controller-Servlet for clustering haskell submissions based on common errors (dynamic/runtime analysis). - * This servlet allows advisors to manage (add, edit, remove) test steps. + * This servlet allows advisors to automatically generate and modify test steps. * * @author Christian Wagner */ @@ -46,6 +55,48 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro @Override public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - getServletContext().getNamedDispatcher(DockerTestManager.class.getSimpleName()).forward(request, response); + Session session = RequestAdapter.getSession(request); + TestDAOIf testDAOIf = DAOFactory.TestDAOIf(session); + Test test = testDAOIf.getTest(Util.parseInteger(request.getParameter("testid"), 0)); + if (!(test instanceof HaskellRuntimeTest haskellRuntimeTest)) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + request.setAttribute("title", "Test nicht gefunden"); + getServletContext().getNamedDispatcher(MessageView.class.getSimpleName()).forward(request, response); + return; + } + + ParticipationDAOIf participationDAO = DAOFactory.ParticipationDAOIf(session); + Participation participation = participationDAO.getParticipation(RequestAdapter.getUser(request), haskellRuntimeTest.getTask().getTaskGroup().getLecture()); + if (participation == null || participation.getRoleType() != ParticipationRole.ADVISOR) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "insufficient rights"); + return; + } + + if ("generateNewTestSteps".equals(request.getParameter("action"))) { + int numberOfTestSteps = Util.parseInteger(request.getParameter("numberOfTestSteps"), 0); + String[][] testcases = new String[numberOfTestSteps][3]; + + // TODO@CHW: this is just a placeholder for the actual testcase generator + for (int testStepId = 0; testStepId < numberOfTestSteps; testStepId++) { + testcases[testStepId][0] = "Testcase " + testStepId; + testcases[testStepId][1] = "ghci -e " + testStepId + "+" + testStepId; + testcases[testStepId][2] = "" + (testStepId + testStepId); + } + + Transaction tx = session.beginTransaction(); + for (int testStepId = 0; testStepId < numberOfTestSteps; testStepId++) { + String title = testcases[testStepId][0]; + String testCode = testcases[testStepId][1].replaceAll("\r\n", "\n"); + String expect = testcases[testStepId][2].replaceAll("\r\n", "\n"); + + DockerTestStep newStep = new DockerTestStep(haskellRuntimeTest, title, testCode, expect); + session.persist(newStep); + } + tx.commit(); + + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + } else { + getServletContext().getNamedDispatcher(DockerTestManager.class.getSimpleName()).forward(request, response); + } } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 2a27ba4a0..3b132bef1 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -20,8 +20,8 @@ package de.tuclausthal.submissioninterface.servlets.view; -import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTestStep; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.servlets.GATEView; import de.tuclausthal.submissioninterface.servlets.controller.HaskellRuntimeTestManager; import de.tuclausthal.submissioninterface.servlets.controller.TaskManager; @@ -94,7 +94,55 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println("
"); - out.println("

Automated testcase generation is not yet implemented.

"); + out.println("

Neue Testschritte automatisch generieren

"); + out.println("

Testschritt Generator ist noch nicht vollständig implementiert.

"); // TODO@CHW + out.println("
"); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.print(""); + out.println(""); + out.println("
Number of test steps
Abbrechen
"); + out.println("
"); + + out.println("

Testschritte bearbeiten

"); + out.println(""); + out.println(/* @formatter:off */ + "" + + "" + + "" + + "" + + "" + + "" + + "" + /* @formatter:on */); + + for (DockerTestStep step : test.getTestSteps()) { + String deleteTestStepLink = Util.generateHTMLLink(HaskellRuntimeTestManager.class.getSimpleName() + + "?testid=" + test.getId() + + "&action=deleteStep&teststepid=" + step.getTeststepid(), response + ); + out.println(/* @formatter:off */ + "" + + "" + + "" + + "" + + "" + /* @formatter:on */); + } + out.println("
TitelTestcodeExpected
" + + Util.escapeHTML(step.getTitle()) + " " + + "" + + "(Löschen)" + + "" + + "" + Util.escapeHTML(step.getTestcode()) + "" + Util.escapeHTML(step.getExpect()) + "
"); template.printTemplateFooter(); } From cded4cf4473e55322d551da6c996b8952d66644a Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Thu, 8 May 2025 21:30:13 +0200 Subject: [PATCH 028/105] Formatting --- .../servlets/controller/TestManager.java | 3 +-- .../servlets/view/PerformStudentTestResultView.java | 4 ++-- .../servlets/view/PerformTestResultView.java | 4 ++-- .../servlets/view/ShowSubmissionStudentView.java | 6 +++--- .../servlets/view/ShowSubmissionView.java | 4 ++-- .../servlets/view/ShowTaskTutorView.java | 1 - .../servlets/view/TestManagerAddTestFormView.java | 1 - 7 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java index e5eaf08ec..e953ccc81 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java @@ -170,10 +170,9 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr test.setTestTitle(title); test.setTestDescription(description); test.setGiveDetailsToStudents(request.getParameter("giveDetailsToStudents") != null); - test.setTimeout(15); // falls du es trotzdem festlegen willst + test.setTimeout(15); session.getTransaction().commit(); - // Zurück zur Aufgabenübersicht (wie bei CompileTest) response.sendRedirect(Util.generateRedirectURL(TaskManager.class.getSimpleName() + "?action=editTask&lecture=" + task.getTaskGroup().getLecture().getId() + "&taskid=" + task.getTaskid(), response)); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java index ea1acd2ce..6f0f722b2 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java @@ -108,9 +108,9 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro if (test.isGiveDetailsToStudents() && !testResult.getTestOutput().isEmpty()) { if (test instanceof JavaAdvancedIOTest jaiot) { ShowJavaAdvancedIOTestResult.printTestResults(out, jaiot, testResult.getTestOutput(), true, null); - } else if (test instanceof HaskellSyntaxTest hst){ + } else if (test instanceof HaskellSyntaxTest hst) { ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), true, null); - } else if (test instanceof DockerTest dt) { + } else if (test instanceof DockerTest dt) { ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), true, null); } else { out.println("Ausgabe:
" + Util.escapeHTML(testResult.getTestOutput()) + "
"); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java index 3c17f6c85..67f19fa26 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java @@ -68,11 +68,11 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr if (!testResult.getTestOutput().isEmpty()) { if (test instanceof JavaAdvancedIOTest jaiot) { ShowJavaAdvancedIOTestResult.printTestResults(out, jaiot, testResult.getTestOutput(), (participation == null || !participation.getRoleType().equals(ParticipationRole.ADVISOR)), null); - } else if (test instanceof HaskellSyntaxTest hst){ + } else if (test instanceof HaskellSyntaxTest hst) { ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), (participation == null || participation.getRoleType().compareTo(ParticipationRole.TUTOR) < 0), null); } else if (test instanceof DockerTest dt) { ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), (participation == null || participation.getRoleType().compareTo(ParticipationRole.TUTOR) < 0), null); - } else { + } else { out.println("Ausgabe:
" + Util.escapeHTML(testResult.getTestOutput()) + "
"); } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java index a032af9b0..af781d7d8 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java @@ -81,9 +81,9 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro if (testResult.getTest() instanceof JavaAdvancedIOTest) { out.println("
"); ShowJavaAdvancedIOTestResult.printTestResults(out, (JavaAdvancedIOTest) testResult.getTest(), testResult.getTestOutput(), true, javaScript); - }else if (testResult.getTest() instanceof HaskellSyntaxTest hst) { - out.println("
"); - ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), false, javaScript); + } else if (testResult.getTest() instanceof HaskellSyntaxTest hst) { + out.println("
"); + ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), false, javaScript); } else if (testResult.getTest() instanceof DockerTest) { out.println("
"); ShowDockerTestResult.printTestResults(out, (DockerTest) testResult.getTest(), testResult.getTestOutput(), true, javaScript); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionView.java index e96a526c0..343ebf4bf 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionView.java @@ -324,8 +324,8 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), false, javaScript); } else if (testResult.getTest() instanceof DockerTest dt) { out.println("
"); - ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), false, javaScript);} - else { + ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), false, javaScript); + } else { out.println("
"); } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorView.java index 7dfdefeaa..838472d16 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorView.java @@ -151,7 +151,6 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro } out.println(""); - if (participation.getRoleType() == ParticipationRole.ADVISOR) { out.println("

"); } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java index 795da2f79..8d1d9d490 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java @@ -390,7 +390,6 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println(""); out.println(""); out.println(""); - template.printTemplateFooter(); } } From 63825f770a74c3993ad34c079ff8447f2118ae3e Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Fri, 9 May 2025 18:24:52 +0200 Subject: [PATCH 029/105] Make the Haskell RegEx Testing static --- .../testanalyzer/CommonErrorAnalyzer.java | 15 +- .../syntax/HaskellErrorClassifierIf.java | 25 --- .../syntax/RegexBasedHaskellClustering.java | 145 ++++++------------ 3 files changed, 63 insertions(+), 122 deletions(-) delete mode 100644 src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/HaskellErrorClassifierIf.java diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java index b10c18b8b..08c68a00b 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java @@ -311,7 +311,6 @@ private void groupDockerTestResults(final DockerTest test, final TestResult test String keyStr = ""; - // Optional: zusätzliche Hinweise sammeln if (testOutputJson.containsKey("exitCode")) { keyStr += "ExitCode: " + testOutputJson.getInt("exitCode") + " "; } @@ -333,8 +332,18 @@ private void groupHaskellSyntaxTestResults(final HaskellSyntaxTest test, final T JsonObject testOutputJson = Json.createReader(new StringReader(testResult.getTestOutput())).readObject(); String stderr = testOutputJson.containsKey("stderr") ? testOutputJson.getString("stderr") : ""; - String keyStr = "HaskellSyntax: "; - new RegexBasedHaskellClustering(session).classify(testResult, stderr, keyStr); + String clusterResult = RegexBasedHaskellClustering.classify(stderr); + + String keyStr = "Syntax: " + clusterResult; + + CommonErrorDAOIf commonErrorDAO = DAOFactory.CommonErrorDAOIf(session); + CommonError commonError = commonErrorDAO.getCommonError(keyStr, testResult.getTest()); + if (commonError != null) { + commonError.getTestResults().add(testResult); + } else { + commonErrorDAO.newCommonError(keyStr, clusterResult, testResult, CommonError.Type.CompileTimeError); + } + } diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/HaskellErrorClassifierIf.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/HaskellErrorClassifierIf.java deleted file mode 100644 index a80ab3cbe..000000000 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/HaskellErrorClassifierIf.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2025 Sven Strickroth - * Copyright 2025 Esat Avci - * - * This file is part of the GATE. - * - * GATE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * GATE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with GATE. If not, see . - */ -package de.tuclausthal.submissioninterface.testanalyzer.haskell.syntax; - -import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; - -public interface HaskellErrorClassifierIf { - void classify(TestResult testResult, String stderr, String keyStr); -} diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java index e3f31dfef..dfa6599c8 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java @@ -21,105 +21,62 @@ import java.util.*; import java.util.regex.Pattern; -import de.tuclausthal.submissioninterface.persistence.dao.CommonErrorDAOIf; -import de.tuclausthal.submissioninterface.persistence.dao.DAOFactory; -import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError; -import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError.Type; -import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; -import org.hibernate.Session; +public class RegexBasedHaskellClustering { + private static final LinkedHashMap CLUSTERS = new LinkedHashMap<>(); -public class RegexBasedHaskellClustering implements HaskellErrorClassifierIf { - - private final Session session; - private final LinkedHashMap clusters; - - public RegexBasedHaskellClustering(Session session) { - this.session = session; - this.clusters = new LinkedHashMap<>(); - - // Muster in Priorisierungsreihenfolge eintragen - clusters.put("Parse-Fehler", Pattern.compile("\\bparse\\s+error\\b", Pattern.CASE_INSENSITIVE)); - clusters.put("Typenkonflikt", Pattern.compile("couldn'?t match (expected type|type)", Pattern.CASE_INSENSITIVE)); - clusters.put("Variable nicht im Gültigkeitsbereich", Pattern.compile("not in scope", Pattern.CASE_INSENSITIVE)); - clusters.put("Leerer do-Block", Pattern.compile("empty\\s+'do'\\s+block", Pattern.CASE_INSENSITIVE)); - clusters.put("Unendlicher Typ", Pattern.compile("occurs check:.*infinite type", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - clusters.put("Modul nicht gefunden", Pattern.compile("could not find module", Pattern.CASE_INSENSITIVE)); - clusters.put("Fehlendes Binding", Pattern.compile("type signature.*lacks an accompanying binding", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - clusters.put("Fehlende Instanz", Pattern.compile("no instance for", Pattern.CASE_INSENSITIVE)); - clusters.put("Abweichende Arity", Pattern.compile("equations for .* have different numbers of arguments", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - clusters.put("Doppelte Signatur", Pattern.compile("duplicate type signatures?", Pattern.CASE_INSENSITIVE)); - clusters.put("Mehrdeutiger Bezeichner", Pattern.compile("ambiguous occurrence", Pattern.CASE_INSENSITIVE)); - clusters.put("Syntaxfehler", Pattern.compile("syntax error", Pattern.CASE_INSENSITIVE)); - clusters.put("Ungültige Typensignatur", Pattern.compile("invalid type signature", Pattern.CASE_INSENSITIVE)); - clusters.put("Mehrfache Deklarationen", Pattern.compile("multiple declarations", Pattern.CASE_INSENSITIVE)); - clusters.put("Fehlerhafter Datenkonstruktor", Pattern.compile("cannot parse data constructor in a data/newtype declaration", Pattern.CASE_INSENSITIVE)); - clusters.put("Fehlerhafter Typ-Header", Pattern.compile("malformed head of type or class declaration", Pattern.CASE_INSENSITIVE)); - clusters.put("Lexikalischer Fehler", Pattern.compile("lexical error at character", Pattern.CASE_INSENSITIVE)); - clusters.put("Kind-Konflikt", Pattern.compile("expected kind .* but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - clusters.put("Mehrdeutiger Typ", Pattern.compile("ambiguous type variable", Pattern.CASE_INSENSITIVE)); - clusters.put("Doppelte Instanz", Pattern.compile("duplicate instance declarations", Pattern.CASE_INSENSITIVE)); - clusters.put("Fehlende Constraint", Pattern.compile("could not deduce.*\\(", Pattern.CASE_INSENSITIVE)); - clusters.put("Konfliktierende Bindings", Pattern.compile("conflicting definitions for", Pattern.CASE_INSENSITIVE)); - clusters.put("Methode nicht in Klasse", Pattern.compile("is not a \\(visible\\) method of class", Pattern.CASE_INSENSITIVE)); - clusters.put("Ungültige Instanz-Signatur", Pattern.compile("illegal type signature in instance declaration", Pattern.CASE_INSENSITIVE)); - clusters.put("Unvollständiger Typ", Pattern.compile("expecting one more argument to .*has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - clusters.put("Constraint erwartet, aber Typ erhalten", Pattern.compile("expected a constraint, but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - clusters.put("Pattern Binding in Instanz", Pattern.compile("pattern bindings.*not allowed in instance declaration", Pattern.CASE_INSENSITIVE)); - clusters.put("Falsche Konstruktor-Arity", Pattern.compile("the constructor ‘.*’ should have \\d+ argument[s]?, but has been given \\d+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - clusters.put("Typed Hole", Pattern.compile("found hole: _ ::", Pattern.CASE_INSENSITIVE)); - clusters.put("Ungültige Binding-Syntax", Pattern.compile("illegal binding of built-in syntax", Pattern.CASE_INSENSITIVE)); - clusters.put("Letzte Anweisung im 'do'-Block", Pattern.compile("the last statement in a 'do' block must be an expression", Pattern.CASE_INSENSITIVE)); - clusters.put("Überlappende Instanzen", Pattern.compile("overlapping instances for", Pattern.CASE_INSENSITIVE)); - clusters.put("Ungültiges Enum-Deriving", Pattern.compile("can't make a derived instance of ['‘`]Enum", Pattern.CASE_INSENSITIVE)); - clusters.put("Ungültige Instanz-Form", Pattern.compile("illegal instance declaration.*flexibleinstances", Pattern.CASE_INSENSITIVE)); - clusters.put("Kein Datenkonstruktor", Pattern.compile("not a data constructor", Pattern.CASE_INSENSITIVE)); - clusters.put("Ungültiges Deriving", Pattern.compile("illegal deriving item", Pattern.CASE_INSENSITIVE)); - clusters.put("Flexible Kontexte benötigt", Pattern.compile("non type-variable argument in the constraint", Pattern.CASE_INSENSITIVE)); - clusters.put("Erneut ungültige Typensignatur", Pattern.compile("illegal type signature", Pattern.CASE_INSENSITIVE)); - clusters.put("Fehlende GADTs-Erweiterung", Pattern.compile("enable the GADTs extension", Pattern.CASE_INSENSITIVE)); - clusters.put("Kind-Konflikt (Constraint vs. Typ)", Pattern.compile("expected (a constraint|a type), but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - clusters.put("Ungültiger Typ-Operator", Pattern.compile("illegal operator .* in type .*", Pattern.CASE_INSENSITIVE)); - clusters.put("Fehlende Klammern im Range-Ausdruck", Pattern.compile("a section must be enclosed in parentheses", Pattern.CASE_INSENSITIVE)); - clusters.put("Warnung", Pattern.compile("warning", Pattern.CASE_INSENSITIVE)); - clusters.put("Sonstiger Fehler", Pattern.compile(".*", Pattern.DOTALL)); + static { + CLUSTERS.put("Parse-Fehler", Pattern.compile("\\bparse\\s+error\\b", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Typenkonflikt", Pattern.compile("couldn'?t match (expected type|type)", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Variable nicht im Gültigkeitsbereich", Pattern.compile("not in scope", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Leerer do-Block", Pattern.compile("empty\\s+'do'\\s+block", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Unendlicher Typ", Pattern.compile("occurs check:.*infinite type", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Modul nicht gefunden", Pattern.compile("could not find module", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlendes Binding", Pattern.compile("type signature.*lacks an accompanying binding", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Fehlende Instanz", Pattern.compile("no instance for", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Abweichende Arity", Pattern.compile("equations for .* have different numbers of arguments", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Doppelte Signatur", Pattern.compile("duplicate type signatures?", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Mehrdeutiger Bezeichner", Pattern.compile("ambiguous occurrence", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Syntaxfehler", Pattern.compile("syntax error", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültige Typensignatur", Pattern.compile("invalid type signature", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Mehrfache Deklarationen", Pattern.compile("multiple declarations", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlerhafter Datenkonstruktor", Pattern.compile("cannot parse data constructor in a data/newtype declaration", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlerhafter Typ-Header", Pattern.compile("malformed head of type or class declaration", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Lexikalischer Fehler", Pattern.compile("lexical error at character", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Kind-Konflikt", Pattern.compile("expected kind .* but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Mehrdeutiger Typ", Pattern.compile("ambiguous type variable", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Doppelte Instanz", Pattern.compile("duplicate instance declarations", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlende Constraint", Pattern.compile("could not deduce.*\\(", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Konfliktierende Bindings", Pattern.compile("conflicting definitions for", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Methode nicht in Klasse", Pattern.compile("is not a \\(visible\\) method of class", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültige Instanz-Signatur", Pattern.compile("illegal type signature in instance declaration", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Unvollständiger Typ", Pattern.compile("expecting one more argument to .*has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Constraint erwartet, aber Typ erhalten", Pattern.compile("expected a constraint, but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Pattern Binding in Instanz", Pattern.compile("pattern bindings.*not allowed in instance declaration", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Falsche Konstruktor-Arity", Pattern.compile("the constructor ‘.*’ should have \\d+ argument[s]?, but has been given \\d+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Typed Hole", Pattern.compile("found hole: _ ::", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültige Binding-Syntax", Pattern.compile("illegal binding of built-in syntax", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Letzte Anweisung im 'do'-Block", Pattern.compile("the last statement in a 'do' block must be an expression", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Überlappende Instanzen", Pattern.compile("overlapping instances for", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültiges Enum-Deriving", Pattern.compile("can't make a derived instance of ['‘`]Enum", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültige Instanz-Form", Pattern.compile("illegal instance declaration.*flexibleinstances", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Kein Datenkonstruktor", Pattern.compile("not a data constructor", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültiges Deriving", Pattern.compile("illegal deriving item", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Flexible Kontexte benötigt", Pattern.compile("non type-variable argument in the constraint", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Erneut ungültige Typensignatur", Pattern.compile("illegal type signature", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlende GADTs-Erweiterung", Pattern.compile("enable the GADTs extension", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Kind-Konflikt (Constraint vs. Typ)", Pattern.compile("expected (a constraint|a type), but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Ungültiger Typ-Operator", Pattern.compile("illegal operator .* in type .*", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlende Klammern im Range-Ausdruck", Pattern.compile("a section must be enclosed in parentheses", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Warnung", Pattern.compile("warning", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Sonstiger Fehler", Pattern.compile(".*", Pattern.DOTALL)); } + public final static String classify(String stderr) { - @Override - public void classify(TestResult testResult, String stderr, String keyStr) { - CommonErrorDAOIf commonErrorDAO = DAOFactory.CommonErrorDAOIf(session); - List matchedClusters = new ArrayList<>(); - - for (var entry : clusters.entrySet()) { + for (Map.Entry entry : CLUSTERS.entrySet()) { if (entry.getValue().matcher(stderr).find()) { - matchedClusters.add(entry.getKey()); - } - } - - - for (String preferred : clusters.keySet()) { - if (!preferred.equals("Sonstiger Fehler") && matchedClusters.contains(preferred)) { - String fullKey = keyStr + preferred; - CommonError commonError = commonErrorDAO.getCommonError(fullKey, testResult.getTest()); - if (commonError != null) { - commonError.getTestResults().add(testResult); - } else { - commonErrorDAO.newCommonError(fullKey, preferred, testResult, Type.CompileTimeError); - } - return; - } - } - - if (matchedClusters.contains("Sonstiger Fehler")) { - String fallbackKey = keyStr + "Sonstiger Fehler"; - CommonError commonError = commonErrorDAO.getCommonError(fallbackKey, testResult.getTest()); - if (commonError != null) { - commonError.getTestResults().add(testResult); - } else { - commonErrorDAO.newCommonError(fallbackKey, "Sonstiger Fehler", testResult, Type.CompileTimeError); + return entry.getKey(); } - return; } - - commonErrorDAO.newCommonError(keyStr + "Nicht klassifiziert", "Unklassifiziert", testResult, null); + return "Sonstige Fehler"; } } From 97596a429ce7f1d556ba7ae62b23cfd1e5ec3504 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Fri, 9 May 2025 18:25:26 +0200 Subject: [PATCH 030/105] implement stricter privacy settings for Methods and variables --- .../testframework/tests/impl/DockerTest.java | 12 ++++++------ .../tests/impl/HaskellSyntaxTest.java | 19 ++++++------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java index c02c446d2..3f504d0f7 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java @@ -42,12 +42,12 @@ * @author Sven Strickroth */ public class DockerTest extends TempDirTest { - final static protected Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + final static private Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); final static public String SAFE_DOCKER_SCRIPT = "/usr/local/bin/safe-docker"; - protected static final Random random = new Random(); - protected final String separator; - protected Path tempDir; + final private static Random random = new Random(); + final private String separator; + private Path tempDir; public DockerTest(final de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest test) { super(test); @@ -55,7 +55,7 @@ public DockerTest(final de.tuclausthal.submissioninterface.persistence.datamodel } @Override - public void performTest(final Path basePath, final Path submissionPath, final TestExecutorTestResult testResult) throws Exception { + public final void performTest(final Path basePath, final Path submissionPath, final TestExecutorTestResult testResult) throws Exception { try { tempDir = Util.createTemporaryDirectory("test"); //Configuration.getInstance().getDataPath() @@ -206,7 +206,7 @@ protected String generateTestShellScript(){ return testCode.toString(); } - protected void debugLog(List params, Path studentDir){ + protected final void debugLog(List params, Path studentDir){ LOG.debug("Executing external process: {} in {}", params, studentDir); } diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java index bbc25128f..9cb9e860b 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java @@ -37,25 +37,23 @@ public class HaskellSyntaxTest extends DockerTest { + private static final Random random = new Random(); + private final String separator; + public HaskellSyntaxTest(de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest test) { super(test); + separator = "##"; } @Override - protected void analyzeAndSetResult(boolean exitedCleanly, StringBuffer stdout, StringBuffer stderr, int exitCode, boolean aborted, TestExecutorTestResult result) { + protected final void analyzeAndSetResult(final boolean exitedCleanly, final StringBuffer stdout, final StringBuffer stderr, final int exitCode, final boolean aborted, final TestExecutorTestResult result) { boolean success = exitedCleanly && !stderr.toString().toLowerCase().contains("error:"); result.setTestPassed(success); result.setTestOutput(createJsonBuilder(success, stdout, stderr, exitCode, aborted).build().toString()); } - - @Override - protected void performTestInTempDir(Path basePath, Path tempDir, TestExecutorTestResult testResult) throws Exception { - //currently unused - } - @Override - protected String generateTestShellScript(){ + protected final String generateTestShellScript(){ return """ #!/bin/bash set -e @@ -65,9 +63,4 @@ protected String generateTestShellScript(){ done """.formatted(separator); } - - @Override - protected void debugLog(List params, Path studentDir){ - LOG.debug("Executing HaskellSyntaxTest docker process: {} in {}", params, studentDir); - } } From 474b947f83448f743f9cd9cb7c21d17e3714bdba Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Fri, 9 May 2025 18:32:10 +0200 Subject: [PATCH 031/105] delete methode for logging --- .../testframework/tests/impl/DockerTest.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java index 3f504d0f7..dbec9a2b4 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java @@ -92,7 +92,7 @@ public final void performTest(final Path basePath, final Path submissionPath, fi /* only forward explicitly specified environment variables to test processes */ pb.environment().keySet().removeIf(key -> !("PATH".equalsIgnoreCase(key) || "USER".equalsIgnoreCase(key) || "LANG".equalsIgnoreCase(key))); - debugLog(params, studentDir); + LOG.debug("Executing external process: {} in {}", params, studentDir); Process process = pb.start(); ProcessOutputGrabber outputGrapper = new ProcessOutputGrabber(process); @@ -206,10 +206,6 @@ protected String generateTestShellScript(){ return testCode.toString(); } - protected final void debugLog(List params, Path studentDir){ - LOG.debug("Executing external process: {} in {}", params, studentDir); - } - @Override protected void performTestInTempDir(Path basePath, Path pTempDir, TestExecutorTestResult testResult) throws Exception {} From 677b2d4150fd27943bacc8872f2b053bd202c5e4 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Sat, 10 May 2025 12:00:41 +0200 Subject: [PATCH 032/105] fix formatting --- .../submissioninterface/testanalyzer/CommonErrorAnalyzer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java index 08c68a00b..9effcf0e8 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java @@ -332,7 +332,7 @@ private void groupHaskellSyntaxTestResults(final HaskellSyntaxTest test, final T JsonObject testOutputJson = Json.createReader(new StringReader(testResult.getTestOutput())).readObject(); String stderr = testOutputJson.containsKey("stderr") ? testOutputJson.getString("stderr") : ""; - String clusterResult = RegexBasedHaskellClustering.classify(stderr); + String clusterResult = RegexBasedHaskellClustering.classify(stderr); String keyStr = "Syntax: " + clusterResult; From 5bf8441edfa5961a742472cf95f2c4c03d7f97ff Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 11 May 2025 13:36:08 +0200 Subject: [PATCH 033/105] Formatting --- .../testframework/tests/impl/DockerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java index dbec9a2b4..2f8106f70 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java @@ -47,7 +47,7 @@ public class DockerTest extends TempDirTest Date: Thu, 15 May 2025 23:56:42 +0200 Subject: [PATCH 034/105] Reformat using Eclipse formatter settings defined in ".settings" --- .../persistence/dao/TestDAOIf.java | 2 +- .../persistence/dao/impl/TestDAO.java | 2 +- .../datamodel/HaskellRuntimeTest.java | 2 +- .../datamodel/HaskellSyntaxTest.java | 11 +- .../controller/DockerTestManager.java | 8 +- .../controller/HaskellRuntimeTestManager.java | 118 +++++++------ .../servlets/controller/TestManager.java | 9 +- .../view/HaskellRuntimeTestManagerView.java | 159 +++++++++--------- .../view/PerformStudentTestResultView.java | 2 +- .../servlets/view/PerformTestResultView.java | 2 +- .../view/ShowSubmissionStudentView.java | 12 +- .../servlets/view/ShowTaskStudentView.java | 15 +- .../servlets/view/TaskManagerView.java | 2 +- .../view/TestManagerAddTestFormView.java | 4 +- .../ShowHaskellSyntaxTestResult.java | 18 +- .../testanalyzer/CommonErrorAnalyzer.java | 10 +- .../syntax/RegexBasedHaskellClustering.java | 113 +++++++------ .../testframework/tests/impl/DockerTest.java | 4 +- .../tests/impl/HaskellSyntaxTest.java | 65 +++---- 19 files changed, 275 insertions(+), 283 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/TestDAOIf.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/TestDAOIf.java index 0fc65e40f..057a7ec9c 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/TestDAOIf.java +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/TestDAOIf.java @@ -24,8 +24,8 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CommentsMetricTest; import de.tuclausthal.submissioninterface.persistence.datamodel.CompileTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; -import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JUnitTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Task; diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/impl/TestDAO.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/impl/TestDAO.java index e3a30415a..fb8e85340 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/impl/TestDAO.java +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/dao/impl/TestDAO.java @@ -35,8 +35,8 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CommentsMetricTest; import de.tuclausthal.submissioninterface.persistence.datamodel.CompileTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; -import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JUnitTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Task; diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTest.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTest.java index eec49224d..a4bd81aaf 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTest.java @@ -19,10 +19,10 @@ package de.tuclausthal.submissioninterface.persistence.datamodel; -import de.tuclausthal.submissioninterface.testframework.tests.AbstractTest; import jakarta.persistence.Entity; import jakarta.persistence.Transient; +import de.tuclausthal.submissioninterface.testframework.tests.AbstractTest; /** * Haskell runtime test, extends the DockerTest by automatically generating haskell testcases and by clustering diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java index 8af4cfa8e..fe78de74b 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java @@ -21,14 +21,13 @@ import jakarta.persistence.Entity; import jakarta.persistence.Transient; - import de.tuclausthal.submissioninterface.testframework.tests.AbstractTest; @Entity public class HaskellSyntaxTest extends DockerTest { - @Override - @Transient - public AbstractTest getTestImpl() { - return new de.tuclausthal.submissioninterface.testframework.tests.impl.HaskellSyntaxTest(this); - } + @Override + @Transient + public AbstractTest getTestImpl() { + return new de.tuclausthal.submissioninterface.testframework.tests.impl.HaskellSyntaxTest(this); + } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/DockerTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/DockerTestManager.java index 5a58196f5..930ef31d6 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/DockerTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/DockerTestManager.java @@ -33,8 +33,8 @@ import de.tuclausthal.submissioninterface.persistence.dao.ParticipationDAOIf; import de.tuclausthal.submissioninterface.persistence.dao.TestDAOIf; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; -import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTestStep; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; import de.tuclausthal.submissioninterface.persistence.datamodel.ParticipationRole; import de.tuclausthal.submissioninterface.persistence.datamodel.Test; @@ -74,8 +74,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro request.setAttribute("test", test); - String testManagerViewClassSimpleName = test instanceof HaskellRuntimeTest ? - HaskellRuntimeTestManagerView.class.getSimpleName() : DockerTestManagerOverView.class.getSimpleName(); + String testManagerViewClassSimpleName = test instanceof HaskellRuntimeTest ? HaskellRuntimeTestManagerView.class.getSimpleName() : DockerTestManagerOverView.class.getSimpleName(); getServletContext().getNamedDispatcher(testManagerViewClassSimpleName).forward(request, response); } @@ -99,8 +98,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr return; } - String testManagerClassSimpleName = test instanceof HaskellRuntimeTest ? - HaskellRuntimeTestManager.class.getSimpleName() : DockerTestManager.class.getSimpleName(); + String testManagerClassSimpleName = test instanceof HaskellRuntimeTest ? HaskellRuntimeTestManager.class.getSimpleName() : DockerTestManager.class.getSimpleName(); if ("edittest".equals(request.getParameter("action"))) { Transaction tx = session.beginTransaction(); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index aa6a68bf8..325bce006 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -19,23 +19,29 @@ package de.tuclausthal.submissioninterface.servlets.controller; -import de.tuclausthal.submissioninterface.persistence.dao.DAOFactory; -import de.tuclausthal.submissioninterface.persistence.dao.ParticipationDAOIf; -import de.tuclausthal.submissioninterface.persistence.dao.TestDAOIf; -import de.tuclausthal.submissioninterface.persistence.datamodel.*; -import de.tuclausthal.submissioninterface.servlets.GATEController; -import de.tuclausthal.submissioninterface.servlets.RequestAdapter; -import de.tuclausthal.submissioninterface.servlets.view.MessageView; -import de.tuclausthal.submissioninterface.util.Util; +import java.io.IOException; +import java.io.Serial; + import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + import org.hibernate.Session; import org.hibernate.Transaction; -import java.io.IOException; -import java.io.Serial; +import de.tuclausthal.submissioninterface.persistence.dao.DAOFactory; +import de.tuclausthal.submissioninterface.persistence.dao.ParticipationDAOIf; +import de.tuclausthal.submissioninterface.persistence.dao.TestDAOIf; +import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTestStep; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; +import de.tuclausthal.submissioninterface.persistence.datamodel.ParticipationRole; +import de.tuclausthal.submissioninterface.persistence.datamodel.Test; +import de.tuclausthal.submissioninterface.servlets.GATEController; +import de.tuclausthal.submissioninterface.servlets.RequestAdapter; +import de.tuclausthal.submissioninterface.servlets.view.MessageView; +import de.tuclausthal.submissioninterface.util.Util; /** * Controller-Servlet for clustering haskell submissions based on common errors (dynamic/runtime analysis). @@ -45,58 +51,58 @@ */ @GATEController public class HaskellRuntimeTestManager extends HttpServlet { - @Serial - private static final long serialVersionUID = 1L; + @Serial + private static final long serialVersionUID = 1L; - @Override - public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - getServletContext().getNamedDispatcher(DockerTestManager.class.getSimpleName()).forward(request, response); - } + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + getServletContext().getNamedDispatcher(DockerTestManager.class.getSimpleName()).forward(request, response); + } - @Override - public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - Session session = RequestAdapter.getSession(request); - TestDAOIf testDAOIf = DAOFactory.TestDAOIf(session); - Test test = testDAOIf.getTest(Util.parseInteger(request.getParameter("testid"), 0)); - if (!(test instanceof HaskellRuntimeTest haskellRuntimeTest)) { - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - request.setAttribute("title", "Test nicht gefunden"); - getServletContext().getNamedDispatcher(MessageView.class.getSimpleName()).forward(request, response); - return; - } + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + Session session = RequestAdapter.getSession(request); + TestDAOIf testDAOIf = DAOFactory.TestDAOIf(session); + Test test = testDAOIf.getTest(Util.parseInteger(request.getParameter("testid"), 0)); + if (!(test instanceof HaskellRuntimeTest haskellRuntimeTest)) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + request.setAttribute("title", "Test nicht gefunden"); + getServletContext().getNamedDispatcher(MessageView.class.getSimpleName()).forward(request, response); + return; + } - ParticipationDAOIf participationDAO = DAOFactory.ParticipationDAOIf(session); - Participation participation = participationDAO.getParticipation(RequestAdapter.getUser(request), haskellRuntimeTest.getTask().getTaskGroup().getLecture()); - if (participation == null || participation.getRoleType() != ParticipationRole.ADVISOR) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "insufficient rights"); - return; - } + ParticipationDAOIf participationDAO = DAOFactory.ParticipationDAOIf(session); + Participation participation = participationDAO.getParticipation(RequestAdapter.getUser(request), haskellRuntimeTest.getTask().getTaskGroup().getLecture()); + if (participation == null || participation.getRoleType() != ParticipationRole.ADVISOR) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "insufficient rights"); + return; + } - if ("generateNewTestSteps".equals(request.getParameter("action"))) { - int numberOfTestSteps = Util.parseInteger(request.getParameter("numberOfTestSteps"), 0); - String[][] testcases = new String[numberOfTestSteps][3]; + if ("generateNewTestSteps".equals(request.getParameter("action"))) { + int numberOfTestSteps = Util.parseInteger(request.getParameter("numberOfTestSteps"), 0); + String[][] testcases = new String[numberOfTestSteps][3]; - // TODO@CHW: this is just a placeholder for the actual testcase generator - for (int testStepId = 0; testStepId < numberOfTestSteps; testStepId++) { - testcases[testStepId][0] = "Testcase " + testStepId; - testcases[testStepId][1] = "ghci -e " + testStepId + "+" + testStepId; - testcases[testStepId][2] = "" + (testStepId + testStepId); - } + // TODO@CHW: this is just a placeholder for the actual testcase generator + for (int testStepId = 0; testStepId < numberOfTestSteps; testStepId++) { + testcases[testStepId][0] = "Testcase " + testStepId; + testcases[testStepId][1] = "ghci -e " + testStepId + "+" + testStepId; + testcases[testStepId][2] = "" + (testStepId + testStepId); + } - Transaction tx = session.beginTransaction(); - for (int testStepId = 0; testStepId < numberOfTestSteps; testStepId++) { - String title = testcases[testStepId][0]; - String testCode = testcases[testStepId][1].replaceAll("\r\n", "\n"); - String expect = testcases[testStepId][2].replaceAll("\r\n", "\n"); + Transaction tx = session.beginTransaction(); + for (int testStepId = 0; testStepId < numberOfTestSteps; testStepId++) { + String title = testcases[testStepId][0]; + String testCode = testcases[testStepId][1].replaceAll("\r\n", "\n"); + String expect = testcases[testStepId][2].replaceAll("\r\n", "\n"); - DockerTestStep newStep = new DockerTestStep(haskellRuntimeTest, title, testCode, expect); - session.persist(newStep); - } - tx.commit(); + DockerTestStep newStep = new DockerTestStep(haskellRuntimeTest, title, testCode, expect); + session.persist(newStep); + } + tx.commit(); - response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); - } else { - getServletContext().getNamedDispatcher(DockerTestManager.class.getSimpleName()).forward(request, response); - } - } + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + } else { + getServletContext().getNamedDispatcher(DockerTestManager.class.getSimpleName()).forward(request, response); + } + } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java index e953ccc81..c5e0a67f3 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java @@ -41,8 +41,8 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CommentsMetricTest; import de.tuclausthal.submissioninterface.persistence.datamodel.CompileTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; -import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JUnitTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; @@ -309,10 +309,9 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr test.setTimeout(Util.parseInteger(request.getParameter("timeout"), 15)); test.setGiveDetailsToStudents(request.getParameter("giveDetailsToStudents") != null); String preparationCode = request.getParameter("preparationcode"); - if (preparationCode == null) preparationCode = ""; - test.setPreparationShellCode( - preparationCode.replaceAll("\r\n", "\n") - ); + if (preparationCode == null) + preparationCode = ""; + test.setPreparationShellCode(preparationCode.replaceAll("\r\n", "\n")); session.getTransaction().commit(); response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + test.getId(), response)); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 3b132bef1..da3914b8e 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -19,6 +19,14 @@ package de.tuclausthal.submissioninterface.servlets.view; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Serial; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTestStep; import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; @@ -28,14 +36,6 @@ import de.tuclausthal.submissioninterface.template.Template; import de.tuclausthal.submissioninterface.template.TemplateFactory; import de.tuclausthal.submissioninterface.util.Util; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.Serial; /** * View-Servlet for clustering haskell submissions based on common errors (dynamic/runtime analysis) @@ -44,77 +44,77 @@ */ @GATEView public class HaskellRuntimeTestManagerView extends HttpServlet { - @Serial - private static final long serialVersionUID = 1L; + @Serial + private static final long serialVersionUID = 1L; - @Override - public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - Template template = TemplateFactory.getTemplate(request, response); + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + Template template = TemplateFactory.getTemplate(request, response); - HaskellRuntimeTest test = (HaskellRuntimeTest) request.getAttribute("test"); + HaskellRuntimeTest test = (HaskellRuntimeTest) request.getAttribute("test"); - template.addKeepAlive(); - template.printEditTaskTemplateHeader("Haskell Runtime Test bearbeiten", test.getTask()); + template.addKeepAlive(); + template.printEditTaskTemplateHeader("Haskell Runtime Test bearbeiten", test.getTask()); - PrintWriter out = response.getWriter(); + PrintWriter out = response.getWriter(); - // similar code in TestManagerAddTestFormView - out.println("

" + Util.escapeHTML(test.getTestTitle()) + "

"); - out.println("
"); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.print(""); - out.println(""); - out.println("
Titel:
Tutorentest: (Ergebnis wird den TutorInnen zur Korrektur angezeigt)
# ausführbar für Studierende:
Studierenden Test-Details anzeigen:
Preparation Code:
Abbrechen
"); - out.println("
"); + // similar code in TestManagerAddTestFormView + out.println("

" + Util.escapeHTML(test.getTestTitle()) + "

"); + out.println("
"); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.print(""); + out.println(""); + out.println("
Titel:
Tutorentest: (Ergebnis wird den TutorInnen zur Korrektur angezeigt)
# ausführbar für Studierende:
Studierenden Test-Details anzeigen:
Preparation Code:
Abbrechen
"); + out.println("
"); - out.println("
"); + out.println("
"); - out.println("

Neue Testschritte automatisch generieren

"); - out.println("

Testschritt Generator ist noch nicht vollständig implementiert.

"); // TODO@CHW - out.println("
"); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.print(""); - out.println(""); - out.println("
Number of test steps
Abbrechen
"); - out.println("
"); + out.println("

Neue Testschritte automatisch generieren

"); + out.println("

Testschritt Generator ist noch nicht vollständig implementiert.

"); // TODO@CHW + out.println("
"); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.print(""); + out.println(""); + out.println("
Number of test steps
Abbrechen
"); + out.println("
"); - out.println("

Testschritte bearbeiten

"); - out.println(""); - out.println(/* @formatter:off */ + out.println("

Testschritte bearbeiten

"); + out.println("
"); + out.println(/* @formatter:off */ "" + "" + "" + @@ -124,12 +124,9 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro "" /* @formatter:on */); - for (DockerTestStep step : test.getTestSteps()) { - String deleteTestStepLink = Util.generateHTMLLink(HaskellRuntimeTestManager.class.getSimpleName() + - "?testid=" + test.getId() + - "&action=deleteStep&teststepid=" + step.getTeststepid(), response - ); - out.println(/* @formatter:off */ + for (DockerTestStep step : test.getTestSteps()) { + String deleteTestStepLink = Util.generateHTMLLink(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + test.getId() + "&action=deleteStep&teststepid=" + step.getTeststepid(), response); + out.println(/* @formatter:off */ "" + "" + "" /* @formatter:on */); - } - out.println("
Titel
" + Util.escapeHTML(step.getTitle()) + " " + @@ -141,9 +138,9 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro "" + Util.escapeHTML(step.getExpect()) + "
"); + } + out.println(""); - template.printTemplateFooter(); - } + template.printTemplateFooter(); + } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java index 6f0f722b2..f357dce10 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformStudentTestResultView.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.io.PrintWriter; -import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -39,6 +38,7 @@ import de.tuclausthal.submissioninterface.servlets.controller.ChecklistTestResponse; import de.tuclausthal.submissioninterface.servlets.controller.ShowTask; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowDockerTestResult; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowJavaAdvancedIOTestResult; import de.tuclausthal.submissioninterface.template.Template; import de.tuclausthal.submissioninterface.template.TemplateFactory; diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java index 67f19fa26..e20de2492 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.io.PrintWriter; -import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -35,6 +34,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.Test; import de.tuclausthal.submissioninterface.servlets.GATEView; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowDockerTestResult; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowJavaAdvancedIOTestResult; import de.tuclausthal.submissioninterface.template.Template; import de.tuclausthal.submissioninterface.template.TemplateFactory; diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java index af781d7d8..ce699e693 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java @@ -23,21 +23,21 @@ import java.io.PrintWriter; import java.util.List; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Submission; import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; -import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - import de.tuclausthal.submissioninterface.servlets.GATEView; import de.tuclausthal.submissioninterface.servlets.controller.ShowFile; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowDockerTestResult; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowJavaAdvancedIOTestResult; import de.tuclausthal.submissioninterface.template.Template; import de.tuclausthal.submissioninterface.template.TemplateFactory; diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java index 5f1bb3ade..b45de4f22 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java @@ -29,8 +29,6 @@ import java.util.List; import java.util.Random; -import de.tuclausthal.submissioninterface.persistence.datamodel.*; -import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -44,7 +42,19 @@ import de.tuclausthal.submissioninterface.persistence.dao.PointGivenDAOIf; import de.tuclausthal.submissioninterface.persistence.dao.TestCountDAOIf; import de.tuclausthal.submissioninterface.persistence.dao.impl.TestResultCommonErrorDAO; +import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError; +import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.MCOption; +import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; +import de.tuclausthal.submissioninterface.persistence.datamodel.PointCategory; +import de.tuclausthal.submissioninterface.persistence.datamodel.PointGiven; import de.tuclausthal.submissioninterface.persistence.datamodel.Points.PointStatus; +import de.tuclausthal.submissioninterface.persistence.datamodel.Submission; +import de.tuclausthal.submissioninterface.persistence.datamodel.Task; +import de.tuclausthal.submissioninterface.persistence.datamodel.Test; +import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; import de.tuclausthal.submissioninterface.servlets.GATEView; import de.tuclausthal.submissioninterface.servlets.RequestAdapter; import de.tuclausthal.submissioninterface.servlets.controller.CloseSubmissionByStudent; @@ -58,6 +68,7 @@ import de.tuclausthal.submissioninterface.servlets.controller.SubmitSolution; import de.tuclausthal.submissioninterface.servlets.controller.WebStart; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowDockerTestResult; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowJavaAdvancedIOTestResult; import de.tuclausthal.submissioninterface.tasktypes.ClozeTaskType; import de.tuclausthal.submissioninterface.template.Template; diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TaskManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TaskManagerView.java index 61f001ce6..da2acef2a 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TaskManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TaskManagerView.java @@ -33,8 +33,8 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CommentsMetricTest; import de.tuclausthal.submissioninterface.persistence.datamodel.CompileTest; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; -import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JUnitTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Lecture; diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java index 8d1d9d490..ddfed9b18 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java @@ -233,9 +233,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println(""); out.println(""); out.print(" Abbrechen"); out.println(""); out.println(""); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java index a93658f67..c5286751e 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java @@ -21,23 +21,23 @@ import java.io.PrintWriter; import java.io.StringReader; -import de.tuclausthal.submissioninterface.util.Util; import jakarta.json.Json; import jakarta.json.JsonObject; import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; +import de.tuclausthal.submissioninterface.util.Util; public class ShowHaskellSyntaxTestResult { - public static void printTestResults(PrintWriter out, HaskellSyntaxTest test, String testOutput, boolean isStudent, StringBuilder javaScript) { + public static void printTestResults(PrintWriter out, HaskellSyntaxTest test, String testOutput, boolean isStudent, StringBuilder javaScript) { - JsonObject json = Json.createReader(new StringReader(testOutput)).readObject(); + JsonObject json = Json.createReader(new StringReader(testOutput)).readObject(); - String stderr = json.getString("stderr", ""); + String stderr = json.getString("stderr", ""); - if (!stderr.isEmpty()) { - out.println("

Fehlerausgabe:

"); - out.println("
" + Util.escapeHTML(stderr) + "
"); - } + if (!stderr.isEmpty()) { + out.println("

Fehlerausgabe:

"); + out.println("
" + Util.escapeHTML(stderr) + "
"); + } - } + } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java index 9effcf0e8..77aaf3f8c 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java @@ -22,7 +22,6 @@ import java.io.StringReader; import java.util.List; -import de.tuclausthal.submissioninterface.testanalyzer.haskell.syntax.RegexBasedHaskellClustering; import jakarta.json.Json; import jakarta.json.JsonArray; import jakarta.json.JsonObject; @@ -33,13 +32,14 @@ import de.tuclausthal.submissioninterface.persistence.dao.DAOFactory; import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError; import de.tuclausthal.submissioninterface.persistence.datamodel.CompileTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JUnitTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; -import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Test; import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; -import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; -import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest; +import de.tuclausthal.submissioninterface.testanalyzer.haskell.syntax.RegexBasedHaskellClustering; public class CommonErrorAnalyzer { //from Literatur @@ -345,6 +345,4 @@ private void groupHaskellSyntaxTestResults(final HaskellSyntaxTest test, final T } } - - } diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java index dfa6599c8..ac6d42d4e 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java @@ -18,65 +18,66 @@ */ package de.tuclausthal.submissioninterface.testanalyzer.haskell.syntax; -import java.util.*; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.regex.Pattern; public class RegexBasedHaskellClustering { - private static final LinkedHashMap CLUSTERS = new LinkedHashMap<>(); + private static final LinkedHashMap CLUSTERS = new LinkedHashMap<>(); - static { - CLUSTERS.put("Parse-Fehler", Pattern.compile("\\bparse\\s+error\\b", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Typenkonflikt", Pattern.compile("couldn'?t match (expected type|type)", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Variable nicht im Gültigkeitsbereich", Pattern.compile("not in scope", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Leerer do-Block", Pattern.compile("empty\\s+'do'\\s+block", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Unendlicher Typ", Pattern.compile("occurs check:.*infinite type", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Modul nicht gefunden", Pattern.compile("could not find module", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Fehlendes Binding", Pattern.compile("type signature.*lacks an accompanying binding", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Fehlende Instanz", Pattern.compile("no instance for", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Abweichende Arity", Pattern.compile("equations for .* have different numbers of arguments", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Doppelte Signatur", Pattern.compile("duplicate type signatures?", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Mehrdeutiger Bezeichner", Pattern.compile("ambiguous occurrence", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Syntaxfehler", Pattern.compile("syntax error", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültige Typensignatur", Pattern.compile("invalid type signature", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Mehrfache Deklarationen", Pattern.compile("multiple declarations", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Fehlerhafter Datenkonstruktor", Pattern.compile("cannot parse data constructor in a data/newtype declaration", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Fehlerhafter Typ-Header", Pattern.compile("malformed head of type or class declaration", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Lexikalischer Fehler", Pattern.compile("lexical error at character", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Kind-Konflikt", Pattern.compile("expected kind .* but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Mehrdeutiger Typ", Pattern.compile("ambiguous type variable", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Doppelte Instanz", Pattern.compile("duplicate instance declarations", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Fehlende Constraint", Pattern.compile("could not deduce.*\\(", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Konfliktierende Bindings", Pattern.compile("conflicting definitions for", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Methode nicht in Klasse", Pattern.compile("is not a \\(visible\\) method of class", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültige Instanz-Signatur", Pattern.compile("illegal type signature in instance declaration", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Unvollständiger Typ", Pattern.compile("expecting one more argument to .*has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Constraint erwartet, aber Typ erhalten", Pattern.compile("expected a constraint, but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Pattern Binding in Instanz", Pattern.compile("pattern bindings.*not allowed in instance declaration", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Falsche Konstruktor-Arity", Pattern.compile("the constructor ‘.*’ should have \\d+ argument[s]?, but has been given \\d+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Typed Hole", Pattern.compile("found hole: _ ::", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültige Binding-Syntax", Pattern.compile("illegal binding of built-in syntax", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Letzte Anweisung im 'do'-Block", Pattern.compile("the last statement in a 'do' block must be an expression", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Überlappende Instanzen", Pattern.compile("overlapping instances for", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültiges Enum-Deriving", Pattern.compile("can't make a derived instance of ['‘`]Enum", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültige Instanz-Form", Pattern.compile("illegal instance declaration.*flexibleinstances", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Kein Datenkonstruktor", Pattern.compile("not a data constructor", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültiges Deriving", Pattern.compile("illegal deriving item", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Flexible Kontexte benötigt", Pattern.compile("non type-variable argument in the constraint", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Erneut ungültige Typensignatur", Pattern.compile("illegal type signature", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Fehlende GADTs-Erweiterung", Pattern.compile("enable the GADTs extension", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Kind-Konflikt (Constraint vs. Typ)", Pattern.compile("expected (a constraint|a type), but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Ungültiger Typ-Operator", Pattern.compile("illegal operator .* in type .*", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Fehlende Klammern im Range-Ausdruck", Pattern.compile("a section must be enclosed in parentheses", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Warnung", Pattern.compile("warning", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Sonstiger Fehler", Pattern.compile(".*", Pattern.DOTALL)); - } - public final static String classify(String stderr) { + static { + CLUSTERS.put("Parse-Fehler", Pattern.compile("\\bparse\\s+error\\b", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Typenkonflikt", Pattern.compile("couldn'?t match (expected type|type)", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Variable nicht im Gültigkeitsbereich", Pattern.compile("not in scope", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Leerer do-Block", Pattern.compile("empty\\s+'do'\\s+block", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Unendlicher Typ", Pattern.compile("occurs check:.*infinite type", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Modul nicht gefunden", Pattern.compile("could not find module", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlendes Binding", Pattern.compile("type signature.*lacks an accompanying binding", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Fehlende Instanz", Pattern.compile("no instance for", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Abweichende Arity", Pattern.compile("equations for .* have different numbers of arguments", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Doppelte Signatur", Pattern.compile("duplicate type signatures?", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Mehrdeutiger Bezeichner", Pattern.compile("ambiguous occurrence", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Syntaxfehler", Pattern.compile("syntax error", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültige Typensignatur", Pattern.compile("invalid type signature", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Mehrfache Deklarationen", Pattern.compile("multiple declarations", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlerhafter Datenkonstruktor", Pattern.compile("cannot parse data constructor in a data/newtype declaration", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlerhafter Typ-Header", Pattern.compile("malformed head of type or class declaration", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Lexikalischer Fehler", Pattern.compile("lexical error at character", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Kind-Konflikt", Pattern.compile("expected kind .* but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Mehrdeutiger Typ", Pattern.compile("ambiguous type variable", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Doppelte Instanz", Pattern.compile("duplicate instance declarations", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlende Constraint", Pattern.compile("could not deduce.*\\(", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Konfliktierende Bindings", Pattern.compile("conflicting definitions for", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Methode nicht in Klasse", Pattern.compile("is not a \\(visible\\) method of class", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültige Instanz-Signatur", Pattern.compile("illegal type signature in instance declaration", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Unvollständiger Typ", Pattern.compile("expecting one more argument to .*has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Constraint erwartet, aber Typ erhalten", Pattern.compile("expected a constraint, but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Pattern Binding in Instanz", Pattern.compile("pattern bindings.*not allowed in instance declaration", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Falsche Konstruktor-Arity", Pattern.compile("the constructor ‘.*’ should have \\d+ argument[s]?, but has been given \\d+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Typed Hole", Pattern.compile("found hole: _ ::", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültige Binding-Syntax", Pattern.compile("illegal binding of built-in syntax", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Letzte Anweisung im 'do'-Block", Pattern.compile("the last statement in a 'do' block must be an expression", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Überlappende Instanzen", Pattern.compile("overlapping instances for", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültiges Enum-Deriving", Pattern.compile("can't make a derived instance of ['‘`]Enum", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültige Instanz-Form", Pattern.compile("illegal instance declaration.*flexibleinstances", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Kein Datenkonstruktor", Pattern.compile("not a data constructor", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültiges Deriving", Pattern.compile("illegal deriving item", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Flexible Kontexte benötigt", Pattern.compile("non type-variable argument in the constraint", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Erneut ungültige Typensignatur", Pattern.compile("illegal type signature", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlende GADTs-Erweiterung", Pattern.compile("enable the GADTs extension", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Kind-Konflikt (Constraint vs. Typ)", Pattern.compile("expected (a constraint|a type), but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Ungültiger Typ-Operator", Pattern.compile("illegal operator .* in type .*", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlende Klammern im Range-Ausdruck", Pattern.compile("a section must be enclosed in parentheses", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Warnung", Pattern.compile("warning", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Sonstiger Fehler", Pattern.compile(".*", Pattern.DOTALL)); + } - for (Map.Entry entry : CLUSTERS.entrySet()) { - if (entry.getValue().matcher(stderr).find()) { - return entry.getKey(); - } - } - return "Sonstige Fehler"; - } + public final static String classify(String stderr) { + for (Map.Entry entry : CLUSTERS.entrySet()) { + if (entry.getValue().matcher(stderr).find()) { + return entry.getKey(); + } + } + return "Sonstige Fehler"; + } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java index 2f8106f70..8a34948ae 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java @@ -126,7 +126,6 @@ protected void analyzeAndSetResult(boolean exitedCleanly, StringBuffer stdout, S result.setTestOutput(stdout.toString()); } - // similar code in JavaAdvancedIOTest protected boolean calculateTestResult(boolean exitedCleanly, final StringBuffer processOutput, final StringBuffer stdErr, final int exitCode, final boolean aborted) { JsonObjectBuilder builder = createJsonBuilder(exitedCleanly, processOutput, stdErr, exitCode, aborted); @@ -186,7 +185,7 @@ protected JsonObjectBuilder createJsonBuilder(boolean exitedCleanly, final Strin } - protected String generateTestShellScript(){ + protected String generateTestShellScript() { StringBuilder testCode = new StringBuilder(); testCode.append("#!/bin/bash\n"); testCode.append("set -e\n"); @@ -206,7 +205,6 @@ protected String generateTestShellScript(){ return testCode.toString(); } - @Override protected void performTestInTempDir(Path basePath, Path pTempDir, TestExecutorTestResult testResult) throws Exception {} } diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java index 9cb9e860b..e3521f2ef 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java @@ -18,49 +18,36 @@ */ package de.tuclausthal.submissioninterface.testframework.tests.impl; -import java.io.IOException; -import java.io.Writer; -import java.lang.invoke.MethodHandles; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; import java.util.Random; -import jakarta.json.Json; -import jakarta.json.JsonObjectBuilder; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import de.tuclausthal.submissioninterface.testframework.executor.TestExecutorTestResult; -import de.tuclausthal.submissioninterface.util.Util; public class HaskellSyntaxTest extends DockerTest { - private static final Random random = new Random(); - private final String separator; - - public HaskellSyntaxTest(de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest test) { - super(test); - separator = "##"; - } - - @Override - protected final void analyzeAndSetResult(final boolean exitedCleanly, final StringBuffer stdout, final StringBuffer stderr, final int exitCode, final boolean aborted, final TestExecutorTestResult result) { - boolean success = exitedCleanly && !stderr.toString().toLowerCase().contains("error:"); - result.setTestPassed(success); - result.setTestOutput(createJsonBuilder(success, stdout, stderr, exitCode, aborted).build().toString()); - } - - @Override - protected final String generateTestShellScript(){ - return """ - #!/bin/bash - set -e - echo '%s' - for file in *.hs; do - ghci -ignore-dot-ghci -v0 -ferror-spans -fdiagnostics-color=never -Wall -e ":load $file" -e ":quit" - done - """.formatted(separator); - } + private static final Random random = new Random(); + private final String separator; + + public HaskellSyntaxTest(de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest test) { + super(test); + separator = "##"; + } + + @Override + protected final void analyzeAndSetResult(final boolean exitedCleanly, final StringBuffer stdout, final StringBuffer stderr, final int exitCode, final boolean aborted, final TestExecutorTestResult result) { + boolean success = exitedCleanly && !stderr.toString().toLowerCase().contains("error:"); + result.setTestPassed(success); + result.setTestOutput(createJsonBuilder(success, stdout, stderr, exitCode, aborted).build().toString()); + } + + @Override + protected final String generateTestShellScript() { + return """ + #!/bin/bash + set -e + echo '%s' + for file in *.hs; do + ghci -ignore-dot-ghci -v0 -ferror-spans -fdiagnostics-color=never -Wall -e ":load $file" -e ":quit" + done + """.formatted(separator); + } } From bc448439227f1d145b414d6844182d99b2fb3cb9 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Fri, 16 May 2025 00:09:11 +0200 Subject: [PATCH 035/105] Formatting --- .../submissioninterface/servlets/controller/TestManager.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java index c5e0a67f3..05ab8e735 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java @@ -173,9 +173,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr test.setTimeout(15); session.getTransaction().commit(); - response.sendRedirect(Util.generateRedirectURL(TaskManager.class.getSimpleName() - + "?action=editTask&lecture=" + task.getTaskGroup().getLecture().getId() - + "&taskid=" + task.getTaskid(), response)); + response.sendRedirect(Util.generateRedirectURL(TaskManager.class.getSimpleName() + "?action=editTask&lecture=" + task.getTaskGroup().getLecture().getId() + "&taskid=" + task.getTaskid(), response)); } else if ("saveNewTest".equals(request.getParameter("action")) && "checklist".equals(request.getParameter("type"))) { session.beginTransaction(); TestDAOIf testDAO = DAOFactory.TestDAOIf(session); From aab38fcc35ccf40a7b73a97b640b500afd19248c Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 18 May 2025 18:23:39 +0200 Subject: [PATCH 036/105] Formatting --- .../view/HaskellRuntimeTestManagerView.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index da3914b8e..a991e931e 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -115,28 +115,28 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println("

Testschritte bearbeiten

"); out.println(""); out.println(/* @formatter:off */ - "" + - "" + - "" + - "" + - "" + - "" + - "" + "" + + "" + + "" + + "" + + "" + + "" + + "" /* @formatter:on */); for (DockerTestStep step : test.getTestSteps()) { String deleteTestStepLink = Util.generateHTMLLink(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + test.getId() + "&action=deleteStep&teststepid=" + step.getTeststepid(), response); out.println(/* @formatter:off */ - "" + - "" + - "" + - "" + - "" + "" + + "" + + "" + + "" + + "" /* @formatter:on */); } out.println("
TitelTestcodeExpected
TitelTestcodeExpected
" + - Util.escapeHTML(step.getTitle()) + " " + - "" + - "(Löschen)" + - "" + - "" + Util.escapeHTML(step.getTestcode()) + "" + Util.escapeHTML(step.getExpect()) + "
" + + Util.escapeHTML(step.getTitle()) + " " + + "" + + "(Löschen)" + + "" + + "" + Util.escapeHTML(step.getTestcode()) + "" + Util.escapeHTML(step.getExpect()) + "
"); From cb66082a153a8e176f1a8c4a6e6f69292ba91578 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 18 May 2025 18:26:09 +0200 Subject: [PATCH 037/105] Formatting --- .../servlets/view/HaskellRuntimeTestManagerView.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index a991e931e..c1566a030 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -122,7 +122,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro "Expected" + "" + "" - /* @formatter:on */); + /* @formatter:on */); for (DockerTestStep step : test.getTestSteps()) { String deleteTestStepLink = Util.generateHTMLLink(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + test.getId() + "&action=deleteStep&teststepid=" + step.getTeststepid(), response); @@ -137,7 +137,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro "" + Util.escapeHTML(step.getTestcode()) + "" + "" + Util.escapeHTML(step.getExpect()) + "" + "" - /* @formatter:on */); + /* @formatter:on */); } out.println(""); From fc6d40e169a255f653526fdfca545b6b9b6ae34c Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Thu, 12 Jun 2025 16:15:05 +0200 Subject: [PATCH 038/105] Edit test using HaskellRuntimeTestManager instead of DockerTestManager - otherwise, POST requests via
elements of HaskellRuntimeTestManagerView are handled by DockerTestManager, which does not support the HaskellRuntimeTest-specific requests --- .../submissioninterface/servlets/view/TaskManagerView.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TaskManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TaskManagerView.java index da2acef2a..4987ce359 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TaskManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TaskManagerView.java @@ -52,6 +52,7 @@ import de.tuclausthal.submissioninterface.servlets.controller.DownloadModelSolutionFile; import de.tuclausthal.submissioninterface.servlets.controller.DownloadTaskFile; import de.tuclausthal.submissioninterface.servlets.controller.DupeCheck; +import de.tuclausthal.submissioninterface.servlets.controller.HaskellRuntimeTestManager; import de.tuclausthal.submissioninterface.servlets.controller.JavaAdvancedIOTestManager; import de.tuclausthal.submissioninterface.servlets.controller.PerformTest; import de.tuclausthal.submissioninterface.servlets.controller.ShowLecture; @@ -478,6 +479,9 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro if (test instanceof JavaAdvancedIOTest adiot) { out.println("Bestehend aus " + adiot.getTestSteps().size() + " Schritten
"); out.println("Test bearbeiten
"); + } else if (test instanceof HaskellRuntimeTest hrt) { + out.println("Bestehend aus " + hrt.getTestSteps().size() + " Schritten
"); + out.println("Test bearbeiten
"); } else if (test instanceof DockerTest dt) { out.println("Bestehend aus " + dt.getTestSteps().size() + " Schritten
"); out.println("Test bearbeiten
"); From 9e9a81569194f2eb46d2c114a01a91926aad915a Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 15 Jun 2025 16:34:25 +0200 Subject: [PATCH 039/105] Add function evaluateWithGhci() in HaskellRuntimeTestManager - intended usage: will be used by the testcase generator of the HaskellRuntimeTest to evaluate arbitrary haskell expressions in ghci --- .../controller/HaskellRuntimeTestManager.java | 133 +++++++++++++++++- .../view/HaskellRuntimeTestManagerView.java | 7 + 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 325bce006..03179c0d1 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -21,6 +21,12 @@ import java.io.IOException; import java.io.Serial; +import java.io.Writer; +import java.lang.invoke.MethodHandles; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; @@ -29,6 +35,8 @@ import org.hibernate.Session; import org.hibernate.Transaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import de.tuclausthal.submissioninterface.persistence.dao.DAOFactory; import de.tuclausthal.submissioninterface.persistence.dao.ParticipationDAOIf; @@ -37,10 +45,14 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; import de.tuclausthal.submissioninterface.persistence.datamodel.ParticipationRole; +import de.tuclausthal.submissioninterface.persistence.datamodel.Task; import de.tuclausthal.submissioninterface.persistence.datamodel.Test; import de.tuclausthal.submissioninterface.servlets.GATEController; import de.tuclausthal.submissioninterface.servlets.RequestAdapter; import de.tuclausthal.submissioninterface.servlets.view.MessageView; +import de.tuclausthal.submissioninterface.testframework.tests.impl.ProcessOutputGrabber; +import de.tuclausthal.submissioninterface.util.Configuration; +import de.tuclausthal.submissioninterface.util.TaskPath; import de.tuclausthal.submissioninterface.util.Util; /** @@ -53,6 +65,10 @@ public class HaskellRuntimeTestManager extends HttpServlet { @Serial private static final long serialVersionUID = 1L; + final static private Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + // TODO@CHW: should safe docker path be part of de.tuclausthal.submissioninterface.util.Configuration? (since it is duplicated from DockerTest) + final static public String SAFE_DOCKER_SCRIPT = "/usr/local/bin/safe-docker"; @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { @@ -78,7 +94,22 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr return; } - if ("generateNewTestSteps".equals(request.getParameter("action"))) { + if ("getHaskellIdentifiers".equals(request.getParameter("action"))) { + // TODO@CHW what happens if no model solution is uploaded? + + SubprocessResult res = evaluateWithGhci(new String[] { "1 + 1", "sum [1,2,3]" }, test.getTask()); + + LOG.info("STDOUT IS {}", res.stdOut()); + LOG.info("STDERR IS {}", res.stdErr()); + LOG.info("EXIT CODE IS {}", res.exitCode()); + LOG.info("ABORTED IS {}", res.aborted()); + + // TODO@CHW + // browse haskell identifiers of model solution + // start transaction to store identifiers in the database and commit + // send redirect to HRTManager + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + } else if ("generateNewTestSteps".equals(request.getParameter("action"))) { int numberOfTestSteps = Util.parseInteger(request.getParameter("numberOfTestSteps"), 0); String[][] testcases = new String[numberOfTestSteps][3]; @@ -105,4 +136,104 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr getServletContext().getNamedDispatcher(DockerTestManager.class.getSimpleName()).forward(request, response); } } + + private record SubprocessResult(String stdOut, String stdErr, int exitCode, boolean aborted) { + } + + /** Evaluate haskell expressions using ghci expression evaluation mode (e.g. ghci -e "sum [1,2,3]"). + * Similar code in "de.tuclausthal.submissioninterface.testframework.tests.impl.DockerTest". + * This code duplicate is justified for the following reasons: + * 1. The DockerTest is specifically designed for testing student submissions, and consists of a sequence + * of DockerTestSteps that together evaluate a student submission. In contrast, this method evaluates + * arbitrary ghci expressions that are used for generating testcases rather than for testing a submission. + * 2. The DockerTest includes logic to analyze the subprocess output, based on the DockerTestSteps it consists + * of. Since this function evaluates arbitrary ghci expressions, this post-processing is not suitable here. + * + * @param expressions List of expressions (e.g. ["expr1", "expr2"]) that should be evaluated by ghci in this order + * (e.g. this will call "ghci -e expr1 -e expr2"). + * @param task Task, for which the testcases should be generated based on the model solution + * @return result of the subprocess + */ + private SubprocessResult evaluateWithGhci(String[] expressions, Task task) throws IOException { + final Path taskPath = Util.constructPath(Configuration.getInstance().getDataPath(), task); + final Path modelSolutionPath = taskPath.resolve(TaskPath.MODELSOLUTIONFILES.getPathComponent()); + final int safeDockerTimeout = 10; + + Path generatorTempDir = null; + try { + generatorTempDir = Util.createTemporaryDirectory("haskellruntimegenerator"); + if (generatorTempDir == null) { + throw new IOException("Failed to create tempdir!"); + } + + final Path modelSolutionDir = generatorTempDir.resolve("modelsolution"); + Files.createDirectories(modelSolutionDir); + + final Path administrativeDir = generatorTempDir.resolve("administrative"); + Files.createDirectories(administrativeDir); + + if (Files.isDirectory(modelSolutionPath)) { + Util.recursiveCopy(modelSolutionPath, modelSolutionDir); + } + + // TODO@CHW: testCode is more complex in DockerTest + StringBuilder testCode = new StringBuilder("ghci"); + for (String expression : expressions) { + testCode.append(" -e \"").append(expression).append("\""); + } + + final Path testDriver = administrativeDir.resolve("test.sh"); + try (Writer fw = Files.newBufferedWriter(testDriver)) { + fw.write(testCode.toString()); + } + + List params = new ArrayList<>(); + params.add("sudo"); + params.add(SAFE_DOCKER_SCRIPT); + params.add("--timeout=" + safeDockerTimeout); + params.add("--dir=" + Util.escapeCommandlineArguments(administrativeDir.toAbsolutePath().toString())); + params.add("--"); + params.add("bash"); + params.add(Util.escapeCommandlineArguments(testDriver.toAbsolutePath().toString())); + + ProcessBuilder pb = new ProcessBuilder(params); + pb.directory(modelSolutionDir.toFile()); + + // only forward explicitly specified environment variables to test processes + pb.environment().keySet().removeIf(key -> !("PATH".equalsIgnoreCase(key) || "USER".equalsIgnoreCase(key) || "LANG".equalsIgnoreCase(key))); + + LOG.debug("Executing external process: {} in {}", params, modelSolutionDir); + + Process process = pb.start(); + ProcessOutputGrabber outputGrabber = new ProcessOutputGrabber(process); + + int exitCode = -1; + + boolean aborted = false; + try { + exitCode = process.waitFor(); + } catch (InterruptedException e) { + aborted = true; + } + + if (exitCode == 23 || exitCode == 24) { // magic value of the safe-docker script (23=timeout, 24=oom) + aborted = true; + } + + try { + outputGrabber.waitFor(); + } catch (InterruptedException e) { + throw new IOException("Running haskell testcase generator failed"); + } + + String stdOut = outputGrabber.getStdOutBuffer().toString(); + String stdErr = outputGrabber.getStdErrBuffer().toString(); + + return new SubprocessResult(stdOut, stdErr, exitCode, aborted); + } finally { + if (generatorTempDir != null) { + Util.recursiveDelete(generatorTempDir); + } + } + } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index c1566a030..23729fe48 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -94,6 +94,13 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println("
"); + out.println("

Benutzerdefinierte Haskell Funktionen und Datentypen der Musterlösung

"); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println(""); + out.println("

Neue Testschritte automatisch generieren

"); out.println("

Testschritt Generator ist noch nicht vollständig implementiert.

"); // TODO@CHW out.println("
"); From 7f37f754a7b1ab17295aa477160d305207110794 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 15 Jun 2025 16:37:39 +0200 Subject: [PATCH 040/105] Add hashable package to safe-docker Dockerfile - needed for using the haskell "hash" function (required by the generator) --- safe-docker/Dockerfile | 2 +- .../servlets/controller/HaskellRuntimeTestManager.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/safe-docker/Dockerfile b/safe-docker/Dockerfile index b38785275..e54090710 100644 --- a/safe-docker/Dockerfile +++ b/safe-docker/Dockerfile @@ -7,7 +7,7 @@ FROM debian:buster RUN apt-get update -qq && apt-get dist-upgrade -y && apt-get clean && rm -rf /var/lib/apt/lists/* # set up Haskell -RUN apt-get -qq update && apt-get install -qq --yes ghc libghc-test-framework-dev libghc-test-framework-hunit-dev libghc-test-framework-quickcheck2-dev && apt-get clean && rm -rf /var/lib/apt/lists/* +RUN apt-get -qq update && apt-get install -qq --yes ghc libghc-test-framework-dev libghc-test-framework-hunit-dev libghc-test-framework-quickcheck2-dev libghc-hashable-dev && apt-get clean && rm -rf /var/lib/apt/lists/* # set default language, but with UTF-8; needed for Haskell ENV LC_ALL="C.UTF-8" diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 03179c0d1..95762cb3c 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -97,7 +97,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr if ("getHaskellIdentifiers".equals(request.getParameter("action"))) { // TODO@CHW what happens if no model solution is uploaded? - SubprocessResult res = evaluateWithGhci(new String[] { "1 + 1", "sum [1,2,3]" }, test.getTask()); + SubprocessResult res = evaluateWithGhci(new String[] { ":set -package hashable", ":m + Data.Hashable", "hash [1,2,3]" }, test.getTask()); LOG.info("STDOUT IS {}", res.stdOut()); LOG.info("STDERR IS {}", res.stdErr()); From 6eb0067f87eef5e77b766e3ea8f48f1b8899b315 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 15 Jun 2025 19:32:22 +0200 Subject: [PATCH 041/105] Disable form button and show "Bitte warten..." while ghci expressions are evaluated in background --- .../servlets/view/HaskellRuntimeTestManagerView.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 23729fe48..7c881c352 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -95,10 +95,11 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println("
"); out.println("

Benutzerdefinierte Haskell Funktionen und Datentypen der Musterlösung

"); - out.println(""); + out.println(""); out.println(""); out.println(""); - out.println(""); + out.println(""); + out.println("
Bitte warten...
\n"); out.println(""); out.println("

Neue Testschritte automatisch generieren

"); From f1a7458c37f5027e471ef77b3932a47704eb4498 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 15 Jun 2025 23:02:19 +0200 Subject: [PATCH 042/105] Load model solution in evaluateWithGhci(); show errors in view --- .../controller/HaskellRuntimeTestManager.java | 61 +++++++++++++------ .../view/HaskellRuntimeTestManagerView.java | 3 + 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 95762cb3c..dde9e589b 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -27,6 +27,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; @@ -97,18 +98,21 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr if ("getHaskellIdentifiers".equals(request.getParameter("action"))) { // TODO@CHW what happens if no model solution is uploaded? - SubprocessResult res = evaluateWithGhci(new String[] { ":set -package hashable", ":m + Data.Hashable", "hash [1,2,3]" }, test.getTask()); - - LOG.info("STDOUT IS {}", res.stdOut()); - LOG.info("STDERR IS {}", res.stdErr()); - LOG.info("EXIT CODE IS {}", res.exitCode()); - LOG.info("ABORTED IS {}", res.aborted()); - - // TODO@CHW - // browse haskell identifiers of model solution - // start transaction to store identifiers in the database and commit - // send redirect to HRTManager - response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + try { + SubprocessResult res = evaluateWithGhci(new String[] { "hashable", "QuickCheck" }, new String[] { "Data.Hashable", "Test.QuickCheck" }, true, new String[] { ":browse", "hash [1,2,3]" }, test.getTask()); + LOG.info("STDOUT IS {}", res.stdOut()); + LOG.info("STDERR IS {}", res.stdErr()); + LOG.info("EXIT CODE IS {}", res.exitCode()); + LOG.info("ABORTED IS {}", res.aborted()); + + // TODO@CHW + // browse haskell identifiers of model solution + // start transaction to store identifiers in the database and commit + // send redirect to HRTManager + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + } catch (IOException e) { + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId() + "&getidentifiererror=" + e.getMessage(), response)); + } } else if ("generateNewTestSteps".equals(request.getParameter("action"))) { int numberOfTestSteps = Util.parseInteger(request.getParameter("numberOfTestSteps"), 0); String[][] testcases = new String[numberOfTestSteps][3]; @@ -149,15 +153,17 @@ private record SubprocessResult(String stdOut, String stdErr, int exitCode, bool * 2. The DockerTest includes logic to analyze the subprocess output, based on the DockerTestSteps it consists * of. Since this function evaluates arbitrary ghci expressions, this post-processing is not suitable here. * - * @param expressions List of expressions (e.g. ["expr1", "expr2"]) that should be evaluated by ghci in this order - * (e.g. this will call "ghci -e expr1 -e expr2"). + * @param packagesToEnable List of packages (e.g. ["QuickCheck"]), that need to be enabled (e.g. ":set -package QuickCheck") + * @param modulesToImport List of modules (e.g. ["Control.Monad", "Test.QuickCheck"]) that need to be imported (e.g. ":m + Control.Monad Test.QuickCheck") + * @param loadModelSolution Whether the model solution should be loaded into ghci (i.e. using "ghci :l") + * @param expressionsToEvaluate List of expressions (e.g. ["expr1", "expr2"]) that should be evaluated by ghci in this order (e.g. this will call "ghci -e expr1 -e expr2"). * @param task Task, for which the testcases should be generated based on the model solution * @return result of the subprocess */ - private SubprocessResult evaluateWithGhci(String[] expressions, Task task) throws IOException { + private SubprocessResult evaluateWithGhci(String[] packagesToEnable, String[] modulesToImport, boolean loadModelSolution, String[] expressionsToEvaluate, Task task) throws IOException { final Path taskPath = Util.constructPath(Configuration.getInstance().getDataPath(), task); final Path modelSolutionPath = taskPath.resolve(TaskPath.MODELSOLUTIONFILES.getPathComponent()); - final int safeDockerTimeout = 10; + final int safeDockerTimeout = 30; Path generatorTempDir = null; try { @@ -176,9 +182,30 @@ private SubprocessResult evaluateWithGhci(String[] expressions, Task task) throw Util.recursiveCopy(modelSolutionPath, modelSolutionDir); } + Path hsFile = null; + if (loadModelSolution) { + // Expect exactly one .hs file among the modelsolution files -> this file will be used to generate the testcases + try (Stream stream = Files.list(modelSolutionDir)) { + List hsFiles = stream.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".hs")).toList(); + + if (hsFiles.size() != 1) { + throw new IOException("Expected exactly one .hs file in modelSolutionDir, found " + hsFiles.size() + " files."); + } else { + hsFile = hsFiles.get(0); + } + } + } + // TODO@CHW: testCode is more complex in DockerTest StringBuilder testCode = new StringBuilder("ghci"); - for (String expression : expressions) { + for (String packageToEnable : packagesToEnable) { + testCode.append(" -e \"").append(":set -package ").append(packageToEnable).append("\""); + } + testCode.append(" -e \":m + ").append(String.join(" ", modulesToImport)).append("\""); + if (hsFile != null) { + testCode.append(" -e \"").append(":load ").append(hsFile.getFileName().toString()).append("\""); + } + for (String expression : expressionsToEvaluate) { testCode.append(" -e \"").append(expression).append("\""); } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 7c881c352..01bb682fc 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -95,6 +95,9 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println("
"); out.println("

Benutzerdefinierte Haskell Funktionen und Datentypen der Musterlösung

"); + if (request.getParameter("getidentifiererror") != null) { + out.println("

Beim Analysieren der Musterlösung ist ein Fehler aufgetreten: " + request.getParameter("getidentifiererror") + "

"); + } out.println("
"); out.println(""); out.println(""); From 9bc321051fed0163ad152f80d9178dc457f82180 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Mon, 16 Jun 2025 00:02:45 +0200 Subject: [PATCH 043/105] Use URLEncoding to show error messages occuring in evaluateWithGhci() --- .../controller/HaskellRuntimeTestManager.java | 19 ++++++++++++++++--- .../view/HaskellRuntimeTestManagerView.java | 5 ++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index dde9e589b..e989f4334 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -23,6 +23,8 @@ import java.io.Serial; import java.io.Writer; import java.lang.invoke.MethodHandles; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -99,7 +101,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr // TODO@CHW what happens if no model solution is uploaded? try { - SubprocessResult res = evaluateWithGhci(new String[] { "hashable", "QuickCheck" }, new String[] { "Data.Hashable", "Test.QuickCheck" }, true, new String[] { ":browse", "hash [1,2,3]" }, test.getTask()); + SubprocessResult res = evaluateWithGhci(new String[] { "hashable", "QuickCheck" }, new String[] { "Data.Hashable", "Test.QuickCheck" }, true, new String[] { ":browse", "hash [1,2,3]" }, test.getTask(), true); LOG.info("STDOUT IS {}", res.stdOut()); LOG.info("STDERR IS {}", res.stdErr()); LOG.info("EXIT CODE IS {}", res.exitCode()); @@ -111,7 +113,8 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr // send redirect to HRTManager response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); } catch (IOException e) { - response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId() + "&getidentifiererror=" + e.getMessage(), response)); + String errorMessage = URLEncoder.encode(Util.escapeHTML(e.getMessage()), StandardCharsets.UTF_8); + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId() + "&getidentifiererror=" + errorMessage, response)); } } else if ("generateNewTestSteps".equals(request.getParameter("action"))) { int numberOfTestSteps = Util.parseInteger(request.getParameter("numberOfTestSteps"), 0); @@ -160,7 +163,7 @@ private record SubprocessResult(String stdOut, String stdErr, int exitCode, bool * @param task Task, for which the testcases should be generated based on the model solution * @return result of the subprocess */ - private SubprocessResult evaluateWithGhci(String[] packagesToEnable, String[] modulesToImport, boolean loadModelSolution, String[] expressionsToEvaluate, Task task) throws IOException { + private SubprocessResult evaluateWithGhci(String[] packagesToEnable, String[] modulesToImport, boolean loadModelSolution, String[] expressionsToEvaluate, Task task, boolean throwIOExceptionOnNonZeroExitCode) throws IOException { final Path taskPath = Util.constructPath(Configuration.getInstance().getDataPath(), task); final Path modelSolutionPath = taskPath.resolve(TaskPath.MODELSOLUTIONFILES.getPathComponent()); final int safeDockerTimeout = 30; @@ -256,6 +259,16 @@ private SubprocessResult evaluateWithGhci(String[] packagesToEnable, String[] mo String stdOut = outputGrabber.getStdOutBuffer().toString(); String stdErr = outputGrabber.getStdErrBuffer().toString(); + if (throwIOExceptionOnNonZeroExitCode) { + if (exitCode == 23) { + throw new IOException("Running haskell testcase generator timed out (Timeout: " + safeDockerTimeout + "s)"); + } else if (exitCode == 24) { + throw new IOException("Running haskell testcase generator failed (Out of memory)"); + } else if (exitCode != 0) { + throw new IOException("Running haskell testcase generator failed with exit code " + exitCode + ". Output on stderr: " + stdErr); + } + } + return new SubprocessResult(stdOut, stdErr, exitCode, aborted); } finally { if (generatorTempDir != null) { diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 01bb682fc..54f8e36f9 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -22,6 +22,8 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.Serial; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; @@ -96,7 +98,8 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println("

Benutzerdefinierte Haskell Funktionen und Datentypen der Musterlösung

"); if (request.getParameter("getidentifiererror") != null) { - out.println("

Beim Analysieren der Musterlösung ist ein Fehler aufgetreten: " + request.getParameter("getidentifiererror") + "

"); + String errorMessage = URLDecoder.decode(request.getParameter("getidentifiererror"), StandardCharsets.UTF_8); + out.println("

Beim Analysieren der Musterlösung ist ein Fehler aufgetreten: " + errorMessage + "

"); } out.println(""); out.println(""); From b22eb124672ac47ace3fdcc551026567d1988b9b Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Fri, 27 Jun 2025 16:39:32 +0200 Subject: [PATCH 044/105] Add base implementation of haskell runtime testcase generator --- .../controller/HaskellRuntimeTestManager.java | 748 +++++++++++++++++- 1 file changed, 736 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index e989f4334..e0daae234 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -19,6 +19,8 @@ package de.tuclausthal.submissioninterface.servlets.controller; +import static java.lang.Math.ceil; + import java.io.IOException; import java.io.Serial; import java.io.Writer; @@ -28,7 +30,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Stream; import jakarta.servlet.ServletException; @@ -118,24 +124,20 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr } } else if ("generateNewTestSteps".equals(request.getParameter("action"))) { int numberOfTestSteps = Util.parseInteger(request.getParameter("numberOfTestSteps"), 0); - String[][] testcases = new String[numberOfTestSteps][3]; - // TODO@CHW: this is just a placeholder for the actual testcase generator - for (int testStepId = 0; testStepId < numberOfTestSteps; testStepId++) { - testcases[testStepId][0] = "Testcase " + testStepId; - testcases[testStepId][1] = "ghci -e " + testStepId + "+" + testStepId; - testcases[testStepId][2] = "" + (testStepId + testStepId); - } + List dockerTestStepDatas = generateTestcases(numberOfTestSteps, test.getTask()); + // TODO@CHW catch IOException Transaction tx = session.beginTransaction(); - for (int testStepId = 0; testStepId < numberOfTestSteps; testStepId++) { - String title = testcases[testStepId][0]; - String testCode = testcases[testStepId][1].replaceAll("\r\n", "\n"); - String expect = testcases[testStepId][2].replaceAll("\r\n", "\n"); + for (DockerTestStepData dockerTestStepData : dockerTestStepDatas) { + String title = dockerTestStepData.title(); + String testCode = dockerTestStepData.testCode().replaceAll("\r\n", "\n"); + String expectedValue = dockerTestStepData.expectedValue().replaceAll("\r\n", "\n"); - DockerTestStep newStep = new DockerTestStep(haskellRuntimeTest, title, testCode, expect); + DockerTestStep newStep = new DockerTestStep(haskellRuntimeTest, title, testCode, expectedValue); session.persist(newStep); } + tx.commit(); response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); @@ -144,6 +146,65 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr } } + private record DockerTestStepData(String title, String testCode, String expectedValue) { + } + + + private List generateTestcases(int numberOfTestSteps, Task task) throws IOException { + List generatedTestcases = new ArrayList<>(); + + List haskellIdentifiers = browseModelSolution(task); + + LOG.info("ALL IDENTIFIERS:"); + for (String haskellIdentifier : haskellIdentifiers) { + LOG.info(haskellIdentifier); + } + + HaskellClassifiedIdentifiers haskellClassifiedIdentifiers = classifyHaskellIdentifiers(haskellIdentifiers); + + List arbitraryInstances = new ArrayList<>(); + + for(HaskellNewtypeOrData haskellNewtypeOrData : haskellClassifiedIdentifiers.getNewtypesAndDatas()) { + String arbitraryInstance = haskellNewtypeOrData.generateArbitraryInstance(); + arbitraryInstances.add(arbitraryInstance); + + LOG.info("Newtype/data: {}", haskellNewtypeOrData.typename); + LOG.info("Arbitrary instance: {}", arbitraryInstance); + } + + for(HaskellFunction haskellFunction : haskellClassifiedIdentifiers.getFunctions()) { + LOG.info("Function: {}", haskellFunction.name); + LOG.info("Type:\t\t\t{}", haskellFunction.typeSignature); + + String defaultTypeSignature = getGhciDefaultTypeSignature(task, haskellFunction.name); + LOG.info("Type (+d):\t\t{}", defaultTypeSignature); + + String concreteTypeSignature = replaceUnconstrainedTypeVariables(defaultTypeSignature, HaskellPrimitiveType.Int); // TODO@CHW other default type + LOG.info("Concrete:\t\t{}", concreteTypeSignature); + + List functionParameterTypes = getFunctionParameterTypes(concreteTypeSignature); + LOG.info("Params:\t\t{}", functionParameterTypes); + + List testcases = generateQuickcheckFunctionTestcases(task, haskellFunction.name, functionParameterTypes, arbitraryInstances, numberOfTestSteps); + + List functionCalls = generateFunctionCalls(haskellFunction.name, testcases); + List expectedValues = computeExpectedValues(functionCalls, task); + + if (functionCalls.size() != expectedValues.size()) { + throw new AssertionError(String.format("Expected values: %d, function calls: %d", expectedValues.size(), functionCalls.size())); + } + + for (int i = 0; i < functionCalls.size(); i++) { + LOG.info(prettyPrintFunctionCall(functionCalls.get(i))); + LOG.info(expectedValues.get(i)); + + generatedTestcases.add(new DockerTestStepData("Testcase " + i, functionCalls.get(i), expectedValues.get(i))); + } + } + + return generatedTestcases; + } + private record SubprocessResult(String stdOut, String stdErr, int exitCode, boolean aborted) { } @@ -164,6 +225,13 @@ private record SubprocessResult(String stdOut, String stdErr, int exitCode, bool * @return result of the subprocess */ private SubprocessResult evaluateWithGhci(String[] packagesToEnable, String[] modulesToImport, boolean loadModelSolution, String[] expressionsToEvaluate, Task task, boolean throwIOExceptionOnNonZeroExitCode) throws IOException { + if (packagesToEnable == null) + packagesToEnable = new String[0]; + if (modulesToImport == null) + modulesToImport = new String[0]; + if (expressionsToEvaluate == null) + expressionsToEvaluate = new String[0]; + final Path taskPath = Util.constructPath(Configuration.getInstance().getDataPath(), task); final Path modelSolutionPath = taskPath.resolve(TaskPath.MODELSOLUTIONFILES.getPathComponent()); final int safeDockerTimeout = 30; @@ -276,4 +344,660 @@ private SubprocessResult evaluateWithGhci(String[] packagesToEnable, String[] mo } } } + + private List browseModelSolution(Task task) throws IOException { + SubprocessResult result = evaluateWithGhci(null, null, true, new String[] { ":browse" }, task, true); + return splitLinesButKeepMultilines(result.stdOut()); + } + + public List splitLinesButKeepMultilines(String resultStdout) { + List haskellIdentifiers = new ArrayList<>(); + + for (String line : resultStdout.split("\\R")) { + if (!(line.startsWith(" ") || line.startsWith("\t"))) { + haskellIdentifiers.add(line); + } else { + // Handle multi-line formatting + int lastIndex = haskellIdentifiers.size() - 1; + if (lastIndex >= 0) { + String updated = haskellIdentifiers.get(lastIndex) + "\n" + line; + haskellIdentifiers.set(lastIndex, updated); + } + } + } + + return haskellIdentifiers; + } + + public HaskellClassifiedIdentifiers classifyHaskellIdentifiers(List haskellIdentifiers) { + HaskellClassifiedIdentifiers classifiedIdentifiers = new HaskellClassifiedIdentifiers(); + + for (String line : haskellIdentifiers) { + if (line.startsWith("class")) { + classifiedIdentifiers.addClass(line); + } else if (line.startsWith("newtype") || line.startsWith("data")) { + classifiedIdentifiers.addNewtypeOrData(line); + } else if (line.contains("::")) { + classifiedIdentifiers.addFunction(line); + } else if (!(line.isEmpty() || line.startsWith("type"))) { + throw new IllegalArgumentException("Invalid Haskell identifier: " + line); + } + } + + return classifiedIdentifiers; + } + + public String getGhciDefaultTypeSignature(Task task, String identifierName) throws IOException { + SubprocessResult result = evaluateWithGhci(null, null, true, new String[] { ":type +d " + identifierName }, task, true); + + String defaultTypeSignature = normalizeTypeSignature(result.stdOut().split("::")[1].trim()); + if (defaultTypeSignature.contains("=>")) { + throw new IllegalArgumentException("Constraint => after :type +d not yet handled"); // TODO@CHW + } else { + return defaultTypeSignature; + } + } + + public String normalizeTypeSignature(String typeSignature) { + typeSignature = typeSignature.replace("\n", ""); + typeSignature = typeSignature.replaceAll("\\s*->\\s*", " -> "); + return typeSignature.trim(); + } + + public static String replaceUnconstrainedTypeVariables(String typeSignature, HaskellPrimitiveType replacementType) { + if (typeSignature.contains("=>")) { + // TODO@CHW: Implementation not yet correct for Functor, Monad, Applicative, etc. + // e.g. (<$>) :: Functor f => (a -> b) -> f a -> f b + // (=<<) :: Monad m => (a -> m b) -> m a -> m b + throw new IllegalArgumentException("Type signature contains constraints, not implemented yet"); + } + + if (typeSignature.contains("::")) { + typeSignature = typeSignature.split("::", 2)[1].trim(); + } + + return typeSignature.replaceAll("(? getFunctionParameterTypes(String concreteTypeSignature) { + if (concreteTypeSignature.contains("::")) { + concreteTypeSignature = concreteTypeSignature.split("::", 2)[1].trim(); + } + if (concreteTypeSignature.contains("=>")) { + throw new IllegalArgumentException("Function type signature should only contain concrete types."); + } + + List parts = splitExceptBetweenParentheses(concreteTypeSignature, '(', ')', "->"); + + return parts.subList(0, parts.size() - 1); // remove return type (last element) + } + + //TODO@CHW maybe replace all trim() by strip()? + + public static boolean parameterTypeIsFunction(String parameterType) { + parameterType = parameterType.strip(); + return parameterType.contains("->") && parameterType.startsWith("(") && parameterType.endsWith(")"); + } + + public static String getFunctionReturnType(String concreteTypeSignature) { + if (concreteTypeSignature.contains("::")) { + concreteTypeSignature = concreteTypeSignature.split("::", 2)[1].strip(); + } + if (concreteTypeSignature.contains("=>")) { + throw new IllegalArgumentException("Function type signature should only contain concrete types."); + } + + List parts = splitExceptBetweenParentheses(concreteTypeSignature, '(', ')', "->"); + + // TODO@CHW: this can throw index out of bounds exception? + return parts.get(parts.size() - 1); // return type (last element) + } + + private record TestcaseSingleParameterWithType(String testcaseParameter, String type) { + } + + private record TestcaseWithTypes(List testcaseParametersWithTypes) { + } + + private List generateQuickcheckFunctionTestcases(Task task, String functionName, List functionParameterTypes, List arbitraryInstances, int numberOfTestcases) throws IOException { + if (functionParameterTypes.isEmpty()) { + return List.of(); + } + + final String TESTCASE_SEPARATOR = "@NEXT-TESTCASE@"; // TODO@CHW: use random value in all separators + final String TESTCASE_VALUE_SEPARATOR = "@NEXT-TESTCASE-VALUE@"; + + /* + * Placeholder type avoids that ghci simplifies tuples. Example: + * - Gen ((Int, Int)) is automatically simplified to Gen (Int, Int) + * - => A single parameter of type (Int, Int) is considered as two parameters of type Int + * - Gen (PlaceholderT, (Int, Int)) is NOT simplified to Gen (PlaceholderT, Int, Int) + */ + final String PLACEHOLDER_TYPE_NAME = "Placeholder"; + final String CYCLIC_INT_MAP_TYPE_NAME = "CyclicIntMap"; + + List quickcheckParameterTypes = withCyclicIntMapTypes(withConstrainedPrimitiveTypes(functionParameterTypes), CYCLIC_INT_MAP_TYPE_NAME); + + List parameterTypesTupleValues = new ArrayList<>(); + parameterTypesTupleValues.add(PLACEHOLDER_TYPE_NAME); + parameterTypesTupleValues.addAll(quickcheckParameterTypes); + + // In ein Tuple-String umwandeln + String parameterTypeTuple = "(" + String.join(", ", parameterTypesTupleValues) + ")"; + + LOG.info("- PARAMS TUPLE:\t\t" + parameterTypeTuple); + + String placeholderType = String.format(""" + data %1$s = %1$s + + instance Show %1$s where + show %1$s = \\"@PLACEHOLDER@\\" + + instance Arbitrary %1$s where + arbitrary = return %1$s + """, PLACEHOLDER_TYPE_NAME); + + String cyclicIntMap = String.format(""" + data %1$s target = %1$s { + name :: String, + cycleLength :: Int, + intMap :: Int -> target + } + + instance Show target => Show (%1$s target) where + show (%1$s name cycleLength intMap) = + name + ++ \\" i = case i of \\" + ++ concatMap (liftM2 (++) show ((\\" -> \\" ++) . (++ \\"; \\") . show . intMap)) [0 .. cycleLength - 1] + ++ \\"x -> \\" + ++ name + ++ \\" (abs (mod x \\" + ++ show cycleLength + ++ \\"))\\" + + instance Arbitrary target => Arbitrary (%1$s target) where + arbitrary = %1$s \\"cyclicIntMap\\" 50 <$> arbitrary + """, CYCLIC_INT_MAP_TYPE_NAME); + + // String safeAsciiValues = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz()[]{}+-*/.,:; _!?#$%&<=>@"; + String safeAsciiValues = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // TODO@CHW + String typenameCharOnlySafeAscii = HaskellConstrainedPrimitiveType.Char_OnlySafeAscii.toString(); + String typenameStringOnlySafeAscii = HaskellConstrainedPrimitiveType.String_OnlySafeAscii.toString(); + + String charStringOnlySafeAscii = String.format(""" + safeAsciiChar :: Gen Char + safeAsciiChar = elements \\"%1$s\\" + + newtype %2$s = %2$s Char + + instance Show %2$s where + show (%2$s c) = show c + + instance Arbitrary %2$s where + arbitrary = %2$s <$> safeAsciiChar + + newtype %3$s = %3$s String + + instance Show %3$s where + show (%3$s s) = show s + + instance Arbitrary %3$s where + arbitrary = %3$s <$> listOf safeAsciiChar + """, safeAsciiValues, typenameCharOnlySafeAscii, typenameStringOnlySafeAscii); + + String toStringList = String.format(""" + class ToStringList a where toStringList :: a -> [String] + + instance (Show a, Show b) => ToStringList (a, b) where toStringList (a, b) = [show a, show b] + + instance (Show a, Show b, Show c) => ToStringList (a, b, c) where + toStringList (a, b, c) = [show a, show b, show c] + + instance (Show a, Show b, Show c, Show d) => ToStringList (a, b, c, d) where + toStringList (a, b, c, d) = [show a, show b, show c, show d] + + instance (Show a, Show b, Show c, Show d, Show e) => ToStringList (a, b, c, d, e) where + toStringList (a, b, c, d, e) = [show a, show b, show c, show d, show e] + + instance (Show a, Show b, Show c, Show d, Show e, Show f) => ToStringList (a, b, c, d, e, f) where + toStringList (a, b, c, d, e, f) = [show a, show b, show c, show d, show e, show f] + + instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g) => ToStringList (a, b, c, d, e, f, g) where + toStringList (a, b, c, d, e, f, g) = [show a, show b, show c, show d, show e, show f, show g] + + instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g, Show h) => ToStringList (a, b, c, d, e, f, g, h) where + toStringList (a, b, c, d, e, f, g, h) = [show a, show b, show c, show d, show e, show f, show g, show h] + + instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g, Show h, Show i) => ToStringList (a, b, c, d, e, f, g, h, i) where + toStringList (a, b, c, d, e, f, g, h, i) = [show a, show b, show c, show d, show e, show f, show g, show h, show i] + + instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g, Show h, Show i, Show j) => ToStringList (a, b, c, d, e, f, g, h, i, j) where + toStringList (a, b, c, d, e, f, g, h, i, j) = [show a, show b, show c, show d, show e, show f, show g, show h, show i, show j] + """); + + String haskellCommand = String.format(""" + replicateM %d (generate (arbitrary :: Gen %s)) + >>= putStrLn . intercalate testcaseSeparator . map + ( intercalate testcaseValueSeparator + . filter (/= show %s) + . toStringList + ) + """, numberOfTestcases, parameterTypeTuple, PLACEHOLDER_TYPE_NAME); + + List expressionsToEvaluate = new ArrayList<>(); + expressionsToEvaluate.add("testcaseSeparator = \\\"" + TESTCASE_SEPARATOR + "\\\""); + expressionsToEvaluate.add("testcaseValueSeparator = \\\"" + TESTCASE_VALUE_SEPARATOR + "\\\""); + expressionsToEvaluate.add(placeholderType); + expressionsToEvaluate.add(charStringOnlySafeAscii); + expressionsToEvaluate.add(toStringList); + expressionsToEvaluate.add(cyclicIntMap); + expressionsToEvaluate.add(String.join("\n", arbitraryInstances)); + expressionsToEvaluate.add(haskellCommand); + + final String[] packagesToEnable = new String[] { "QuickCheck" }; + final String[] modulesToImport = new String[] { "Control.Monad", "Test.QuickCheck", "Data.List" }; + SubprocessResult result = evaluateWithGhci(packagesToEnable, modulesToImport, true, expressionsToEvaluate.toArray(new String[0]), task, true); + + String rawOutput = result.stdOut(); + if (rawOutput.endsWith("\n")) { + rawOutput = rawOutput.substring(0, rawOutput.length() - 1); + } + + List> testcases = Arrays.stream(rawOutput.split(TESTCASE_SEPARATOR)).map(testcase -> Arrays.asList(testcase.split(TESTCASE_VALUE_SEPARATOR))).toList(); + + List testcasesWithTypes = new ArrayList<>(); + + for (List testcase : testcases) { + if (testcase.size() != functionParameterTypes.size()) { + throw new AssertionError("Testcase length does not match function parameter types length"); + } + + List testcaseWithType = new ArrayList<>(); + + for (int i = 0; i < testcase.size(); i++) { + testcaseWithType.add(new TestcaseSingleParameterWithType(testcase.get(i), functionParameterTypes.get(i))); + } + + for (int i = 0; i < testcaseWithType.size(); i++) { + TestcaseSingleParameterWithType current = testcaseWithType.get(i); + String testcaseValue = current.testcaseParameter(); + String testcaseValueType = current.type(); + + if (parameterTypeIsFunction(testcaseValueType)) { + // remove outer parentheses + String innerType = testcaseValueType.strip(); + innerType = innerType.substring(1, innerType.length() - 1).strip(); + + int numberOfParameters = getFunctionParameterTypes(innerType).size(); + + String newTestcaseValue = embedCyclicIntMapInRandomFunction(numberOfParameters, testcaseValue); + testcaseWithType.set(i, new TestcaseSingleParameterWithType(newTestcaseValue, testcaseValueType)); + } + } + testcasesWithTypes.add(new TestcaseWithTypes(testcaseWithType)); + } + + return testcasesWithTypes; + } + + private List withConstrainedPrimitiveTypes(List parameterTypes) { + Map replacementDict = Map.of(HaskellPrimitiveType.Char.toString(), HaskellConstrainedPrimitiveType.Char_OnlySafeAscii.toString(), HaskellPrimitiveType.String.toString(), HaskellConstrainedPrimitiveType.String_OnlySafeAscii.toString()); + + String patternString = String.join("|", replacementDict.keySet().stream().map(key -> "(? constrainedTypes = new ArrayList<>(); + + for (String parameterType : parameterTypes) { + Matcher matcher = pattern.matcher(parameterType); + StringBuilder sb = new StringBuilder(); + + // TODO@CHW: not verified this while but looks fine + while (matcher.find()) { + String matchedKey = matcher.group(); + String replacement = replacementDict.get(matchedKey); + matcher.appendReplacement(sb, replacement); + } + matcher.appendTail(sb); + + constrainedTypes.add(sb.toString()); + } + + LOG.info("- PARAMS (constr.):\t" + constrainedTypes); + return constrainedTypes; + } + + private List withCyclicIntMapTypes(List parameterTypes, String cyclicIntMapConstructor) { + List cyclicIntMapTypes = new ArrayList<>(); + + for (String parameterType : parameterTypes) { + if (parameterTypeIsFunction(parameterType)) { + parameterType = parameterType.strip().substring(1, parameterType.length() - 1).strip(); + + String returnType = getFunctionReturnType(parameterType); + cyclicIntMapTypes.add(cyclicIntMapConstructor + " " + returnType); + } else { + cyclicIntMapTypes.add(parameterType); + } + } + + return cyclicIntMapTypes; + } + + private String embedCyclicIntMapInRandomFunction(int numberOfParameters, String cyclicIntMapDefinition) { + String cyclicIntMapName = cyclicIntMapDefinition.split("\\s+")[0].trim(); + + List parameters = new ArrayList<>(); + for (int i = 0; i < numberOfParameters; i++) { + parameters.add("p" + i); + } + + return String.format("(let %s in let randomFunction %s = (%s . hash . show) (%s) in randomFunction)", cyclicIntMapDefinition, String.join(" ", parameters), cyclicIntMapName, String.join(", ", parameters)); + } + + public List generateFunctionCalls(String functionName, List testcasesWithTypes) { + List functionCalls = new ArrayList<>(); + + for (TestcaseWithTypes testcaseWithTypes : testcasesWithTypes) { + // testcaseWithTypes contains for example: [('9', 'Int'), ('Just 18', 'Maybe Int')] + List parts = new ArrayList<>(); + parts.add(functionName); + + for (TestcaseSingleParameterWithType testcaseSingleParameterWithType : testcaseWithTypes.testcaseParametersWithTypes()) { + String value = testcaseSingleParameterWithType.testcaseParameter(); + String hsType = testcaseSingleParameterWithType.type(); + parts.add(String.format("(%s :: %s)", value, hsType)); + } + + String functionCall = String.join(" ", parts); + functionCalls.add(functionCall); + } + + return functionCalls; + } + + // TODO@CHW return value was list[str|None] in python, double check that this is fine + public List computeExpectedValues(List functionCalls, Task task) throws IOException { + final String exceptionLinePrefix = "@EXCEPTION@"; + + List wrappedFunctionCalls = functionCalls.stream().map(functionCall -> wrapGhciExpressionInCatch(functionCall, exceptionLinePrefix)).toList(); + + String expectedValueSeparator = "@NEXT-EXPECTED-VALUE@"; + + List expressionsToEvaluate = new ArrayList<>(); + for (String wrappedFunctionCall : wrappedFunctionCalls) { + expressionsToEvaluate.add(wrappedFunctionCall); + expressionsToEvaluate.add(String.format("putStr \\\"%s\\\"", expectedValueSeparator)); + } + + String[] packagesToEnable = new String[] { "hashable" }; + String[] modulesToImport = new String[] { "Control.Exception Data.Hashable" }; + SubprocessResult result = evaluateWithGhci(packagesToEnable, modulesToImport, true, expressionsToEvaluate.toArray(new String[0]), task, true); + + List expectedValues = new ArrayList<>(); + + for (String outputValue : result.stdOut().split(expectedValueSeparator)) { + if (!outputValue.trim().isEmpty()) { + expectedValues.add(outputValue.startsWith(exceptionLinePrefix) ? null : outputValue); + } + } + + if (expectedValues.size() != wrappedFunctionCalls.size()) { + throw new AssertionError(String.format("Expected values: %d, function calls: %d", expectedValues.size(), wrappedFunctionCalls.size())); + } + + return expectedValues; + } + + public String wrapGhciExpressionInCatch(String expression, String exceptionLinePrefix) { + return String.format("catch (putStr (show (%s))) ((putStr . (\\\"%s\\\" ++) . show) :: SomeException -> IO ())", expression, exceptionLinePrefix); + } + + public static String prettyPrintFunctionCall(String functionCall) { + return functionCall.replaceAll("\\(let cyclicIntMap .*? let randomFunction .*? in randomFunction\\)", ""); + } + + // BEGIN OF HASKELL UTILS / UTILS -------------------------------------------------------- + + private class HaskellClassifiedIdentifiers { + private final List classes = new ArrayList<>(); + private final List newtypesAndDatas = new ArrayList<>(); + private final List functions = new ArrayList<>(); + + public void addClass(String hsClass) { + classes.add(new HaskellClass(hsClass)); + } + + public void addNewtypeOrData(String hsNewtypeOrData) { + newtypesAndDatas.add(new HaskellNewtypeOrData(hsNewtypeOrData)); + } + + public void addFunction(String hsFunction) { + functions.add(new HaskellFunction(hsFunction)); + } + + public List getClasses() { + return classes; + } + + public List getNewtypesAndDatas() { + return newtypesAndDatas; + } + + public List getFunctions() { + return functions; + } + } + + private class HaskellClass { + private final String hsClass; + + public HaskellClass(String hsClass) { + if (!hsClass.startsWith("class") || !hsClass.contains("where")) { + throw new IllegalArgumentException("Invalid class definition: " + hsClass); + } + this.hsClass = hsClass; + } + + public String getHsClass() { + return hsClass; + } + } + + private class HaskellFunction { + private final String name; + private final String typeSignature; + + public HaskellFunction(String hsFunction) { + if (!hsFunction.contains("::")) { + throw new IllegalArgumentException("Invalid function definition: " + hsFunction); + } + + String[] parts = hsFunction.split("::", 2); + this.name = parts[0].trim(); + this.typeSignature = parts[1].trim(); + } + + public String getName() { + return name; + } + + public String getTypeSignature() { + return typeSignature; + } + + @Override + public String toString() { + return "FUNCTION " + name + " :: " + typeSignature; + } + } + + private class HaskellNewtypeOrData { + private final String typename; + private final List constructors = new ArrayList<>(); + + public HaskellNewtypeOrData(String hsNewtypeOrData) { + String normalizedInput = hsNewtypeOrData.replace("\n", " ").trim(); + + Pattern typePattern = Pattern.compile("\\b(?:data|newtype)\\s+" + "(?[\\w\\s]+?)" + "\\s*=\\s*" + "(?.*?)(?=\\s+deriving\\b|$)"); + + Matcher matcher = typePattern.matcher(normalizedInput); + if (matcher.find()) { + this.typename = matcher.group("typename").trim().replace("\n", " "); + + Pattern namedConstructorPattern = Pattern.compile("(?\\b\\w+\\b)\\s*\\{\\s*(?.*?)\\s*}"); + + for (String constructor : matcher.group("constructors").replace("\n", " ").trim().split("\\|")) { + Matcher namedConstructorPatternMatch = namedConstructorPattern.matcher(constructor.trim()); + + if (namedConstructorPatternMatch.find()) { + String constr = namedConstructorPatternMatch.group("constr"); + String fields = namedConstructorPatternMatch.group("fields"); + List fieldTypes = new ArrayList<>(); + + for (String f : fields.split(",")) { + String[] parts = f.split("::"); + if (parts.length == 2) { + fieldTypes.add(parts[1].trim()); + } + } + constructors.add(constr + " " + String.join(" ", fieldTypes)); + } else { + constructors.add(constructor.trim()); + } + } + } else { + throw new IllegalArgumentException("Invalid newtype/data definition: " + hsNewtypeOrData); + } + } + + @Override + public String toString() { + return "NEWTYPE/DATA " + typename + " = " + constructors; + } + + public String generateArbitraryInstance() { + List recursiveReturnExpressions = new ArrayList<>(); + List nonrecursiveReturnExpressions = new ArrayList<>(); + + for (String constructor : constructors) { + List parts = splitExceptBetweenParentheses(constructor.trim(), '(', ')', " "); // TOOD@CHW this line is not yet correct + String constructorName = constructor.trim().split("\\s+")[0]; + List constructorArgs = parts.subList(1, parts.size()); + + StringBuilder returnExpression = new StringBuilder("return " + constructorName); + + int totalNumRecursiveCalls = 0; + for (String arg : constructorArgs) { + if (constructorArgIsRecursive(arg)) { + totalNumRecursiveCalls++; + } + } + + int recursiveCallId = 0; + for (String constructorArg : constructorArgs) { + if (constructorArgIsRecursive(constructorArg)) { + returnExpression.append(" <*> _gen (splitElements (n - 1) ").append(totalNumRecursiveCalls).append(" ").append(recursiveCallId).append(")"); + recursiveCallId++; + } else { + returnExpression.append(" <*> arbitrary"); + } + } + + if (recursiveCallId > 0) { + recursiveReturnExpressions.add(returnExpression.toString()); + } else { + nonrecursiveReturnExpressions.add(returnExpression.toString()); + } + } + + String[] typenameWithTypeVariables = typename.split(" "); + List typeVariables = Arrays.asList(typenameWithTypeVariables).subList(1, typenameWithTypeVariables.length); + String constraint = typeVariables.isEmpty() ? "" : "(" + String.join(", ", typeVariables.stream().map(v -> "Arbitrary " + v).toList()) + ") => "; + + List freqTuples = getFreqTuples(recursiveReturnExpressions, nonrecursiveReturnExpressions); + + // TODO@CHW: is the indentation correct? + return String.format(""" + instance %sArbitrary (%s) where + arbitrary = sized _gen + where + _gen n + | n > 10 = _gen 10 + | n > 0 && %s = frequency [%s] + | otherwise = oneof [%s] + where + splitElements numElements numRecursiveCalls recursiveCallId = + div numElements numRecursiveCalls + intDivRoundingCompensation + where + intDivRoundingCompensation = + if recursiveCallId < mod numElements numRecursiveCalls then 1 else 0 + """, constraint, typename, recursiveReturnExpressions.isEmpty() ? "False" : "True", String.join(", ", freqTuples), String.join(", ", nonrecursiveReturnExpressions)); + } + + private static List getFreqTuples(List recursiveReturnExpressions, List nonrecursiveReturnExpressions) { + int recursiveProb = recursiveReturnExpressions.isEmpty() ? 0 : (int) ceil(82.0 / recursiveReturnExpressions.size()); + int nonrecursiveProb = nonrecursiveReturnExpressions.isEmpty() ? 0 : (int) ceil(18.0 / nonrecursiveReturnExpressions.size()); + + List freqTuples = new ArrayList<>(); + for (String r : recursiveReturnExpressions) + freqTuples.add("(" + recursiveProb + ", " + r + ")"); + for (String n : nonrecursiveReturnExpressions) + freqTuples.add("(" + nonrecursiveProb + ", " + n + ")"); + return freqTuples; + } + + private boolean constructorArgIsRecursive(String arg) { + arg = arg.trim(); + if (arg.startsWith("(") && arg.endsWith(")")) { + return arg.equals("(" + this.typename + ")"); + } else { + return arg.equals(this.typename); + } + } + } + + public enum HaskellPrimitiveType { + Integer, Int, Float, Double, Rational, Bool, Char, String + } + + public enum HaskellConstrainedPrimitiveType { + Char_OnlySafeAscii, String_OnlySafeAscii + // TODO@CHW more ideas: Int_OnlyPositive, Float_OnlyPositive, Double_OnlyPositive + } + + // TODO@CHW check all visibilities (public/private) in this file + + public static List splitExceptBetweenParentheses(String expression, char openingParenthesis, char closingParenthesis, String splitAt) { + List tokens = new ArrayList<>(); + int parenthesisDepth = 0; + StringBuilder currentToken = new StringBuilder(); + + int index = 0; + while (index < expression.length()) { + char ch = expression.charAt(index); + + if (ch == openingParenthesis) { + parenthesisDepth++; + currentToken.append(ch); + } else if (ch == closingParenthesis) { + parenthesisDepth--; + currentToken.append(ch); + } else if (expression.startsWith(splitAt, index) && parenthesisDepth == 0) { + tokens.add(currentToken.toString().trim()); + currentToken.setLength(0); + index += splitAt.length() - 1; // skip already-processed characters + } else { + currentToken.append(ch); + } + index++; + } + + tokens.add(currentToken.toString().trim()); + return tokens; + } + + // END OF HASKELL UTILS / UTILS ----------------------------------------------------------------------------------- + } + From 235106c5833247b144cd050858c2a6fce3ecaf99 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 29 Jun 2025 12:11:40 +0200 Subject: [PATCH 045/105] Correctly escape arguments to ghci -e; minor fixes - ghci -e '...' instead of ghci -e "..." allows every character in ... except of single quote (') - single quote (') can be escaped using '"'"' - removed manual character escaping - replace \t by four spaces --- .../controller/HaskellRuntimeTestManager.java | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index e0daae234..392cc897e 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -268,16 +268,16 @@ private SubprocessResult evaluateWithGhci(String[] packagesToEnable, String[] mo } // TODO@CHW: testCode is more complex in DockerTest - StringBuilder testCode = new StringBuilder("ghci"); + StringBuilder testCode = new StringBuilder("ghci -XInstanceSigs"); for (String packageToEnable : packagesToEnable) { - testCode.append(" -e \"").append(":set -package ").append(packageToEnable).append("\""); + appendGhciEvaluateArgument(testCode, ":set -package " + packageToEnable); } - testCode.append(" -e \":m + ").append(String.join(" ", modulesToImport)).append("\""); + appendGhciEvaluateArgument(testCode, ":m + " + String.join(" ", modulesToImport)); if (hsFile != null) { - testCode.append(" -e \"").append(":load ").append(hsFile.getFileName().toString()).append("\""); + appendGhciEvaluateArgument(testCode, ":load " + hsFile.getFileName().toString()); } for (String expression : expressionsToEvaluate) { - testCode.append(" -e \"").append(expression).append("\""); + appendGhciEvaluateArgument(testCode, expression); } final Path testDriver = administrativeDir.resolve("test.sh"); @@ -345,6 +345,10 @@ private SubprocessResult evaluateWithGhci(String[] packagesToEnable, String[] mo } } + private void appendGhciEvaluateArgument(StringBuilder testCode, String argument) { + testCode.append(" -e '").append(argument.replace("'", "'\"'\"'").replace("\t", " ")).append("'"); + } + private List browseModelSolution(Task task) throws IOException { SubprocessResult result = evaluateWithGhci(null, null, true, new String[] { ":browse" }, task, true); return splitLinesButKeepMultilines(result.stdOut()); @@ -491,7 +495,7 @@ private List generateQuickcheckFunctionTestcases(Task task, S data %1$s = %1$s instance Show %1$s where - show %1$s = \\"@PLACEHOLDER@\\" + show %1$s = "@PLACEHOLDER@" instance Arbitrary %1$s where arbitrary = return %1$s @@ -507,16 +511,16 @@ private List generateQuickcheckFunctionTestcases(Task task, S instance Show target => Show (%1$s target) where show (%1$s name cycleLength intMap) = name - ++ \\" i = case i of \\" - ++ concatMap (liftM2 (++) show ((\\" -> \\" ++) . (++ \\"; \\") . show . intMap)) [0 .. cycleLength - 1] - ++ \\"x -> \\" + ++ " i = case i of " + ++ concatMap (liftM2 (++) show ((" -> " ++) . (++ "; ") . show . intMap)) [0 .. cycleLength - 1] + ++ "x -> " ++ name - ++ \\" (abs (mod x \\" + ++ " (abs (mod x " ++ show cycleLength - ++ \\"))\\" + ++ "))" instance Arbitrary target => Arbitrary (%1$s target) where - arbitrary = %1$s \\"cyclicIntMap\\" 50 <$> arbitrary + arbitrary = %1$s "cyclicIntMap" 50 <$> arbitrary """, CYCLIC_INT_MAP_TYPE_NAME); // String safeAsciiValues = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz()[]{}+-*/.,:; _!?#$%&<=>@"; @@ -526,7 +530,7 @@ private List generateQuickcheckFunctionTestcases(Task task, S String charStringOnlySafeAscii = String.format(""" safeAsciiChar :: Gen Char - safeAsciiChar = elements \\"%1$s\\" + safeAsciiChar = elements "%1$s" newtype %2$s = %2$s Char @@ -585,8 +589,8 @@ class ToStringList a where toStringList :: a -> [String] """, numberOfTestcases, parameterTypeTuple, PLACEHOLDER_TYPE_NAME); List expressionsToEvaluate = new ArrayList<>(); - expressionsToEvaluate.add("testcaseSeparator = \\\"" + TESTCASE_SEPARATOR + "\\\""); - expressionsToEvaluate.add("testcaseValueSeparator = \\\"" + TESTCASE_VALUE_SEPARATOR + "\\\""); + expressionsToEvaluate.add("testcaseSeparator = \"" + TESTCASE_SEPARATOR + "\""); + expressionsToEvaluate.add("testcaseValueSeparator = \"" + TESTCASE_VALUE_SEPARATOR + "\""); expressionsToEvaluate.add(placeholderType); expressionsToEvaluate.add(charStringOnlySafeAscii); expressionsToEvaluate.add(toStringList); @@ -727,7 +731,7 @@ public List computeExpectedValues(List functionCalls, Task task) List expressionsToEvaluate = new ArrayList<>(); for (String wrappedFunctionCall : wrappedFunctionCalls) { expressionsToEvaluate.add(wrappedFunctionCall); - expressionsToEvaluate.add(String.format("putStr \\\"%s\\\"", expectedValueSeparator)); + expressionsToEvaluate.add(String.format("putStr \"%s\"", expectedValueSeparator)); } String[] packagesToEnable = new String[] { "hashable" }; @@ -750,7 +754,7 @@ public List computeExpectedValues(List functionCalls, Task task) } public String wrapGhciExpressionInCatch(String expression, String exceptionLinePrefix) { - return String.format("catch (putStr (show (%s))) ((putStr . (\\\"%s\\\" ++) . show) :: SomeException -> IO ())", expression, exceptionLinePrefix); + return String.format("catch (putStr (show (%s))) ((putStr . (\"%s\" ++) . show) :: SomeException -> IO ())", expression, exceptionLinePrefix); } public static String prettyPrintFunctionCall(String functionCall) { @@ -929,9 +933,9 @@ public String generateArbitraryInstance() { where splitElements numElements numRecursiveCalls recursiveCallId = div numElements numRecursiveCalls + intDivRoundingCompensation - where - intDivRoundingCompensation = - if recursiveCallId < mod numElements numRecursiveCalls then 1 else 0 + where + intDivRoundingCompensation = + if recursiveCallId < mod numElements numRecursiveCalls then 1 else 0 """, constraint, typename, recursiveReturnExpressions.isEmpty() ? "False" : "True", String.join(", ", freqTuples), String.join(", ", nonrecursiveReturnExpressions)); } From fa8799ccd63eca61cd021b07f1ae114a8a39d5f6 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 29 Jun 2025 16:02:33 +0200 Subject: [PATCH 046/105] Change datamodel: store haskell identifiers of each haskell runtime test --- mysql.sql | 30 ++ sql-update.sql | 21 ++ .../datamodel/HaskellRuntimeTest.java | 39 +++ .../HaskellRuntimeTestIdentifier.java | 264 ++++++++++++++++++ 4 files changed, 354 insertions(+) create mode 100644 src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTestIdentifier.java diff --git a/mysql.sql b/mysql.sql index 5f9f274e7..3c3708b94 100644 --- a/mysql.sql +++ b/mysql.sql @@ -77,6 +77,30 @@ CREATE TABLE IF NOT EXISTS `groups_tutors` ( -- -------------------------------------------------------- +-- +-- Tabellenstruktur für Tabelle `haskellruntimetestidentifier` +-- + +DROP TABLE IF EXISTS `haskellruntimetestidentifier`; +CREATE TABLE `haskellruntimetestidentifier` +( + `identifierid` integer NOT NULL AUTO_INCREMENT, + `classdefinition` longtext, + `classname` varchar(255), + `functionconcretetype` varchar(255), + `functiondefaulttype` varchar(255), + `functionname` varchar(255), + `functiontype` varchar(255), + `identifierclass` varchar(255) NOT NULL, + `newtypeordataarbitraryinstance` longtext, + `newtypeordatadefinition` varchar(255), + `newtypeordatatypename` varchar(255), + `testid` integer NOT NULL, + PRIMARY KEY (`identifierid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- -------------------------------------------------------- + -- -- Tabellenstruktur für Tabelle `javaadvancedioteststep` -- @@ -509,6 +533,12 @@ ALTER TABLE `groups_tutors` ADD CONSTRAINT `FK8EAE7CC842D82B98` FOREIGN KEY (`tutors_id`) REFERENCES `participations` (`id`), ADD CONSTRAINT `FK8EAE7CC8BB3EB910` FOREIGN KEY (`groups_gid`) REFERENCES `groups` (`gid`); +-- +-- Constraints der Tabelle `haskellruntimetestidentifier` +-- +ALTER TABLE `haskellruntimetestidentifier` + ADD CONSTRAINT FKimd1t2tucxm6cy4b7l1vxj7b2 FOREIGN KEY (`testid`) REFERENCES `tests` (`id`) ON DELETE CASCADE; + -- -- Constraints der Tabelle `javaadvancedioteststep` -- diff --git a/sql-update.sql b/sql-update.sql index d16003539..0274715f3 100644 --- a/sql-update.sql +++ b/sql-update.sql @@ -165,3 +165,24 @@ alter table commonerrors add constraint FK84b477b3adpufhy6yca79d79r foreign key alter table testresults_commonerror add constraint FK787leerpp7s7yak6btbhtbh48 foreign key (testresultid) references testresults (id); alter table testresults_commonerror add constraint FKcfm4aoanp948updtgcn1pn07j foreign key (errorid) references commonerrors (errorid); ALTER TABLE `testresults_commonerror` DROP FOREIGN KEY `FK787leerpp7s7yak6btbhtbh48`; ALTER TABLE `testresults_commonerror` ADD CONSTRAINT `FK787leerpp7s7yak6btbhtbh48` FOREIGN KEY (`testresultid`) REFERENCES `testresults`(`id`) ON DELETE CASCADE ON UPDATE RESTRICT; ALTER TABLE `testresults_commonerror` DROP FOREIGN KEY `FKcfm4aoanp948updtgcn1pn07j`; ALTER TABLE `testresults_commonerror` ADD CONSTRAINT `FKcfm4aoanp948updtgcn1pn07j` FOREIGN KEY (`errorid`) REFERENCES `commonerrors`(`errorid`) ON DELETE CASCADE ON UPDATE RESTRICT; + +-- haskell runtime test +CREATE TABLE `haskellruntimetestidentifier` +( + `identifierid` integer NOT NULL AUTO_INCREMENT, + `classdefinition` longtext, + `classname` varchar(255), + `functionconcretetype` varchar(255), + `functiondefaulttype` varchar(255), + `functionname` varchar(255), + `functiontype` varchar(255), + `identifierclass` varchar(255) NOT NULL, + `newtypeordataarbitraryinstance` longtext, + `newtypeordatadefinition` varchar(255), + `newtypeordatatypename` varchar(255), + `testid` integer NOT NULL, + PRIMARY KEY (`identifierid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +ALTER TABLE IF EXISTS `haskellruntimetestidentifier` + ADD CONSTRAINT FKimd1t2tucxm6cy4b7l1vxj7b2 FOREIGN KEY (`testid`) REFERENCES `tests` (`id`) ON DELETE CASCADE; diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTest.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTest.java index a4bd81aaf..8412017c8 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTest.java @@ -19,9 +19,23 @@ package de.tuclausthal.submissioninterface.persistence.datamodel; +import java.io.Serial; +import java.util.ArrayList; +import java.util.List; + +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; import jakarta.persistence.Transient; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; + import de.tuclausthal.submissioninterface.testframework.tests.AbstractTest; /** @@ -30,9 +44,34 @@ */ @Entity public class HaskellRuntimeTest extends DockerTest { + @Serial + private static final long serialVersionUID = 1L; + + @OneToMany(mappedBy = "haskellRuntimeTest", cascade = CascadeType.PERSIST) + @OnDelete(action = OnDeleteAction.CASCADE) + @OrderBy("identifierid asc") + @JacksonXmlElementWrapper(localName = "identifiers") + @JacksonXmlProperty(localName = "identifier") + @JsonManagedReference + private List identifiers = new ArrayList<>(); + @Override @Transient public AbstractTest getTestImpl() { return new de.tuclausthal.submissioninterface.testframework.tests.impl.HaskellRuntimeTest(this); } + + /** + * @return the haskell runtime test identifiers + */ + public List getIdentifiers() { + return identifiers; + } + + /** + * @param identifiers the haskell runtime test identifiers to set + */ + public void setIdentifiers(List identifiers) { + this.identifiers = identifiers; + } } diff --git a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTestIdentifier.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTestIdentifier.java new file mode 100644 index 000000000..83a8c04ed --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTestIdentifier.java @@ -0,0 +1,264 @@ +/* + * Copyright 2025 Sven Strickroth + * Copyright 2025 Christian Wagner + * + * This file is part of the GATE. + * + * GATE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * GATE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GATE. If not, see . + */ + +package de.tuclausthal.submissioninterface.persistence.datamodel; + +import java.io.Serial; +import java.io.Serializable; +import java.lang.invoke.MethodHandles; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonIgnore; + +@Entity +@Table(name = "haskellruntimetestidentifier") +public class HaskellRuntimeTestIdentifier implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JsonIgnore + private int identifierid; + + @ManyToOne + @JoinColumn(name = "testid", nullable = false) + @JsonBackReference + private HaskellRuntimeTest haskellRuntimeTest; + + @Column(nullable = false) + private String identifierClass; + + @Column + private String functionName; + + @Column + private String functionType; + + @Column + private String functionDefaultType; + + @Column + private String functionConcreteType; + + @Column + private String newtypeOrDataTypename; + + @Column + private String newtypeOrDataDefinition; + + @Column(length = 65536) + private String newtypeOrDataArbitraryInstance; + + @Column + private String className; + + @Column(length = 65536) + private String classDefinition; + + // for Hibernate + protected HaskellRuntimeTestIdentifier() {} + + public HaskellRuntimeTestIdentifier(HaskellRuntimeTest haskellRuntimeTest, String identifierClass) { + this.haskellRuntimeTest = haskellRuntimeTest; + this.identifierClass = identifierClass; + } + + /** + * @return the identifierid + */ + public int getIdentifierid() { + return identifierid; + } + + /** + * @param identifierid the identifierid to set + */ + public void setIdentifierid(int identifierid) { + this.identifierid = identifierid; + } + + /** + * @return the haskellRuntimeTest + */ + public HaskellRuntimeTest getHaskellRuntimeTest() { + return haskellRuntimeTest; + } + + /** + * @param haskellRuntimeTest the haskellRuntimeTest to set + */ + public void setHaskellRuntimeTest(HaskellRuntimeTest haskellRuntimeTest) { + this.haskellRuntimeTest = haskellRuntimeTest; + } + + /** + * @return the identifierClass + */ + public String getIdentifierClass() { + return identifierClass; + } + + /** + * @param identifierClass the identifierClass to set + */ + public void setIdentifierClass(String identifierClass) { + this.identifierClass = identifierClass; + } + + /** + * @return the functionName + */ + public String getFunctionName() { + return functionName; + } + + /** + * @param functionName the functionName to set + */ + public void setFunctionName(String functionName) { + this.functionName = functionName; + } + + /** + * @return the functionType + */ + public String getFunctionType() { + return functionType; + } + + /** + * @param functionType the functionType to set + */ + public void setFunctionType(String functionType) { + this.functionType = functionType; + } + + /** + * @return the functionDefaultType + */ + public String getFunctionDefaultType() { + return functionDefaultType; + } + + /** + * @param functionDefaultType the functionDefaultType to set + */ + public void setFunctionDefaultType(String functionDefaultType) { + this.functionDefaultType = functionDefaultType; + } + + /** + * @return the functionConcreteType + */ + public String getFunctionConcreteType() { + return functionConcreteType; + } + + /** + * @param functionConcreteType the functionConcreteType to set + */ + public void setFunctionConcreteType(String functionConcreteType) { + this.functionConcreteType = functionConcreteType; + } + + /** + * @return the newtypeOrDataTypename + */ + public String getNewtypeOrDataTypename() { + return newtypeOrDataTypename; + } + + /** + * @param newtypeOrDataTypename the newtypeOrDataTypename to set + */ + public void setNewtypeOrDataTypename(String newtypeOrDataTypename) { + this.newtypeOrDataTypename = newtypeOrDataTypename; + } + + /** + * @return the newtypeOrDataDefinition + */ + public String getNewtypeOrDataDefinition() { + return newtypeOrDataDefinition; + } + + /** + * @param newtypeOrDataDefinition the newtypeOrDataDefinition to set + */ + public void setNewtypeOrDataDefinition(String newtypeOrDataDefinition) { + this.newtypeOrDataDefinition = newtypeOrDataDefinition; + } + + /** + * @return the newtypeOrDataArbitraryInstance + */ + public String getNewtypeOrDataArbitraryInstance() { + return newtypeOrDataArbitraryInstance; + } + + /** + * @param newtypeOrDataArbitraryInstance the newtypeOrDataArbitraryInstance to set + */ + public void setNewtypeOrDataArbitraryInstance(String newtypeOrDataArbitraryInstance) { + this.newtypeOrDataArbitraryInstance = newtypeOrDataArbitraryInstance; + } + + /** + * @return the className + */ + public String getClassName() { + return className; + } + + /** + * @param className the className to set + */ + public void setClassName(String className) { + this.className = className; + } + + /** + * @return the classDefinition + */ + public String getClassDefinition() { + return classDefinition; + } + + /** + * @param classDefinition the classDefinition to set + */ + public void setClassDefinition(String classDefinition) { + this.classDefinition = classDefinition; + } + + @Override + public String toString() { + return MethodHandles.lookup().lookupClass().getSimpleName() + " (" + Integer.toHexString(hashCode()) + "): identifierid:" + getIdentifierid() + "; testid: " + (getHaskellRuntimeTest() == null ? "null" : getHaskellRuntimeTest().getId()); + } +} From a73f8eb074bbce72c3451a79505c4d61f75c6d19 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 5 Jul 2025 00:06:24 +0200 Subject: [PATCH 047/105] Browse haskell file, list all haskell identifiers and generate testcases for each function individually --- .../controller/HaskellRuntimeTestManager.java | 163 ++++++++++------ .../view/HaskellRuntimeTestManagerView.java | 181 +++++++++++++++--- 2 files changed, 252 insertions(+), 92 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 392cc897e..80e9aefd5 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -25,8 +25,6 @@ import java.io.Serial; import java.io.Writer; import java.lang.invoke.MethodHandles; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -52,6 +50,7 @@ import de.tuclausthal.submissioninterface.persistence.dao.TestDAOIf; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTestStep; import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTestIdentifier; import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; import de.tuclausthal.submissioninterface.persistence.datamodel.ParticipationRole; import de.tuclausthal.submissioninterface.persistence.datamodel.Task; @@ -103,43 +102,36 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr return; } - if ("getHaskellIdentifiers".equals(request.getParameter("action"))) { - // TODO@CHW what happens if no model solution is uploaded? - + if ("browseModelSolution".equals(request.getParameter("action"))) { try { - SubprocessResult res = evaluateWithGhci(new String[] { "hashable", "QuickCheck" }, new String[] { "Data.Hashable", "Test.QuickCheck" }, true, new String[] { ":browse", "hash [1,2,3]" }, test.getTask(), true); - LOG.info("STDOUT IS {}", res.stdOut()); - LOG.info("STDERR IS {}", res.stdErr()); - LOG.info("EXIT CODE IS {}", res.exitCode()); - LOG.info("ABORTED IS {}", res.aborted()); - - // TODO@CHW - // browse haskell identifiers of model solution - // start transaction to store identifiers in the database and commit - // send redirect to HRTManager - response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + browseModelSolutionAndStoreClassifiedIdentifiers(haskellRuntimeTest, session); } catch (IOException e) { - String errorMessage = URLEncoder.encode(Util.escapeHTML(e.getMessage()), StandardCharsets.UTF_8); - response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId() + "&getidentifiererror=" + errorMessage, response)); + request.getSession().setAttribute("haskellRuntimeTestBrowseError", e.getMessage()); } - } else if ("generateNewTestSteps".equals(request.getParameter("action"))) { + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + } else if ("generateFunctionTestcases".equals(request.getParameter("action"))) { + int identifierId = Util.parseInteger(request.getParameter("identifierid"), -1); int numberOfTestSteps = Util.parseInteger(request.getParameter("numberOfTestSteps"), 0); - List dockerTestStepDatas = generateTestcases(numberOfTestSteps, test.getTask()); - // TODO@CHW catch IOException + if (identifierId != -1 && numberOfTestSteps > 0) { + try { + List dockerTestStepDatas = readClassifiedIdentifiersAndGenerateFunctionTestcases(haskellRuntimeTest, identifierId, numberOfTestSteps); - Transaction tx = session.beginTransaction(); - for (DockerTestStepData dockerTestStepData : dockerTestStepDatas) { - String title = dockerTestStepData.title(); - String testCode = dockerTestStepData.testCode().replaceAll("\r\n", "\n"); - String expectedValue = dockerTestStepData.expectedValue().replaceAll("\r\n", "\n"); + Transaction tx = session.beginTransaction(); + for (DockerTestStepData dockerTestStepData : dockerTestStepDatas) { + String title = dockerTestStepData.title(); + String testCode = dockerTestStepData.testCode().replaceAll("\r\n", "\n"); + String expectedValue = dockerTestStepData.expectedValue().replaceAll("\r\n", "\n"); - DockerTestStep newStep = new DockerTestStep(haskellRuntimeTest, title, testCode, expectedValue); - session.persist(newStep); - } - - tx.commit(); + DockerTestStep newStep = new DockerTestStep(haskellRuntimeTest, title, testCode, expectedValue); + session.persist(newStep); + } + tx.commit(); + } catch (IOException e) { + request.getSession().setAttribute("haskellRuntimeTestGenerateError", e.getMessage()); + } + } response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); } else { getServletContext().getNamedDispatcher(DockerTestManager.class.getSimpleName()).forward(request, response); @@ -149,57 +141,81 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr private record DockerTestStepData(String title, String testCode, String expectedValue) { } + private void browseModelSolutionAndStoreClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session) throws IOException { + List haskellIdentifiers = browseModelSolution(haskellRuntimeTest.getTask()); + HaskellClassifiedIdentifiers haskellClassifiedIdentifiers = classifyHaskellIdentifiers(haskellIdentifiers); - private List generateTestcases(int numberOfTestSteps, Task task) throws IOException { - List generatedTestcases = new ArrayList<>(); + Transaction tx = session.beginTransaction(); - List haskellIdentifiers = browseModelSolution(task); - - LOG.info("ALL IDENTIFIERS:"); - for (String haskellIdentifier : haskellIdentifiers) { - LOG.info(haskellIdentifier); + for (HaskellNewtypeOrData haskellNewtypeOrData : haskellClassifiedIdentifiers.getNewtypesAndDatas()) { + HaskellRuntimeTestIdentifier haskellRuntimeTestIdentifier = new HaskellRuntimeTestIdentifier(haskellRuntimeTest, "newtypeordata"); + + haskellRuntimeTestIdentifier.setNewtypeOrDataTypename(haskellNewtypeOrData.getTypename()); + haskellRuntimeTestIdentifier.setNewtypeOrDataDefinition(haskellNewtypeOrData.getTypeDefinition()); + haskellRuntimeTestIdentifier.setNewtypeOrDataArbitraryInstance(haskellNewtypeOrData.getArbitraryInstance()); + + session.persist(haskellRuntimeTestIdentifier); } - HaskellClassifiedIdentifiers haskellClassifiedIdentifiers = classifyHaskellIdentifiers(haskellIdentifiers); + for (HaskellFunction haskellFunction : haskellClassifiedIdentifiers.getFunctions()) { + HaskellRuntimeTestIdentifier haskellRuntimeTestIdentifier = new HaskellRuntimeTestIdentifier(haskellRuntimeTest, "function"); - List arbitraryInstances = new ArrayList<>(); + haskellRuntimeTestIdentifier.setFunctionName(haskellFunction.getName()); + haskellRuntimeTestIdentifier.setFunctionType(haskellFunction.getTypeSignature()); - for(HaskellNewtypeOrData haskellNewtypeOrData : haskellClassifiedIdentifiers.getNewtypesAndDatas()) { - String arbitraryInstance = haskellNewtypeOrData.generateArbitraryInstance(); - arbitraryInstances.add(arbitraryInstance); + String defaultTypeSignature = getGhciDefaultTypeSignature(haskellRuntimeTest.getTask(), haskellFunction.getName()); + haskellRuntimeTestIdentifier.setFunctionDefaultType(defaultTypeSignature); - LOG.info("Newtype/data: {}", haskellNewtypeOrData.typename); - LOG.info("Arbitrary instance: {}", arbitraryInstance); + String concreteTypeSignature = replaceUnconstrainedTypeVariables(defaultTypeSignature, HaskellPrimitiveType.Int); // TODO@CHW other default type + haskellRuntimeTestIdentifier.setFunctionConcreteType(concreteTypeSignature); + + session.persist(haskellRuntimeTestIdentifier); } - for(HaskellFunction haskellFunction : haskellClassifiedIdentifiers.getFunctions()) { - LOG.info("Function: {}", haskellFunction.name); - LOG.info("Type:\t\t\t{}", haskellFunction.typeSignature); + tx.commit(); + } - String defaultTypeSignature = getGhciDefaultTypeSignature(task, haskellFunction.name); - LOG.info("Type (+d):\t\t{}", defaultTypeSignature); + private List readClassifiedIdentifiersAndGenerateFunctionTestcases(HaskellRuntimeTest haskellRuntimeTest, int identifierId, int numberOfTestSteps) throws IOException { + List generatedTestcases = new ArrayList<>(); + HaskellRuntimeTestIdentifier functionIdentifier = null; + List arbitraryInstances = new ArrayList<>(); - String concreteTypeSignature = replaceUnconstrainedTypeVariables(defaultTypeSignature, HaskellPrimitiveType.Int); // TODO@CHW other default type - LOG.info("Concrete:\t\t{}", concreteTypeSignature); + for (HaskellRuntimeTestIdentifier identifier : haskellRuntimeTest.getIdentifiers()) { + switch (identifier.getIdentifierClass()) { + case "newtypeordata": + if (identifier.getNewtypeOrDataArbitraryInstance() != null) { + arbitraryInstances.add(identifier.getNewtypeOrDataArbitraryInstance()); + } + break; + case "function": + if (identifier.getIdentifierid() == identifierId) { + functionIdentifier = identifier; + } + break; + } + } - List functionParameterTypes = getFunctionParameterTypes(concreteTypeSignature); - LOG.info("Params:\t\t{}", functionParameterTypes); + if (functionIdentifier != null) { + // TODO@CHW maybe throw error if functionIdentifier does not contain all required fields / if some are null + String functionName = functionIdentifier.getFunctionName(); + String functionType = functionIdentifier.getFunctionType(); + String functionConcreteType = functionIdentifier.getFunctionConcreteType(); + List functionParameterTypes = getFunctionParameterTypes(functionConcreteType); - List testcases = generateQuickcheckFunctionTestcases(task, haskellFunction.name, functionParameterTypes, arbitraryInstances, numberOfTestSteps); + List testcases = generateQuickcheckFunctionTestcases(haskellRuntimeTest.getTask(), functionName, functionParameterTypes, arbitraryInstances, numberOfTestSteps); - List functionCalls = generateFunctionCalls(haskellFunction.name, testcases); - List expectedValues = computeExpectedValues(functionCalls, task); + List functionCalls = generateFunctionCalls(functionName, testcases); + List expectedValues = computeExpectedValues(functionCalls, haskellRuntimeTest.getTask()); if (functionCalls.size() != expectedValues.size()) { throw new AssertionError(String.format("Expected values: %d, function calls: %d", expectedValues.size(), functionCalls.size())); } for (int i = 0; i < functionCalls.size(); i++) { - LOG.info(prettyPrintFunctionCall(functionCalls.get(i))); - LOG.info(expectedValues.get(i)); - - generatedTestcases.add(new DockerTestStepData("Testcase " + i, functionCalls.get(i), expectedValues.get(i))); + generatedTestcases.add(new DockerTestStepData(functionName + " :: " + functionType, functionCalls.get(i), expectedValues.get(i))); } + } else { + throw new IOException("Invalid identifier id."); } return generatedTestcases; @@ -333,7 +349,7 @@ private SubprocessResult evaluateWithGhci(String[] packagesToEnable, String[] mo } else if (exitCode == 24) { throw new IOException("Running haskell testcase generator failed (Out of memory)"); } else if (exitCode != 0) { - throw new IOException("Running haskell testcase generator failed with exit code " + exitCode + ". Output on stderr: " + stdErr); + throw new IOException("Running haskell testcase generator failed with exit code " + exitCode + ". Output on stderr:\n" + stdErr); } } @@ -580,7 +596,7 @@ class ToStringList a where toStringList :: a -> [String] """); String haskellCommand = String.format(""" - replicateM %d (generate (arbitrary :: Gen %s)) + replicateM (%d) (generate (arbitrary :: Gen %s)) >>= putStrLn . intercalate testcaseSeparator . map ( intercalate testcaseValueSeparator . filter (/= show %s) @@ -838,7 +854,9 @@ public String toString() { private class HaskellNewtypeOrData { private final String typename; + private final String typeDefinition; private final List constructors = new ArrayList<>(); + private final String arbitraryInstance; public HaskellNewtypeOrData(String hsNewtypeOrData) { String normalizedInput = hsNewtypeOrData.replace("\n", " ").trim(); @@ -848,6 +866,7 @@ public HaskellNewtypeOrData(String hsNewtypeOrData) { Matcher matcher = typePattern.matcher(normalizedInput); if (matcher.find()) { this.typename = matcher.group("typename").trim().replace("\n", " "); + this.typeDefinition = hsNewtypeOrData; Pattern namedConstructorPattern = Pattern.compile("(?\\b\\w+\\b)\\s*\\{\\s*(?.*?)\\s*}"); @@ -870,11 +889,29 @@ public HaskellNewtypeOrData(String hsNewtypeOrData) { constructors.add(constructor.trim()); } } + + this.arbitraryInstance = generateArbitraryInstance(); } else { throw new IllegalArgumentException("Invalid newtype/data definition: " + hsNewtypeOrData); } } + public String getArbitraryInstance() { + return arbitraryInstance; + } + + public String getTypename() { + return typename; + } + + public String getTypeDefinition() { + return typeDefinition; + } + + public List getConstructors() { + return constructors; + } + @Override public String toString() { return "NEWTYPE/DATA " + typename + " = " + constructors; diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 54f8e36f9..e4d2371c8 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -22,16 +22,16 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.Serial; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTestStep; import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTestIdentifier; import de.tuclausthal.submissioninterface.servlets.GATEView; import de.tuclausthal.submissioninterface.servlets.controller.HaskellRuntimeTestManager; import de.tuclausthal.submissioninterface.servlets.controller.TaskManager; @@ -55,11 +55,54 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro HaskellRuntimeTest test = (HaskellRuntimeTest) request.getAttribute("test"); + HttpSession httpSession = request.getSession(false); + template.addKeepAlive(); template.printEditTaskTemplateHeader("Haskell Runtime Test bearbeiten", test.getTask()); PrintWriter out = response.getWriter(); + if (httpSession != null) { + out.println(errorBoxIfErrorOccurred(httpSession, "haskellRuntimeTestBrowseError", "Beim Analysieren der Musterlösung ist ein Fehler aufgetreten")); + out.println(errorBoxIfErrorOccurred(httpSession, "haskellRuntimeTestGenerateError", "Beim Generieren der Testfälle ist ein Fehler aufgetreten")); + } + + out.println(""" + + + """); + + out.println(""); + // similar code in TestManagerAddTestFormView out.println("

" + Util.escapeHTML(test.getTestTitle()) + "

"); out.println(""); @@ -97,34 +140,95 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println("
"); out.println("

Benutzerdefinierte Haskell Funktionen und Datentypen der Musterlösung

"); - if (request.getParameter("getidentifiererror") != null) { - String errorMessage = URLDecoder.decode(request.getParameter("getidentifiererror"), StandardCharsets.UTF_8); - out.println("

Beim Analysieren der Musterlösung ist ein Fehler aufgetreten: " + errorMessage + "

"); + StringBuilder newtypeOrDatasHtml = new StringBuilder(); + newtypeOrDatasHtml.append(""" + + Typname + Typdefinition und Arbitrary Instanz + + """); + + StringBuilder functionsHtml = new StringBuilder(); + functionsHtml.append(""" + + Funktion + Typsignatur + Typsignatur (+d) + Konkrete Typsignatur + Generator ausführen + + """); + + boolean showFunctionTable = false; + boolean showNewtypeOrDataTable = false; + + for (HaskellRuntimeTestIdentifier identifier : test.getIdentifiers()) { + switch (identifier.getIdentifierClass()) { + case "newtypeordata": + showNewtypeOrDataTable = true; + newtypeOrDatasHtml.append(String.format(""" + + %1$s + +
+ %2$s +
%3$s
+
+ + + """, Util.escapeHTML(identifier.getNewtypeOrDataTypename()), Util.escapeHTML(identifier.getNewtypeOrDataDefinition()), Util.escapeHTML(identifier.getNewtypeOrDataArbitraryInstance()))); + break; //TODO@CHW: 60vw might not be a good width depending on the page template + case "function": + showFunctionTable = true; + functionsHtml.append(String.format(""" + + %2$s + %3$s + %6$s + %7$s + + + + + + + + + + + """, identifier.getIdentifierid(), Util.escapeHTML(identifier.getFunctionName()), Util.escapeHTML(identifier.getFunctionType()), Util.generateHTMLLink("?", response), test.getId(), Util.escapeHTML(identifier.getFunctionDefaultType()), Util.escapeHTML(identifier.getFunctionConcreteType()))); + break; + } } - out.println("
"); - out.println(""); - out.println(""); - out.println(""); - out.println("
Bitte warten...
\n"); - out.println("
"); - out.println("

Neue Testschritte automatisch generieren

"); - out.println("

Testschritt Generator ist noch nicht vollständig implementiert.

"); // TODO@CHW - out.println("
"); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.print(""); - out.println(""); + out.println("
Number of test steps
Abbrechen
"); + if (showNewtypeOrDataTable) { + out.println(newtypeOrDatasHtml); + } + if (showNewtypeOrDataTable && showFunctionTable) { + out.println(""); + } + if (showFunctionTable) { + out.println(functionsHtml); + } out.println("
"); - out.println("
"); + + out.println(String.format(""" +
+
+
+ + + +
+
+ """, Util.generateHTMLLink("?", response), test.getId())); out.println("

Testschritte bearbeiten

"); out.println(""); @@ -148,13 +252,32 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro "(Löschen)" + "" + "" + - "" + - "" + + "" + + "" + "" /* @formatter:on */); } out.println("
" + Util.escapeHTML(step.getTestcode()) + "" + Util.escapeHTML(step.getExpect()) + "" + Util.escapeHTML(step.getTestcode()) + "" + Util.escapeHTML(step.getExpect()) + "
"); + out.println(""); template.printTemplateFooter(); } + + private String errorBoxIfErrorOccurred(HttpSession httpSession, String httpSessionAttributeName, String errorTitle) { + String errorMessage = (String) httpSession.getAttribute(httpSessionAttributeName); + httpSession.removeAttribute(httpSessionAttributeName); + + return (errorMessage == null) ? "" : String.format(""" +
+
+ %1$s +
+
+
%2$s
+
+
+
+ + """, errorTitle, Util.escapeHTML(errorMessage)); + } } From 21fc4c753529722741d782bad3551c3f3dc13ce0 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 5 Jul 2025 00:30:24 +0200 Subject: [PATCH 048/105] Fix warnings and stricter visibilities --- .../controller/HaskellRuntimeTestManager.java | 108 +++++++----------- 1 file changed, 39 insertions(+), 69 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 80e9aefd5..45d9ef2f9 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -76,7 +76,7 @@ public class HaskellRuntimeTestManager extends HttpServlet { final static private Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); // TODO@CHW: should safe docker path be part of de.tuclausthal.submissioninterface.util.Configuration? (since it is duplicated from DockerTest) - final static public String SAFE_DOCKER_SCRIPT = "/usr/local/bin/safe-docker"; + final static private String SAFE_DOCKER_SCRIPT = "/usr/local/bin/safe-docker"; @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { @@ -202,7 +202,7 @@ private List readClassifiedIdentifiersAndGenerateFunctionTes String functionConcreteType = functionIdentifier.getFunctionConcreteType(); List functionParameterTypes = getFunctionParameterTypes(functionConcreteType); - List testcases = generateQuickcheckFunctionTestcases(haskellRuntimeTest.getTask(), functionName, functionParameterTypes, arbitraryInstances, numberOfTestSteps); + List testcases = generateQuickcheckFunctionTestcases(haskellRuntimeTest.getTask(), functionParameterTypes, arbitraryInstances, numberOfTestSteps); List functionCalls = generateFunctionCalls(functionName, testcases); List expectedValues = computeExpectedValues(functionCalls, haskellRuntimeTest.getTask()); @@ -370,7 +370,7 @@ private List browseModelSolution(Task task) throws IOException { return splitLinesButKeepMultilines(result.stdOut()); } - public List splitLinesButKeepMultilines(String resultStdout) { + private List splitLinesButKeepMultilines(String resultStdout) { List haskellIdentifiers = new ArrayList<>(); for (String line : resultStdout.split("\\R")) { @@ -389,7 +389,7 @@ public List splitLinesButKeepMultilines(String resultStdout) { return haskellIdentifiers; } - public HaskellClassifiedIdentifiers classifyHaskellIdentifiers(List haskellIdentifiers) { + private HaskellClassifiedIdentifiers classifyHaskellIdentifiers(List haskellIdentifiers) { HaskellClassifiedIdentifiers classifiedIdentifiers = new HaskellClassifiedIdentifiers(); for (String line : haskellIdentifiers) { @@ -407,7 +407,7 @@ public HaskellClassifiedIdentifiers classifyHaskellIdentifiers(List hask return classifiedIdentifiers; } - public String getGhciDefaultTypeSignature(Task task, String identifierName) throws IOException { + private String getGhciDefaultTypeSignature(Task task, String identifierName) throws IOException { SubprocessResult result = evaluateWithGhci(null, null, true, new String[] { ":type +d " + identifierName }, task, true); String defaultTypeSignature = normalizeTypeSignature(result.stdOut().split("::")[1].trim()); @@ -418,13 +418,13 @@ public String getGhciDefaultTypeSignature(Task task, String identifierName) thro } } - public String normalizeTypeSignature(String typeSignature) { + private String normalizeTypeSignature(String typeSignature) { typeSignature = typeSignature.replace("\n", ""); typeSignature = typeSignature.replaceAll("\\s*->\\s*", " -> "); return typeSignature.trim(); } - public static String replaceUnconstrainedTypeVariables(String typeSignature, HaskellPrimitiveType replacementType) { + private static String replaceUnconstrainedTypeVariables(String typeSignature, HaskellPrimitiveType replacementType) { if (typeSignature.contains("=>")) { // TODO@CHW: Implementation not yet correct for Functor, Monad, Applicative, etc. // e.g. (<$>) :: Functor f => (a -> b) -> f a -> f b @@ -439,7 +439,7 @@ public static String replaceUnconstrainedTypeVariables(String typeSignature, Has return typeSignature.replaceAll("(? getFunctionParameterTypes(String concreteTypeSignature) { + private static List getFunctionParameterTypes(String concreteTypeSignature) { if (concreteTypeSignature.contains("::")) { concreteTypeSignature = concreteTypeSignature.split("::", 2)[1].trim(); } @@ -454,12 +454,12 @@ public static List getFunctionParameterTypes(String concreteTypeSignatur //TODO@CHW maybe replace all trim() by strip()? - public static boolean parameterTypeIsFunction(String parameterType) { + private static boolean parameterTypeIsFunction(String parameterType) { parameterType = parameterType.strip(); return parameterType.contains("->") && parameterType.startsWith("(") && parameterType.endsWith(")"); } - public static String getFunctionReturnType(String concreteTypeSignature) { + private static String getFunctionReturnType(String concreteTypeSignature) { if (concreteTypeSignature.contains("::")) { concreteTypeSignature = concreteTypeSignature.split("::", 2)[1].strip(); } @@ -479,7 +479,7 @@ private record TestcaseSingleParameterWithType(String testcaseParameter, String private record TestcaseWithTypes(List testcaseParametersWithTypes) { } - private List generateQuickcheckFunctionTestcases(Task task, String functionName, List functionParameterTypes, List arbitraryInstances, int numberOfTestcases) throws IOException { + private List generateQuickcheckFunctionTestcases(Task task, List functionParameterTypes, List arbitraryInstances, int numberOfTestcases) throws IOException { if (functionParameterTypes.isEmpty()) { return List.of(); } @@ -565,7 +565,7 @@ private List generateQuickcheckFunctionTestcases(Task task, S arbitrary = %3$s <$> listOf safeAsciiChar """, safeAsciiValues, typenameCharOnlySafeAscii, typenameStringOnlySafeAscii); - String toStringList = String.format(""" + String toStringList = """ class ToStringList a where toStringList :: a -> [String] instance (Show a, Show b) => ToStringList (a, b) where toStringList (a, b) = [show a, show b] @@ -593,7 +593,7 @@ class ToStringList a where toStringList :: a -> [String] instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g, Show h, Show i, Show j) => ToStringList (a, b, c, d, e, f, g, h, i, j) where toStringList (a, b, c, d, e, f, g, h, i, j) = [show a, show b, show c, show d, show e, show f, show g, show h, show i, show j] - """); + """; String haskellCommand = String.format(""" replicateM (%d) (generate (arbitrary :: Gen %s)) @@ -715,7 +715,7 @@ private String embedCyclicIntMapInRandomFunction(int numberOfParameters, String return String.format("(let %s in let randomFunction %s = (%s . hash . show) (%s) in randomFunction)", cyclicIntMapDefinition, String.join(" ", parameters), cyclicIntMapName, String.join(", ", parameters)); } - public List generateFunctionCalls(String functionName, List testcasesWithTypes) { + private List generateFunctionCalls(String functionName, List testcasesWithTypes) { List functionCalls = new ArrayList<>(); for (TestcaseWithTypes testcaseWithTypes : testcasesWithTypes) { @@ -737,7 +737,7 @@ public List generateFunctionCalls(String functionName, List computeExpectedValues(List functionCalls, Task task) throws IOException { + private List computeExpectedValues(List functionCalls, Task task) throws IOException { final String exceptionLinePrefix = "@EXCEPTION@"; List wrappedFunctionCalls = functionCalls.stream().map(functionCall -> wrapGhciExpressionInCatch(functionCall, exceptionLinePrefix)).toList(); @@ -769,66 +769,57 @@ public List computeExpectedValues(List functionCalls, Task task) return expectedValues; } - public String wrapGhciExpressionInCatch(String expression, String exceptionLinePrefix) { + private String wrapGhciExpressionInCatch(String expression, String exceptionLinePrefix) { return String.format("catch (putStr (show (%s))) ((putStr . (\"%s\" ++) . show) :: SomeException -> IO ())", expression, exceptionLinePrefix); } - public static String prettyPrintFunctionCall(String functionCall) { + private static String prettyPrintFunctionCall(String functionCall) { return functionCall.replaceAll("\\(let cyclicIntMap .*? let randomFunction .*? in randomFunction\\)", ""); } - // BEGIN OF HASKELL UTILS / UTILS -------------------------------------------------------- - - private class HaskellClassifiedIdentifiers { + private static class HaskellClassifiedIdentifiers { private final List classes = new ArrayList<>(); private final List newtypesAndDatas = new ArrayList<>(); private final List functions = new ArrayList<>(); - public void addClass(String hsClass) { + private void addClass(String hsClass) { classes.add(new HaskellClass(hsClass)); } - public void addNewtypeOrData(String hsNewtypeOrData) { + private void addNewtypeOrData(String hsNewtypeOrData) { newtypesAndDatas.add(new HaskellNewtypeOrData(hsNewtypeOrData)); } - public void addFunction(String hsFunction) { + private void addFunction(String hsFunction) { functions.add(new HaskellFunction(hsFunction)); } - public List getClasses() { + private List getClasses() { return classes; } - public List getNewtypesAndDatas() { + private List getNewtypesAndDatas() { return newtypesAndDatas; } - public List getFunctions() { + private List getFunctions() { return functions; } } - private class HaskellClass { - private final String hsClass; - - public HaskellClass(String hsClass) { + private record HaskellClass(String hsClass) { + private HaskellClass { if (!hsClass.startsWith("class") || !hsClass.contains("where")) { throw new IllegalArgumentException("Invalid class definition: " + hsClass); } - this.hsClass = hsClass; - } - - public String getHsClass() { - return hsClass; } } - private class HaskellFunction { + private static class HaskellFunction { private final String name; private final String typeSignature; - public HaskellFunction(String hsFunction) { + private HaskellFunction(String hsFunction) { if (!hsFunction.contains("::")) { throw new IllegalArgumentException("Invalid function definition: " + hsFunction); } @@ -838,27 +829,22 @@ public HaskellFunction(String hsFunction) { this.typeSignature = parts[1].trim(); } - public String getName() { + private String getName() { return name; } - public String getTypeSignature() { + private String getTypeSignature() { return typeSignature; } - - @Override - public String toString() { - return "FUNCTION " + name + " :: " + typeSignature; - } } - private class HaskellNewtypeOrData { + private static class HaskellNewtypeOrData { private final String typename; private final String typeDefinition; private final List constructors = new ArrayList<>(); private final String arbitraryInstance; - public HaskellNewtypeOrData(String hsNewtypeOrData) { + private HaskellNewtypeOrData(String hsNewtypeOrData) { String normalizedInput = hsNewtypeOrData.replace("\n", " ").trim(); Pattern typePattern = Pattern.compile("\\b(?:data|newtype)\\s+" + "(?[\\w\\s]+?)" + "\\s*=\\s*" + "(?.*?)(?=\\s+deriving\\b|$)"); @@ -896,28 +882,19 @@ public HaskellNewtypeOrData(String hsNewtypeOrData) { } } - public String getArbitraryInstance() { + private String getArbitraryInstance() { return arbitraryInstance; } - public String getTypename() { + private String getTypename() { return typename; } - public String getTypeDefinition() { + private String getTypeDefinition() { return typeDefinition; } - public List getConstructors() { - return constructors; - } - - @Override - public String toString() { - return "NEWTYPE/DATA " + typename + " = " + constructors; - } - - public String generateArbitraryInstance() { + private String generateArbitraryInstance() { List recursiveReturnExpressions = new ArrayList<>(); List nonrecursiveReturnExpressions = new ArrayList<>(); @@ -958,7 +935,6 @@ public String generateArbitraryInstance() { List freqTuples = getFreqTuples(recursiveReturnExpressions, nonrecursiveReturnExpressions); - // TODO@CHW: is the indentation correct? return String.format(""" instance %sArbitrary (%s) where arbitrary = sized _gen @@ -998,18 +974,16 @@ private boolean constructorArgIsRecursive(String arg) { } } - public enum HaskellPrimitiveType { + private enum HaskellPrimitiveType { Integer, Int, Float, Double, Rational, Bool, Char, String } - public enum HaskellConstrainedPrimitiveType { + private enum HaskellConstrainedPrimitiveType { Char_OnlySafeAscii, String_OnlySafeAscii // TODO@CHW more ideas: Int_OnlyPositive, Float_OnlyPositive, Double_OnlyPositive } - // TODO@CHW check all visibilities (public/private) in this file - - public static List splitExceptBetweenParentheses(String expression, char openingParenthesis, char closingParenthesis, String splitAt) { + private static List splitExceptBetweenParentheses(String expression, char openingParenthesis, char closingParenthesis, String splitAt) { List tokens = new ArrayList<>(); int parenthesisDepth = 0; StringBuilder currentToken = new StringBuilder(); @@ -1037,8 +1011,4 @@ public static List splitExceptBetweenParentheses(String expression, char tokens.add(currentToken.toString().trim()); return tokens; } - - // END OF HASKELL UTILS / UTILS ----------------------------------------------------------------------------------- - } - From b750f35b1604f77928ae9668bd77f83ea19f38cb Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 5 Jul 2025 00:50:33 +0200 Subject: [PATCH 049/105] Minor improvements in HaskellRuntimeTestManagerView --- .../view/HaskellRuntimeTestManagerView.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index e4d2371c8..8b3e995aa 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -152,8 +152,8 @@ function submitGeneratorForm(formId, button) { functionsHtml.append(""" Funktion - Typsignatur - Typsignatur (+d) + Typsignatur (:t) + Default Typsignatur (:t +d) Konkrete Typsignatur Generator ausführen @@ -224,7 +224,7 @@ function submitGeneratorForm(formId, button) { @@ -232,15 +232,15 @@ function submitGeneratorForm(formId, button) { out.println("

Testschritte bearbeiten

"); out.println(""); - out.println(/* @formatter:off */ - "" + - "" + - "" + - "" + - "" + - "" + - "" - /* @formatter:on */); + out.println(""" + + + + + + + + """); for (DockerTestStep step : test.getTestSteps()) { String deleteTestStepLink = Util.generateHTMLLink(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + test.getId() + "&action=deleteStep&teststepid=" + step.getTeststepid(), response); From 95db9383341939b62ebf32e539d6802f324168b4 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 5 Jul 2025 01:34:11 +0200 Subject: [PATCH 050/105] Delete haskell identifiers before (re-)browsing the file --- .../controller/HaskellRuntimeTestManager.java | 14 ++++++++++++++ .../view/HaskellRuntimeTestManagerView.java | 10 ++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 45d9ef2f9..709559585 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -103,12 +103,16 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr } if ("browseModelSolution".equals(request.getParameter("action"))) { + deleteStoredClassifiedIdentifiers(haskellRuntimeTest, session); try { browseModelSolutionAndStoreClassifiedIdentifiers(haskellRuntimeTest, session); } catch (IOException e) { request.getSession().setAttribute("haskellRuntimeTestBrowseError", e.getMessage()); } response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + } else if ("deleteHaskellIdentifiers".equals(request.getParameter("action"))) { + deleteStoredClassifiedIdentifiers(haskellRuntimeTest, session); + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); } else if ("generateFunctionTestcases".equals(request.getParameter("action"))) { int identifierId = Util.parseInteger(request.getParameter("identifierid"), -1); int numberOfTestSteps = Util.parseInteger(request.getParameter("numberOfTestSteps"), 0); @@ -141,6 +145,16 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr private record DockerTestStepData(String title, String testCode, String expectedValue) { } + private void deleteStoredClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session) { + Transaction tx = session.beginTransaction(); + + for (HaskellRuntimeTestIdentifier identifier : haskellRuntimeTest.getIdentifiers()) { + session.remove(identifier); + } + + tx.commit(); + } + private void browseModelSolutionAndStoreClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session) throws IOException { List haskellIdentifiers = browseModelSolution(haskellRuntimeTest.getTask()); HaskellClassifiedIdentifiers haskellClassifiedIdentifiers = classifyHaskellIdentifiers(haskellIdentifiers); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 8b3e995aa..945392ef5 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -215,9 +215,11 @@ out.println(functionsHtml); } out.println("
TitelTestcodeExpected
TitelTestcodeExpected
Default Typsignatur (:t +d)
"); + if (showNewtypeOrDataTable || showFunctionTable) { + out.println("
"); + } out.println(String.format(""" -
@@ -226,9 +228,13 @@ Default Typsignatur (:t +d) onclick="submitGeneratorForm('browseModelSolutionForm', this)"> Musterlösung analysieren (:browse) + + (Zurücksetzen) +
- """, Util.generateHTMLLink("?", response), test.getId())); + """, Util.generateHTMLLink("?", response), test.getId(), Util.generateHTMLLink(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + test.getId() + "&action=deleteHaskellIdentifiers", response))); out.println("

Testschritte bearbeiten

"); out.println(""); From 085e4220d89d44241648375edfa4b3480b286dbb Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 5 Jul 2025 15:08:30 +0200 Subject: [PATCH 051/105] Duplicate/delete a selection of testcases; group testcases by function --- .../controller/HaskellRuntimeTestManager.java | 72 +++++++++++- .../view/HaskellRuntimeTestManagerView.java | 108 +++++++++++++----- 2 files changed, 149 insertions(+), 31 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 709559585..74c9c1509 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -31,8 +31,10 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.stream.Stream; import jakarta.servlet.ServletException; @@ -123,9 +125,10 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr Transaction tx = session.beginTransaction(); for (DockerTestStepData dockerTestStepData : dockerTestStepDatas) { - String title = dockerTestStepData.title(); - String testCode = dockerTestStepData.testCode().replaceAll("\r\n", "\n"); - String expectedValue = dockerTestStepData.expectedValue().replaceAll("\r\n", "\n"); + // NOTE: DockerTestStep title is used for storing the function signature (needed for grouping the testcases in the view) + String title = dockerTestStepData.getFunctionNameWithType(); + String testCode = dockerTestStepData.getTestCode(); + String expectedValue = dockerTestStepData.getExpectedValue(); DockerTestStep newStep = new DockerTestStep(haskellRuntimeTest, title, testCode, expectedValue); session.persist(newStep); @@ -137,12 +140,47 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr } } response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + } else if ("duplicateMultipleTestSteps".equals(request.getParameter("action"))) { + String[] selectedIds = request.getParameterValues("selectedTestStepIds"); + if (selectedIds != null) { + Set testStepIds = Arrays.stream(selectedIds).map(s -> Util.parseInteger(s, -1)).filter(i -> i != -1).collect(Collectors.toSet()); + duplicateTestStepsWithIds(haskellRuntimeTest, session, testStepIds); + } + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + } else if ("deleteMultipleTestSteps".equals(request.getParameter("action"))) { + String[] selectedIds = request.getParameterValues("selectedTestStepIds"); + if (selectedIds != null) { + Set testStepIds = Arrays.stream(selectedIds).map(s -> Util.parseInteger(s, -1)).filter(i -> i != -1).collect(Collectors.toSet()); + deleteTestStepsWithIds(haskellRuntimeTest, session, testStepIds); + } + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); } else { getServletContext().getNamedDispatcher(DockerTestManager.class.getSimpleName()).forward(request, response); } } - private record DockerTestStepData(String title, String testCode, String expectedValue) { + private static class DockerTestStepData { + private final String functionNameWithType; + private final String testCode; + private final String expectedValue; + + private DockerTestStepData(String functionName, String functionType, String testCode, String expectedValue) { + this.functionNameWithType = functionName + " :: " + functionType; + this.testCode = testCode.replaceAll("\r\n", "\n"); // TODO@CHW wrap in ghci -e '...' + this.expectedValue = expectedValue.replaceAll("\r\n", "\n"); // TODO@CHW handle float values + } + + private String getFunctionNameWithType() { + return functionNameWithType; + } + + private String getTestCode() { + return testCode; + } + + private String getExpectedValue() { + return expectedValue; + } } private void deleteStoredClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session) { @@ -226,7 +264,7 @@ private List readClassifiedIdentifiersAndGenerateFunctionTes } for (int i = 0; i < functionCalls.size(); i++) { - generatedTestcases.add(new DockerTestStepData(functionName + " :: " + functionType, functionCalls.get(i), expectedValues.get(i))); + generatedTestcases.add(new DockerTestStepData(functionName, functionType, functionCalls.get(i), expectedValues.get(i))); } } else { throw new IOException("Invalid identifier id."); @@ -235,6 +273,30 @@ private List readClassifiedIdentifiersAndGenerateFunctionTes return generatedTestcases; } + private void duplicateTestStepsWithIds(HaskellRuntimeTest haskellRuntimeTest, Session session, Set testStepIds) { + Transaction tx = session.beginTransaction(); + + for (DockerTestStep step : haskellRuntimeTest.getTestSteps()) { + if (testStepIds.contains(step.getTeststepid())) { + session.persist(new DockerTestStep(haskellRuntimeTest, step.getTitle(), step.getTestcode(), step.getExpect())); + } + } + + tx.commit(); + } + + private void deleteTestStepsWithIds(HaskellRuntimeTest haskellRuntimeTest, Session session, Set testStepIds) { + Transaction tx = session.beginTransaction(); + + for (DockerTestStep step : haskellRuntimeTest.getTestSteps()) { + if (testStepIds.contains(step.getTeststepid())) { + session.remove(step); + } + } + + tx.commit(); + } + private record SubprocessResult(String stdOut, String stdErr, int exitCode, boolean aborted) { } diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 945392ef5..70691766a 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -22,6 +22,9 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.Serial; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; @@ -71,8 +74,8 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro """); @@ -237,33 +254,72 @@ """, Util.generateHTMLLink("?", response), test.getId(), Util.generateHTMLLink(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + test.getId() + "&action=deleteHaskellIdentifiers", response))); out.println("

Testschritte bearbeiten

"); - out.println("
Default Typsignatur (:t +d)
"); - out.println(""" - + // NOTE: DockerTestStep title is used for storing the function signature (see controller servlet) + final Map> testStepsGroupedByFunctionNameWithType = test.getTestSteps().stream().collect(Collectors.groupingBy(DockerTestStep::getTitle)); + List sortedKeys = testStepsGroupedByFunctionNameWithType.keySet().stream().sorted().toList(); + + for (int i = 0; i < sortedKeys.size(); i++) { + String functionNameWithType = sortedKeys.get(i); + + final int numberOfTestSteps = testStepsGroupedByFunctionNameWithType.get(functionNameWithType).size(); + final String numberOfTestStepsText = numberOfTestSteps + " " + (numberOfTestSteps == 1 ? "Testschritt" : "Testschritte"); + + out.println("

Funktion " + functionNameWithType + " (" + numberOfTestStepsText + ")

"); + + String formId = "deleteOrDuplicateMultipleTestStepsForm" + i; + String formActionInputFieldId = "deleteOrDuplicateMultipleTestStepsFormAction" + i; + + out.println(String.format(""" +
+ +
+ + + + + + + + """, Util.generateHTMLLink("?", response), formId, test.getId())); + // TODO@CHW: add a master checkbox that toggles all checkboxes of this form + + for (DockerTestStep step : testStepsGroupedByFunctionNameWithType.get(functionNameWithType)) { + out.println(String.format(""" + + + + + + """, Util.escapeHTML(step.getTestcode()), Util.escapeHTML(step.getExpect()), step.getTeststepid())); + } + + out.println(String.format(""" - - - + - - """); - - for (DockerTestStep step : test.getTestSteps()) { - String deleteTestStepLink = Util.generateHTMLLink(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + test.getId() + "&action=deleteStep&teststepid=" + step.getTeststepid(), response); - out.println(/* @formatter:off */ - "" + - "" + - "" + - "" + - "" - /* @formatter:on */); + """, formId, formActionInputFieldId)); + // TODO@CHW: add button "Duplikate in Selektion entfernen" + out.println("
TestcodeErwartete Ausgabe
+ + %1$s%2$s
TitelTestcodeExpected + Aktionen für selektierte Testschritte: + + +
" + - Util.escapeHTML(step.getTitle()) + " " + - "" + - "(Löschen)" + - "" + - "" + Util.escapeHTML(step.getTestcode()) + "" + Util.escapeHTML(step.getExpect()) + "
"); + out.println(""); + out.println("
"); } - out.println(""); out.println(""); template.printTemplateFooter(); From 55f217e0fd710b989860fd4f379764351f20e5b2 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 5 Jul 2025 16:05:37 +0200 Subject: [PATCH 052/105] Add master checkbox to select all testcases of a function at once --- .../view/HaskellRuntimeTestManagerView.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 70691766a..78b697339 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -97,6 +97,19 @@ function toggleTableRowHighlight(checkbox) { function setActionInputField(actionInputFieldId, actionName) { document.getElementById(actionInputFieldId).value = actionName; } + function toggleAllTestcaseSelectionCheckboxes(masterCheckbox, formId) { + const checkboxes = document.getElementById(formId).getElementsByClassName('testcaseSelectionCheckbox'); + Array.from(checkboxes).forEach(cb => { + cb.checked = masterCheckbox.checked; + toggleTableRowHighlight(cb); + }); + } + function syncTestcaseSelectionMasterCheckbox(formId) { + const checkboxes = document.getElementById(formId).getElementsByClassName('testcaseSelectionCheckbox'); + const masterCheckbox = document.getElementById(formId).getElementsByClassName('testcaseSelectionMasterCheckbox')[0]; + const allChecked = Array.from(checkboxes).every(cb => cb.checked); + masterCheckbox.checked = allChecked; + } """); @@ -280,7 +296,7 @@ Default Typsignatur (:t +d) final int numberOfTestSteps = testStepsGroupedByFunctionNameWithType.get(functionNameWithType).size(); final String numberOfTestStepsText = numberOfTestSteps + " " + (numberOfTestSteps == 1 ? "Testschritt" : "Testschritte"); - out.println("

Funktion " + functionNameWithType + " (" + numberOfTestStepsText + ")

"); + out.println("

Funktion " + Util.escapeHTML(functionNameWithType) + " (" + numberOfTestStepsText + ")

"); String formId = "deleteOrDuplicateMultipleTestStepsForm" + i; String formActionInputFieldId = "deleteOrDuplicateMultipleTestStepsFormAction" + i; @@ -348,16 +364,16 @@ private String errorBoxIfErrorOccurred(HttpSession httpSession, String httpSessi httpSession.removeAttribute(httpSessionAttributeName); return (errorMessage == null) ? "" : String.format(""" -
-
+
+
%1$s
-
+
%2$s

- """, errorTitle, Util.escapeHTML(errorMessage)); + """, Util.escapeHTML(errorTitle), Util.escapeHTML(errorMessage)); } } From 975a2a6c382719f5bf0d0d70a3cf201a89f4e09b Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Fri, 11 Jul 2025 11:39:52 +0200 Subject: [PATCH 062/105] Move CSS styling into si.css --- .../view/HaskellRuntimeTestManagerView.java | 44 +++---------------- src/main/webapp/template/simple/si.css | 34 ++++++++++++++ 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index ea947db04..23b526230 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -89,9 +89,9 @@ function submitGeneratorForm(formId, button) { function toggleTableRowHighlight(checkbox) { const row = checkbox.closest('tr'); if (checkbox.checked) { - row.classList.add('selected-row'); + row.classList.add('selected-table-row'); } else { - row.classList.remove('selected-row'); + row.classList.remove('selected-table-row'); } } function setActionInputField(actionInputFieldId, actionName) { @@ -111,40 +111,6 @@ function syncTestcaseSelectionMasterCheckbox(formId) { masterCheckbox.checked = allChecked; } - """); out.println(""); @@ -364,11 +330,11 @@ private String errorBoxIfErrorOccurred(HttpSession httpSession, String httpSessi httpSession.removeAttribute(httpSessionAttributeName); return (errorMessage == null) ? "" : String.format(""" -
-
+
+
%1$s
-
+
%2$s
diff --git a/src/main/webapp/template/simple/si.css b/src/main/webapp/template/simple/si.css index fb61a5462..5cb8bf758 100644 --- a/src/main/webapp/template/simple/si.css +++ b/src/main/webapp/template/simple/si.css @@ -205,3 +205,37 @@ a[href ^="mailto:"] { .abgenfailed, .abgenfailed a { color: blue !important; } + +span.spinner { + width: 0.8em; + height: 0.8em; + border: 2px solid #ccc; + border-top: 2px solid #333; + border-radius: 50%; + animation: spinLoadingAnimation 0.6s linear infinite; + display: inline-block; + vertical-align: middle; +} + +@keyframes spinLoadingAnimation { + to { transform: rotate(360deg); } +} + +.selected-table-row { + background-color: #d0e7ff; +} + +.error-title { + background-color: red; + color: white; + padding: 4px; + font-weight: bold; + overflow: auto; +} + +.error-message { + background-color: #ffe5e5; + color: red; + padding: 4px; + overflow: auto; +} From 37ef4a1198b053c5c951380560e429913c2280c1 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Fri, 11 Jul 2025 17:00:07 +0200 Subject: [PATCH 063/105] remove unnecessary code from file --- .../testframework/tests/impl/HaskellSyntaxTest.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java index e3521f2ef..bdb306f6c 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java @@ -18,18 +18,12 @@ */ package de.tuclausthal.submissioninterface.testframework.tests.impl; -import java.util.Random; - import de.tuclausthal.submissioninterface.testframework.executor.TestExecutorTestResult; public class HaskellSyntaxTest extends DockerTest { - private static final Random random = new Random(); - private final String separator; - public HaskellSyntaxTest(de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest test) { super(test); - separator = "##"; } @Override @@ -48,6 +42,6 @@ protected final String generateTestShellScript() { for file in *.hs; do ghci -ignore-dot-ghci -v0 -ferror-spans -fdiagnostics-color=never -Wall -e ":load $file" -e ":quit" done - """.formatted(separator); + """; } } From 62d26e28274cd6a23eb5e4a69ae3644ffd28cf6f Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 12 Jul 2025 18:07:01 +0200 Subject: [PATCH 064/105] Differentiate between different exceptions by printing the exception message - remove the call stack before printing the exception, since it includes line numbers that are different for each submission --- .../servlets/controller/HaskellRuntimeTestManager.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 1c093ca39..3cf211b30 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -166,7 +166,7 @@ private DockerTestStepData(String functionName, String functionType, String func this.functionNameWithType = functionName + " :: " + functionType; StringBuilder testCode = new StringBuilder("ghci -XInstanceSigs"); - appendGhciEvaluateArgument(testCode, ":m + Control.Exception System.Timeout"); + appendGhciEvaluateArgument(testCode, ":m + Control.Exception Data.List Data.Maybe System.Timeout"); appendGhciEvaluateArgument(testCode, wrapGhciExpressionInCatchAndTimeout(functionCall.replaceAll("\r\n", "\n"))); testCode.append(" ").append(filename); this.testCode = testCode.toString(); @@ -836,7 +836,7 @@ private List computeExpectedValues(List functionCalls, Task task } String[] packagesToEnable = new String[] { "hashable" }; - String[] modulesToImport = new String[] { "Control.Exception Data.Hashable System.Timeout" }; + String[] modulesToImport = new String[] { "Control.Exception Data.Hashable Data.List Data.Maybe System.Timeout" }; SubprocessResult result = evaluateWithGhci(packagesToEnable, modulesToImport, true, expressionsToEvaluate.toArray(new String[0]), task, true); List expectedValues = new ArrayList<>(); @@ -856,12 +856,12 @@ private List computeExpectedValues(List functionCalls, Task task private String wrapGhciExpressionInCatchAndTimeout(String expression) { // TODO@CHW: add setup option for timeout of single testcase - return String.format("timeout 1000000 (catch (putStr (show (%s))) ((const (putStr \"\")) :: SomeException -> IO ())) >> return ()", expression); + return String.format("timeout 1000000 (catch (putStr (show (%s))) (putStr . (\"EXCEPTION: \" ++) . (let cutCallStack exc = take (fromMaybe (length exc) (findIndex (isPrefixOf \"\\nCallStack (from HasCallStack)\") (tails exc))) exc in cutCallStack) . show :: SomeException -> IO ())) >> return ()", expression); } public static String extractUnescapedGhciExpressionWrappedInCatchAndTimeout(String wrappedExpression) { - // timeout\s+\d+\s+\(catch \(putStr \(show \(.*?\)\)\s+\(\(const \(putStr \"\"\)\) :: SomeException -> IO \(\)\)\) >> return \(\) - String pattern = "timeout\\s+\\d+\\s+\\(catch \\(putStr \\(show \\((.*?)\\)\\)\\s+\\(\\(const \\(putStr \"\"\\)\\) :: SomeException -> IO \\(\\)\\)\\) >> return \\(\\)"; + // timeout\s+\d+\s+\(catch \(putStr \(show \((.*?)\)\)\)\s+\(putStr\s+\.\s+\("EXCEPTION: "\s+\+\+\)\s+\.\s+\(let cutCallStack exc.*? in cutCallStack\)\s+\.\s+show :: SomeException -> IO \(\)\)\) >> return \(\) + String pattern = "timeout\\s+\\d+\\s+\\(catch \\(putStr \\(show \\((.*?)\\)\\)\\)\\s+\\(putStr\\s+\\.\\s+\\(\"EXCEPTION: \"\\s+\\+\\+\\)\\s+\\.\\s+\\(let cutCallStack exc.*? in cutCallStack\\)\\s+\\.\\s+show :: SomeException -> IO \\(\\)\\)\\) >> return \\(\\)"; Pattern regex = Pattern.compile(pattern); Matcher matcher = regex.matcher(wrappedExpression); From a8ade07198529e39fecfbe6e87ff12c75052b7a9 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 12 Jul 2025 19:52:53 +0200 Subject: [PATCH 065/105] Bugfix: add import of Data.Hashable to DockerTestSteps (needed for testcases of higher order functions) --- .../servlets/controller/HaskellRuntimeTestManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 3cf211b30..6d2ef43d2 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -166,7 +166,8 @@ private DockerTestStepData(String functionName, String functionType, String func this.functionNameWithType = functionName + " :: " + functionType; StringBuilder testCode = new StringBuilder("ghci -XInstanceSigs"); - appendGhciEvaluateArgument(testCode, ":m + Control.Exception Data.List Data.Maybe System.Timeout"); + appendGhciEvaluateArgument(testCode, ":set -package hashable"); + appendGhciEvaluateArgument(testCode, ":m + Control.Exception Data.Hashable Data.List Data.Maybe System.Timeout"); appendGhciEvaluateArgument(testCode, wrapGhciExpressionInCatchAndTimeout(functionCall.replaceAll("\r\n", "\n"))); testCode.append(" ").append(filename); this.testCode = testCode.toString(); From 6a71005e1d670ba6b2bbd3bce2237c216a716840 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 12 Jul 2025 20:02:11 +0200 Subject: [PATCH 066/105] Improve view fragment ShowHaskellRuntimeTestResult - show testcase instead of test title, since this makes the expected value more clear - scale table to full page width and enable horizontal scrollbar if the table is too big - separate table for each testcase with testcase, expected, got and ok-status in a separate line each (better readability for long testcases) --- .../controller/HaskellRuntimeTestManager.java | 4 +- .../ShowHaskellRuntimeTestResult.java | 50 +++++++++++++------ 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 6d2ef43d2..1f2001654 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -874,8 +874,8 @@ public static String extractUnescapedGhciExpressionWrappedInCatchAndTimeout(Stri } } - private static String prettyPrintFunctionCall(String functionCall) { - return functionCall.replaceAll("\\(let cyclicIntMap .*? let randomFunction .*? in randomFunction\\)", ""); + public static String prettyPrintCyclicIntMappers(String functionCall) { + return functionCall.replaceAll("\\(let cyclicIntMap.*? let randomFunction.*? in randomFunction\\)", ""); } private static class HaskellClassifiedIdentifiers { diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeTestResult.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeTestResult.java index ab801efc2..f0390658b 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeTestResult.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeTestResult.java @@ -18,6 +18,9 @@ package de.tuclausthal.submissioninterface.servlets.view.fragments; +import static de.tuclausthal.submissioninterface.servlets.controller.HaskellRuntimeTestManager.extractUnescapedGhciExpressionWrappedInCatchAndTimeout; +import static de.tuclausthal.submissioninterface.servlets.controller.HaskellRuntimeTestManager.prettyPrintCyclicIntMappers; + import java.io.PrintWriter; import java.io.StringReader; @@ -31,7 +34,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.util.Util; -public class ShowHaskellRuntimeTestResult { // similar code in ShowDockerTestResult +public class ShowHaskellRuntimeTestResult { // similar code in ShowDockerTestResult public static void printTestResults(PrintWriter out, HaskellRuntimeTest haskellRuntimeTest, String testOutput, boolean forStudent, StringBuilder javaScript) { JsonObject object = null; try (JsonReader jsonReader = Json.createReader(new StringReader(testOutput))) { @@ -61,18 +64,9 @@ public static void printTestResults(PrintWriter out, HaskellRuntimeTest haskellR } } } else if (arr.getValueType().equals(JsonValue.ValueType.ARRAY)) { - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); - out.println(""); + out.println("
"); JsonArray array = arr.asJsonArray(); for (int i = 0; i < array.size(); ++i) { - // TODO make nicer! JsonObject stepObject = array.get(i).asJsonObject(); int foundTest = -1; for (int j = 0; j < haskellRuntimeTest.getTestSteps().size(); ++j) { @@ -82,15 +76,41 @@ public static void printTestResults(PrintWriter out, HaskellRuntimeTest haskellR } } if (foundTest >= 0) { + out.println("
"); + out.println("
TestErwartetErhaltenOK?
"); + out.println(""); out.println(""); - out.println(""); - out.println(""); - out.println(""); + out.println(""); + String testCode = haskellRuntimeTest.getTestSteps().get(foundTest).getTestcode(); + String simplifiedTestCode = extractUnescapedGhciExpressionWrappedInCatchAndTimeout(testCode); + if (simplifiedTestCode != null) { + simplifiedTestCode = prettyPrintCyclicIntMappers(simplifiedTestCode); + } + out.println(""); + out.println(""); + out.println(""); + + out.println(""); + out.println(""); + out.println(""); + out.println(""); + + out.println(""); + out.println(""); + out.println(""); + out.println(""); + + out.println(""); + out.println(""); out.println(""); out.println(""); + + out.println("
" + Util.escapeHTML(haskellRuntimeTest.getTestSteps().get(foundTest).getTitle()) + "
" + Util.escapeHTML(stepObject.getString("expected")) + "
" + Util.escapeHTML(cleanup(object, stepObject.getString("got"))) + "
Testfall" + Util.escapeHTML(simplifiedTestCode != null ? simplifiedTestCode : testCode) + "
Erwartet
" + Util.escapeHTML(stepObject.getString("expected")) + "
Erhalten
" + Util.escapeHTML(cleanup(object, stepObject.getString("got"))) + "
OK?" + Util.boolToHTML(stepObject.getBoolean("ok")) + (stepObject.getBoolean("ok") ? "" : " (Diff)") + "
"); + out.println("
"); + out.println("
"); } } - out.println(""); + boolean wasError = false; if (object.containsKey("missing-tests") && object.getBoolean("missing-tests")) { out.println("

Nicht alle Tests wurden durchlaufen.

"); From 4e5a0067be152300f7b3cd73ecb2b3f328d1683e Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 12 Jul 2025 23:56:01 +0200 Subject: [PATCH 067/105] Optimize tables in haskell runtime test manager for long content inside the table cells - fixed table layout / column width - horizontal scrollbar, if content is too wide --- .../view/HaskellRuntimeTestManagerView.java | 64 +++++++++++++------ 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 23b526230..724ab284f 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -155,8 +155,8 @@ function syncTestcaseSelectionMasterCheckbox(formId) { StringBuilder newtypeOrDatasHtml = new StringBuilder(); newtypeOrDatasHtml.append(""" - Typname - Typdefinition und Arbitrary Instanz + Typname + Typdefinition und Arbitrary Instanz """); @@ -180,24 +180,38 @@ Default Typsignatur (:t +d) showNewtypeOrDataTable = true; newtypeOrDatasHtml.append(String.format(""" - %1$s - -
+ +
+ %1$s +
+ + +
+
%2$s
%3$s
+
""", Util.escapeHTML(identifier.getNewtypeOrDataTypename()), Util.escapeHTML(identifier.getNewtypeOrDataDefinition()), Util.escapeHTML(identifier.getNewtypeOrDataArbitraryInstance()))); - break; //TODO@CHW: 60vw might not be a good width depending on the page template + break; case "function": showFunctionTable = true; functionsHtml.append(String.format(""" - %2$s - %3$s - %6$s - %7$s +
+ %2$s +
+
+ %3$s +
+
+ %6$s +
+
+ %7$s +
@@ -216,17 +230,23 @@ Default Typsignatur (:t +d) } } - out.println(""); if (showNewtypeOrDataTable) { + out.println("
"); + out.println("
"); out.println(newtypeOrDatasHtml); + out.println("
"); + out.println("
"); } if (showNewtypeOrDataTable && showFunctionTable) { - out.println(""); + out.println("
"); } if (showFunctionTable) { + out.println("
"); + out.println(""); out.println(functionsHtml); + out.println("
"); + out.println("
"); } - out.println(""); if (showNewtypeOrDataTable || showFunctionTable) { out.println("
"); } @@ -270,13 +290,14 @@ Default Typsignatur (:t +d) out.println(String.format(""" - +
+
- - + @@ -285,15 +306,19 @@ for (DockerTestStep step : testStepsGroupedByFunctionNameWithType.get(functionNameWithType)) { out.println(String.format(""" - - - + + """, Util.escapeHTML(step.getTestcode()), Util.escapeHTML(step.getExpect()), step.getTeststepid(), formId)); } @@ -317,6 +342,7 @@ """, formId, formActionInputFieldId)); // TODO@CHW: add button "Duplikate in Selektion entfernen" out.println("
+ TestcodeTestcode Erwartete Ausgabe
Default Typsignatur (:t +d)
+ %1$s%2$s
+ %1$s +
+ %2$s +
Default Typsignatur (:t +d)
"); + out.println("
"); out.println(""); out.println("
"); } From 0d89dc90cb04427def0097900fd7e703a1cc86b2 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 13 Jul 2025 00:17:24 +0200 Subject: [PATCH 068/105] Run testcases with modelsolution directly from HaskellRuntimeTestManagerView --- .../servlets/view/HaskellRuntimeTestManagerView.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 724ab284f..14908f347 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -37,6 +37,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTestIdentifier; import de.tuclausthal.submissioninterface.servlets.GATEView; import de.tuclausthal.submissioninterface.servlets.controller.HaskellRuntimeTestManager; +import de.tuclausthal.submissioninterface.servlets.controller.PerformTest; import de.tuclausthal.submissioninterface.servlets.controller.TaskManager; import de.tuclausthal.submissioninterface.template.Template; import de.tuclausthal.submissioninterface.template.TemplateFactory; @@ -273,7 +274,7 @@ Default Typsignatur (:t +d) List sortedKeys = testStepsGroupedByFunctionNameWithType.keySet().stream().sorted().toList(); if (!sortedKeys.isEmpty()) { - out.println("

Testschritte bearbeiten

"); + out.println("

Testschritte bearbeiten (mit Musterlösung testen)

"); } for (int i = 0; i < sortedKeys.size(); i++) { From 7cf187b18a43f564f8a7fd44cd3729309756dcb0 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 13 Jul 2025 10:31:27 +0200 Subject: [PATCH 069/105] Continue running haskell runtime testcases, even if a previous case failed - subsequent cases might be correct again, which is relevant for clustering the submissions correctly --- .../testframework/tests/impl/DockerTest.java | 5 ++++ .../tests/impl/HaskellRuntimeTest.java | 25 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java index 8a34948ae..afdb9a508 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/DockerTest.java @@ -205,6 +205,11 @@ protected String generateTestShellScript() { return testCode.toString(); } + protected String getSeparator() { + // needed for HaskellRuntimeTest subclass + return separator; + } + @Override protected void performTestInTempDir(Path basePath, Path pTempDir, TestExecutorTestResult testResult) throws Exception {} } diff --git a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellRuntimeTest.java b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellRuntimeTest.java index e8ed0b0bd..3c3f6683f 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellRuntimeTest.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellRuntimeTest.java @@ -19,6 +19,8 @@ package de.tuclausthal.submissioninterface.testframework.tests.impl; +import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTestStep; + /** * @author Christian Wagner */ @@ -27,5 +29,26 @@ public HaskellRuntimeTest(final de.tuclausthal.submissioninterface.persistence.d super(test); } - // TODO@CHW: Override: public void performTest(final Path basePath, final Path submissionPath, final TestExecutorTestResult testResult) throws Exception { + @Override + protected String generateTestShellScript() { + // Difference to DockerTest.generateTestShellScript(): continue executing testcases, even if a previous case failed + // Reason: subsequent cases might be correct again, which is relevant for clustering the submissions correctly. + StringBuilder testCode = new StringBuilder(); + testCode.append("#!/bin/bash\n"); + testCode.append("set -e\n"); + testCode.append(test.getPreparationShellCode()); + testCode.append("\n"); + + for (DockerTestStep testStep : test.getTestSteps()) { + testCode.append("echo '").append(getSeparator()).append("'\n"); + testCode.append("echo '").append(getSeparator()).append("' >&2\n"); + testCode.append("{\n"); + testCode.append("set +e\n"); + testCode.append(testStep.getTestcode()); + testCode.append("\n"); + testCode.append("} || echo \"ERROR: syntax error or missing function\"\n"); + } + + return testCode.toString(); + } } From 2751db8bbf1f1dd01e5675e65bc244ae8763564f Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 13 Jul 2025 10:35:31 +0200 Subject: [PATCH 070/105] Don't hide member field testCode in DockerTestStepData constructor --- .../controller/HaskellRuntimeTestManager.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 1f2001654..f3a5aab06 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -165,12 +165,12 @@ private class DockerTestStepData { private DockerTestStepData(String functionName, String functionType, String functionCall, String expectedValue, String filename) { this.functionNameWithType = functionName + " :: " + functionType; - StringBuilder testCode = new StringBuilder("ghci -XInstanceSigs"); - appendGhciEvaluateArgument(testCode, ":set -package hashable"); - appendGhciEvaluateArgument(testCode, ":m + Control.Exception Data.Hashable Data.List Data.Maybe System.Timeout"); - appendGhciEvaluateArgument(testCode, wrapGhciExpressionInCatchAndTimeout(functionCall.replaceAll("\r\n", "\n"))); - testCode.append(" ").append(filename); - this.testCode = testCode.toString(); + StringBuilder testCodeStringBuilder = new StringBuilder("ghci -XInstanceSigs"); + appendGhciEvaluateArgument(testCodeStringBuilder, ":set -package hashable"); + appendGhciEvaluateArgument(testCodeStringBuilder, ":m + Control.Exception Data.Hashable Data.List Data.Maybe System.Timeout"); + appendGhciEvaluateArgument(testCodeStringBuilder, wrapGhciExpressionInCatchAndTimeout(functionCall.replaceAll("\r\n", "\n"))); + testCodeStringBuilder.append(" ").append(filename); + this.testCode = testCodeStringBuilder.toString(); this.expectedValue = expectedValue.replaceAll("\r\n", "\n"); // TODO@CHW handle float values } From 063a0a43e2019ca6546da64db4b337b419491d5d Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 13 Jul 2025 11:23:13 +0200 Subject: [PATCH 071/105] Make all methods static in HaskellRuntimeTestManager if possible --- .../controller/HaskellRuntimeTestManager.java | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index f3a5aab06..d36bbfbe6 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -157,7 +157,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr } } - private class DockerTestStepData { + private static class DockerTestStepData { private final String functionNameWithType; private final String testCode; private final String expectedValue; @@ -188,7 +188,7 @@ private String getExpectedValue() { } } - private void deleteStoredClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session) { + private static void deleteStoredClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session) { Transaction tx = session.beginTransaction(); for (HaskellRuntimeTestIdentifier identifier : haskellRuntimeTest.getIdentifiers()) { @@ -198,7 +198,7 @@ private void deleteStoredClassifiedIdentifiers(HaskellRuntimeTest haskellRuntime tx.commit(); } - private void browseModelSolutionAndStoreClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session) throws IOException { + private static void browseModelSolutionAndStoreClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session) throws IOException { List haskellIdentifiers = browseModelSolution(haskellRuntimeTest.getTask()); HaskellClassifiedIdentifiers haskellClassifiedIdentifiers = classifyHaskellIdentifiers(haskellIdentifiers); @@ -232,7 +232,7 @@ private void browseModelSolutionAndStoreClassifiedIdentifiers(HaskellRuntimeTest tx.commit(); } - private List readClassifiedIdentifiersAndGenerateFunctionTestcases(HaskellRuntimeTest haskellRuntimeTest, int identifierId, int numberOfTestSteps) throws IOException, IllegalArgumentException { + private static List readClassifiedIdentifiersAndGenerateFunctionTestcases(HaskellRuntimeTest haskellRuntimeTest, int identifierId, int numberOfTestSteps) throws IOException, IllegalArgumentException { List generatedTestcases = new ArrayList<>(); HaskellRuntimeTestIdentifier functionIdentifier = null; List arbitraryInstances = new ArrayList<>(); @@ -278,14 +278,14 @@ private List readClassifiedIdentifiersAndGenerateFunctionTes return generatedTestcases; } - private String getModelSolutionFilename(HaskellRuntimeTest haskellRuntimeTest) throws IOException { + private static String getModelSolutionFilename(HaskellRuntimeTest haskellRuntimeTest) throws IOException { final Path taskPath = Util.constructPath(Configuration.getInstance().getDataPath(), haskellRuntimeTest.getTask()); final Path modelSolutionPath = taskPath.resolve(TaskPath.MODELSOLUTIONFILES.getPathComponent()); return getModelSolutionFile(modelSolutionPath).getFileName().toString(); } - private void duplicateTestStepsWithIds(HaskellRuntimeTest haskellRuntimeTest, Session session, Set testStepIds) { + private static void duplicateTestStepsWithIds(HaskellRuntimeTest haskellRuntimeTest, Session session, Set testStepIds) { Transaction tx = session.beginTransaction(); for (DockerTestStep step : haskellRuntimeTest.getTestSteps()) { @@ -297,7 +297,7 @@ private void duplicateTestStepsWithIds(HaskellRuntimeTest haskellRuntimeTest, Se tx.commit(); } - private void deleteTestStepsWithIds(HaskellRuntimeTest haskellRuntimeTest, Session session, Set testStepIds) { + private static void deleteTestStepsWithIds(HaskellRuntimeTest haskellRuntimeTest, Session session, Set testStepIds) { Transaction tx = session.beginTransaction(); for (DockerTestStep step : haskellRuntimeTest.getTestSteps()) { @@ -328,7 +328,7 @@ private record SubprocessResult(String stdOut, String stdErr, int exitCode, bool * @param task Task, for which the testcases should be generated based on the model solution * @return result of the subprocess */ - private SubprocessResult evaluateWithGhci(String[] packagesToEnable, String[] modulesToImport, boolean loadModelSolution, String[] expressionsToEvaluate, Task task, boolean throwIOExceptionOnNonZeroExitCode) throws IOException { + private static SubprocessResult evaluateWithGhci(String[] packagesToEnable, String[] modulesToImport, boolean loadModelSolution, String[] expressionsToEvaluate, Task task, boolean throwIOExceptionOnNonZeroExitCode) throws IOException { if (packagesToEnable == null) packagesToEnable = new String[0]; if (modulesToImport == null) @@ -437,7 +437,7 @@ private SubprocessResult evaluateWithGhci(String[] packagesToEnable, String[] mo } } - private Path getModelSolutionFile(Path modelSolutionDirectory) throws IOException { + private static Path getModelSolutionFile(Path modelSolutionDirectory) throws IOException { // Expect exactly one .hs file among the modelsolution files -> this file will be used to generate the testcases try (Stream stream = Files.list(modelSolutionDirectory)) { List hsFiles = stream.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".hs")).toList(); @@ -454,12 +454,12 @@ private static void appendGhciEvaluateArgument(StringBuilder testCode, String ar testCode.append(" -e '").append(argument.replace("'", "'\"'\"'").replace("\t", " ")).append("'"); } - private List browseModelSolution(Task task) throws IOException { + private static List browseModelSolution(Task task) throws IOException { SubprocessResult result = evaluateWithGhci(null, null, true, new String[] { ":browse" }, task, true); return splitLinesButKeepMultilines(result.stdOut()); } - private List splitLinesButKeepMultilines(String resultStdout) { + private static List splitLinesButKeepMultilines(String resultStdout) { List haskellIdentifiers = new ArrayList<>(); for (String line : resultStdout.split("\\R")) { @@ -478,7 +478,7 @@ private List splitLinesButKeepMultilines(String resultStdout) { return haskellIdentifiers; } - private HaskellClassifiedIdentifiers classifyHaskellIdentifiers(List haskellIdentifiers) { + private static HaskellClassifiedIdentifiers classifyHaskellIdentifiers(List haskellIdentifiers) { HaskellClassifiedIdentifiers classifiedIdentifiers = new HaskellClassifiedIdentifiers(); for (String line : haskellIdentifiers) { @@ -496,7 +496,7 @@ private HaskellClassifiedIdentifiers classifyHaskellIdentifiers(List has return classifiedIdentifiers; } - private String getGhciDefaultTypeSignature(Task task, String identifierName) throws IOException { + private static String getGhciDefaultTypeSignature(Task task, String identifierName) throws IOException { SubprocessResult result = evaluateWithGhci(null, null, true, new String[] { ":type +d " + identifierName }, task, true); String defaultTypeSignature = normalizeTypeSignature(result.stdOut().split("::")[1].trim()); @@ -507,7 +507,7 @@ private String getGhciDefaultTypeSignature(Task task, String identifierName) thr } } - private String normalizeTypeSignature(String typeSignature) { + private static String normalizeTypeSignature(String typeSignature) { typeSignature = typeSignature.replace("\n", ""); typeSignature = typeSignature.replaceAll("\\s*->\\s*", " -> "); return typeSignature.trim(); @@ -568,7 +568,7 @@ private record TestcaseSingleParameterWithType(String testcaseParameter, String private record TestcaseWithTypes(List testcaseParametersWithTypes) { } - private List generateQuickcheckFunctionTestcases(Task task, List functionParameterTypes, List arbitraryInstances, int numberOfTestcases) throws IOException { + private static List generateQuickcheckFunctionTestcases(Task task, List functionParameterTypes, List arbitraryInstances, int numberOfTestcases) throws IOException { if (functionParameterTypes.isEmpty()) { return List.of(); } @@ -749,7 +749,7 @@ class ToStringList a where toStringList :: a -> [String] return testcasesWithTypes; } - private List withConstrainedPrimitiveTypes(List parameterTypes) { + private static List withConstrainedPrimitiveTypes(List parameterTypes) { Map replacementDict = Map.of(HaskellPrimitiveType.Char.toString(), HaskellConstrainedPrimitiveType.Char_OnlySafeAscii.toString(), HaskellPrimitiveType.String.toString(), HaskellConstrainedPrimitiveType.String_OnlySafeAscii.toString()); String patternString = String.join("|", replacementDict.keySet().stream().map(key -> "(? withConstrainedPrimitiveTypes(List parameterTypes) return constrainedTypes; } - private List withCyclicIntMapTypes(List parameterTypes, String cyclicIntMapConstructor) { + private static List withCyclicIntMapTypes(List parameterTypes, String cyclicIntMapConstructor) { List cyclicIntMapTypes = new ArrayList<>(); for (String parameterType : parameterTypes) { @@ -793,7 +793,7 @@ private List withCyclicIntMapTypes(List parameterTypes, String c return cyclicIntMapTypes; } - private String embedCyclicIntMapInRandomFunction(int numberOfParameters, String cyclicIntMapDefinition) { + private static String embedCyclicIntMapInRandomFunction(int numberOfParameters, String cyclicIntMapDefinition) { String cyclicIntMapName = cyclicIntMapDefinition.split("\\s+")[0].trim(); List parameters = new ArrayList<>(); @@ -804,7 +804,7 @@ private String embedCyclicIntMapInRandomFunction(int numberOfParameters, String return String.format("(let %s in let randomFunction %s = (%s . hash . show) (%s) in randomFunction)", cyclicIntMapDefinition, String.join(" ", parameters), cyclicIntMapName, String.join(", ", parameters)); } - private List generateFunctionCalls(String functionName, List testcasesWithTypes) { + private static List generateFunctionCalls(String functionName, List testcasesWithTypes) { List functionCalls = new ArrayList<>(); for (TestcaseWithTypes testcaseWithTypes : testcasesWithTypes) { @@ -825,8 +825,8 @@ private List generateFunctionCalls(String functionName, List computeExpectedValues(List functionCalls, Task task) throws IOException { - List wrappedFunctionCalls = functionCalls.stream().map(this::wrapGhciExpressionInCatchAndTimeout).toList(); + private static List computeExpectedValues(List functionCalls, Task task) throws IOException { + List wrappedFunctionCalls = functionCalls.stream().map(HaskellRuntimeTestManager::wrapGhciExpressionInCatchAndTimeout).toList(); String expectedValueSeparator = "@NEXT-EXPECTED-VALUE@"; @@ -855,7 +855,7 @@ private List computeExpectedValues(List functionCalls, Task task return expectedValues; } - private String wrapGhciExpressionInCatchAndTimeout(String expression) { + private static String wrapGhciExpressionInCatchAndTimeout(String expression) { // TODO@CHW: add setup option for timeout of single testcase return String.format("timeout 1000000 (catch (putStr (show (%s))) (putStr . (\"EXCEPTION: \" ++) . (let cutCallStack exc = take (fromMaybe (length exc) (findIndex (isPrefixOf \"\\nCallStack (from HasCallStack)\") (tails exc))) exc in cutCallStack) . show :: SomeException -> IO ())) >> return ()", expression); } From c00b5c414d9cee0d53b12d87a2ec7756e968840c Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 13 Jul 2025 11:28:28 +0200 Subject: [PATCH 072/105] Remove old LOG.info() calls --- .../servlets/controller/HaskellRuntimeTestManager.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index d36bbfbe6..f3d99756e 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -594,8 +594,6 @@ private static List generateQuickcheckFunctionTestcases(Task // In ein Tuple-String umwandeln String parameterTypeTuple = "(" + String.join(", ", parameterTypesTupleValues) + ")"; - LOG.info("- PARAMS TUPLE:\t\t" + parameterTypeTuple); - String placeholderType = String.format(""" data %1$s = %1$s @@ -772,7 +770,6 @@ private static List withConstrainedPrimitiveTypes(List parameter constrainedTypes.add(sb.toString()); } - LOG.info("- PARAMS (constr.):\t" + constrainedTypes); return constrainedTypes; } From 3b868a11762014386b7ed039375d7fe3e04437af Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 13 Jul 2025 11:50:25 +0200 Subject: [PATCH 073/105] Bugfix: explicitly load .hs file using ":load" - if additional packages are enabled using ":set -package", appending the filename at the end of the ghci call is not enough - in this case, the file needs to be loaded using ":load", otherwise it is not loaded at all --- .../servlets/controller/HaskellRuntimeTestManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index f3d99756e..75da85ba1 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -168,8 +168,8 @@ private DockerTestStepData(String functionName, String functionType, String func StringBuilder testCodeStringBuilder = new StringBuilder("ghci -XInstanceSigs"); appendGhciEvaluateArgument(testCodeStringBuilder, ":set -package hashable"); appendGhciEvaluateArgument(testCodeStringBuilder, ":m + Control.Exception Data.Hashable Data.List Data.Maybe System.Timeout"); + appendGhciEvaluateArgument(testCodeStringBuilder, ":load " + filename); appendGhciEvaluateArgument(testCodeStringBuilder, wrapGhciExpressionInCatchAndTimeout(functionCall.replaceAll("\r\n", "\n"))); - testCodeStringBuilder.append(" ").append(filename); this.testCode = testCodeStringBuilder.toString(); this.expectedValue = expectedValue.replaceAll("\r\n", "\n"); // TODO@CHW handle float values From 663bd9043a91ae1b7dac935273c5b2c4c5211001 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 13 Jul 2025 12:08:23 +0200 Subject: [PATCH 074/105] Show total number of test steps in HaskellRuntimeTestManagerView --- .../servlets/view/HaskellRuntimeTestManagerView.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 14908f347..9cef89d79 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -274,7 +274,9 @@ Default Typsignatur (:t +d) List sortedKeys = testStepsGroupedByFunctionNameWithType.keySet().stream().sorted().toList(); if (!sortedKeys.isEmpty()) { - out.println("

Testschritte bearbeiten (mit Musterlösung testen)

"); + final int numberOfTestSteps = test.getTestSteps().size(); + final String numberOfTestStepsText = numberOfTestSteps + " " + (numberOfTestSteps == 1 ? "Testschritt" : "Testschritte"); + out.println("

Testschritte bearbeiten (" + numberOfTestStepsText + ", mit Musterlösung testen)

"); } for (int i = 0; i < sortedKeys.size(); i++) { From c2cc6c5631246339c27ad11ecd5bcfb491705210 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 13 Jul 2025 12:24:54 +0200 Subject: [PATCH 075/105] Increase (default) safe-docker timeout to 45s (when generating and when running the testcases) - this is needed if several testcases to not terminate - timeout-wrapping of ghci expressions ensures, that single testcases are aborted soon enough if they don't terminate --- .../servlets/controller/HaskellRuntimeTestManager.java | 2 +- .../servlets/view/TestManagerAddTestFormView.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 75da85ba1..f21d79c95 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -338,7 +338,7 @@ private static SubprocessResult evaluateWithGhci(String[] packagesToEnable, Stri final Path taskPath = Util.constructPath(Configuration.getInstance().getDataPath(), task); final Path modelSolutionPath = taskPath.resolve(TaskPath.MODELSOLUTIONFILES.getPathComponent()); - final int safeDockerTimeout = 30; + final int safeDockerTimeout = 45; Path generatorTempDir = null; try { diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java index ddfed9b18..300a2ecb9 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TestManagerAddTestFormView.java @@ -218,7 +218,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro out.println(""); out.println(""); out.println("Timeout (s):"); - out.println(""); + out.println(""); out.println(""); out.println(""); out.println("Studierenden Test-Details anzeigen:"); From 6acde5e9eef1bbd528422c8b43e4572dfce117d9 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 13 Jul 2025 19:25:12 +0200 Subject: [PATCH 076/105] Checkboxes to simplify or expand the testcode - default: random functions abbreviated by , no ghci wrapper code displayed - checkbox to show random function definitions - checkbox to show full ghci wrapper, including the random function definitions --- .../view/HaskellRuntimeTestManagerView.java | 72 +++++++++++++++++-- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 9cef89d79..23602cc5b 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -19,6 +19,9 @@ package de.tuclausthal.submissioninterface.servlets.view; +import static de.tuclausthal.submissioninterface.servlets.controller.HaskellRuntimeTestManager.extractUnescapedGhciExpressionWrappedInCatchAndTimeout; +import static de.tuclausthal.submissioninterface.servlets.controller.HaskellRuntimeTestManager.prettyPrintCyclicIntMappers; + import java.io.IOException; import java.io.PrintWriter; import java.io.Serial; @@ -111,6 +114,25 @@ function syncTestcaseSelectionMasterCheckbox(formId) { const allChecked = Array.from(checkboxes).every(cb => cb.checked); masterCheckbox.checked = allChecked; } + function updateTestcodeSimplification(fullTestcodeClass, noGhciFullFunctionsClass, simpleTestcodeClass, showGhciEmbeddingCheckboxId, showRandomFunctionsCheckboxId) { + const showGhciEmbedding = document.getElementById(showGhciEmbeddingCheckboxId).checked; + const showRandomFunctions = document.getElementById(showRandomFunctionsCheckboxId).checked; + + Array.from(document.getElementsByClassName(fullTestcodeClass)).forEach(elem => elem.style.display = 'none'); + Array.from(document.getElementsByClassName(noGhciFullFunctionsClass)).forEach(elem => elem.style.display = 'none'); + Array.from(document.getElementsByClassName(simpleTestcodeClass)).forEach(elem => elem.style.display = 'none'); + document.getElementById(showRandomFunctionsCheckboxId).disabled = false; + + if (showGhciEmbedding) { + Array.from(document.getElementsByClassName(fullTestcodeClass)).forEach(elem => elem.style.display = 'block'); + document.getElementById(showRandomFunctionsCheckboxId).checked = true; + document.getElementById(showRandomFunctionsCheckboxId).disabled = true; + } else if (showRandomFunctions) { + Array.from(document.getElementsByClassName(noGhciFullFunctionsClass)).forEach(elem => elem.style.display = 'block'); + } else { + Array.from(document.getElementsByClassName(simpleTestcodeClass)).forEach(elem => elem.style.display = 'block'); + } + } """); @@ -290,6 +312,13 @@ Default Typsignatur (:t +d) String formId = "deleteOrDuplicateMultipleTestStepsForm" + i; String formActionInputFieldId = "deleteOrDuplicateMultipleTestStepsFormAction" + i; + String showGhciEmbeddingCheckboxId = "showGhciEmbeddingCheckbox" + i; + String showRandomFunctionsCheckboxId = "showRandomFunctionsCheckbox" + i; + + String fullTestcodeClass = "fullTestcode" + i; + String noGhciFullFunctionsClass = "noGhciFullFunctions" + i; + String simpleTestcodeClass = "simpleTestcode" + i; + out.println(String.format("""
@@ -300,13 +329,36 @@ Default Typsignatur (:t +d) - Testcode + + Testcode +
+ + +
+ Erwartete Ausgabe - """, Util.generateHTMLLink("?", response), formId, test.getId())); + """, Util.generateHTMLLink("?", response), formId, test.getId(), showGhciEmbeddingCheckboxId, showRandomFunctionsCheckboxId, fullTestcodeClass, noGhciFullFunctionsClass, simpleTestcodeClass)); for (DockerTestStep step : testStepsGroupedByFunctionNameWithType.get(functionNameWithType)) { + String testcodeWithoutWrapperCode = extractUnescapedGhciExpressionWrappedInCatchAndTimeout(step.getTestcode()); + if (testcodeWithoutWrapperCode == null) { + testcodeWithoutWrapperCode = step.getTestcode(); + } + + String testcodeWithoutWrapperCodeWithoutCyclicIntMappers = prettyPrintCyclicIntMappers(testcodeWithoutWrapperCode); + out.println(String.format(""" @@ -316,14 +368,22 @@ Default Typsignatur (:t +d) value="%3$s" onchange="syncTestcaseSelectionMasterCheckbox('%4$s'); toggleTableRowHighlight(this)"> -
- %1$s -
+ + + +
+ %6$s +
+
%2$s
- """, Util.escapeHTML(step.getTestcode()), Util.escapeHTML(step.getExpect()), step.getTeststepid(), formId)); + """, Util.escapeHTML(step.getTestcode()), Util.escapeHTML(step.getExpect()), step.getTeststepid(), formId, Util.escapeHTML(testcodeWithoutWrapperCode), Util.escapeHTML(testcodeWithoutWrapperCodeWithoutCyclicIntMappers), fullTestcodeClass, noGhciFullFunctionsClass, simpleTestcodeClass)); } out.println(String.format(""" From 6ba3589a2a1f472e44d477ba4b5127fee79c58a3 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sun, 13 Jul 2025 22:48:57 +0200 Subject: [PATCH 077/105] Enter and leave edit mode for each testcase individually (saving the edited testcase is not yet implemented, will follow soon) --- .../view/HaskellRuntimeTestManagerView.java | 65 ++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 23602cc5b..0bb914830 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -114,6 +114,35 @@ function syncTestcaseSelectionMasterCheckbox(formId) { const allChecked = Array.from(checkboxes).every(cb => cb.checked); masterCheckbox.checked = allChecked; } + function enterEditMode(formId, noEditModeDivId, editModeDivId, testcaseSelectionCheckboxId) { + setElementWithIdVisible(noEditModeDivId, false); + setElementWithIdVisible(editModeDivId, true); + const checkboxes = document.getElementById(formId).getElementsByClassName('testcaseSelectionCheckbox'); + const masterCheckbox = document.getElementById(formId).getElementsByClassName('testcaseSelectionMasterCheckbox')[0]; + Array.from(checkboxes).forEach(cb => { + cb.checked = false; + toggleTableRowHighlight(cb); + }); + masterCheckbox.checked = false; + const testcaseSelectionCheckbox = document.getElementById(testcaseSelectionCheckboxId); + testcaseSelectionCheckbox.checked = true; + toggleTableRowHighlight(testcaseSelectionCheckbox); + Array.from(checkboxes).forEach(cb => cb.disabled = true); + masterCheckbox.disabled = true; + } + function leaveEditMode(formId, noEditModeDivId, editModeDivId) { + setElementWithIdVisible(noEditModeDivId, true); + setElementWithIdVisible(editModeDivId, false); + const checkboxes = document.getElementById(formId).getElementsByClassName('testcaseSelectionCheckbox'); + const masterCheckbox = document.getElementById(formId).getElementsByClassName('testcaseSelectionMasterCheckbox')[0]; + Array.from(checkboxes).forEach(cb => { + cb.checked = false; + cb.disabled = false; + toggleTableRowHighlight(cb); + }); + masterCheckbox.checked = false; + masterCheckbox.disabled = false; + } function updateTestcodeSimplification(fullTestcodeClass, noGhciFullFunctionsClass, simpleTestcodeClass, showGhciEmbeddingCheckboxId, showRandomFunctionsCheckboxId) { const showGhciEmbedding = document.getElementById(showGhciEmbeddingCheckboxId).checked; const showRandomFunctions = document.getElementById(showRandomFunctionsCheckboxId).checked; @@ -133,6 +162,9 @@ function updateTestcodeSimplification(fullTestcodeClass, noGhciFullFunctionsClas Array.from(document.getElementsByClassName(simpleTestcodeClass)).forEach(elem => elem.style.display = 'block'); } } + function setElementWithIdVisible(elementId, visible) { + document.getElementById(elementId).style.display = visible ? 'block' : 'none'; + } """); @@ -359,31 +391,48 @@ Default Typsignatur (:t +d) String testcodeWithoutWrapperCodeWithoutCyclicIntMappers = prettyPrintCyclicIntMappers(testcodeWithoutWrapperCode); + String noEditModeDivId = "noEditModeDiv" + step.getTeststepid(); + String editModeDivId = "editModeDiv" + step.getTeststepid(); + String testcaseSelectionCheckboxId = "testcaseSelectionCheckbox" + step.getTeststepid(); + out.println(String.format(""" - - @@ -432,7 +440,7 @@ Default Typsignatur (:t +d) %2$s
- """, Util.escapeHTML(step.getTestcode()), Util.escapeHTML(step.getExpect()), step.getTeststepid(), formId, Util.escapeHTML(testcodeWithoutWrapperCode), Util.escapeHTML(testcodeWithoutWrapperCodeWithoutCyclicIntMappers), fullTestcodeClass, noGhciFullFunctionsClass, simpleTestcodeClass, noEditModeDivId, editModeDivId, testcaseSelectionCheckboxId)); + """, Util.escapeHTML(step.getTestcode()), Util.escapeHTML(step.getExpect()), step.getTeststepid(), formId, Util.escapeHTML(testcodeWithoutWrapperCode), Util.escapeHTML(testcodeWithoutWrapperCodeWithoutCyclicIntMappers), fullTestcodeClass, noGhciFullFunctionsClass, simpleTestcodeClass, noEditModeDivId, editModeDivId, testcaseSelectionCheckboxId, formActionInputFieldId)); } out.println(String.format(""" From 624b0ea74294b50dced14e46782cd5271a0842f5 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Wed, 16 Jul 2025 13:04:54 +0200 Subject: [PATCH 079/105] Create common error title in JSON format for haskell runtime tests - reason: the common error title is very long and barely readable, as it contains all wrong testcases along with their wrong output values - this needs to be parsed in the view servlet and formatted more readable --- .../testanalyzer/CommonErrorAnalyzer.java | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java index b34ae1e1f..7b9760764 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java @@ -22,11 +22,14 @@ import static de.tuclausthal.submissioninterface.servlets.controller.HaskellRuntimeTestManager.extractUnescapedGhciExpressionWrappedInCatchAndTimeout; import java.io.StringReader; +import java.util.ArrayList; import java.util.List; import jakarta.json.Json; import jakarta.json.JsonArray; +import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; import org.hibernate.Session; @@ -168,42 +171,55 @@ private void groupHaskellRuntimeTestResults(final HaskellRuntimeTest test, final return; } + // TODO@CHW cluster for each function individually final JsonObject testOutputJson = Json.createReader(new StringReader(testResult.getTestOutput())).readObject(); + JsonObjectBuilder commonErrorTitleJsonObjectBuilder = Json.createObjectBuilder(); - String stepsStr = ""; if (testOutputJson.containsKey("steps")) { final JsonArray steps = testOutputJson.getJsonArray("steps"); + JsonArrayBuilder testcaseArrayBuilder = Json.createArrayBuilder(); - StringBuilder stepsStrBuilder = new StringBuilder(); for (int i = 0; i < steps.size(); i++) { if (!steps.getJsonObject(i).getBoolean("ok")) { - String gotValue = steps.getJsonObject(i).getString("got"); - - String testcaseIdentifier = extractUnescapedGhciExpressionWrappedInCatchAndTimeout(test.getTestSteps().get(i).getTestcode()); + String gotValue = steps.getJsonObject(i).getString("got").strip(); // TODO@CHW remove line numbers from the exceptions here + String testCodeWrappedInCatchAndTimeout = test.getTestSteps().get(i).getTestcode(); + String testcaseIdentifier = extractUnescapedGhciExpressionWrappedInCatchAndTimeout(testCodeWrappedInCatchAndTimeout); if (testcaseIdentifier == null) { - testcaseIdentifier = "unknown case"; + testcaseIdentifier = testCodeWrappedInCatchAndTimeout; } - stepsStrBuilder.append("Case \"").append(testcaseIdentifier).append("\" failed with \"").append(gotValue).append("\";"); + testcaseArrayBuilder.add(Json.createObjectBuilder().add("testcase", testcaseIdentifier).add("got", gotValue)); } } - - stepsStr = stepsStrBuilder.toString(); + commonErrorTitleJsonObjectBuilder.add("testcases", testcaseArrayBuilder); } - String keyStr = ""; + List flags = new ArrayList<>(); if (testOutputJson.containsKey("stderr") && !testOutputJson.getString("stderr").isEmpty()) { - keyStr += "stderr not empty; "; + flags.add("stderr not empty"); + } + if (testOutputJson.containsKey("stdout") && testOutputJson.getString("stdout").isEmpty()) { + flags.add("stdout empty"); + } + if (testOutputJson.containsKey("exitedCleanly") && testOutputJson.getBoolean("exitedCleanly")) { + flags.add("exited cleanly"); + + } + if (testOutputJson.containsKey("missing-tests")) { + flags.add("missing tests"); + } + if (testOutputJson.containsKey("time-exceeded")) { + flags.add("time exceeded"); + } + + StringBuilder keyStrStringBuilder = new StringBuilder(); + JsonArrayBuilder flagsArrayBuilder = Json.createArrayBuilder(); + for (String flag : flags) { + keyStrStringBuilder.append(flag).append("; "); + flagsArrayBuilder.add(flag); } - if (testOutputJson.containsKey("stdout") && testOutputJson.getString("stdout").isEmpty()) - keyStr += "stdout empty; "; - if (testOutputJson.containsKey("exitedCleanly") && testOutputJson.getBoolean("exitedCleanly")) - keyStr += "exited cleanly; "; - if (testOutputJson.containsKey("missing-tests")) - keyStr += "missing tests; "; - if (testOutputJson.containsKey("time-exceeded")) - keyStr += "time exceeded; "; - bindCommonError(testResult, stepsStr + keyStr, keyStr, CommonError.Type.RunTimeError); + commonErrorTitleJsonObjectBuilder.add("flags", flagsArrayBuilder); + bindCommonError(testResult, commonErrorTitleJsonObjectBuilder.build().toString(), keyStrStringBuilder.toString(), CommonError.Type.RunTimeError); } private void groupJUnitTestResults(JUnitTest test, final TestResult testResult) { From a01d8326c8a5421bae5ac4934cd50e9e659b607d Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Fri, 18 Jul 2025 18:19:16 +0200 Subject: [PATCH 080/105] Add view fragment ShowHaskellRuntimeCommonErrorTitle to format common error titles of haskell runtime tests --- .../view/ShowSubmissionStudentView.java | 5 ++ .../ShowTaskStudentCommonErrorOverView.java | 10 ++- .../servlets/view/ShowTaskStudentView.java | 25 +++++-- .../view/ShowTaskTutorTestOverView.java | 10 ++- .../ShowHaskellRuntimeCommonErrorTitle.java | 70 +++++++++++++++++++ .../template/Template.java | 4 +- 6 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java index 0d8127be8..b4a370154 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java @@ -38,6 +38,7 @@ import de.tuclausthal.submissioninterface.servlets.GATEView; import de.tuclausthal.submissioninterface.servlets.controller.ShowFile; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowDockerTestResult; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellRuntimeCommonErrorTitle; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellRuntimeTestResult; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowJavaAdvancedIOTestResult; @@ -71,6 +72,10 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro template.printTemplateHeader(commonError, submission, "Testübersicht"); PrintWriter out = response.getWriter(); + if (commonError.getTest() instanceof HaskellRuntimeTest) { + ShowHaskellRuntimeCommonErrorTitle.formatCommonErrorTitle(out, commonError.getTitle()); + } + StringBuilder javaScript = new StringBuilder(); if (!submission.getTestResults().isEmpty()) { diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentCommonErrorOverView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentCommonErrorOverView.java index fcb7bd576..a078a3cb0 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentCommonErrorOverView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentCommonErrorOverView.java @@ -40,12 +40,14 @@ import de.tuclausthal.submissioninterface.persistence.dao.CommonErrorDAOIf; import de.tuclausthal.submissioninterface.persistence.dao.DAOFactory; import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Submission; import de.tuclausthal.submissioninterface.persistence.datamodel.Task; import de.tuclausthal.submissioninterface.persistence.datamodel.Test; import de.tuclausthal.submissioninterface.servlets.GATEView; import de.tuclausthal.submissioninterface.servlets.RequestAdapter; import de.tuclausthal.submissioninterface.servlets.controller.ShowSubmissionStudent; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellRuntimeCommonErrorTitle; import de.tuclausthal.submissioninterface.template.Template; import de.tuclausthal.submissioninterface.template.TemplateFactory; import de.tuclausthal.submissioninterface.util.Util; @@ -126,7 +128,13 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro while (it.hasNext()) { CommonError commonError = it.next(); out.println(""); - out.println("" + Util.escapeHTML(commonError.getTitle()) + ""); + if (test instanceof HaskellRuntimeTest) { + out.println(""); + ShowHaskellRuntimeCommonErrorTitle.formatCommonErrorTitle(out, commonError.getTitle()); + out.println(""); + } else { + out.println("" + Util.escapeHTML(commonError.getTitle()) + ""); + } out.println("" + commonErrorFrequency.get(commonError) + ""); out.println(""); for (Entry> entry : subCommonErrorMap.entrySet()) { diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java index 6ffc7f058..b5f97bb20 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java @@ -69,6 +69,7 @@ import de.tuclausthal.submissioninterface.servlets.controller.SubmitSolution; import de.tuclausthal.submissioninterface.servlets.controller.WebStart; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowDockerTestResult; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellRuntimeCommonErrorTitle; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellRuntimeTestResult; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellSyntaxTestResult; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowJavaAdvancedIOTestResult; @@ -380,11 +381,27 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro if (!testResult.getPassedTest()) { TestResultCommonErrorDAO trce = new TestResultCommonErrorDAO(session); List ces = trce.getCommonError(testResult); - String commonErrorString = "| "; - for (CommonError ce : ces) { - commonErrorString = commonErrorString + Util.escapeHTML(ce.getTitle()) + " | "; + + if (testResult.getTest() instanceof HaskellRuntimeTest) { + out.println("
"); + out.println("Fehlermeldung:"); + out.println(""); + + for (CommonError ce : ces) { + out.println(""); + } + + out.println("
"); + ShowHaskellRuntimeCommonErrorTitle.formatCommonErrorTitle(out, ce.getTitle()); + out.println("
"); + out.println("
"); + } else { + String commonErrorString = "| "; + for (CommonError ce : ces) { + commonErrorString = commonErrorString + Util.escapeHTML(ce.getTitle()) + " | "; + } + out.println("
Fehlermeldung: " + commonErrorString + "
"); } - out.println("
Fehlermeldung: " + commonErrorString + "
"); } if (!testResult.getTestOutput().isEmpty() && testResult.getTest().isGiveDetailsToStudents()) { if (testResult.getTest() instanceof JavaAdvancedIOTest) { diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorTestOverView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorTestOverView.java index 21ca69be9..83b7fe169 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorTestOverView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorTestOverView.java @@ -42,6 +42,7 @@ import de.tuclausthal.submissioninterface.persistence.dao.DAOFactory; import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError; import de.tuclausthal.submissioninterface.persistence.datamodel.Group; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Participation; import de.tuclausthal.submissioninterface.persistence.datamodel.Submission; import de.tuclausthal.submissioninterface.persistence.datamodel.Task; @@ -49,6 +50,7 @@ import de.tuclausthal.submissioninterface.servlets.GATEView; import de.tuclausthal.submissioninterface.servlets.RequestAdapter; import de.tuclausthal.submissioninterface.servlets.controller.ShowSubmission; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellRuntimeCommonErrorTitle; import de.tuclausthal.submissioninterface.template.Template; import de.tuclausthal.submissioninterface.template.TemplateFactory; import de.tuclausthal.submissioninterface.util.Util; @@ -168,7 +170,13 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro CommonError commonError = it.next(); out.println(""); //out.println("" + Util.escapeHTML(commonError.getCommonErrorName()) + ""); - out.println("" + Util.escapeHTML(commonError.getTitle()) + ""); + if (test instanceof HaskellRuntimeTest) { + out.println(""); + ShowHaskellRuntimeCommonErrorTitle.formatCommonErrorTitle(out, commonError.getTitle()); + out.println(""); + } else { + out.println("" + Util.escapeHTML(commonError.getTitle()) + ""); + } out.println("" + commonErrorFrequency.get(commonError) + ""); //out.println("Beispiel"); out.println(""); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java new file mode 100644 index 000000000..50eebdbf6 --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java @@ -0,0 +1,70 @@ +/* + * Copyright 2025 Sven Strickroth + * Copyright 2025 Christian Wagner + * + * This file is part of the GATE. + * + * GATE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * GATE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GATE. If not, see . + */ + +package de.tuclausthal.submissioninterface.servlets.view.fragments; + +import java.io.PrintWriter; +import java.io.StringReader; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; + +import de.tuclausthal.submissioninterface.util.Util; + +public class ShowHaskellRuntimeCommonErrorTitle { + public static void formatCommonErrorTitle(PrintWriter out, String commonErrorTitle) { + try { + // TODO@CHW handle random functions + JsonObject commonErrorTitleAsJson = Json.createReader(new StringReader(commonErrorTitle)).readObject(); + + if (commonErrorTitleAsJson.containsKey("testcases")) { + out.println("Fehlgeschlagene Testfälle:"); + + out.println("
    "); + for (JsonValue testCaseVal : commonErrorTitleAsJson.getJsonArray("testcases")) { + JsonObject testCase = testCaseVal.asJsonObject(); + String testcase = testCase.getString("testcase", ""); + String got = testCase.getString("got", ""); + + out.println(String.format(""" +
  • + %1$s +
    + %2$s +
  • + """, Util.escapeHTML(testcase), Util.escapeHTML(got))); + } + out.println("
"); + } + + if (commonErrorTitleAsJson.containsKey("flags")) { + out.println("

Ausführungsstatus des Tests:

"); + out.println("
    "); + for (JsonValue flagValue : commonErrorTitleAsJson.getJsonArray("flags")) { + out.println("
  • " + Util.escapeHTML(flagValue.toString().replace("\"", "")) + "
  • "); + } + out.println("
"); + } + } catch (Exception e) { + out.println("

Fehler beim Parsen des Fehler-Titels. Original Fehler-Titel:

"); + out.println("

Original Fehler-Titel: " + Util.escapeHTML(commonErrorTitle) + "

"); + } + } +} diff --git a/src/main/java/de/tuclausthal/submissioninterface/template/Template.java b/src/main/java/de/tuclausthal/submissioninterface/template/Template.java index 9225c9127..b27f52e63 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/template/Template.java +++ b/src/main/java/de/tuclausthal/submissioninterface/template/Template.java @@ -31,6 +31,7 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError; import de.tuclausthal.submissioninterface.persistence.datamodel.Group; +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.Lecture; import de.tuclausthal.submissioninterface.persistence.datamodel.Submission; import de.tuclausthal.submissioninterface.persistence.datamodel.Task; @@ -126,7 +127,8 @@ public void printTemplateHeader(Group group) throws IOException { } public void printTemplateHeader(CommonError commonError, Submission submission, String title) throws IOException { - printTemplateHeader("Fehler \"" + Util.escapeHTML(commonError.getTitle()) + "\"", "
  • Meine Veranstaltungen
  • Veranstaltung \"" + Util.escapeHTML(submission.getTask().getTaskGroup().getLecture().getName()) + "\"
  • Aufgabe \"" + Util.escapeHTML(submission.getTask().getTitle()) + "\"
  • Testübersicht
  • Fehler \"" + Util.escapeHTML(commonError.getTitle()) + "\"
  • "); + String templateHeaderTitle = commonError.getTest() instanceof HaskellRuntimeTest ? "Haskell Runtime Test Fehlergruppe" : "Fehler \"" + Util.escapeHTML(commonError.getTitle()) + "\""; + printTemplateHeader(templateHeaderTitle, "
  • Meine Veranstaltungen
  • Veranstaltung \"" + Util.escapeHTML(submission.getTask().getTaskGroup().getLecture().getName()) + "\"
  • Aufgabe \"" + Util.escapeHTML(submission.getTask().getTitle()) + "\"
  • Testübersicht
  • " + templateHeaderTitle + "
  • "); } final public void addHead(String header) { From f3f76c826d042ab6c2cef0f0cb9aef1ff438be30 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 19 Jul 2025 13:56:58 +0200 Subject: [PATCH 081/105] Fix ArrayIndexOutOfBoundsException caused by newer ghci version --- .../servlets/controller/HaskellRuntimeTestManager.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 619e3dba0..6ab75fd66 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -532,10 +532,8 @@ private static HaskellClassifiedIdentifiers classifyHaskellIdentifiers(List Date: Sat, 19 Jul 2025 13:58:19 +0200 Subject: [PATCH 082/105] Use debian:bookworm in Dockerfile, since debian:buster is an archived release --- safe-docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/safe-docker/Dockerfile b/safe-docker/Dockerfile index e54090710..23dc0d1a9 100644 --- a/safe-docker/Dockerfile +++ b/safe-docker/Dockerfile @@ -1,7 +1,7 @@ # inspired by https://github.com/KITPraktomatTeam/Praktomat/tree/master/docker-image # version e0d53616b7a81f15d6717f76ba53d9415913f7b5 -FROM debian:buster +FROM debian:bookworm # make sure we have a fully patched image RUN apt-get update -qq && apt-get dist-upgrade -y && apt-get clean && rm -rf /var/lib/apt/lists/* From 3cbeef4cf8c8df0a57ced6716b3180c6fdf24d04 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 19 Jul 2025 15:28:24 +0200 Subject: [PATCH 083/105] Add experimental type defaulting; this uses "default (Int, Double, ())" in ghci to prefer Int/Double over () when calling :type +d --- .../controller/HaskellRuntimeTestManager.java | 20 ++++++++++++++----- .../view/HaskellRuntimeTestManagerView.java | 15 ++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 6ab75fd66..ca81bb811 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -105,7 +105,15 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr if ("browseModelSolution".equals(request.getParameter("action"))) { deleteStoredClassifiedIdentifiers(haskellRuntimeTest, session); try { - browseModelSolutionAndStoreClassifiedIdentifiers(haskellRuntimeTest, session); + browseModelSolutionAndStoreClassifiedIdentifiers(haskellRuntimeTest, session, false); + } catch (IOException e) { + request.getSession().setAttribute("haskellRuntimeTestBrowseError", e.getMessage()); + } + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + } else if ("browseModelSolutionExperimentalDefaulting".equals(request.getParameter("action"))) { + deleteStoredClassifiedIdentifiers(haskellRuntimeTest, session); + try { + browseModelSolutionAndStoreClassifiedIdentifiers(haskellRuntimeTest, session, true); } catch (IOException e) { request.getSession().setAttribute("haskellRuntimeTestBrowseError", e.getMessage()); } @@ -244,7 +252,7 @@ private static void deleteStoredClassifiedIdentifiers(HaskellRuntimeTest haskell tx.commit(); } - private static void browseModelSolutionAndStoreClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session) throws IOException { + private static void browseModelSolutionAndStoreClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session, boolean useExperimentalDefaultingRules) throws IOException { List haskellIdentifiers = browseModelSolution(haskellRuntimeTest.getTask()); HaskellClassifiedIdentifiers haskellClassifiedIdentifiers = classifyHaskellIdentifiers(haskellIdentifiers); @@ -266,7 +274,7 @@ private static void browseModelSolutionAndStoreClassifiedIdentifiers(HaskellRunt haskellRuntimeTestIdentifier.setFunctionName(haskellFunction.getName()); haskellRuntimeTestIdentifier.setFunctionType(haskellFunction.getTypeSignature()); - String defaultTypeSignature = getGhciDefaultTypeSignature(haskellRuntimeTest.getTask(), haskellFunction.getName()); + String defaultTypeSignature = getGhciDefaultTypeSignature(haskellRuntimeTest.getTask(), haskellFunction.getName(), useExperimentalDefaultingRules); haskellRuntimeTestIdentifier.setFunctionDefaultType(defaultTypeSignature); String concreteTypeSignature = replaceUnconstrainedTypeVariables(defaultTypeSignature, HaskellPrimitiveType.Int); // TODO@CHW other default type @@ -540,8 +548,10 @@ private static HaskellClassifiedIdentifiers classifyHaskellIdentifiers(List")) { diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 8ed2da7e6..0ccc9f166 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -327,6 +327,21 @@ Default Typsignatur (:t +d)
    """, Util.generateHTMLLink("?", response), test.getId(), Util.generateHTMLLink(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + test.getId() + "&action=deleteHaskellIdentifiers", response))); + // TODO@CHW: this is an experimental feature + out.println(String.format(""" +
    +
    + + + + + +
    + """, Util.generateHTMLLink("?", response), test.getId())); + // NOTE: DockerTestStep title is used for storing the function signature (see controller servlet) final Map> testStepsGroupedByFunctionNameWithType = test.getTestSteps().stream().collect(Collectors.groupingBy(DockerTestStep::getTitle)); List sortedKeys = testStepsGroupedByFunctionNameWithType.keySet().stream().sorted().toList(); From 8e773bec769dbc9d0e54f0d6642e847d9eb9c224 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 19 Jul 2025 16:14:28 +0200 Subject: [PATCH 084/105] Pretty print cyclic int mappers in ShowHaskellRuntimeCommonErrorTitle view fragment --- .../view/fragments/ShowHaskellRuntimeCommonErrorTitle.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java index 50eebdbf6..23f64097b 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java @@ -19,6 +19,8 @@ package de.tuclausthal.submissioninterface.servlets.view.fragments; +import static de.tuclausthal.submissioninterface.servlets.controller.HaskellRuntimeTestManager.prettyPrintCyclicIntMappers; + import java.io.PrintWriter; import java.io.StringReader; @@ -40,7 +42,7 @@ public static void formatCommonErrorTitle(PrintWriter out, String commonErrorTit out.println("
      "); for (JsonValue testCaseVal : commonErrorTitleAsJson.getJsonArray("testcases")) { JsonObject testCase = testCaseVal.asJsonObject(); - String testcase = testCase.getString("testcase", ""); + String testcase = prettyPrintCyclicIntMappers(testCase.getString("testcase", "")); String got = testCase.getString("got", ""); out.println(String.format(""" From 13ad1236377c8c4274bce0f4ae066c2417692a99 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 19 Jul 2025 16:32:56 +0200 Subject: [PATCH 085/105] Remove old TODO comment --- .../view/fragments/ShowHaskellRuntimeCommonErrorTitle.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java index 23f64097b..f94eebd17 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java @@ -33,7 +33,6 @@ public class ShowHaskellRuntimeCommonErrorTitle { public static void formatCommonErrorTitle(PrintWriter out, String commonErrorTitle) { try { - // TODO@CHW handle random functions JsonObject commonErrorTitleAsJson = Json.createReader(new StringReader(commonErrorTitle)).readObject(); if (commonErrorTitleAsJson.containsKey("testcases")) { From 5465d338d79eb07b7c24a9b8a9ee76555214bdb2 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 19 Jul 2025 16:37:17 +0200 Subject: [PATCH 086/105] Remove line numbers from exceptions (should not be considered when clustering) --- .../testanalyzer/CommonErrorAnalyzer.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java index 7b9760764..ceedc41ef 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java @@ -181,7 +181,13 @@ private void groupHaskellRuntimeTestResults(final HaskellRuntimeTest test, final for (int i = 0; i < steps.size(); i++) { if (!steps.getJsonObject(i).getBoolean("ok")) { - String gotValue = steps.getJsonObject(i).getString("got").strip(); // TODO@CHW remove line numbers from the exceptions here + String gotValue = steps.getJsonObject(i).getString("got").strip(); + + if (gotValue.startsWith("EXCEPTION")) { + // remove line numbers from exceptions, since differences in line numbers should not be considered for clustering + gotValue = gotValue.replaceAll("\\b\\d+\\b", "-"); + } + String testCodeWrappedInCatchAndTimeout = test.getTestSteps().get(i).getTestcode(); String testcaseIdentifier = extractUnescapedGhciExpressionWrappedInCatchAndTimeout(testCodeWrappedInCatchAndTimeout); if (testcaseIdentifier == null) { From b1327e7d661acf06332ecf7c066238c2ef176cc6 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 19 Jul 2025 18:08:30 +0200 Subject: [PATCH 087/105] Cluster haskell runtime test results for each function individually --- .../ShowHaskellRuntimeCommonErrorTitle.java | 5 ++++ .../testanalyzer/CommonErrorAnalyzer.java | 30 ++++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java index f94eebd17..7cb31c9bc 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java @@ -35,6 +35,11 @@ public static void formatCommonErrorTitle(PrintWriter out, String commonErrorTit try { JsonObject commonErrorTitleAsJson = Json.createReader(new StringReader(commonErrorTitle)).readObject(); + if (commonErrorTitleAsJson.containsKey("function")) { + out.println("Funktion: " + Util.escapeHTML(commonErrorTitleAsJson.getString("function", "")) + "
      "); + out.println("
      "); + } + if (commonErrorTitleAsJson.containsKey("testcases")) { out.println("Fehlgeschlagene Testfälle:"); diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java index ceedc41ef..e3148a16b 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java @@ -23,13 +23,14 @@ import java.io.StringReader; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import jakarta.json.Json; import jakarta.json.JsonArray; import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; import org.hibernate.Session; @@ -171,13 +172,11 @@ private void groupHaskellRuntimeTestResults(final HaskellRuntimeTest test, final return; } - // TODO@CHW cluster for each function individually final JsonObject testOutputJson = Json.createReader(new StringReader(testResult.getTestOutput())).readObject(); - JsonObjectBuilder commonErrorTitleJsonObjectBuilder = Json.createObjectBuilder(); + Map> failedTestcasesByFunction = new HashMap<>(); if (testOutputJson.containsKey("steps")) { final JsonArray steps = testOutputJson.getJsonArray("steps"); - JsonArrayBuilder testcaseArrayBuilder = Json.createArrayBuilder(); for (int i = 0; i < steps.size(); i++) { if (!steps.getJsonObject(i).getBoolean("ok")) { @@ -193,10 +192,12 @@ private void groupHaskellRuntimeTestResults(final HaskellRuntimeTest test, final if (testcaseIdentifier == null) { testcaseIdentifier = testCodeWrappedInCatchAndTimeout; } - testcaseArrayBuilder.add(Json.createObjectBuilder().add("testcase", testcaseIdentifier).add("got", gotValue)); + + String functionTitle = test.getTestSteps().get(i).getTitle(); + JsonObject failedTestcase = Json.createObjectBuilder().add("testcase", testcaseIdentifier).add("got", gotValue).build(); + failedTestcasesByFunction.computeIfAbsent(functionTitle, k -> new ArrayList<>()).add(failedTestcase); } } - commonErrorTitleJsonObjectBuilder.add("testcases", testcaseArrayBuilder); } List flags = new ArrayList<>(); @@ -208,7 +209,6 @@ private void groupHaskellRuntimeTestResults(final HaskellRuntimeTest test, final } if (testOutputJson.containsKey("exitedCleanly") && testOutputJson.getBoolean("exitedCleanly")) { flags.add("exited cleanly"); - } if (testOutputJson.containsKey("missing-tests")) { flags.add("missing tests"); @@ -218,14 +218,22 @@ private void groupHaskellRuntimeTestResults(final HaskellRuntimeTest test, final } StringBuilder keyStrStringBuilder = new StringBuilder(); - JsonArrayBuilder flagsArrayBuilder = Json.createArrayBuilder(); for (String flag : flags) { keyStrStringBuilder.append(flag).append("; "); - flagsArrayBuilder.add(flag); } - commonErrorTitleJsonObjectBuilder.add("flags", flagsArrayBuilder); - bindCommonError(testResult, commonErrorTitleJsonObjectBuilder.build().toString(), keyStrStringBuilder.toString(), CommonError.Type.RunTimeError); + for (Map.Entry> entry : failedTestcasesByFunction.entrySet()) { + String functionTitle = entry.getKey(); + List testcases = entry.getValue(); + + JsonArrayBuilder testcaseArrayBuilder = Json.createArrayBuilder(); + for (JsonObject testcase : testcases) { + testcaseArrayBuilder.add(testcase); + } + + JsonObject commonErrorTitleJsonObject = Json.createObjectBuilder().add("function", functionTitle).add("testcases", testcaseArrayBuilder).build(); + bindCommonError(testResult, commonErrorTitleJsonObject.toString(), keyStrStringBuilder.toString(), CommonError.Type.RunTimeError); + } } private void groupJUnitTestResults(JUnitTest test, final TestResult testResult) { From 24704bea5341a24654a21eb7cc6aa1b0ac6486c8 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Tue, 22 Jul 2025 19:37:11 +0200 Subject: [PATCH 088/105] implement Version 2 of regex clustering --- .../syntax/RegexBasedHaskellClustering.java | 94 ++++++++++++------- 1 file changed, 62 insertions(+), 32 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java index ac6d42d4e..f9b9d7dcd 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java @@ -26,48 +26,78 @@ public class RegexBasedHaskellClustering { private static final LinkedHashMap CLUSTERS = new LinkedHashMap<>(); static { + // Parse-Errors + CLUSTERS.put("GHCi Kontext in Abgabe", Pattern.compile("(^|\\n).*?(ghci>|Prelude>|parse error on input\\s+‘(:\\{|}:)’|:\\{|}:)", Pattern.MULTILINE)); + CLUSTERS.put("Ungültige Top-Level-Deklaration", Pattern.compile("Parse error: module header, import declaration\\s+or\\s+top-level declaration expected\\.", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Parse-Fehler durch Import-Fehler", Pattern.compile("(parse error on input)[\\s\\S]+?‘?import", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Parse-Fehler in 'let'-Binding", Pattern.compile("(?:\\(let.*in.*\\)-syntax\\s+in\\s+pattern|\\bparse\\s+error\\s*\\(possibly\\s+incorrect\\s+indentation[^\\)]*\\))[\\s\\S]*?\\n\\s*\\d+\\s*\\|\\s+.*\\blet\\b[^\\n]*=", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Parse-Fehler in Funktionsdeklaration", Pattern.compile("parse error.*?\\n\\s*\\|\\s*(\\d+)\\s*\\|\\s([a-z]\\w*)\\s*::", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Parse-Fehler", Pattern.compile("\\bparse\\s+error\\b", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Typed Hole", Pattern.compile("found hole: _ ::", Pattern.CASE_INSENSITIVE)); + + // Type Errors + CLUSTERS.put("Falsche Funktionsarität", Pattern.compile("\\bapplied to too (few|many) arguments\\b|\\bhas \\w+ arguments, but its type .*? has only \\w+", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Inkonsistenter Rückgabetyp", Pattern.compile("Couldn't match type[:\\s]*.*with[:\\s]*.*In a case alternative", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Implementierung verletzt Typsignatur", Pattern.compile("is a rigid type variable bound by", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Numerischer Typenkonflikt", Pattern.compile("No instance for .*Num .*|No instance for .*Fractional .*|Couldn't match expected type\\s+‘?(Double|Float|Rational|Int|Integer|Num\\s+[a-zA-Z0-9_]*)’?\\s+with actual type\\s+‘?(Double|Float|Rational|Int|Integer|Num\\s+[a-zA-Z0-9_]*)’?", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Doppelte Signatur", Pattern.compile("duplicate type signatures?", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Typenkonflikt", Pattern.compile("couldn'?t match (expected type|type)", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Variable nicht im Gültigkeitsbereich", Pattern.compile("not in scope", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Leerer do-Block", Pattern.compile("empty\\s+'do'\\s+block", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Unendlicher Typ", Pattern.compile("occurs check:.*infinite type", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Modul nicht gefunden", Pattern.compile("could not find module", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Mehrfache Deklarationen", Pattern.compile("multiple declarations", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Mehrdeutiger Bezeichner", Pattern.compile("ambiguous occurrence", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültiger Typ-Operator", Pattern.compile("illegal operator .* in type .*", Pattern.CASE_INSENSITIVE)); + + // Not in scope + CLUSTERS.put("Typenkonstruktor oder Klasse nicht definiert", Pattern.compile("Not in scope: type constructor or class ‘[A-Z][a-zA-Z0-9_']*’", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Nicht definierter Datenkonstruktor", Pattern.compile("Not in scope: data constructor ‘[A-Z][a-zA-Z0-9_']*’", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Funktion nicht definiert", Pattern.compile("Variable not in scope: ([a-zA-Z_][a-zA-Z0-9_']*)\\s*::\\s*[^:\\n]+->.*", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Variable nicht im Gültigkeitsbereich", Pattern.compile("not in scope", Pattern.CASE_INSENSITIVE)); + + // Binding and Signature + CLUSTERS.put("Pattern Binding in Instanz", Pattern.compile("pattern bindings.*not allowed in instance declaration", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Fehlendes Binding", Pattern.compile("type signature.*lacks an accompanying binding", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Fehlende Instanz", Pattern.compile("no instance for", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Falsche Arität für Konstruktor", Pattern.compile("the constructor .* should have \\d+ argument[s]?, but has been given \\d+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Abweichende Arity", Pattern.compile("equations for .* have different numbers of arguments", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Doppelte Signatur", Pattern.compile("duplicate type signatures?", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Mehrdeutiger Bezeichner", Pattern.compile("ambiguous occurrence", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Syntaxfehler", Pattern.compile("syntax error", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültige Typensignatur", Pattern.compile("invalid type signature", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Mehrfache Deklarationen", Pattern.compile("multiple declarations", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Fehlerhafter Datenkonstruktor", Pattern.compile("cannot parse data constructor in a data/newtype declaration", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Fehlerhafter Typ-Header", Pattern.compile("malformed head of type or class declaration", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Lexikalischer Fehler", Pattern.compile("lexical error at character", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Kind-Konflikt", Pattern.compile("expected kind .* but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Mehrdeutiger Typ", Pattern.compile("ambiguous type variable", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Doppelte Instanz", Pattern.compile("duplicate instance declarations", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Fehlende Constraint", Pattern.compile("could not deduce.*\\(", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Konfliktierende Bindings", Pattern.compile("conflicting definitions for", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Methode nicht in Klasse", Pattern.compile("is not a \\(visible\\) method of class", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Constraint erwartet, aber Typ erhalten", Pattern.compile("expected a constraint, but .*(has kind|is a type)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Ungültige Instanz-Signatur", Pattern.compile("illegal type signature in instance declaration", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Unvollständiger Typ", Pattern.compile("expecting one more argument to .*has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Constraint erwartet, aber Typ erhalten", Pattern.compile("expected a constraint, but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Pattern Binding in Instanz", Pattern.compile("pattern bindings.*not allowed in instance declaration", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Falsche Konstruktor-Arity", Pattern.compile("the constructor ‘.*’ should have \\d+ argument[s]?, but has been given \\d+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Typed Hole", Pattern.compile("found hole: _ ::", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültige Binding-Syntax", Pattern.compile("illegal binding of built-in syntax", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Letzte Anweisung im 'do'-Block", Pattern.compile("the last statement in a 'do' block must be an expression", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültige Typensignatur", Pattern.compile("(invalid|illegal) type signature", Pattern.CASE_INSENSITIVE)); + + // instance and class CLUSTERS.put("Überlappende Instanzen", Pattern.compile("overlapping instances for", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültiges Enum-Deriving", Pattern.compile("can't make a derived instance of ['‘`]Enum", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültige Instanz-Form", Pattern.compile("illegal instance declaration.*flexibleinstances", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Kein Datenkonstruktor", Pattern.compile("not a data constructor", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültiges Deriving", Pattern.compile("illegal deriving item", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlende Constraint bei Funktionssignatur", Pattern.compile("No instance for [\\(‘]\\w+ [a-z][\\)’] arising from a use of", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Fehlende Superklassen-Instanz", Pattern.compile("no\\s+instance\\s+for.*arising\\s+from\\s+the\\s+superclasses", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Fehlende Instanz bei 'deriving'", Pattern.compile("When deriving the instance for", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlende Instanz", Pattern.compile("no instance for", Pattern.CASE_INSENSITIVE)); + + // definition and declaration + CLUSTERS.put("Konflikt in 'data'-Deklaration", Pattern.compile("Conflicting definitions for\\s+['‘`]?.+?['’`]?\\s+.*?\\n\\s*\\|\\s*\\n\\s*\\d+\\s*\\|\\s*data", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Mehrfachdefinition in Funktionsgleichung", Pattern.compile("conflicting definitions for.*in an equation for", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Konfliktierende Bindings", Pattern.compile("conflicting definitions for", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Modul nicht gefunden", Pattern.compile("could not find module", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehler mit Datenkonstruktoren", Pattern.compile("(cannot parse data constructor in a data/newtype declaration|not a data constructor)", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Mehrfache Instanzdeklaration", Pattern.compile("duplicate instance declarations", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Methode nicht in Klasse", Pattern.compile("is not a \\(visible\\) method of class", Pattern.CASE_INSENSITIVE)); + + // others + CLUSTERS.put("Ungültige Instanz-Form", Pattern.compile("illegal instance declaration|Use FlexibleInstances", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Falsche Anzahl von Typ-Argumenten", Pattern.compile("expecting one more argument to .*has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Kind-Konflikt", Pattern.compile("expected kind .* but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Kind-Konflikt (Constraint vs. Typ)", Pattern.compile("expected (a constraint|a type), but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Mehrdeutiger Typ", Pattern.compile("ambiguous type variable", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Constraint nicht erfüllbar", Pattern.compile("could not deduce", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Flexible Kontexte benötigt", Pattern.compile("non type-variable argument in the constraint", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Erneut ungültige Typensignatur", Pattern.compile("illegal type signature", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Fehlende GADTs-Erweiterung", Pattern.compile("enable the GADTs extension", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Kind-Konflikt (Constraint vs. Typ)", Pattern.compile("expected (a constraint|a type), but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Ungültiger Typ-Operator", Pattern.compile("illegal operator .* in type .*", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültiges Zeichen", Pattern.compile("syntax error", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Lexikalischer Fehler", Pattern.compile("(lexical error at character|Unicode character .+ looks like .+ but it is not)", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Fehlerhafter Typ-Header", Pattern.compile("malformed head of type or class declaration", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Leerer do-Block", Pattern.compile("empty\\s+'do'\\s+block", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Letzte Anweisung im 'do'-Block", Pattern.compile("the last statement in a 'do' block must be an expression", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültige Binding-Syntax", Pattern.compile("illegal binding of built-in syntax", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Fehlende Klammern im Range-Ausdruck", Pattern.compile("a section must be enclosed in parentheses", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültiges Enum-Deriving", Pattern.compile("can't make a derived instance of ['‘`]Enum", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültiges Deriving", Pattern.compile("illegal deriving item", Pattern.CASE_INSENSITIVE)); + + // fallback CLUSTERS.put("Warnung", Pattern.compile("warning", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Sonstiger Fehler", Pattern.compile(".*", Pattern.DOTALL)); } From 390106b2e91433eee684e68278edf2355ae59f3f Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Tue, 22 Jul 2025 19:37:29 +0200 Subject: [PATCH 089/105] implement Unit Test for HaskellSyntaxTest --- .../tests/impl/HaskellSyntaxTestTest.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java diff --git a/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java b/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java new file mode 100644 index 000000000..824f79c36 --- /dev/null +++ b/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2021-2024 Sven Strickroth + * + * This file is part of the GATE. + * + * GATE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * GATE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GATE. If not, see . + */ + +package de.tuclausthal.submissioninterface.testframework.tests.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.tuclausthal.submissioninterface.testanalyzer.haskell.syntax.RegexBasedHaskellClustering; +import de.tuclausthal.submissioninterface.testframework.executor.TestExecutorTestResult; + +public class HaskellSyntaxTestTest { + + private HaskellSyntaxTest haskellSyntaxTest; + + @BeforeEach + void setUp() { + var haskellSyntaxTestEntity = new de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest(); + haskellSyntaxTest = new HaskellSyntaxTest(haskellSyntaxTestEntity); + } + + @Test + void testHaskellSyntaxTestOK() { + var result = new TestExecutorTestResult(); + var stdout = new StringBuffer("Test"); + var stderr = new StringBuffer(""); + haskellSyntaxTest.analyzeAndSetResult(true, stdout, stderr, 0, false, result); + assertTrue(result.isTestPassed(), "Test should pass when stderr is empty."); + } + + @Test + void testHaskellSyntaxTestSyntaxError() { + var result = new TestExecutorTestResult(); + var stdout = new StringBuffer(""); + var stderr = new StringBuffer("[1 of 1] Compiling Main ( Main.hs, interpreted )\n\nMain.hs:3:1: error:\n parse error on input `='"); + haskellSyntaxTest.analyzeAndSetResult(true, stdout, stderr, 1, false, result); + assertFalse(result.isTestPassed(), "Test should fail when stderr contains 'error:'."); + } + + @Test + void testHaskellSyntaxTestAnotherError() { + var result = new TestExecutorTestResult(); + var stdout = new StringBuffer(""); + var stderr = new StringBuffer("" + "Test:1:1-12: error:\n" + " Parse error in pattern: test In a function binding for the ‘-’ operator.\n" + " |" + " |\n" + "8 | test 0 = 1\n" + " | ^^^^^^^^^"); + haskellSyntaxTest.analyzeAndSetResult(true, stdout, stderr, 1, false, result); + assertFalse(result.isTestPassed(), "Test should fail for any stderr containing 'error:'."); + } + + @Test + void testRegexBasedHaskellClustering() { + String stderr = "Test:1:1-12: error:\n" + " Parse error in pattern: test In a function binding for the ‘-’ operator.\n" + " |" + " |\n" + "8 | test 0 = 1\n" + " | ^^^^^^^^^"; + String cluster = RegexBasedHaskellClustering.classify(stderr); + assertEquals("Parse-Fehler", cluster, "The stderr should be classified as 'Parse-Fehler'."); + } + + @Test + void testRegexBasedHaskellClusteringComplex() { + String stderr = "Test:6:1-8: warning: [-Wtabs]\n" + " Tab character found here, and in two further locations.\n" + " Suggested fix: Please use spaces instead.\n" + " |\n" + "6 | where test f [] acc =acc\n" + " | ^^^^^^^^\n" + "\n" + "Test:14:26-33: error:\n" + " • Expected kind ‘* -> * -> Constraint’, but ‘Test’ has kind ‘*’\n" + " • In the instance declaration for ‘Test a b’\n" + " |\n" + "14 | instance (Eq a, Eq b) => Test a b where\n" + " | ^^^^^^^^" + "Test:7:51: error: parse error on input ‘=’\n" + " |\n" + "7 | Test f (x:xs) (first, second) = test f xs (first ++ [fst (f x)], second ++ [snd (f x)])\n" + " | ^"; + String cluster = RegexBasedHaskellClustering.classify(stderr); + assertEquals("Parse-Fehler", cluster, "The stderr should be classified as 'Parse-Fehler'."); + } +} + From 9f6d7f0c84102a88d36b798854b4ff04065f6a69 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Tue, 22 Jul 2025 20:19:46 +0200 Subject: [PATCH 090/105] resolve warnings --- .../haskell/syntax/RegexBasedHaskellClustering.java | 6 +++--- .../tests/impl/HaskellSyntaxTestTest.java | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java index f9b9d7dcd..793ad1578 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java @@ -30,7 +30,7 @@ public class RegexBasedHaskellClustering { CLUSTERS.put("GHCi Kontext in Abgabe", Pattern.compile("(^|\\n).*?(ghci>|Prelude>|parse error on input\\s+‘(:\\{|}:)’|:\\{|}:)", Pattern.MULTILINE)); CLUSTERS.put("Ungültige Top-Level-Deklaration", Pattern.compile("Parse error: module header, import declaration\\s+or\\s+top-level declaration expected\\.", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Parse-Fehler durch Import-Fehler", Pattern.compile("(parse error on input)[\\s\\S]+?‘?import", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Parse-Fehler in 'let'-Binding", Pattern.compile("(?:\\(let.*in.*\\)-syntax\\s+in\\s+pattern|\\bparse\\s+error\\s*\\(possibly\\s+incorrect\\s+indentation[^\\)]*\\))[\\s\\S]*?\\n\\s*\\d+\\s*\\|\\s+.*\\blet\\b[^\\n]*=", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Parse-Fehler in 'let'-Binding", Pattern.compile("(?:\\(let.*in.*\\)-syntax\\s+in\\s+pattern|\\bparse\\s+error\\s*\\(possibly\\s+incorrect\\s+indentation[^)]*\\))[\\s\\S]*?\\n\\s*\\d+\\s*\\|\\s+.*\\blet\\b[^\\n]*=", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Parse-Fehler in Funktionsdeklaration", Pattern.compile("parse error.*?\\n\\s*\\|\\s*(\\d+)\\s*\\|\\s([a-z]\\w*)\\s*::", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Parse-Fehler", Pattern.compile("\\bparse\\s+error\\b", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Typed Hole", Pattern.compile("found hole: _ ::", Pattern.CASE_INSENSITIVE)); @@ -64,7 +64,7 @@ public class RegexBasedHaskellClustering { // instance and class CLUSTERS.put("Überlappende Instanzen", Pattern.compile("overlapping instances for", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Fehlende Constraint bei Funktionssignatur", Pattern.compile("No instance for [\\(‘]\\w+ [a-z][\\)’] arising from a use of", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Fehlende Constraint bei Funktionssignatur", Pattern.compile("No instance for [(‘]\\w+ [a-z][)’] arising from a use of", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Fehlende Superklassen-Instanz", Pattern.compile("no\\s+instance\\s+for.*arising\\s+from\\s+the\\s+superclasses", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Fehlende Instanz bei 'deriving'", Pattern.compile("When deriving the instance for", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Fehlende Instanz", Pattern.compile("no instance for", Pattern.CASE_INSENSITIVE)); @@ -102,7 +102,7 @@ public class RegexBasedHaskellClustering { CLUSTERS.put("Sonstiger Fehler", Pattern.compile(".*", Pattern.DOTALL)); } - public final static String classify(String stderr) { + public static String classify(String stderr) { for (Map.Entry entry : CLUSTERS.entrySet()) { if (entry.getValue().matcher(stderr).find()) { return entry.getKey(); diff --git a/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java b/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java index 824f79c36..3a375c0a7 100644 --- a/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java +++ b/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java @@ -42,7 +42,7 @@ void setUp() { void testHaskellSyntaxTestOK() { var result = new TestExecutorTestResult(); var stdout = new StringBuffer("Test"); - var stderr = new StringBuffer(""); + var stderr = new StringBuffer("testtext"); haskellSyntaxTest.analyzeAndSetResult(true, stdout, stderr, 0, false, result); assertTrue(result.isTestPassed(), "Test should pass when stderr is empty."); } @@ -50,7 +50,7 @@ void testHaskellSyntaxTestOK() { @Test void testHaskellSyntaxTestSyntaxError() { var result = new TestExecutorTestResult(); - var stdout = new StringBuffer(""); + var stdout = new StringBuffer("test"); var stderr = new StringBuffer("[1 of 1] Compiling Main ( Main.hs, interpreted )\n\nMain.hs:3:1: error:\n parse error on input `='"); haskellSyntaxTest.analyzeAndSetResult(true, stdout, stderr, 1, false, result); assertFalse(result.isTestPassed(), "Test should fail when stderr contains 'error:'."); @@ -59,22 +59,22 @@ void testHaskellSyntaxTestSyntaxError() { @Test void testHaskellSyntaxTestAnotherError() { var result = new TestExecutorTestResult(); - var stdout = new StringBuffer(""); - var stderr = new StringBuffer("" + "Test:1:1-12: error:\n" + " Parse error in pattern: test In a function binding for the ‘-’ operator.\n" + " |" + " |\n" + "8 | test 0 = 1\n" + " | ^^^^^^^^^"); + var stdout = new StringBuffer("test"); + var stderr = new StringBuffer("Test:1:1-12: error:\n Parse error in pattern: test In a function binding for the ‘-’ operator.\n | |\n" + "8 | test 0 = 1\n | ^^^^^^^^^"); haskellSyntaxTest.analyzeAndSetResult(true, stdout, stderr, 1, false, result); assertFalse(result.isTestPassed(), "Test should fail for any stderr containing 'error:'."); } @Test void testRegexBasedHaskellClustering() { - String stderr = "Test:1:1-12: error:\n" + " Parse error in pattern: test In a function binding for the ‘-’ operator.\n" + " |" + " |\n" + "8 | test 0 = 1\n" + " | ^^^^^^^^^"; + String stderr = "Test:1:1-12: error:\n Parse error in pattern: test In a function binding for the ‘-’ operator.\n |\n | 8 | test 0 = 1\n | ^^^^^^^^^"; String cluster = RegexBasedHaskellClustering.classify(stderr); assertEquals("Parse-Fehler", cluster, "The stderr should be classified as 'Parse-Fehler'."); } @Test void testRegexBasedHaskellClusteringComplex() { - String stderr = "Test:6:1-8: warning: [-Wtabs]\n" + " Tab character found here, and in two further locations.\n" + " Suggested fix: Please use spaces instead.\n" + " |\n" + "6 | where test f [] acc =acc\n" + " | ^^^^^^^^\n" + "\n" + "Test:14:26-33: error:\n" + " • Expected kind ‘* -> * -> Constraint’, but ‘Test’ has kind ‘*’\n" + " • In the instance declaration for ‘Test a b’\n" + " |\n" + "14 | instance (Eq a, Eq b) => Test a b where\n" + " | ^^^^^^^^" + "Test:7:51: error: parse error on input ‘=’\n" + " |\n" + "7 | Test f (x:xs) (first, second) = test f xs (first ++ [fst (f x)], second ++ [snd (f x)])\n" + " | ^"; + String stderr = "Test:6:1-8: warning: [-Wtabs]\n Tab character found here, and in two further locations.\n Suggested fix: Please use spaces instead.\n |\n" + "6 | where test f [] acc =acc\n | ^^^^^^^^\n" + "\n" + "Test:14:26-33: error:\n • Expected kind ‘* -> * -> Constraint’, but ‘Test’ has kind ‘*’\n • In the instance declaration for ‘Test a b’\n |\n" + "14 | instance (Eq a, Eq b) => Test a b where\n | ^^^^^^^^" + "Test:7:51: error: parse error on input ‘=’\n |\n" + "7 | Test f (x:xs) (first, second) = test f xs (first ++ [fst (f x)], second ++ [snd (f x)])\n | ^"; String cluster = RegexBasedHaskellClustering.classify(stderr); assertEquals("Parse-Fehler", cluster, "The stderr should be classified as 'Parse-Fehler'."); } From 6b84098fdde74de85071297635a1eb5b2be9c4ef Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Tue, 22 Jul 2025 20:21:23 +0200 Subject: [PATCH 091/105] resolve warnings --- .../testframework/tests/impl/HaskellSyntaxTestTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java b/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java index 3a375c0a7..c7c96c575 100644 --- a/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java +++ b/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java @@ -60,21 +60,21 @@ void testHaskellSyntaxTestSyntaxError() { void testHaskellSyntaxTestAnotherError() { var result = new TestExecutorTestResult(); var stdout = new StringBuffer("test"); - var stderr = new StringBuffer("Test:1:1-12: error:\n Parse error in pattern: test In a function binding for the ‘-’ operator.\n | |\n" + "8 | test 0 = 1\n | ^^^^^^^^^"); + var stderr = new StringBuffer("Test:1:1-12: error:\n Parse error in pattern: test In a function binding for the ‘-’ operator.\n | |\n 8 | test 0 = 1\n | ^^^^^^^^^"); haskellSyntaxTest.analyzeAndSetResult(true, stdout, stderr, 1, false, result); assertFalse(result.isTestPassed(), "Test should fail for any stderr containing 'error:'."); } @Test void testRegexBasedHaskellClustering() { - String stderr = "Test:1:1-12: error:\n Parse error in pattern: test In a function binding for the ‘-’ operator.\n |\n | 8 | test 0 = 1\n | ^^^^^^^^^"; + String stderr = "Test:1:1-12: error:\n Parse error in pattern: test In a function binding for the ‘-’ operator.\n |\n 8 | test 0 = 1\n | ^^^^^^^^^"; String cluster = RegexBasedHaskellClustering.classify(stderr); assertEquals("Parse-Fehler", cluster, "The stderr should be classified as 'Parse-Fehler'."); } @Test void testRegexBasedHaskellClusteringComplex() { - String stderr = "Test:6:1-8: warning: [-Wtabs]\n Tab character found here, and in two further locations.\n Suggested fix: Please use spaces instead.\n |\n" + "6 | where test f [] acc =acc\n | ^^^^^^^^\n" + "\n" + "Test:14:26-33: error:\n • Expected kind ‘* -> * -> Constraint’, but ‘Test’ has kind ‘*’\n • In the instance declaration for ‘Test a b’\n |\n" + "14 | instance (Eq a, Eq b) => Test a b where\n | ^^^^^^^^" + "Test:7:51: error: parse error on input ‘=’\n |\n" + "7 | Test f (x:xs) (first, second) = test f xs (first ++ [fst (f x)], second ++ [snd (f x)])\n | ^"; + String stderr = "Test:6:1-8: warning: [-Wtabs]\n Tab character found here, and in two further locations.\n Suggested fix: Please use spaces instead.\n |\n6 | where test f [] acc =acc\n | ^^^^^^^^\n" + "\n" + "Test:14:26-33: error:\n • Expected kind ‘* -> * -> Constraint’, but ‘Test’ has kind ‘*’\n • In the instance declaration for ‘Test a b’\n |\n" + "14 | instance (Eq a, Eq b) => Test a b where\n | ^^^^^^^^" + "Test:7:51: error: parse error on input ‘=’\n |\n" + "7 | Test f (x:xs) (first, second) = test f xs (first ++ [fst (f x)], second ++ [snd (f x)])\n | ^"; String cluster = RegexBasedHaskellClustering.classify(stderr); assertEquals("Parse-Fehler", cluster, "The stderr should be classified as 'Parse-Fehler'."); } From e21228c1a43a6d835fd95d44ebe2f9ceb4b2b83a Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Tue, 22 Jul 2025 20:22:27 +0200 Subject: [PATCH 092/105] resolve warnings --- .../testframework/tests/impl/HaskellSyntaxTestTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java b/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java index c7c96c575..932392e57 100644 --- a/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java +++ b/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java @@ -74,7 +74,7 @@ void testRegexBasedHaskellClustering() { @Test void testRegexBasedHaskellClusteringComplex() { - String stderr = "Test:6:1-8: warning: [-Wtabs]\n Tab character found here, and in two further locations.\n Suggested fix: Please use spaces instead.\n |\n6 | where test f [] acc =acc\n | ^^^^^^^^\n" + "\n" + "Test:14:26-33: error:\n • Expected kind ‘* -> * -> Constraint’, but ‘Test’ has kind ‘*’\n • In the instance declaration for ‘Test a b’\n |\n" + "14 | instance (Eq a, Eq b) => Test a b where\n | ^^^^^^^^" + "Test:7:51: error: parse error on input ‘=’\n |\n" + "7 | Test f (x:xs) (first, second) = test f xs (first ++ [fst (f x)], second ++ [snd (f x)])\n | ^"; + String stderr = "Test:6:1-8: warning: [-Wtabs]\n Tab character found here, and in two further locations.\n Suggested fix: Please use spaces instead.\n |\n6 | where test f [] acc =acc\n | ^^^^^^^^\n\nTest:14:26-33: error:\n • Expected kind ‘* -> * -> Constraint’, but ‘Test’ has kind ‘*’\n • In the instance declaration for ‘Test a b’\n |\n14 | instance (Eq a, Eq b) => Test a b where\n | ^^^^^^^^Test:7:51: error: parse error on input ‘=’\n |\n7 | Test f (x:xs) (first, second) = test f xs (first ++ [fst (f x)], second ++ [snd (f x)])\n | ^"; String cluster = RegexBasedHaskellClustering.classify(stderr); assertEquals("Parse-Fehler", cluster, "The stderr should be classified as 'Parse-Fehler'."); } From 151830242f7fd8e8b430c16cc8a3e6bf42a60140 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Sun, 27 Jul 2025 21:59:13 +0200 Subject: [PATCH 093/105] adjust copyright --- .../testframework/tests/impl/HaskellSyntaxTestTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java b/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java index 932392e57..88fe1f887 100644 --- a/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java +++ b/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2021-2024 Sven Strickroth + * Copyright 2025 Sven Strickroth + * Copyright 2025 Esat Avci * * This file is part of the GATE. * From a21b614ea7854b870ea59148ff42b7850be2c6e5 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Sun, 27 Jul 2025 22:09:44 +0200 Subject: [PATCH 094/105] fix minor bugs in regex pattern --- .../haskell/syntax/RegexBasedHaskellClustering.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java index 793ad1578..57f046ec1 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java @@ -30,16 +30,16 @@ public class RegexBasedHaskellClustering { CLUSTERS.put("GHCi Kontext in Abgabe", Pattern.compile("(^|\\n).*?(ghci>|Prelude>|parse error on input\\s+‘(:\\{|}:)’|:\\{|}:)", Pattern.MULTILINE)); CLUSTERS.put("Ungültige Top-Level-Deklaration", Pattern.compile("Parse error: module header, import declaration\\s+or\\s+top-level declaration expected\\.", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Parse-Fehler durch Import-Fehler", Pattern.compile("(parse error on input)[\\s\\S]+?‘?import", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Parse-Fehler in 'let'-Binding", Pattern.compile("(?:\\(let.*in.*\\)-syntax\\s+in\\s+pattern|\\bparse\\s+error\\s*\\(possibly\\s+incorrect\\s+indentation[^)]*\\))[\\s\\S]*?\\n\\s*\\d+\\s*\\|\\s+.*\\blet\\b[^\\n]*=", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Parse-Fehler in 'let'-Binding", Pattern.compile("(?:\\(let.*in.*\\)-syntax\\s+in\\s+pattern|parse\\s+error\\s*\\(possibly\\s+incorrect\\s+indentation[^)]*\\)[\\s\\S]*?\\n\\s*\\d+\\s*\\|\\s+.*\\blet\\b[^\\n]*=)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Parse-Fehler in Funktionsdeklaration", Pattern.compile("parse error.*?\\n\\s*\\|\\s*(\\d+)\\s*\\|\\s([a-z]\\w*)\\s*::", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Parse-Fehler", Pattern.compile("\\bparse\\s+error\\b", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Typed Hole", Pattern.compile("found hole: _ ::", Pattern.CASE_INSENSITIVE)); // Type Errors - CLUSTERS.put("Falsche Funktionsarität", Pattern.compile("\\bapplied to too (few|many) arguments\\b|\\bhas \\w+ arguments, but its type .*? has only \\w+", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Falsche Funktionsarität", Pattern.compile("applied to too (?:few|many) value arguments|applied to \\w+ value arguments,.*?\\bbut its type.*?has only \\w+|\\bhas \\w+ arguments, but its type .*? has only \\w+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Inkonsistenter Rückgabetyp", Pattern.compile("Couldn't match type[:\\s]*.*with[:\\s]*.*In a case alternative", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Implementierung verletzt Typsignatur", Pattern.compile("is a rigid type variable bound by", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Numerischer Typenkonflikt", Pattern.compile("No instance for .*Num .*|No instance for .*Fractional .*|Couldn't match expected type\\s+‘?(Double|Float|Rational|Int|Integer|Num\\s+[a-zA-Z0-9_]*)’?\\s+with actual type\\s+‘?(Double|Float|Rational|Int|Integer|Num\\s+[a-zA-Z0-9_]*)’?", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Numerischer Typenkonflikt", Pattern.compile("No instance for .*Num .*|No instance for .*Fractional .*|Couldn't match expected type\\s+.(Double|Float|Rational|Int|Integer|Num\\s+a\\d*).\\s+with actual type\\s+.(Double|Float|Rational|Int|Integer|Num\\s+a\\d*).", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Doppelte Signatur", Pattern.compile("duplicate type signatures?", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Typenkonflikt", Pattern.compile("couldn'?t match (expected type|type)", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Unendlicher Typ", Pattern.compile("occurs check:.*infinite type", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); From 153de006aaea11d5a89f2bb0b6b85f210f70aacd Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Sun, 27 Jul 2025 23:20:11 +0200 Subject: [PATCH 095/105] fix a sematic error in the HaskellSyntaxTestTest.java --- .../testframework/tests/impl/HaskellSyntaxTestTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java b/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java index 88fe1f887..185670681 100644 --- a/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java +++ b/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java @@ -43,9 +43,9 @@ void setUp() { void testHaskellSyntaxTestOK() { var result = new TestExecutorTestResult(); var stdout = new StringBuffer("Test"); - var stderr = new StringBuffer("testtext"); + var stderr = new StringBuffer("warning: something something"); haskellSyntaxTest.analyzeAndSetResult(true, stdout, stderr, 0, false, result); - assertTrue(result.isTestPassed(), "Test should pass when stderr is empty."); + assertTrue(result.isTestPassed(), "Test should pass when stderr doesn't contain 'error:'."); } @Test From f708c7c3490cdedbb7a84c1fb4dd2fe9037d3ab4 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Mon, 28 Jul 2025 19:32:43 +0200 Subject: [PATCH 096/105] fix some clustering regex for downwards and upwards compatibility --- .../syntax/RegexBasedHaskellClustering.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java index 57f046ec1..58a3a5a65 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java @@ -22,9 +22,11 @@ import java.util.Map; import java.util.regex.Pattern; -public class RegexBasedHaskellClustering { +public final class RegexBasedHaskellClustering { private static final LinkedHashMap CLUSTERS = new LinkedHashMap<>(); + private RegexBasedHaskellClustering() {} + static { // Parse-Errors CLUSTERS.put("GHCi Kontext in Abgabe", Pattern.compile("(^|\\n).*?(ghci>|Prelude>|parse error on input\\s+‘(:\\{|}:)’|:\\{|}:)", Pattern.MULTILINE)); @@ -36,7 +38,7 @@ public class RegexBasedHaskellClustering { CLUSTERS.put("Typed Hole", Pattern.compile("found hole: _ ::", Pattern.CASE_INSENSITIVE)); // Type Errors - CLUSTERS.put("Falsche Funktionsarität", Pattern.compile("applied to too (?:few|many) value arguments|applied to \\w+ value arguments,.*?\\bbut its type.*?has only \\w+|\\bhas \\w+ arguments, but its type .*? has only \\w+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Falsche Funktionsarität", Pattern.compile("applied to too (?:few|many) value arguments|applied to \\w+ value arguments,.*?\\bbut its type.*?has only \\w+|\\bhas \\w+ arguments, but its type .*? has only \\w+|is applied to .*? (?:visible )?arguments,.*?but its type .*? has only", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Inkonsistenter Rückgabetyp", Pattern.compile("Couldn't match type[:\\s]*.*with[:\\s]*.*In a case alternative", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Implementierung verletzt Typsignatur", Pattern.compile("is a rigid type variable bound by", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Numerischer Typenkonflikt", Pattern.compile("No instance for .*Num .*|No instance for .*Fractional .*|Couldn't match expected type\\s+.(Double|Float|Rational|Int|Integer|Num\\s+a\\d*).\\s+with actual type\\s+.(Double|Float|Rational|Int|Integer|Num\\s+a\\d*).", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); @@ -56,11 +58,11 @@ public class RegexBasedHaskellClustering { // Binding and Signature CLUSTERS.put("Pattern Binding in Instanz", Pattern.compile("pattern bindings.*not allowed in instance declaration", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Fehlendes Binding", Pattern.compile("type signature.*lacks an accompanying binding", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Falsche Arität für Konstruktor", Pattern.compile("the constructor .* should have \\d+ argument[s]?, but has been given \\d+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Falsche Arität für Konstruktor", Pattern.compile("the (?:data )?constructor .* should have \\d+ argument[s]?, but has been given \\d+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Abweichende Arity", Pattern.compile("equations for .* have different numbers of arguments", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Constraint erwartet, aber Typ erhalten", Pattern.compile("expected a constraint, but .*(has kind|is a type)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Ungültige Instanz-Signatur", Pattern.compile("illegal type signature in instance declaration", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültige Typensignatur", Pattern.compile("(invalid|illegal) type signature", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültige Typensignatur", Pattern.compile("((invalid|illegal) type signature|Invalid data constructor .* in type signature)", Pattern.CASE_INSENSITIVE)); // instance and class CLUSTERS.put("Überlappende Instanzen", Pattern.compile("overlapping instances for", Pattern.CASE_INSENSITIVE)); @@ -82,7 +84,7 @@ public class RegexBasedHaskellClustering { CLUSTERS.put("Ungültige Instanz-Form", Pattern.compile("illegal instance declaration|Use FlexibleInstances", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Falsche Anzahl von Typ-Argumenten", Pattern.compile("expecting one more argument to .*has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Kind-Konflikt", Pattern.compile("expected kind .* but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - CLUSTERS.put("Kind-Konflikt (Constraint vs. Typ)", Pattern.compile("expected (a constraint|a type), but .* has kind", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Kind-Konflikt (Constraint vs. Typ)", Pattern.compile("expected (a constraint|a type), but .* (?:has kind|is a (?:constraint|type))", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Mehrdeutiger Typ", Pattern.compile("ambiguous type variable", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Constraint nicht erfüllbar", Pattern.compile("could not deduce", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Flexible Kontexte benötigt", Pattern.compile("non type-variable argument in the constraint", Pattern.CASE_INSENSITIVE)); @@ -92,9 +94,9 @@ public class RegexBasedHaskellClustering { CLUSTERS.put("Fehlerhafter Typ-Header", Pattern.compile("malformed head of type or class declaration", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Leerer do-Block", Pattern.compile("empty\\s+'do'\\s+block", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Letzte Anweisung im 'do'-Block", Pattern.compile("the last statement in a 'do' block must be an expression", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültige Binding-Syntax", Pattern.compile("illegal binding of built-in syntax", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültige Binding-Syntax", Pattern.compile("illegal binding of (?:built-in syntax|an existing name)", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Fehlende Klammern im Range-Ausdruck", Pattern.compile("a section must be enclosed in parentheses", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültiges Enum-Deriving", Pattern.compile("can't make a derived instance of ['‘`]Enum", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültiges Enum-Deriving", Pattern.compile("can't make a derived instance of [''`]Enum.*[''`]", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Ungültiges Deriving", Pattern.compile("illegal deriving item", Pattern.CASE_INSENSITIVE)); // fallback From e503f1d04ca193c373889e7d510f5e8ae93552d2 Mon Sep 17 00:00:00 2001 From: Esat Avci Date: Mon, 28 Jul 2025 19:44:02 +0200 Subject: [PATCH 097/105] resolve warnings and fix minor bug for compatibility --- .../haskell/syntax/RegexBasedHaskellClustering.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java index 58a3a5a65..d3e3986be 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java @@ -32,7 +32,7 @@ private RegexBasedHaskellClustering() {} CLUSTERS.put("GHCi Kontext in Abgabe", Pattern.compile("(^|\\n).*?(ghci>|Prelude>|parse error on input\\s+‘(:\\{|}:)’|:\\{|}:)", Pattern.MULTILINE)); CLUSTERS.put("Ungültige Top-Level-Deklaration", Pattern.compile("Parse error: module header, import declaration\\s+or\\s+top-level declaration expected\\.", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Parse-Fehler durch Import-Fehler", Pattern.compile("(parse error on input)[\\s\\S]+?‘?import", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Parse-Fehler in 'let'-Binding", Pattern.compile("(?:\\(let.*in.*\\)-syntax\\s+in\\s+pattern|parse\\s+error\\s*\\(possibly\\s+incorrect\\s+indentation[^)]*\\)[\\s\\S]*?\\n\\s*\\d+\\s*\\|\\s+.*\\blet\\b[^\\n]*=)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + CLUSTERS.put("Parse-Fehler in 'let'-Binding", Pattern.compile("\\(let.*in.*\\)-syntax\\s+in\\s+pattern|parse\\s+error\\s*\\(possibly\\s+incorrect\\s+indentation[^)]*\\)[\\s\\S]*?\\n\\s*\\d+\\s*\\|\\s+.*\\blet\\b[^\\n]*=", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); CLUSTERS.put("Parse-Fehler in Funktionsdeklaration", Pattern.compile("parse error.*?\\n\\s*\\|\\s*(\\d+)\\s*\\|\\s([a-z]\\w*)\\s*::", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Parse-Fehler", Pattern.compile("\\bparse\\s+error\\b", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Typed Hole", Pattern.compile("found hole: _ ::", Pattern.CASE_INSENSITIVE)); @@ -96,7 +96,7 @@ private RegexBasedHaskellClustering() {} CLUSTERS.put("Letzte Anweisung im 'do'-Block", Pattern.compile("the last statement in a 'do' block must be an expression", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Ungültige Binding-Syntax", Pattern.compile("illegal binding of (?:built-in syntax|an existing name)", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Fehlende Klammern im Range-Ausdruck", Pattern.compile("a section must be enclosed in parentheses", Pattern.CASE_INSENSITIVE)); - CLUSTERS.put("Ungültiges Enum-Deriving", Pattern.compile("can't make a derived instance of [''`]Enum.*[''`]", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Ungültiges Enum-Deriving", Pattern.compile("(?:can't|Can't) make a derived instance of [‘'`]Enum\\b", Pattern.CASE_INSENSITIVE)); CLUSTERS.put("Ungültiges Deriving", Pattern.compile("illegal deriving item", Pattern.CASE_INSENSITIVE)); // fallback From 5c8bd61dd7c429233b8af77532d669a796a136fd Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 23 Aug 2025 18:03:50 +0200 Subject: [PATCH 098/105] Implement custom type defaulting for common type classes --- .../controller/HaskellRuntimeTestManager.java | 149 +++++++++++++++--- 1 file changed, 130 insertions(+), 19 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index ca81bb811..03b059d58 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -29,6 +29,8 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -78,6 +80,34 @@ public class HaskellRuntimeTestManager extends HttpServlet { private static final long serialVersionUID = 1L; final static private Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final Map> CONSTRAINT_TO_TYPES = new HashMap<>(); + + static { + CONSTRAINT_TO_TYPES.put("Eq", Arrays.asList("Bool", "Char", "Ordering", "Int", "Float", "Double", "String")); + CONSTRAINT_TO_TYPES.put("Ord", Arrays.asList("Bool", "Char", "Ordering", "Int", "Float", "Double", "String")); + CONSTRAINT_TO_TYPES.put("Show", Arrays.asList("Bool", "Char", "Ordering", "Int", "Float", "Double", "String")); + CONSTRAINT_TO_TYPES.put("Read", Arrays.asList("Bool", "Char", "Ordering", "Int", "Float", "Double", "String")); + + CONSTRAINT_TO_TYPES.put("Enum", Arrays.asList("Bool", "Char", "Ordering", "Int", "Float", "Double")); + CONSTRAINT_TO_TYPES.put("Bounded", Arrays.asList("Int", "Char", "Bool", "Ordering")); + + CONSTRAINT_TO_TYPES.put("Num", Arrays.asList("Int", "Float", "Double")); + CONSTRAINT_TO_TYPES.put("Integral", List.of("Int")); + CONSTRAINT_TO_TYPES.put("Real", Arrays.asList("Int", "Float", "Double")); + CONSTRAINT_TO_TYPES.put("Fractional", Arrays.asList("Float", "Double")); + CONSTRAINT_TO_TYPES.put("RealFrac", Arrays.asList("Float", "Double")); + CONSTRAINT_TO_TYPES.put("Floating", Arrays.asList("Float", "Double")); + CONSTRAINT_TO_TYPES.put("RealFloat", Arrays.asList("Float", "Double")); + + CONSTRAINT_TO_TYPES.put("Semigroup", Arrays.asList("[Int]", "String", "Ordering")); + CONSTRAINT_TO_TYPES.put("Monoid", Arrays.asList("[Int]", "String", "Ordering")); + + CONSTRAINT_TO_TYPES.put("Functor", List.of("Maybe")); + CONSTRAINT_TO_TYPES.put("Applicative", List.of("Maybe")); + CONSTRAINT_TO_TYPES.put("Monad", List.of("Maybe")); + CONSTRAINT_TO_TYPES.put("Foldable", List.of("Maybe")); + } + @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { getServletContext().getNamedDispatcher(DockerTestManager.class.getSimpleName()).forward(request, response); @@ -274,10 +304,8 @@ private static void browseModelSolutionAndStoreClassifiedIdentifiers(HaskellRunt haskellRuntimeTestIdentifier.setFunctionName(haskellFunction.getName()); haskellRuntimeTestIdentifier.setFunctionType(haskellFunction.getTypeSignature()); - String defaultTypeSignature = getGhciDefaultTypeSignature(haskellRuntimeTest.getTask(), haskellFunction.getName(), useExperimentalDefaultingRules); - haskellRuntimeTestIdentifier.setFunctionDefaultType(defaultTypeSignature); - - String concreteTypeSignature = replaceUnconstrainedTypeVariables(defaultTypeSignature, HaskellPrimitiveType.Int); // TODO@CHW other default type + String typeSignatureWithConcreteTypesForKnownConstraints = deriveConcreteTypesForConstrainedTypeVariables(haskellFunction.getTypeSignature()); + String concreteTypeSignature = replaceUnconstrainedTypeVariables(typeSignatureWithConcreteTypesForKnownConstraints, HaskellPrimitiveType.Int); haskellRuntimeTestIdentifier.setFunctionConcreteType(concreteTypeSignature); session.persist(haskellRuntimeTestIdentifier); @@ -548,17 +576,107 @@ private static HaskellClassifiedIdentifiers classifyHaskellIdentifiers(List"); + String constraintsPart; + String typePart; - String defaultTypeSignature = normalizeTypeSignature(result.stdOut().split("::")[1].trim()); - if (defaultTypeSignature.contains("=>")) { - throw new IllegalArgumentException("Constraint => after :type +d not yet handled"); // TODO@CHW + if (parts.length == 1) { + return functionTypeSignature; } else { - return defaultTypeSignature; + constraintsPart = parts[0].trim(); + typePart = parts[1].trim(); } + + Map> constraintsByTypeVariable = new HashMap<>(); + for (String c : constraintsPart.split(",")) { + c = c.trim(); + c = c.replaceAll("[()]", ""); + if (c.isEmpty()) + continue; + String[] constraintParts = c.split("\\s+"); + if (constraintParts.length == 2) { + String constraint = constraintParts[0]; + String typeVariable = constraintParts[1]; + constraintsByTypeVariable.computeIfAbsent(typeVariable, k -> new HashSet<>()).add(constraint); + } + } + + Map concreteTypeReplacementByTypeVariable = new HashMap<>(); + for (Map.Entry> e : constraintsByTypeVariable.entrySet()) { + String typeVariable = e.getKey(); + Set constraints = e.getValue(); + + Set possibleConcreteTypes = null; + boolean noUnknownConstraints = true; + + for (String constraint : constraints) { + List concreteTypesForCurrentConstraint = CONSTRAINT_TO_TYPES.get(constraint); + if (concreteTypesForCurrentConstraint == null) { + noUnknownConstraints = false; + break; + } + if (possibleConcreteTypes == null) { + possibleConcreteTypes = new HashSet<>(concreteTypesForCurrentConstraint); + } else { + possibleConcreteTypes.retainAll(concreteTypesForCurrentConstraint); + } + } + + if (noUnknownConstraints && possibleConcreteTypes != null && !possibleConcreteTypes.isEmpty()) { + List prioritizedTypes = Arrays.asList("Int", "Char", "Double", "Bool", "[Int]", "Ordering"); + + String chosen = null; + for (String preferred : prioritizedTypes) { + if (possibleConcreteTypes.contains(preferred)) { + chosen = preferred; + break; + } + } + + if (chosen == null) { + chosen = possibleConcreteTypes.iterator().next(); + } + concreteTypeReplacementByTypeVariable.put(typeVariable, chosen); + } + } + + StringBuilder newTypeSignature = new StringBuilder(); + + List remainingConstraints = new ArrayList<>(); + for (String constraint : constraintsPart.split(",")) { + constraint = constraint.trim(); // e.g. "(Eq a" + constraint = constraint.replaceAll("[()]", ""); // e.g. "Eq a" + if (constraint.isEmpty()) + continue; + String[] constraintParts = constraint.split("\\s+"); + if (constraintParts.length == 2) { + String typeVariable = constraintParts[1]; // e.g. "a" + if (!concreteTypeReplacementByTypeVariable.containsKey(typeVariable)) { + remainingConstraints.add(constraint); // e.g. "Eq a" + } + } + } + + if (!remainingConstraints.isEmpty()) { + newTypeSignature.append("(").append(String.join(", ", remainingConstraints)).append(") => "); + } + + String finalTypePart = typePart; + for (Map.Entry e : concreteTypeReplacementByTypeVariable.entrySet()) { + String typeVariable = e.getKey(); + String concreteReplacementType = e.getValue(); + finalTypePart = finalTypePart.replaceAll("\\b" + Pattern.quote(typeVariable) + "\\b", concreteReplacementType); + } + + newTypeSignature.append(finalTypePart); + return normalizeTypeSignature(newTypeSignature.toString()); } private static String normalizeTypeSignature(String typeSignature) { @@ -568,13 +686,6 @@ private static String normalizeTypeSignature(String typeSignature) { } private static String replaceUnconstrainedTypeVariables(String typeSignature, HaskellPrimitiveType replacementType) { - if (typeSignature.contains("=>")) { - // TODO@CHW: Implementation not yet correct for Functor, Monad, Applicative, etc. - // e.g. (<$>) :: Functor f => (a -> b) -> f a -> f b - // (=<<) :: Monad m => (a -> m b) -> m a -> m b - throw new IllegalArgumentException("Type signature contains constraints, not implemented yet"); - } - if (typeSignature.contains("::")) { typeSignature = typeSignature.split("::", 2)[1].trim(); } From 315023e479506f3eccbf8e62bf09bbd20de0e779 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 23 Aug 2025 19:32:36 +0200 Subject: [PATCH 099/105] Bugfix: set type of CyclicIntMap in parentheses, for example "CyclicIntMap (Maybe Int) instead of "CyclicIntMap MaybeInt" - Now, functions from a -> Maybe b are supported as well --- .../servlets/controller/HaskellRuntimeTestManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 03b059d58..f6b9386f7 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -946,7 +946,7 @@ private static List withCyclicIntMapTypes(List parameterTypes, S parameterType = parameterType.strip().substring(1, parameterType.length() - 1).strip(); String returnType = getFunctionReturnType(parameterType); - cyclicIntMapTypes.add(cyclicIntMapConstructor + " " + returnType); + cyclicIntMapTypes.add(cyclicIntMapConstructor + " (" + returnType + ")"); } else { cyclicIntMapTypes.add(parameterType); } From 06624882c7eca46e20d98663799491eee04a2820 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 23 Aug 2025 19:53:58 +0200 Subject: [PATCH 100/105] Remove default type signature from identifier table; disable generator form when concrete type signature is unavailable --- .../view/HaskellRuntimeTestManagerView.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 0ccc9f166..72ccd8422 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -27,6 +27,7 @@ import java.io.Serial; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import jakarta.servlet.ServletException; @@ -224,7 +225,6 @@ function setElementWithIdVisible(elementId, visible) { Funktion Typsignatur (:t) - Default Typsignatur (:t +d) Konkrete Typsignatur Generator ausführen @@ -257,6 +257,9 @@ Default Typsignatur (:t +d) break; case "function": showFunctionTable = true; + boolean functionHasConcreteType = !Objects.equals(identifier.getFunctionConcreteType(), "") && !identifier.getFunctionConcreteType().contains("=>"); + String formHiddenState = functionHasConcreteType ? "" : "hidden"; + String missingConcreteWarningHiddenState = functionHasConcreteType ? "hidden" : ""; functionsHtml.append(String.format("""
      @@ -265,14 +268,11 @@ Default Typsignatur (:t +d)
      %3$s
      -
      - %6$s -
      %7$s
      -
      + @@ -282,9 +282,10 @@ Default Typsignatur (:t +d) Testfälle generieren
      +

      Fehler: Konkrete Typsignatur enthält Constraints

      - """, identifier.getIdentifierid(), Util.escapeHTML(identifier.getFunctionName()), Util.escapeHTML(identifier.getFunctionType()), Util.generateHTMLLink("?", response), test.getId(), Util.escapeHTML(identifier.getFunctionDefaultType()), Util.escapeHTML(identifier.getFunctionConcreteType()))); + """, identifier.getIdentifierid(), Util.escapeHTML(identifier.getFunctionName()), Util.escapeHTML(identifier.getFunctionType()), Util.generateHTMLLink("?", response), test.getId(), Util.escapeHTML(identifier.getFunctionDefaultType()), Util.escapeHTML(identifier.getFunctionConcreteType()), formHiddenState, missingConcreteWarningHiddenState)); break; } } From 0c44a588ebed064e3814f1eac83bd5b56a83b0ca Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 23 Aug 2025 20:01:56 +0200 Subject: [PATCH 101/105] Remove experimental type defaulting feature --- .../controller/HaskellRuntimeTestManager.java | 12 ++---------- .../view/HaskellRuntimeTestManagerView.java | 15 --------------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index f6b9386f7..2dd44a418 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -135,15 +135,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr if ("browseModelSolution".equals(request.getParameter("action"))) { deleteStoredClassifiedIdentifiers(haskellRuntimeTest, session); try { - browseModelSolutionAndStoreClassifiedIdentifiers(haskellRuntimeTest, session, false); - } catch (IOException e) { - request.getSession().setAttribute("haskellRuntimeTestBrowseError", e.getMessage()); - } - response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); - } else if ("browseModelSolutionExperimentalDefaulting".equals(request.getParameter("action"))) { - deleteStoredClassifiedIdentifiers(haskellRuntimeTest, session); - try { - browseModelSolutionAndStoreClassifiedIdentifiers(haskellRuntimeTest, session, true); + browseModelSolutionAndStoreClassifiedIdentifiers(haskellRuntimeTest, session); } catch (IOException e) { request.getSession().setAttribute("haskellRuntimeTestBrowseError", e.getMessage()); } @@ -282,7 +274,7 @@ private static void deleteStoredClassifiedIdentifiers(HaskellRuntimeTest haskell tx.commit(); } - private static void browseModelSolutionAndStoreClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session, boolean useExperimentalDefaultingRules) throws IOException { + private static void browseModelSolutionAndStoreClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session) throws IOException { List haskellIdentifiers = browseModelSolution(haskellRuntimeTest.getTask()); HaskellClassifiedIdentifiers haskellClassifiedIdentifiers = classifyHaskellIdentifiers(haskellIdentifiers); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index 72ccd8422..db0e08077 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -328,21 +328,6 @@ function setElementWithIdVisible(elementId, visible) {
      """, Util.generateHTMLLink("?", response), test.getId(), Util.generateHTMLLink(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + test.getId() + "&action=deleteHaskellIdentifiers", response))); - // TODO@CHW: this is an experimental feature - out.println(String.format(""" -
      -
      -
      - - - -
      -
      - """, Util.generateHTMLLink("?", response), test.getId())); - // NOTE: DockerTestStep title is used for storing the function signature (see controller servlet) final Map> testStepsGroupedByFunctionNameWithType = test.getTestSteps().stream().collect(Collectors.groupingBy(DockerTestStep::getTitle)); List sortedKeys = testStepsGroupedByFunctionNameWithType.keySet().stream().sorted().toList(); From bcc1effcab34d5c317409d076fc9eab333e85605 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 23 Aug 2025 20:35:43 +0200 Subject: [PATCH 102/105] Add form to manually add function identifiers --- .../controller/HaskellRuntimeTestManager.java | 22 +++++++++++++++++++ .../view/HaskellRuntimeTestManagerView.java | 21 ++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 2dd44a418..2db75f54c 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -140,6 +140,28 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr request.getSession().setAttribute("haskellRuntimeTestBrowseError", e.getMessage()); } response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + } else if ("addFunction".equals(request.getParameter("action"))) { + String functionName = request.getParameter("functionName"); + String functionType = request.getParameter("functionType"); + + if (functionName != null && !functionName.isEmpty() && functionType != null && !functionType.isEmpty()) { + Transaction tx = session.beginTransaction(); + functionName = functionName.strip(); + functionType = normalizeTypeSignature(functionType).strip(); + + HaskellRuntimeTestIdentifier newFunctionIdentifier = new HaskellRuntimeTestIdentifier(haskellRuntimeTest, "function"); + newFunctionIdentifier.setFunctionName(functionName); + newFunctionIdentifier.setFunctionType(functionType); + + String typeSignatureWithConcreteTypesForKnownConstraints = deriveConcreteTypesForConstrainedTypeVariables(functionType); + String concreteTypeSignature = replaceUnconstrainedTypeVariables(typeSignatureWithConcreteTypesForKnownConstraints, HaskellPrimitiveType.Int); + newFunctionIdentifier.setFunctionConcreteType(concreteTypeSignature); + + session.persist(newFunctionIdentifier); + + tx.commit(); + } + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); } else if ("deleteHaskellIdentifiers".equals(request.getParameter("action"))) { deleteStoredClassifiedIdentifiers(haskellRuntimeTest, session); response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java index db0e08077..25375ef30 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -304,6 +304,27 @@ function setElementWithIdVisible(elementId, visible) { out.println("
      "); out.println(""); out.println(functionsHtml); + + out.println(String.format(""" + + """, Util.generateHTMLLink("?", response), test.getId())); + out.println("
      +
      +
      + + + Funktion manuell hinzufügen: + + + + + +
      +
      +
      "); out.println("
      "); } From 7036aa8d32e28fc1663cb9b76f248972ca298ade Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Sat, 23 Aug 2025 22:30:31 +0200 Subject: [PATCH 103/105] Remove resolved TODOs --- .../servlets/controller/HaskellRuntimeTestManager.java | 4 +--- .../submissioninterface/servlets/controller/TestManager.java | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 2db75f54c..537c8a117 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -805,8 +805,7 @@ private static List generateQuickcheckFunctionTestcases(Task arbitrary = %1$s "cyclicIntMap" 50 <$> arbitrary """, CYCLIC_INT_MAP_TYPE_NAME); - // String safeAsciiValues = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz()[]{}+-*/.,:; _!?#$%&<=>@"; - String safeAsciiValues = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // TODO@CHW + String safeAsciiValues = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; String typenameCharOnlySafeAscii = HaskellConstrainedPrimitiveType.Char_OnlySafeAscii.toString(); String typenameStringOnlySafeAscii = HaskellConstrainedPrimitiveType.String_OnlySafeAscii.toString(); @@ -938,7 +937,6 @@ private static List withConstrainedPrimitiveTypes(List parameter Matcher matcher = pattern.matcher(parameterType); StringBuilder sb = new StringBuilder(); - // TODO@CHW: not verified this while but looks fine while (matcher.find()) { String matchedKey = matcher.group(); String replacement = replacementDict.get(matchedKey); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java index 05ab8e735..361eed2ce 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/TestManager.java @@ -294,8 +294,6 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr session.getTransaction().commit(); response.sendRedirect(Util.generateRedirectURL(TaskManager.class.getSimpleName() + "?action=editTask&lecture=" + task.getTaskGroup().getLecture().getId() + "&taskid=" + task.getTaskid(), response)); } else if ("saveNewTest".equals(request.getParameter("action")) && "haskellruntime".equals(request.getParameter("type"))) { - // TODO@CHW: make sure all parameters accessed by request.getParameter() are defined in HaskellRuntimeTestManagerView - session.beginTransaction(); TestDAOIf testDAO = DAOFactory.TestDAOIf(session); From 60eada5f03994614bd31795cf7b2eb42cffa2ddd Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Mon, 25 Aug 2025 14:01:58 +0200 Subject: [PATCH 104/105] Allow generating test cases for constants --- .../controller/HaskellRuntimeTestManager.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java index 537c8a117..89e9bb71e 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -355,6 +355,17 @@ private static List readClassifiedIdentifiersAndGenerateFunc String functionConcreteType = functionIdentifier.getFunctionConcreteType(); List functionParameterTypes = getFunctionParameterTypes(functionConcreteType); + if (functionParameterTypes.isEmpty()) { + List functionCalls = List.of(functionName); + List expectedValues = computeExpectedValues(functionCalls, haskellRuntimeTest.getTask()); + + if (expectedValues.size() == 1) { + String expectedValue = expectedValues.get(0); + generatedTestcases.add(new DockerTestStepData(functionName, functionType, functionName, expectedValue, getModelSolutionFilename(haskellRuntimeTest))); + } + return generatedTestcases; + } + List testcases = generateQuickcheckFunctionTestcases(haskellRuntimeTest.getTask(), functionParameterTypes, arbitraryInstances, numberOfTestSteps); List functionCalls = generateFunctionCalls(functionName, testcases); From 407fc6ddf789cfbd60397d2d894b4b1f8aefe0c9 Mon Sep 17 00:00:00 2001 From: Christian Wagner Date: Mon, 25 Aug 2025 16:18:52 +0200 Subject: [PATCH 105/105] Stricter Exception normalization --- .../testanalyzer/CommonErrorAnalyzer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java index e3148a16b..3d92ab0bd 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java @@ -182,9 +182,10 @@ private void groupHaskellRuntimeTestResults(final HaskellRuntimeTest test, final if (!steps.getJsonObject(i).getBoolean("ok")) { String gotValue = steps.getJsonObject(i).getString("got").strip(); - if (gotValue.startsWith("EXCEPTION")) { + if (gotValue.contains("EXCEPTION")) { // remove line numbers from exceptions, since differences in line numbers should not be considered for clustering - gotValue = gotValue.replaceAll("\\b\\d+\\b", "-"); + // gotValue = gotValue.replaceAll("\\b\\d+\\b", "-"); + gotValue = gotValue.replaceAll("[^\\p{L} ]", ""); } String testCodeWrappedInCatchAndTimeout = test.getTestSteps().get(i).getTestcode();