diff --git a/.idea/runConfigurations/Artemis__Server__Aeolus_.xml b/.idea/runConfigurations/Artemis__Server__Aeolus_.xml deleted file mode 100644 index 13c00126a34e..000000000000 --- a/.idea/runConfigurations/Artemis__Server__Aeolus_.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/.idea/runConfigurations/Artemis__Server__GitLabCI___Gitlab_.xml b/.idea/runConfigurations/Artemis__Server__GitLabCI___Gitlab_.xml deleted file mode 100644 index 9c36e532820f..000000000000 --- a/.idea/runConfigurations/Artemis__Server__GitLabCI___Gitlab_.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/.idea/runConfigurations/Artemis__Server__Jenkins___Gitlab_.xml b/.idea/runConfigurations/Artemis__Server__Jenkins___Gitlab_.xml deleted file mode 100644 index 7ffecef4daef..000000000000 --- a/.idea/runConfigurations/Artemis__Server__Jenkins___Gitlab_.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/.idea/runConfigurations/Artemis__Server__Jenkins___LocalVC_.xml b/.idea/runConfigurations/Artemis__Server__LocalVC___Jenkins_.xml similarity index 80% rename from .idea/runConfigurations/Artemis__Server__Jenkins___LocalVC_.xml rename to .idea/runConfigurations/Artemis__Server__LocalVC___Jenkins_.xml index 47dd1c05a2fa..217477ace79d 100644 --- a/.idea/runConfigurations/Artemis__Server__Jenkins___LocalVC_.xml +++ b/.idea/runConfigurations/Artemis__Server__LocalVC___Jenkins_.xml @@ -1,13 +1,12 @@ - + - + \ No newline at end of file diff --git a/.idea/runConfigurations/Artemis__Server__LocalVC___LocalCI__IRIS_.xml b/.idea/runConfigurations/Artemis__Server__LocalVC___LocalCI__IRIS_.xml deleted file mode 100644 index 1e5bda2c16f7..000000000000 --- a/.idea/runConfigurations/Artemis__Server__LocalVC___LocalCI__IRIS_.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/README.md b/README.md index 556d5b40ac88..139f6ceb7137 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,17 @@ Artemis brings interactive learning to life with instant, individual feedback on programming exercises, quizzes, modeling tasks, and more. Offering customization for instructors and real-time collaboration for students, this platform bridges creativity and education. Embrace a new era of engaging, adaptive learning and artificial intelligence support with Artemis, where innovation meets inclusivity. Find out more on https://artemisapp.github.io +## Main goals + +1. **User experience**: Provide an intuitive and engaging interface that enhances the learning experience for both students and instructors. Ensure ease of use across diverse learning scenarios, fostering seamless interaction and accessibility. + +2. **Scalable infrastructure**: Build a robust platform capable of supporting large-scale courses with thousands of participants simultaneously. Ensure high availability, performance, and adaptability as the platform grows in functionality and user base. + +3. **Constructive alignment**: Align learning goals, activities, and assessments through well-integrated features such as the exam mode that support instructional design principles. Enable the seamless implementation of interactive learning methodologies, ensuring that every feature contributes meaningfully to learning goals. + +4. **Learning analytics**: Leverage data to provide actionable insights into student performance and engagement. Offer detailed feedback to both learners and instructors, enhancing personalized learning experiences and supporting data-driven decisions for educational improvement while ensuring data privacy and security. + + ## Main features and modules 1. **[Exercises](https://docs.artemis.cit.tum.de/user/exercises)** with automatic and manual assessment. Instructors have many configuration options. @@ -83,7 +94,7 @@ The Artemis development team prioritizes the following areas in the future. We w * **Long term**: Allow students to take notes on lecture slides and support the automatic updates of lecture slides * **Long term**: Develop an exchange platform for exercises -## Contributing to This Project +## Contributing We welcome contributions from both members of our organization and external contributors. To maintain transparency and trust: @@ -96,28 +107,38 @@ We follow a pull request contribution model. For detailed guidelines, please ref #### Maintainers + The following members of the project management team are responsible for specific feature areas in Artemis. Contact them if you have questions or if you want to develop new features in this area. -| # | Feature / Module | Responsible maintainer | -|----|-----------------------|------------------------------------------------------------------------------------| -| 0 | Core | Stephan Krusche ([@krusche](https://github.com/krusche)) | -| 1 | Exercises | Stephan Krusche ([@krusche](https://github.com/krusche)) | -| 2 | Programming exercises | Stephan Krusche ([@krusche](https://github.com/krusche)) | -| 3 | Build agents | Stephan Krusche ([@krusche](https://github.com/krusche)) | -| 4 | Quiz exercises | Felix Dietrich ([@FelixTJDietrich](https://github.com/FelixTJDietrich)) | -| 5 | Modeling exercises | Stephan Krusche ([@krusche](https://github.com/krusche)) | -| 6 | Text exercises | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) | -| 7 | File upload exercises | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) | -| 8 | Exam mode | Stephan Krusche ([@krusche](https://github.com/krusche)) | -| 9 | Assessment | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) | -| 10 | Communication | Stephan Krusche ([@krusche](https://github.com/krusche)) | -| 11 | Lectures | Maximilian Anzinger ([@maximiliananzinger](https://github.com/maximiliananzinger)) | -| 12 | Atlas | Maximilian Anzinger ([@maximiliananzinger](https://github.com/maximiliananzinger)) | -| 13 | Iris | Patrick Bassner ([@bassner](https://github.com/bassner)) | -| 13 | Athena | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) | -| 15 | Tutorial Groups | Felix Dietrich ([@FelixTJDietrich](https://github.com/FelixTJDietrich)) | -| 16 | Plagiarism checks | Markus Paulsen ([@MarkusPaulsen](https://github.com/MarkusPaulsen)) | -| 17 | LTI | Matthias Linhuber ([@mtze](https://github.com/mtze)) | + +| Goal | Responsible maintainer | +|-------------------------|------------------------------------------------------------------------------------| +| User experience | Ramona Beinstingel ([@rabeatwork](https://github.com/rabeatwork)) | +| Scalable infrastructure | Matthias Linhuber ([@mtze](https://github.com/mtze)) | +| Constructive alignment | Stephan Krusche ([@krusche](https://github.com/krusche)) | +| Learning analytics | Maximilian Anzinger ([@maximiliananzinger](https://github.com/maximiliananzinger)) | + +| Feature / Module | Responsible maintainer | +|-----------------------|------------------------------------------------------------------------------------| +| Exercises | Stephan Krusche ([@krusche](https://github.com/krusche)) | +| Programming exercises | Stephan Krusche ([@krusche](https://github.com/krusche)) | +| Build agents | Robert Jandow ([@robertjndw](https://github.com/robertjndw)) | +| Quiz exercises | Felix Dietrich ([@FelixTJDietrich](https://github.com/FelixTJDietrich)) | +| Modeling exercises | Felix Dietrich ([@FelixTJDietrich](https://github.com/FelixTJDietrich)) | +| Text exercises | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) | +| File upload exercises | Elisabeth Friesinger ([@easy-lisi](https://github.com/easy-lisi)) | +| Exam mode | Stephan Krusche ([@krusche](https://github.com/krusche)) | +| Assessment | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) | +| Communication | Stephan Krusche ([@krusche](https://github.com/krusche)) | +| Lectures | Patrick Bassner ([@bassner](https://github.com/bassner)) | +| Atlas | Maximilian Anzinger ([@maximiliananzinger](https://github.com/maximiliananzinger)) | +| Iris | Patrick Bassner ([@bassner](https://github.com/bassner)) | +| Athena | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) | +| Tutorial Groups | Ramona Beinstingel ([@rabeatwork](https://github.com/rabeatwork)) | +| Plagiarism checks | Markus Paulsen ([@MarkusPaulsen](https://github.com/MarkusPaulsen)) | +| LTI | Maximilian Anzinger ([@maximiliananzinger](https://github.com/maximiliananzinger)) | + + ## Setup and guidelines diff --git a/build.gradle b/build.gradle index dff55628b0fc..ff065674558f 100644 --- a/build.gradle +++ b/build.gradle @@ -290,7 +290,7 @@ dependencies { implementation "org.apache.sshd:sshd-sftp:${sshd_version}" // https://mvnrepository.com/artifact/net.sourceforge.plantuml/plantuml - implementation "net.sourceforge.plantuml:plantuml:1.2024.5" + implementation "net.sourceforge.plantuml:plantuml:1.2024.7" implementation "org.jasypt:jasypt:1.9.3" implementation "me.xdrop:fuzzywuzzy:1.4.0" implementation("org.yaml:snakeyaml") { @@ -368,7 +368,7 @@ dependencies { implementation "javax.cache:cache-api:1.1.1" implementation "org.hibernate.orm:hibernate-core:${hibernate_version}" - implementation "com.zaxxer:HikariCP:5.1.0" + implementation "com.zaxxer:HikariCP:6.0.0" implementation "org.apache.commons:commons-text:1.12.0" implementation "org.apache.commons:commons-math3:3.6.1" @@ -447,7 +447,7 @@ dependencies { implementation "org.apache.maven:maven-model:3.9.9" // NOTE: 3.0.2 is broken for splitting lecture specific PDFs implementation "org.apache.pdfbox:pdfbox:3.0.1" - implementation "org.apache.commons:commons-csv:1.11.0" + implementation "org.apache.commons:commons-csv:1.12.0" implementation "org.commonmark:commonmark:0.23.0" implementation "commons-fileupload:commons-fileupload:1.5" implementation "net.lingala.zip4j:zip4j:2.11.5" @@ -457,7 +457,7 @@ dependencies { implementation "org.apfloat:apfloat:1.14.0" // use newest version of guava to avoid security issues through outdated dependencies - implementation "com.google.guava:guava:33.3.0-jre" + implementation "com.google.guava:guava:33.3.1-jre" implementation "com.sun.activation:jakarta.activation:2.0.1" // use newest version of gson to avoid security issues through outdated dependencies @@ -531,7 +531,7 @@ dependencies { testImplementation "io.github.classgraph:classgraph:4.8.176" testImplementation "org.awaitility:awaitility:4.2.2" testImplementation "org.apache.maven.shared:maven-invoker:3.3.0" - testImplementation "org.gradle:gradle-tooling-api:8.10.1" + testImplementation "org.gradle:gradle-tooling-api:8.10.2" testImplementation "org.apache.maven.surefire:surefire-report-parser:3.5.0" testImplementation "com.opencsv:opencsv:5.9" testImplementation("io.zonky.test:embedded-database-spring-test:2.5.1") { @@ -613,7 +613,7 @@ tasks.withType(Test).configureEach { } wrapper { - gradleVersion = "8.10.1" + gradleVersion = "8.10.2" } tasks.register("stage") { diff --git a/gradle.properties b/gradle.properties index 0ddd8b8f3fdc..fc54fad2849b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ node_version=20.16.0 npm_version=10.8.0 # Dependency versions -jhipster_dependencies_version=8.7.0 +jhipster_dependencies_version=8.7.1 spring_boot_version=3.3.4 spring_security_version=6.3.3 # TODO: upgrading to 6.6.0 currently leads to issues due to internal changes in Hibernate and potentially wrong use in Artemis server code @@ -16,26 +16,26 @@ opensaml_version=4.3.2 jwt_version=0.12.6 jaxb_runtime_version=4.0.5 hazelcast_version=5.5.0 -fasterxml_version=2.17.2 +fasterxml_version=2.18.0 jgit_version=7.0.0.202409031743-r sshd_version=2.13.2 checkstyle_version=10.18.1 jplag_version=5.1.0 # not really used in Artemis, nor Jplag, nor the used version of Stanford CoreNLP, but we use the latest to avoid security vulnerabilities # NOTE: we do not need to use the latest version 9.x here as long as Stanford CoreNLP does not reference it -lucene_version=8.11.3 +lucene_version=8.11.4 slf4j_version=2.0.16 sentry_version=7.14.0 liquibase_version=4.29.2 docker_java_version=3.4.0 logback_version=1.5.8 java_parser_version=3.26.2 -byte_buddy_version=1.15.1 +byte_buddy_version=1.15.3 # testing # make sure both versions are compatible junit_version=5.11.0 -junit_platform_version=1.11.0 +junit_platform_version=1.11.1 mockito_version=5.13.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0aaefbcaf0f1..df97d72b8b91 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathNgxService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathNgxService.java index 4bad211efcc5..8a3b228a27e5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathNgxService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathNgxService.java @@ -13,7 +13,6 @@ import jakarta.validation.constraints.NotNull; -import org.jgrapht.alg.util.UnionFind; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -24,6 +23,7 @@ import de.tum.cit.aet.artemis.atlas.domain.competency.RelationType; import de.tum.cit.aet.artemis.atlas.dto.NgxLearningPathDTO; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository; +import de.tum.cit.aet.artemis.atlas.service.util.UnionFind; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.lecture.domain.LectureUnit; diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/util/UnionFind.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/util/UnionFind.java new file mode 100644 index 000000000000..a2643b4c486f --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/util/UnionFind.java @@ -0,0 +1,131 @@ +package de.tum.cit.aet.artemis.atlas.service.util; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +public class UnionFind { + + private final Map parentMap = new LinkedHashMap(); + + private final Map rankMap = new HashMap(); + + private int count; + + public UnionFind(Collection elements) { + for (var element : elements) { + parentMap.put(element, element); + rankMap.put(element, 0); + } + count = elements.size(); + } + + /** + * Adds an element to the UnionFind data structure. + * + * @param element the element + */ + public void addElement(T element) { + if (parentMap.containsKey(element)) { + return; + } + parentMap.put(element, element); + rankMap.put(element, 0); + count++; + } + + /** + * Finds the representative element of the set that the given element is in. + * + * @param element the element + * @return the representative element of the set + */ + public T find(T element) { + if (!this.parentMap.containsKey(element)) { + throw new IllegalArgumentException("element is not contained in this UnionFind data structure: " + element); + } + else { + T current = element; + + while (true) { + T root = this.parentMap.get(current); + if (root.equals(current)) { + root = current; + + T parent; + for (current = element; !current.equals(root); current = parent) { + parent = this.parentMap.get(current); + this.parentMap.put(current, root); + } + + return root; + } + + current = root; + } + } + } + + /** + * Unions two sets that are represented by the given elements. + * + * @param element1 the first element + * @param element2 the second element + */ + public void union(T element1, T element2) { + if (!this.parentMap.containsKey(element1)) { + throw new IllegalArgumentException("element1 is not contained in this UnionFind data structure: " + element1); + } + else if (!this.parentMap.containsKey(element2)) { + throw new IllegalArgumentException("element2 is not contained in this UnionFind data structure: " + element2); + } + T parent1 = this.find(element1); + T parent2 = this.find(element2); + if (!parent1.equals(parent2)) { + int rank1 = this.rankMap.get(parent1); + int rank2 = this.rankMap.get(parent2); + if (rank1 > rank2) { + this.parentMap.put(parent2, parent1); + } + else if (rank1 < rank2) { + this.parentMap.put(parent1, parent2); + } + else { + this.parentMap.put(parent2, parent1); + this.rankMap.put(parent1, rank1 + 1); + } + + this.count--; + } + } + + /** + * Checks if two elements are in the same set. + * + * @param element1 the first element + * @param element2 the second element + * @return true if the elements are in the same set, false otherwise + */ + public boolean inSameSet(T element1, T element2) { + return this.find(element1).equals(this.find(element2)); + } + + /** + * Returns the number of sets. + * + * @return the number of sets + */ + public int numberOfSets() { + return this.count; + } + + /** + * Returns the number of elements in the union find data structure. + * + * @return the number of elements + */ + public int size() { + return this.parentMap.size(); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/ReportParser.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/ReportParser.java index 74f53cb16f64..9b8f59766ac2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/ReportParser.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/ReportParser.java @@ -21,6 +21,8 @@ */ public class ReportParser { + private final ObjectMapper mapper = new ObjectMapper(); + // Reports that are bigger then the threshold will not be parsed // and an issue will be generated. The unit is in megabytes. private static final int STATIC_CODE_ANALYSIS_REPORT_FILESIZE_LIMIT_IN_MB = 1; @@ -36,7 +38,6 @@ public class ReportParser { public String transformToJSONReport(File file) throws ParserException { try { StaticCodeAnalysisReportDTO report = transformToReport(file); - ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(report); } catch (Exception e) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/PMDParser.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/PMDParser.java index 4c19428da9c9..ee7ce091b37b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/PMDParser.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/PMDParser.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; @@ -25,22 +26,18 @@ record FileViolation(@JacksonXmlProperty(isAttribute = true, localName = "name") } @JsonIgnoreProperties(ignoreUnknown = true) -record Violation(@JacksonXmlProperty(isAttribute = true, localName = "rule") String rule, - - @JacksonXmlProperty(isAttribute = true, localName = "ruleset") String ruleset, - - @JacksonXmlProperty(isAttribute = true, localName = "priority") String priority, - - @JacksonXmlProperty(isAttribute = true, localName = "beginline") int beginLine, - - @JacksonXmlProperty(isAttribute = true, localName = "endline") int endLine, - - @JacksonXmlProperty(isAttribute = true, localName = "begincolumn") int beginColumn, - - @JacksonXmlProperty(isAttribute = true, localName = "endcolumn") int endColumn, - - @JacksonXmlProperty(localName = "") @JacksonXmlText String message // inner text -) { +record Violation(String rule, String ruleset, String priority, int beginLine, int endLine, int beginColumn, int endColumn, String message) { + + // NOTE: we need the json creator here, otherwise parsing does not work with the newest version of Jackson (2.18.0) + @JsonCreator + public static Violation createViolation(@JacksonXmlProperty(isAttribute = true, localName = "rule") String rule, + @JacksonXmlProperty(isAttribute = true, localName = "ruleset") String ruleset, @JacksonXmlProperty(isAttribute = true, localName = "priority") String priority, + @JacksonXmlProperty(isAttribute = true, localName = "beginline") int beginLine, @JacksonXmlProperty(isAttribute = true, localName = "endline") int endLine, + @JacksonXmlProperty(isAttribute = true, localName = "begincolumn") int beginColumn, @JacksonXmlProperty(isAttribute = true, localName = "endcolumn") int endColumn, + @JacksonXmlProperty(localName = "") @JacksonXmlText String message // inner text + ) { + return new Violation(rule, ruleset, priority, beginLine, endLine, beginColumn, endColumn, message); + } } class PMDParser implements ParserStrategy { @@ -54,7 +51,7 @@ public StaticCodeAnalysisReportDTO parse(String xmlContent) { return createReportFromPMDReport(pmdReport); } catch (IOException e) { - throw new RuntimeException("Failed to parse XML", e); + throw new RuntimeException("Failed to parse XML: " + e.getMessage(), e); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/UnionFindTest.java b/src/test/java/de/tum/cit/aet/artemis/UnionFindTest.java new file mode 100644 index 000000000000..4d6b0b8ebbc6 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/UnionFindTest.java @@ -0,0 +1,83 @@ +package de.tum.cit.aet.artemis; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.tum.cit.aet.artemis.atlas.service.util.UnionFind; + +class UnionFindTest { + + private UnionFind unionFind; + + @BeforeEach + void setUp() { + List elements = Arrays.asList(1, 2, 3, 4, 5); + unionFind = new UnionFind<>(elements); + } + + @Test + void testInitialization() { + assertThat(unionFind.size()).isEqualTo(5); + assertThat(unionFind.numberOfSets()).isEqualTo(5); + } + + @Test + void testAddElement() { + unionFind.addElement(6); + assertThat(unionFind.size()).isEqualTo(6); + assertThat(unionFind.numberOfSets()).isEqualTo(6); + } + + @Test + void testFind() { + assertThat(unionFind.find(1)).isEqualTo(1); + assertThat(unionFind.find(2)).isEqualTo(2); + } + + @Test + void testUnion() { + unionFind.union(1, 2); + assertThat(unionFind.numberOfSets()).isEqualTo(4); + assertThat(unionFind.inSameSet(1, 2)).isTrue(); + } + + @Test + void testInSameSet() { + unionFind.union(1, 2); + assertThat(unionFind.inSameSet(1, 2)).isTrue(); + assertThat(unionFind.inSameSet(1, 3)).isFalse(); + } + + @Test + void testNumberOfSets() { + unionFind.union(1, 2); + unionFind.union(3, 4); + assertThat(unionFind.numberOfSets()).isEqualTo(3); + } + + @Test + void testSize() { + assertThat(unionFind.size()).isEqualTo(5); + unionFind.addElement(6); + assertThat(unionFind.size()).isEqualTo(6); + } + + @Test + void testComplexSequence() { + unionFind.union(1, 2); + unionFind.union(3, 4); + unionFind.union(1, 3); + assertThat(unionFind.numberOfSets()).isEqualTo(2); + assertThat(unionFind.inSameSet(1, 2)).isTrue(); + assertThat(unionFind.inSameSet(1, 3)).isTrue(); + assertThat(unionFind.inSameSet(1, 4)).isTrue(); + assertThat(unionFind.inSameSet(2, 3)).isTrue(); + assertThat(unionFind.inSameSet(2, 4)).isTrue(); + assertThat(unionFind.inSameSet(3, 4)).isTrue(); + } +}