diff --git a/mysql.sql b/mysql.sql index 5f9f274e7..812f9820b 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` -- @@ -473,7 +497,7 @@ CREATE TABLE IF NOT EXISTS `users` ( UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; -create table commonerrors (errorid integer not null auto_increment, commonerrorname varchar(255) not null, title varchar(255) not null, type tinyint(4), testid integer not null, primary key (errorid)) engine=InnoDB; +create table commonerrors (errorid integer not null auto_increment, commonerrorname longtext not null, title longtext not null, type tinyint(4), testid integer not null, primary key (errorid)) engine=InnoDB; create table testresults_commonerror (errorid integer not null, testresultid integer not null, primary key (errorid, testresultid)) engine=InnoDB; alter table commonerrors add constraint FK84b477b3adpufhy6yca79d79r foreign key (testid) references tests (id) on delete cascade; alter table testresults_commonerror add constraint FK787leerpp7s7yak6btbhtbh48 foreign key (testresultid) references testresults (id); @@ -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/safe-docker/Dockerfile b/safe-docker/Dockerfile index b38785275..23dc0d1a9 100644 --- a/safe-docker/Dockerfile +++ b/safe-docker/Dockerfile @@ -1,13 +1,13 @@ # 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/* # 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/sql-update.sql b/sql-update.sql index d16003539..42ccaeb84 100644 --- a/sql-update.sql +++ b/sql-update.sql @@ -165,3 +165,28 @@ 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; + +ALTER TABLE IF EXISTS `commonerrors` + MODIFY COLUMN `title` longtext NOT NULL, + MODIFY COLUMN `commonerrorname` longtext NOT NULL; 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..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,6 +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.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; @@ -62,6 +64,10 @@ 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 b657d1f6f..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,6 +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.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; @@ -162,6 +164,24 @@ 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(); + 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/persistence/datamodel/CommonError.java b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/CommonError.java index fc3fade2d..1ddf88c6d 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/CommonError.java +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/CommonError.java @@ -75,7 +75,7 @@ public void setErrorID(int errorID) { this.errorID = errorID; } - @Column(nullable = false) + @Column(nullable = false, length = 65536) public String getTitle() { return title; } @@ -116,7 +116,7 @@ public Type getTypedType() { return Type.values()[this.type]; } - @Column(nullable = false) + @Column(nullable = false, length = 65536) public String getCommonErrorName() { return commonErrorName; } 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..8412017c8 --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellRuntimeTest.java @@ -0,0 +1,77 @@ +/* + * 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.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; + +/** + * Haskell runtime test, extends the DockerTest by automatically generating haskell testcases and by clustering + * @author Christian Wagner + */ +@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()); + } +} 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..fe78de74b --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/persistence/datamodel/HaskellSyntaxTest.java @@ -0,0 +1,33 @@ +/* + * 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 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); + } +} 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..930ef31d6 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/DockerTestManager.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/DockerTestManager.java @@ -34,12 +34,14 @@ import de.tuclausthal.submissioninterface.persistence.dao.TestDAOIf; import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTest; 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.DockerTestManagerOverView; +import de.tuclausthal.submissioninterface.servlets.view.HaskellRuntimeTestManagerView; import de.tuclausthal.submissioninterface.servlets.view.MessageView; import de.tuclausthal.submissioninterface.util.Util; @@ -71,7 +73,10 @@ 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 +98,8 @@ 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 +108,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 +118,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 +137,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 +152,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 new file mode 100644 index 000000000..89e9bb71e --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/controller/HaskellRuntimeTestManager.java @@ -0,0 +1,1300 @@ +/* + * 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 static java.lang.Math.ceil; + +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.Arrays; +import java.util.HashMap; +import java.util.HashSet; +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; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +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; +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; +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.DockerTest; +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; + +/** + * Controller-Servlet for clustering haskell submissions based on common errors (dynamic/runtime analysis). + * This servlet allows advisors to automatically generate and modify test steps. + * + * @author Christian Wagner + */ +@GATEController +public class HaskellRuntimeTestManager extends HttpServlet { + @Serial + 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); + } + + @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; + } + + 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 ("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)); + } else if ("generateFunctionTestcases".equals(request.getParameter("action"))) { + int identifierId = Util.parseInteger(request.getParameter("identifierid"), -1); + int numberOfTestSteps = Util.parseInteger(request.getParameter("numberOfTestSteps"), 0); + + if (identifierId != -1 && numberOfTestSteps > 0) { + try { + List dockerTestStepDatas = readClassifiedIdentifiersAndGenerateFunctionTestcases(haskellRuntimeTest, identifierId, numberOfTestSteps); + + Transaction tx = session.beginTransaction(); + for (DockerTestStepData dockerTestStepData : dockerTestStepDatas) { + // 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); + } + + tx.commit(); + } catch (IOException | IllegalArgumentException e) { + request.getSession().setAttribute("haskellRuntimeTestGenerateError", e.getMessage()); + } + } + response.sendRedirect(Util.generateRedirectURL(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + haskellRuntimeTest.getId(), response)); + } else if ("editSingleTestStep".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()); + + if (testStepIds.size() == 1) { + int testStepId = testStepIds.iterator().next(); + String userEnteredFunctionCall = request.getParameter("userEnteredFunctionCall" + testStepId); + + if (userEnteredFunctionCall != null) { + try { + userEnteredFunctionCall = userEnteredFunctionCall.replaceAll("\r\n", "\n"); + + List expectedValues = computeExpectedValues(List.of(userEnteredFunctionCall), haskellRuntimeTest.getTask()); + if (expectedValues.size() != 1) { + throw new AssertionError(String.format("Expected values: %d (only one function call)", expectedValues.size())); + } + String expectedValue = expectedValues.get(0); + + DockerTestStep stepToEdit = null; + for (DockerTestStep step : haskellRuntimeTest.getTestSteps()) { + if (step.getTeststepid() == testStepId) { + stepToEdit = step; + break; + } + } + if (stepToEdit != null) { + Transaction tx = session.beginTransaction(); + stepToEdit.setTestcode(DockerTestStepData.getTestcodeFromFunctionCallAndFilename(userEnteredFunctionCall, getModelSolutionFilename(haskellRuntimeTest))); + stepToEdit.setExpect(DockerTestStepData.getExpectedValueFromGeneratorExpectedValue(expectedValue)); + tx.commit(); + } + } catch (IOException | AssertionError e) { + request.getSession().setAttribute("haskellRuntimeTestEditTestcaseError", e.getMessage()); + } + } + } + } + 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 static class DockerTestStepData { + private final String functionNameWithType; + private final String testCode; + private final String expectedValue; + + private DockerTestStepData(String functionName, String functionType, String functionCall, String expectedValue, String filename) { + this.functionNameWithType = functionName + " :: " + functionType; + this.testCode = getTestcodeFromFunctionCallAndFilename(functionCall, filename); + this.expectedValue = getExpectedValueFromGeneratorExpectedValue(expectedValue); + } + + private static String getTestcodeFromFunctionCallAndFilename(String functionCall, String filename) { + 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").strip())); + return testCodeStringBuilder.toString(); + } + + private static String getExpectedValueFromGeneratorExpectedValue(String expectedValue) { + return 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 static void deleteStoredClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session) { + Transaction tx = session.beginTransaction(); + + for (HaskellRuntimeTestIdentifier identifier : haskellRuntimeTest.getIdentifiers()) { + session.remove(identifier); + } + + tx.commit(); + } + + private static void browseModelSolutionAndStoreClassifiedIdentifiers(HaskellRuntimeTest haskellRuntimeTest, Session session) throws IOException { + List haskellIdentifiers = browseModelSolution(haskellRuntimeTest.getTask()); + HaskellClassifiedIdentifiers haskellClassifiedIdentifiers = classifyHaskellIdentifiers(haskellIdentifiers); + + Transaction tx = session.beginTransaction(); + + 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); + } + + for (HaskellFunction haskellFunction : haskellClassifiedIdentifiers.getFunctions()) { + HaskellRuntimeTestIdentifier haskellRuntimeTestIdentifier = new HaskellRuntimeTestIdentifier(haskellRuntimeTest, "function"); + + haskellRuntimeTestIdentifier.setFunctionName(haskellFunction.getName()); + haskellRuntimeTestIdentifier.setFunctionType(haskellFunction.getTypeSignature()); + + String typeSignatureWithConcreteTypesForKnownConstraints = deriveConcreteTypesForConstrainedTypeVariables(haskellFunction.getTypeSignature()); + String concreteTypeSignature = replaceUnconstrainedTypeVariables(typeSignatureWithConcreteTypesForKnownConstraints, HaskellPrimitiveType.Int); + haskellRuntimeTestIdentifier.setFunctionConcreteType(concreteTypeSignature); + + session.persist(haskellRuntimeTestIdentifier); + } + + tx.commit(); + } + + private static List readClassifiedIdentifiersAndGenerateFunctionTestcases(HaskellRuntimeTest haskellRuntimeTest, int identifierId, int numberOfTestSteps) throws IOException, IllegalArgumentException { + List generatedTestcases = new ArrayList<>(); + HaskellRuntimeTestIdentifier functionIdentifier = null; + List arbitraryInstances = new ArrayList<>(); + + 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; + } + } + + 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); + + 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); + 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++) { + generatedTestcases.add(new DockerTestStepData(functionName, functionType, functionCalls.get(i), expectedValues.get(i), getModelSolutionFilename(haskellRuntimeTest))); + } + } else { + throw new IllegalArgumentException("Invalid identifier id."); + } + + return generatedTestcases; + } + + 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 static 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 static 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) { + } + + /** 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 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 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) + 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 = 45; + + 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); + } + + Path hsFile = loadModelSolution ? getModelSolutionFile(modelSolutionDir) : null; + + // TODO@CHW: testCode is more complex in DockerTest + StringBuilder testCode = new StringBuilder("ghci -XInstanceSigs"); + for (String packageToEnable : packagesToEnable) { + appendGhciEvaluateArgument(testCode, ":set -package " + packageToEnable); + } + appendGhciEvaluateArgument(testCode, ":m + " + String.join(" ", modulesToImport)); + if (hsFile != null) { + appendGhciEvaluateArgument(testCode, ":load " + hsFile.getFileName().toString()); + } + for (String expression : expressionsToEvaluate) { + appendGhciEvaluateArgument(testCode, expression); + } + + 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(DockerTest.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(); + + 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:\n" + stdErr); + } + } + + return new SubprocessResult(stdOut, stdErr, exitCode, aborted); + } finally { + if (generatorTempDir != null) { + Util.recursiveDelete(generatorTempDir); + } + } + } + + 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(); + + if (hsFiles.size() != 1) { + throw new IOException("Expected exactly one model solution .hs file, found " + hsFiles.size() + " files."); + } else { + return hsFiles.get(0); + } + } + } + + private static void appendGhciEvaluateArgument(StringBuilder testCode, String argument) { + testCode.append(" -e '").append(argument.replace("'", "'\"'\"'").replace("\t", " ")).append("'"); + } + + private static List browseModelSolution(Task task) throws IOException { + SubprocessResult result = evaluateWithGhci(null, null, true, new String[] { ":browse" }, task, true); + return splitLinesButKeepMultilines(result.stdOut()); + } + + private static 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; + } + + private static 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("::") && !line.startsWith("type")) { + classifiedIdentifiers.addFunction(line); + } + } + + return classifiedIdentifiers; + } + + private static String deriveConcreteTypesForConstrainedTypeVariables(String functionTypeSignature) { + if (functionTypeSignature.contains("::")) { + functionTypeSignature = functionTypeSignature.split("::", 2)[1].trim(); + } + + functionTypeSignature = functionTypeSignature.trim(); + + String[] parts = functionTypeSignature.split("=>"); + String constraintsPart; + String typePart; + + if (parts.length == 1) { + return functionTypeSignature; + } else { + 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) { + typeSignature = typeSignature.replace("\n", ""); + typeSignature = typeSignature.replaceAll("\\s*->\\s*", " -> "); + return typeSignature.trim(); + } + + private static String replaceUnconstrainedTypeVariables(String typeSignature, HaskellPrimitiveType replacementType) { + 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()? + + private static boolean parameterTypeIsFunction(String parameterType) { + parameterType = parameterType.strip(); + return parameterType.contains("->") && parameterType.startsWith("(") && parameterType.endsWith(")"); + } + + private 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 static List generateQuickcheckFunctionTestcases(Task task, 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) + ")"; + + 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 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 = """ + 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 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 -> "(? constrainedTypes = new ArrayList<>(); + + for (String parameterType : parameterTypes) { + Matcher matcher = pattern.matcher(parameterType); + StringBuilder sb = new StringBuilder(); + + while (matcher.find()) { + String matchedKey = matcher.group(); + String replacement = replacementDict.get(matchedKey); + matcher.appendReplacement(sb, replacement); + } + matcher.appendTail(sb); + + constrainedTypes.add(sb.toString()); + } + + return constrainedTypes; + } + + private static 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 static 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)); + } + + private static 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; + } + + private static List computeExpectedValues(List functionCalls, Task task) throws IOException { + List wrappedFunctionCalls = functionCalls.stream().map(HaskellRuntimeTestManager::wrapGhciExpressionInCatchAndTimeout).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 Data.List Data.Maybe System.Timeout" }; + 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); + } + } + + if (expectedValues.size() != wrappedFunctionCalls.size()) { + throw new AssertionError(String.format("Expected values: %d, function calls: %d", expectedValues.size(), wrappedFunctionCalls.size())); + } + + return expectedValues; + } + + 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); + } + + public static String extractUnescapedGhciExpressionWrappedInCatchAndTimeout(String wrappedExpression) { + // 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); + + if (matcher.find()) { + return matcher.group(1).replace("'\"'\"'", "'"); + } else { + return null; + } + } + + public static String prettyPrintCyclicIntMappers(String functionCall) { + return functionCall.replaceAll("\\(let cyclicIntMap.*? let randomFunction.*? in randomFunction\\)", ""); + } + + private static class HaskellClassifiedIdentifiers { + private final List classes = new ArrayList<>(); + private final List newtypesAndDatas = new ArrayList<>(); + private final List functions = new ArrayList<>(); + + private void addClass(String hsClass) { + classes.add(new HaskellClass(hsClass)); + } + + private void addNewtypeOrData(String hsNewtypeOrData) { + newtypesAndDatas.add(new HaskellNewtypeOrData(hsNewtypeOrData)); + } + + private void addFunction(String hsFunction) { + functions.add(new HaskellFunction(hsFunction)); + } + + private List getClasses() { + return classes; + } + + private List getNewtypesAndDatas() { + return newtypesAndDatas; + } + + private List getFunctions() { + return functions; + } + } + + private record HaskellClass(String hsClass) { + private HaskellClass { + if (!hsClass.startsWith("class") || !hsClass.contains("where")) { + throw new IllegalArgumentException("Invalid class definition: " + hsClass); + } + } + } + + private static class HaskellFunction { + private final String name; + private final String typeSignature; + + private 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(); + } + + private String getName() { + return name; + } + + private String getTypeSignature() { + return typeSignature; + } + } + + private static class HaskellNewtypeOrData { + private final String typename; + private final String typeDefinition; + private final List constructors = new ArrayList<>(); + private final String arbitraryInstance; + + 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|$)"); + + 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*}"); + + 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()); + } + } + + this.arbitraryInstance = generateArbitraryInstance(); + } else { + throw new IllegalArgumentException("Invalid newtype/data definition: " + hsNewtypeOrData); + } + } + + private String getArbitraryInstance() { + return arbitraryInstance; + } + + private String getTypename() { + return typename; + } + + private String getTypeDefinition() { + return typeDefinition; + } + + private 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); + + 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); + } + } + } + + private enum HaskellPrimitiveType { + Integer, Int, Float, Double, Rational, Bool, Char, String + } + + private enum HaskellConstrainedPrimitiveType { + Char_OnlySafeAscii, String_OnlySafeAscii + // TODO@CHW more ideas: Int_OnlyPositive, Float_OnlyPositive, Double_OnlyPositive + } + + private 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; + } +} 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..361eed2ce 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,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.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; @@ -153,6 +155,25 @@ 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); + 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")) && "checklist".equals(request.getParameter("type"))) { session.beginTransaction(); TestDAOIf testDAO = DAOFactory.TestDAOIf(session); @@ -272,6 +293,24 @@ 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")) && "haskellruntime".equals(request.getParameter("type"))) { + 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/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.

"); diff --git a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java new file mode 100644 index 000000000..25375ef30 --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/HaskellRuntimeTestManagerView.java @@ -0,0 +1,513 @@ +/* + * 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 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; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +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.PerformTest; +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; + +/** + * View-Servlet for clustering haskell submissions based on common errors (dynamic/runtime analysis) + * + * @author Christian Wagner + */ +@GATEView +public class HaskellRuntimeTestManagerView 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); + + 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(errorBoxIfErrorOccurred(httpSession, "haskellRuntimeTestEditTestcaseError", "Beim Bearbeiten des Testfalls ist ein Fehler aufgetreten")); + } + + out.println(""" + + """); + + 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("

Benutzerdefinierte Haskell Funktionen und Datentypen der Musterlösung

"); + StringBuilder newtypeOrDatasHtml = new StringBuilder(); + newtypeOrDatasHtml.append(""" + + Typname + Typdefinition und Arbitrary Instanz + + """); + + StringBuilder functionsHtml = new StringBuilder(); + functionsHtml.append(""" + + Funktion + Typsignatur (:t) + 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; + 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(""" + +
+ %2$s +
+
+ %3$s +
+
+ %7$s +
+ +
+ + + + + +
+

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()), formHiddenState, missingConcreteWarningHiddenState)); + break; + } + } + + if (showNewtypeOrDataTable) { + out.println("
"); + out.println(""); + out.println(newtypeOrDatasHtml); + out.println("
"); + out.println("
"); + } + if (showNewtypeOrDataTable && showFunctionTable) { + out.println("
"); + } + if (showFunctionTable) { + out.println("
"); + out.println(""); + out.println(functionsHtml); + + out.println(String.format(""" + + """, Util.generateHTMLLink("?", response), test.getId())); + + out.println("
+
+
+ + + Funktion manuell hinzufügen: + + + + + +
+
+
"); + out.println("
"); + } + if (showNewtypeOrDataTable || showFunctionTable) { + out.println("
"); + } + + out.println(String.format(""" +
+
+ + + + + (Zurücksetzen) + +
+
+ """, Util.generateHTMLLink("?", response), test.getId(), Util.generateHTMLLink(HaskellRuntimeTestManager.class.getSimpleName() + "?testid=" + test.getId() + "&action=deleteHaskellIdentifiers", response))); + + // 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(); + + if (!sortedKeys.isEmpty()) { + 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++) { + String functionNameWithType = sortedKeys.get(i); + + final int numberOfTestSteps = testStepsGroupedByFunctionNameWithType.get(functionNameWithType).size(); + final String numberOfTestStepsText = numberOfTestSteps + " " + (numberOfTestSteps == 1 ? "Testschritt" : "Testschritte"); + + out.println("

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

"); + + 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(""" +
+ +
+ + + + + + + + + """, 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); + + String noEditModeDivId = "noEditModeDiv" + step.getTeststepid(); + String editModeDivId = "editModeDiv" + step.getTeststepid(); + String testcaseSelectionCheckboxId = "testcaseSelectionCheckbox" + step.getTeststepid(); + + out.println(String.format(""" + + + + + + """, 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(""" + + + + """, formId, formActionInputFieldId)); + // TODO@CHW: add button "Duplikate in Selektion entfernen" + out.println("
+ + + Testcode +
+ + +
+
Erwartete Ausgabe
+ + +
+ + +
+ %6$s + bearbeiten +
+
+ +
+ %2$s +
+ Aktionen für selektierte Testschritte: + + +
"); + out.println("
"); + out.println("
"); + out.println("
"); + } + + 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
+
+
+
+ + """, Util.escapeHTML(errorTitle), Util.escapeHTML(errorMessage)); + } +} 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..999ac4f9b 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,8 @@ 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.HaskellRuntimeTest; +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; @@ -37,6 +39,8 @@ 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.ShowHaskellRuntimeTestResult; +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; @@ -106,6 +110,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 HaskellSyntaxTest hst) { + ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), true, null); + } else if (test instanceof HaskellRuntimeTest hrt) { + ShowHaskellRuntimeTestResult.printTestResults(out, hrt, testResult.getTestOutput(), true, null); } else if (test instanceof DockerTest dt) { ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), true, null); } else { 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..aa930e860 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/PerformTestResultView.java @@ -27,12 +27,16 @@ import jakarta.servlet.http.HttpServletResponse; 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.JavaAdvancedIOTest; 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.GATEView; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowDockerTestResult; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellRuntimeTestResult; +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; @@ -66,6 +70,10 @@ 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 HaskellRuntimeTest hrt) { + ShowHaskellRuntimeTestResult.printTestResults(out, hrt, 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 { 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..b4a370154 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowSubmissionStudentView.java @@ -30,12 +30,17 @@ import de.tuclausthal.submissioninterface.persistence.datamodel.CommonError; 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.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; +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; import de.tuclausthal.submissioninterface.template.Template; import de.tuclausthal.submissioninterface.template.TemplateFactory; @@ -67,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()) { @@ -79,6 +88,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 HaskellRuntimeTest hrt) { + out.println("
"); + ShowHaskellRuntimeTestResult.printTestResults(out, hrt, testResult.getTestOutput(), true, 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 2a118774b..21f38b703 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,8 @@ 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.HaskellRuntimeTest; +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 +66,8 @@ 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.ShowHaskellRuntimeTestResult; +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; @@ -317,6 +321,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 HaskellSyntaxTest hst) { + out.println("
"); + ShowHaskellSyntaxTestResult.printTestResults(out, hst, testResult.getTestOutput(), false, javaScript); + } else if (testResult.getTest() instanceof HaskellRuntimeTest hrt) { + out.println("
"); + ShowHaskellRuntimeTestResult.printTestResults(out, hrt, testResult.getTestOutput(), false, javaScript); } else if (testResult.getTest() instanceof DockerTest dt) { out.println("
"); ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), false, javaScript); 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 29329ed6b..b5f97bb20 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskStudentView.java @@ -44,6 +44,8 @@ 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.HaskellRuntimeTest; +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; @@ -67,6 +69,9 @@ 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; import de.tuclausthal.submissioninterface.tasktypes.ClozeTaskType; import de.tuclausthal.submissioninterface.template.Template; @@ -376,15 +381,35 @@ 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) { 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 HaskellRuntimeTest hrt) { + ShowHaskellRuntimeTestResult.printTestResults(out, hrt, 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/servlets/view/ShowTaskTutorAllSubmissionsView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorAllSubmissionsView.java index 7e456634e..b3fd73e46 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorAllSubmissionsView.java +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/ShowTaskTutorAllSubmissionsView.java @@ -38,6 +38,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.HaskellRuntimeTest; import de.tuclausthal.submissioninterface.persistence.datamodel.JavaAdvancedIOTest; import de.tuclausthal.submissioninterface.persistence.datamodel.MCOption; import de.tuclausthal.submissioninterface.persistence.datamodel.Points.PointStatus; @@ -56,6 +57,7 @@ import de.tuclausthal.submissioninterface.servlets.controller.ShowTask; import de.tuclausthal.submissioninterface.servlets.controller.ShowTaskAllSubmissions; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowDockerTestResult; +import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowHaskellRuntimeTestResult; import de.tuclausthal.submissioninterface.servlets.view.fragments.ShowJavaAdvancedIOTestResult; import de.tuclausthal.submissioninterface.tasktypes.ClozeTaskType; import de.tuclausthal.submissioninterface.template.Template; @@ -121,6 +123,9 @@ 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 HaskellRuntimeTest hrt) { + out.println("
"); + ShowHaskellRuntimeTestResult.printTestResults(out, hrt, testResult.getTestOutput(), false, javaScript); } else if (testResult.getTest() instanceof DockerTest dt) { out.println("
"); ShowDockerTestResult.printTestResults(out, dt, testResult.getTestOutput(), false, javaScript); 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/TaskManagerView.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/TaskManagerView.java index 791c665a2..4987ce359 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.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; @@ -50,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; @@ -452,6 +455,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) { @@ -472,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
"); 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..300a2ecb9 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(""); @@ -158,8 +158,88 @@ 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.print(""); + out.println(""); + out.println("
Titel:
Beschreibung:
Tutorentest:
# ausführbar für Studierende:
Studierenden Test-Details anzeigen:
Abbrechen
"); + 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 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..7cb31c9bc --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeCommonErrorTitle.java @@ -0,0 +1,76 @@ +/* + * 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 static de.tuclausthal.submissioninterface.servlets.controller.HaskellRuntimeTestManager.prettyPrintCyclicIntMappers; + +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 { + 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:"); + + out.println("
    "); + for (JsonValue testCaseVal : commonErrorTitleAsJson.getJsonArray("testcases")) { + JsonObject testCase = testCaseVal.asJsonObject(); + String testcase = prettyPrintCyclicIntMappers(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/servlets/view/fragments/ShowHaskellRuntimeTestResult.java b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeTestResult.java new file mode 100644 index 000000000..f0390658b --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellRuntimeTestResult.java @@ -0,0 +1,158 @@ +/* + * Copyright 2021-2023 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.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; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonParsingException; + +import de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest; +import de.tuclausthal.submissioninterface.util.Util; + +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))) { + object = jsonReader.readObject(); + } catch (JsonParsingException ignored) { + } + + if (object == null) { + out.println("Keine gültige Ausgabe erhalten."); + } else if (object.containsKey("steps")) { + JsonValue arr = object.get("steps"); + if (arr.getValueType().equals(JsonValue.ValueType.ARRAY) && arr.asJsonArray().isEmpty()) { + // TODO@CHW make this nicer + if ((object.containsKey("exitedCleanly") && !object.getBoolean("exitedCleanly")) || (object.containsKey("time-exceeded") && object.getBoolean("time-exceeded")) || (object.containsKey("missing-tests") && object.getBoolean("missing-tests"))) { + if (object.containsKey("stderr")) { + if (forStudent) { // TODO show stderr to students? + if (object.containsKey("exitCode") && object.getInt("exitCode") == 15) { + out.println("Der zu testende Code ist syntaktisch nicht korrekt und kann daher nicht getestet werden.
"); + } else { + out.println("Syntaxfehler:
" + Util.escapeHTML(cleanup(object, object.getString("stderr"))) + "
"); + } + } else { + out.println(""); + } + } else { + out.println("Ein unbekannter Fehler ist aufgetreten."); + } + } + } else if (arr.getValueType().equals(JsonValue.ValueType.ARRAY)) { + out.println("
"); + JsonArray array = arr.asJsonArray(); + for (int i = 0; i < array.size(); ++i) { + JsonObject stepObject = array.get(i).asJsonObject(); + int foundTest = -1; + for (int j = 0; j < haskellRuntimeTest.getTestSteps().size(); ++j) { + if (haskellRuntimeTest.getTestSteps().get(j).getTeststepid() == stepObject.getInt("id")) { + foundTest = j; + break; + } + } + if (foundTest >= 0) { + 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("
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("
"); + } + } + + boolean wasError = false; + if (object.containsKey("missing-tests") && object.getBoolean("missing-tests")) { + out.println("

Nicht alle Tests wurden durchlaufen.

"); + wasError = true; + } + if (object.containsKey("time-exceeded") && object.getBoolean("time-exceeded")) { + out.println("

Der Test wurde zwangweise beendet, da er das Zeitlimit überschritten hat.

"); + wasError = true; + } + if (object.containsKey("exitedCleanly") && !object.getBoolean("exitedCleanly")) { + out.println("

Das Program wurde nicht ordentlich beendet.

"); + wasError = true; + } + if (wasError && object.containsKey("stderr")) { + String stderr = object.getString("stderr"); + if (object.containsKey("separator")) { + String separator = object.getString("separator"); + int lastIndex = stderr.lastIndexOf(separator); + if (lastIndex >= 0) { + stderr = stderr.substring(lastIndex + separator.length()); + } + } + if (!stderr.trim().isEmpty()) { + if (forStudent) { // TODO show stderr to students? + out.println("Laufzeitfehler/Warnungen:
" + Util.escapeHTML(cleanup(object, stderr)) + "
"); + } else { + out.println(""); + } + } + } + } + } + } + + private static String cleanup(JsonObject object, String string) { + if (object.containsKey("tmpdir")) { + string = string.replace(object.getString("tmpdir") + "/administrative/", ""); + string = string.replace(object.getString("tmpdir") + "/administrative", ""); + string = string.replace(object.getString("tmpdir") + "/student/", ""); + string = string.replace(object.getString("tmpdir") + "/student", ""); + string = string.replace(object.getString("tmpdir"), ""); + } + return string; + } +} 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..c5286751e --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/servlets/view/fragments/ShowHaskellSyntaxTestResult.java @@ -0,0 +1,43 @@ +/* + * 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; +import java.io.StringReader; + +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) { + + JsonObject json = Json.createReader(new StringReader(testOutput)).readObject(); + + String stderr = json.getString("stderr", ""); + + if (!stderr.isEmpty()) { + out.println("

Fehlerausgabe:

"); + out.println("
" + Util.escapeHTML(stderr) + "
"); + } + + } +} 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) { diff --git a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java index 51e2224bb..3d92ab0bd 100644 --- a/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/CommonErrorAnalyzer.java @@ -19,11 +19,17 @@ package de.tuclausthal.submissioninterface.testanalyzer; +import static de.tuclausthal.submissioninterface.servlets.controller.HaskellRuntimeTestManager.extractUnescapedGhciExpressionWrappedInCatchAndTimeout; + 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 org.hibernate.Session; @@ -32,10 +38,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.Test; import de.tuclausthal.submissioninterface.persistence.datamodel.TestResult; +import de.tuclausthal.submissioninterface.testanalyzer.haskell.syntax.RegexBasedHaskellClustering; public class CommonErrorAnalyzer { //from Literatur @@ -60,6 +70,12 @@ public void runAnalysis(final TestResult testResult) { groupJavaIOTestResults((JavaAdvancedIOTest) test, testResult); } else if (test instanceof JUnitTest) { groupJUnitTestResults((JUnitTest) test, testResult); + } 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); } } @@ -151,6 +167,76 @@ 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; + } + + final JsonObject testOutputJson = Json.createReader(new StringReader(testResult.getTestOutput())).readObject(); + Map> failedTestcasesByFunction = new HashMap<>(); + + if (testOutputJson.containsKey("steps")) { + final JsonArray steps = testOutputJson.getJsonArray("steps"); + + for (int i = 0; i < steps.size(); i++) { + if (!steps.getJsonObject(i).getBoolean("ok")) { + String gotValue = steps.getJsonObject(i).getString("got").strip(); + + 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("[^\\p{L} ]", ""); + } + + String testCodeWrappedInCatchAndTimeout = test.getTestSteps().get(i).getTestcode(); + String testcaseIdentifier = extractUnescapedGhciExpressionWrappedInCatchAndTimeout(testCodeWrappedInCatchAndTimeout); + if (testcaseIdentifier == null) { + testcaseIdentifier = testCodeWrappedInCatchAndTimeout; + } + + 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); + } + } + } + + List flags = new ArrayList<>(); + if (testOutputJson.containsKey("stderr") && !testOutputJson.getString("stderr").isEmpty()) { + 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(); + for (String flag : flags) { + keyStrStringBuilder.append(flag).append("; "); + } + + 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) { for (final TestResult otherTestResult : testResult.getSubmission().getTestResults()) { if (!otherTestResult.getPassedTest() && otherTestResult.getTest() instanceof CompileTest) { @@ -274,4 +360,49 @@ 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 = ""; + + 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); + } + + 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 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/RegexBasedHaskellClustering.java b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java new file mode 100644 index 000000000..d3e3986be --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/testanalyzer/haskell/syntax/RegexBasedHaskellClustering.java @@ -0,0 +1,115 @@ +/* + * 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 java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Pattern; + +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)); + 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 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("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)); + 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)); + 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("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|Invalid data constructor .* in type signature)", Pattern.CASE_INSENSITIVE)); + + // 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 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|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)); + CLUSTERS.put("Fehlende GADTs-Erweiterung", Pattern.compile("enable the GADTs extension", 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|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|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 + CLUSTERS.put("Warnung", Pattern.compile("warning", Pattern.CASE_INSENSITIVE)); + CLUSTERS.put("Sonstiger Fehler", Pattern.compile(".*", Pattern.DOTALL)); + } + + public 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 14e8572ea..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 @@ -45,8 +45,8 @@ public class DockerTest extends TempDirTest&2\n"); - testCode.append("{\n"); - testCode.append("set -e\n"); - testCode.append(testStep.getTestcode()); - testCode.append("\n"); - testCode.append("}\n"); - } + String testCode = generateTestShellScript(); final Path testDriver = administrativeDir.resolve("test.sh"); try (Writer fw = Files.newBufferedWriter(testDriver)) { @@ -105,7 +91,9 @@ public void performTest(final Path basePath, final Path submissionPath, final Te 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); + Process process = pb.start(); ProcessOutputGrabber outputGrapper = new ProcessOutputGrabber(process); // no need to check for timeout, we fully rely on the safe-docker script here @@ -122,8 +110,9 @@ public void performTest(final Path basePath, final Path submissionPath, final Te } boolean exitedCleanly = (exitValue == 0); - testResult.setTestPassed(calculateTestResult(exitedCleanly, outputGrapper.getStdOutBuffer(), outputGrapper.getStdErrBuffer(), exitValue, aborted)); - testResult.setTestOutput(outputGrapper.getStdOutBuffer().toString()); + + // for modularization and flexibility in child classes + analyzeAndSetResult(exitedCleanly, outputGrapper.getStdOutBuffer(), outputGrapper.getStdErrBuffer(), exitValue, aborted, testResult); } finally { if (tempDir != null) { Util.recursiveDelete(tempDir); @@ -131,22 +120,15 @@ public void performTest(final Path basePath, final Path submissionPath, final Te } } + 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()); + } + // similar code in JavaAdvancedIOTest protected boolean calculateTestResult(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); - } + JsonObjectBuilder builder = createJsonBuilder(exitedCleanly, processOutput, stdErr, exitCode, aborted); int start = 0; int splitterPos; @@ -183,6 +165,51 @@ protected boolean calculateTestResult(boolean exitedCleanly, final StringBuffer 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 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 new file mode 100644 index 000000000..3c3f6683f --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellRuntimeTest.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.testframework.tests.impl; + +import de.tuclausthal.submissioninterface.persistence.datamodel.DockerTestStep; + +/** + * @author Christian Wagner + */ +public class HaskellRuntimeTest extends DockerTest { + public HaskellRuntimeTest(final de.tuclausthal.submissioninterface.persistence.datamodel.HaskellRuntimeTest test) { + super(test); + } + + @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(); + } +} 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..bdb306f6c --- /dev/null +++ b/src/main/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTest.java @@ -0,0 +1,47 @@ +/* + * 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 de.tuclausthal.submissioninterface.testframework.executor.TestExecutorTestResult; + +public class HaskellSyntaxTest extends DockerTest { + + public HaskellSyntaxTest(de.tuclausthal.submissioninterface.persistence.datamodel.HaskellSyntaxTest test) { + super(test); + } + + @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 + """; + } +} 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; +} 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..185670681 --- /dev/null +++ b/src/test/java/de/tuclausthal/submissioninterface/testframework/tests/impl/HaskellSyntaxTestTest.java @@ -0,0 +1,83 @@ +/* + * 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 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("warning: something something"); + haskellSyntaxTest.analyzeAndSetResult(true, stdout, stderr, 0, false, result); + assertTrue(result.isTestPassed(), "Test should pass when stderr doesn't contain 'error:'."); + } + + @Test + void testHaskellSyntaxTestSyntaxError() { + var result = new TestExecutorTestResult(); + 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:'."); + } + + @Test + 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 | ^^^^^^^^^"); + 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 |\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'."); + } +} +