Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/main/java/com/thealgorithms/graph/HierholzerAlgorithm.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ public HierholzerAlgorithm(Map<Integer, LinkedList<Integer>> graph) {
this.graph = (graph == null) ? new HashMap<>() : graph;
}

/**
* Checks whether the graph has an Eulerian circuit.
*
* <p>Returns {@code true} for an empty graph and {@code false} if any vertex has odd degree or
* if non-zero-degree vertices span multiple disconnected components.
*
* @return {@code true} if an Eulerian circuit exists, {@code false} otherwise
*/
public boolean hasEulerianCircuit() {
if (graph.isEmpty()) {
return true;
Expand All @@ -52,6 +60,16 @@ public boolean hasEulerianCircuit() {
return isCoherentlyConnected();
}

/**
* Finds an Eulerian circuit in the graph without mutating the caller's adjacency lists.
*
* <p>Failure / absent-result paths: returns {@code Collections.emptyList()} when
* {@link #hasEulerianCircuit()} is {@code false} (odd-degree vertex or disconnected edges) or
* when the graph is empty ({@code null} or {@code {}}), and returns a single-element list when
* only isolated degree-0 vertices are present.
*
* @return vertex sequence forming an Eulerian circuit, or an empty list if none exists
*/
public List<Integer> findEulerianCircuit() {
if (!hasEulerianCircuit()) {
return Collections.emptyList();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package com.thealgorithms.graph;

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 java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;

/**
* Failure-path and empty-result regression tests for {@link HierholzerAlgorithm}.
*
* <p>Source preconditions ({@code HierholzerAlgorithm.java}):
* <ul>
* <li>lines 37-39: {@code HierholzerAlgorithm(Map)} stores {@code graph}, or {@code new HashMap<>()}
* when {@code graph == null} (line 38)</li>
* <li>lines 49-61: {@code hasEulerianCircuit()} returns {@code true} on empty {@code graph}
* (lines 50-52), returns {@code false} when any vertex has odd degree (lines 54-58), and
* otherwise delegates to {@code isCoherentlyConnected()} (line 60)</li>
* <li>lines 73-76: {@code findEulerianCircuit()} returns {@code Collections.emptyList()} when
* {@code !hasEulerianCircuit()} (absent-result sentinel)</li>
* <li>lines 78-81: {@code tempGraph} copies adjacency lists so caller-owned {@code graph} is
* never mutated</li>
* <li>lines 94-99: when no vertex has edges ({@code startVertex == -1}), returns
* {@code Collections.emptyList()} if {@code graph.isEmpty()} (lines 95-97) or a singleton list
* for an isolated vertex (line 98)</li>
* <li>lines 140-143: {@code isCoherentlyConnected()} returns {@code false} if any vertex with
* non-empty edges was not visited from {@code startNode}</li>
* </ul>
*/
public class HierholzerAlgorithmFailureTest {

private static Map<Integer, LinkedList<Integer>> copyGraph(Map<Integer, LinkedList<Integer>> src) {
Map<Integer, LinkedList<Integer>> copy = new HashMap<>();
for (Map.Entry<Integer, LinkedList<Integer>> e : src.entrySet()) {
copy.put(e.getKey(), new LinkedList<>(e.getValue()));
}
return copy;
}

private static void logCase(String id, String input, Object expected, Object actual) {
System.out.println("[R45-T018] " + id + " | input=" + input
+ " | expected=" + expected + " | actual=" + actual);
}

/**
* Smallest valid inputs and empty/null fallback branches (lines 38, 50-52, 95-98):
* {@code null} and empty {@code {}} yield {@code Collections.emptyList()}; single isolated
* vertex {@code {0=[]}} yields {@code [0]}.
*/
@Test
public void nullEmptyAndSingleIsolatedVertexFallbacksMatchContract() {
HierholzerAlgorithm fromNull = new HierholzerAlgorithm(null);
assertTrue(fromNull.hasEulerianCircuit());
List<Integer> nullCircuit = fromNull.findEulerianCircuit();
logCase("fallback-null-graph", "graph=null", Collections.emptyList(), nullCircuit);
assertEquals(Collections.emptyList(), nullCircuit);

Map<Integer, LinkedList<Integer>> emptyMap = new HashMap<>();
HierholzerAlgorithm fromEmpty = new HierholzerAlgorithm(emptyMap);
assertTrue(fromEmpty.hasEulerianCircuit());
List<Integer> emptyCircuit = fromEmpty.findEulerianCircuit();
logCase("fallback-empty-graph", "graph={}", Collections.emptyList(), emptyCircuit);
assertEquals(Collections.emptyList(), emptyCircuit);
assertTrue(emptyMap.isEmpty());

Map<Integer, LinkedList<Integer>> singleVertex = new HashMap<>();
singleVertex.put(0, new LinkedList<>());
HierholzerAlgorithm fromIsolated = new HierholzerAlgorithm(singleVertex);
assertTrue(fromIsolated.hasEulerianCircuit());
List<Integer> isolatedCircuit = fromIsolated.findEulerianCircuit();
logCase("smallest-isolated-vertex", "graph={0=[]}", List.of(0), isolatedCircuit);
assertEquals(List.of(0), isolatedCircuit);
assertEquals(Map.of(0, new LinkedList<>()), singleVertex);
}

/**
* Smallest odd-degree failure input ({@code {0=[1], 1=[0]}}, degree 1 at line 55):
* asserts {@code hasEulerianCircuit() == false} (line 56) and
* {@code findEulerianCircuit() == []} (line 75), verifies caller-owned {@code graph} stays
* intact, and verifies an ordinary valid call succeeds afterwards.
*/
@Test
public void oddDegreeFailureReturnsEmptyAndPreservesCallerStateBeforeValidCall() {
Map<Integer, LinkedList<Integer>> oddGraph = new HashMap<>();
oddGraph.put(0, new LinkedList<>(Collections.singletonList(1)));
oddGraph.put(1, new LinkedList<>(Collections.singletonList(0)));
Map<Integer, LinkedList<Integer>> snapshotBefore = copyGraph(oddGraph);

HierholzerAlgorithm failedAlgo = new HierholzerAlgorithm(oddGraph);
boolean hasCircuit = failedAlgo.hasEulerianCircuit();
List<Integer> failedResult = failedAlgo.findEulerianCircuit();
logCase("failure-odd-degree", "{0=[1], 1=[0]}",
"hasCircuit=false,circuit=[]", "hasCircuit=" + hasCircuit + ",circuit=" + failedResult);

assertFalse(hasCircuit);
assertEquals(Collections.emptyList(), failedResult);
assertEquals(snapshotBefore, oddGraph);

// Ordinary valid call succeeds after the failure case (and caller-owned graph remains valid).
Map<Integer, LinkedList<Integer>> validTriangle = new HashMap<>();
validTriangle.put(0, new LinkedList<>(Arrays.asList(1, 2)));
validTriangle.put(1, new LinkedList<>(Arrays.asList(0, 2)));
validTriangle.put(2, new LinkedList<>(Arrays.asList(0, 1)));
Map<Integer, LinkedList<Integer>> triangleSnapshot = copyGraph(validTriangle);

HierholzerAlgorithm validAlgo = new HierholzerAlgorithm(validTriangle);
assertTrue(validAlgo.hasEulerianCircuit());
List<Integer> validCircuit = validAlgo.findEulerianCircuit();
logCase("post-failure-valid-triangle", "{0=[1,2], 1=[0,2], 2=[0,1]}",
"[0, 1, 2, 0]", validCircuit);

assertEquals(List.of(0, 1, 2, 0), validCircuit);
assertEquals(triangleSnapshot, validTriangle);
// Repeated call on the same instance succeeds because tempGraph (lines 78-81) copied lists.
assertEquals(List.of(0, 1, 2, 0), validAlgo.findEulerianCircuit());
}

/**
* Disconnected even-degree failure input (two disjoint triangles; passes lines 54-58, fails
* connectivity check at lines 141-142): asserts empty result, unmutated caller state, and a
* subsequent successful ordinary call.
*/
@Test
public void disconnectedEvenDegreeFailureReturnsEmptyBeforeSubsequentValidCall() {
Map<Integer, LinkedList<Integer>> disconnected = new HashMap<>();
disconnected.put(0, new LinkedList<>(Arrays.asList(1, 2)));
disconnected.put(1, new LinkedList<>(Arrays.asList(0, 2)));
disconnected.put(2, new LinkedList<>(Arrays.asList(0, 1)));
disconnected.put(3, new LinkedList<>(Arrays.asList(4, 5)));
disconnected.put(4, new LinkedList<>(Arrays.asList(3, 5)));
disconnected.put(5, new LinkedList<>(Arrays.asList(3, 4)));
Map<Integer, LinkedList<Integer>> snapshotBefore = copyGraph(disconnected);

HierholzerAlgorithm failedAlgo = new HierholzerAlgorithm(disconnected);
boolean hasCircuit = failedAlgo.hasEulerianCircuit();
List<Integer> failedResult = failedAlgo.findEulerianCircuit();
logCase("failure-disconnected-even", "2 disjoint triangles {0,1,2} & {3,4,5}",
"hasCircuit=false,circuit=[]", "hasCircuit=" + hasCircuit + ",circuit=" + failedResult);

assertFalse(hasCircuit);
assertEquals(Collections.emptyList(), failedResult);
assertEquals(snapshotBefore, disconnected);

// Connect the caller-owned graph into a valid single Eulerian component or run a fresh valid one.
Map<Integer, LinkedList<Integer>> validTriangle = new HashMap<>();
validTriangle.put(0, new LinkedList<>(Arrays.asList(1, 2)));
validTriangle.put(1, new LinkedList<>(Arrays.asList(0, 2)));
validTriangle.put(2, new LinkedList<>(Arrays.asList(0, 1)));
HierholzerAlgorithm validAlgo = new HierholzerAlgorithm(validTriangle);
List<Integer> validCircuit = validAlgo.findEulerianCircuit();
logCase("post-disconnected-valid", "{0=[1,2], 1=[0,2], 2=[0,1]}",
"[0, 1, 2, 0]", validCircuit);

assertTrue(validAlgo.hasEulerianCircuit());
assertEquals(List.of(0, 1, 2, 0), validCircuit);
}
}
70 changes: 70 additions & 0 deletions work/R45-T018/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Runner for task R45-T018: failure-path checks for HierholzerAlgorithm.

Runs the focused JUnit test through Maven, requires a nonzero test count,
saves inputs, expected versus actual results, and tool versions into work/R45-T018/,
and exits non-zero on any failure.
"""
import glob
import subprocess
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

HERE = Path(__file__).resolve().parent
REPO = HERE.parent.parent
TEST_CLASS = "HierholzerAlgorithmFailureTest"
CMD = ["mvn", "-B", "test", f"-Dtest={TEST_CLASS}", "-Dsurefire.failIfNoSpecifiedTests=false"]


def tool_version(cmd):
try:
out = subprocess.run(cmd, capture_output=True, text=True, cwd=REPO)
return "\n".join(l for l in (out.stdout + out.stderr).splitlines()
if not l.startswith("Picked up")).strip()
except FileNotFoundError:
return f"{cmd[0]}: not found"


def main():
(HERE / "tool_versions.txt").write_text(
"java:\n" + tool_version(["java", "-version"]) + "\n\nmvn:\n"
+ tool_version(["mvn", "-v"]) + "\n\npython:\n" + sys.version + "\n")

print("Running:", " ".join(CMD))
proc = subprocess.run(CMD, capture_output=True, text=True, cwd=REPO)
combined_output = proc.stdout + proc.stderr
(HERE / "mvn_output.txt").write_text(combined_output)

records = [line.strip() for line in combined_output.splitlines()
if line.strip().startswith("[R45-T018]")]
(HERE / "comparisons.txt").write_text(
("\n".join(records) + "\n") if records else "No failure-path records captured.\n")

reports = glob.glob(str(REPO / "target/surefire-reports" / f"TEST-*{TEST_CLASS}.xml"))
if not reports:
(HERE / "result.txt").write_text(
f"command: {' '.join(CMD)}\nFAIL: no surefire report found\n")
print("FAIL: no surefire report found")
return 1

suite = ET.parse(reports[0]).getroot()
tests = int(suite.get("tests", 0))
failures = int(suite.get("failures", 0)) + int(suite.get("errors", 0))
skipped = int(suite.get("skipped", 0))
passed = proc.returncode == 0 and failures == 0 and tests > 0 and len(records) > 0
status = "PASS" if passed else "FAIL"
summary = (
f"command: {' '.join(CMD)}\n"
f"tests={tests} failures/errors={failures} skipped={skipped} "
f"records={len(records)} mvn_exit={proc.returncode}\n"
f"status={status}\n"
)
(HERE / "result.txt").write_text(summary)
print(summary)

return 0 if passed else 1


if __name__ == "__main__":
sys.exit(main())
7 changes: 7 additions & 0 deletions work/R45-T018/comparisons.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[R45-T018] failure-odd-degree | input={0=[1], 1=[0]} | expected=hasCircuit=false,circuit=[] | actual=hasCircuit=false,circuit=[]
[R45-T018] post-failure-valid-triangle | input={0=[1,2], 1=[0,2], 2=[0,1]} | expected=[0, 1, 2, 0] | actual=[0, 1, 2, 0]
[R45-T018] failure-disconnected-even | input=2 disjoint triangles {0,1,2} & {3,4,5} | expected=hasCircuit=false,circuit=[] | actual=hasCircuit=false,circuit=[]
[R45-T018] post-disconnected-valid | input={0=[1,2], 1=[0,2], 2=[0,1]} | expected=[0, 1, 2, 0] | actual=[0, 1, 2, 0]
[R45-T018] fallback-null-graph | input=graph=null | expected=[] | actual=[]
[R45-T018] fallback-empty-graph | input=graph={} | expected=[] | actual=[]
[R45-T018] smallest-isolated-vertex | input=graph={0=[]} | expected=[0] | actual=[0]
56 changes: 56 additions & 0 deletions work/R45-T018/mvn_output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
[INFO] Scanning for projects...
[INFO] Loaded 24447 auto-discovered prefixes for remote repository central (prefixes-central.txt)
[INFO]
[INFO] -----------------------< com.thealgorithms:Java >-----------------------
[INFO] Building Java 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- jacoco:0.8.16:prepare-agent (default) @ Java ---
[INFO] Loaded 64 auto-discovered prefixes for remote repository apache.snapshots (prefixes-apache.snapshots.txt)
[INFO] argLine set to -javaagent:/Users/shyamlibakale/.m2/repository/org/jacoco/org.jacoco.agent/0.8.16/org.jacoco.agent-0.8.16-runtime.jar=destfile=/Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/target/jacoco.exec
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ Java ---
[INFO] skip non existing resourceDirectory /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/src/main/resources
[INFO]
[INFO] --- compiler:3.16.0:compile (default-compile) @ Java ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 848 source files with javac [debug release 21] to target/classes
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ Java ---
[INFO] skip non existing resourceDirectory /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/src/test/resources
[INFO]
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ Java ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 797 source files with javac [debug release 21] to target/test-classes
[INFO]
[INFO] --- surefire:3.6.0:test (default-test) @ Java ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.thealgorithms.graph.HierholzerAlgorithmFailureTest
[R45-T018] failure-odd-degree | input={0=[1], 1=[0]} | expected=hasCircuit=false,circuit=[] | actual=hasCircuit=false,circuit=[]
[R45-T018] post-failure-valid-triangle | input={0=[1,2], 1=[0,2], 2=[0,1]} | expected=[0, 1, 2, 0] | actual=[0, 1, 2, 0]
[R45-T018] failure-disconnected-even | input=2 disjoint triangles {0,1,2} & {3,4,5} | expected=hasCircuit=false,circuit=[] | actual=hasCircuit=false,circuit=[]
[R45-T018] post-disconnected-valid | input={0=[1,2], 1=[0,2], 2=[0,1]} | expected=[0, 1, 2, 0] | actual=[0, 1, 2, 0]
[R45-T018] fallback-null-graph | input=graph=null | expected=[] | actual=[]
[R45-T018] fallback-empty-graph | input=graph={} | expected=[] | actual=[]
[R45-T018] smallest-isolated-vertex | input=graph={0=[]} | expected=[0] | actual=[0]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.044 s -- in com.thealgorithms.graph.HierholzerAlgorithmFailureTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- jacoco:0.8.16:report (generate-code-coverage-report) @ Java ---
[INFO] Loading execution data file /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/target/jacoco.exec
[INFO] Analyzed bundle 'Java' with 1073 classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.856 s
[INFO] Finished at: 2026-11-11T19:22:36+05:30
[INFO] ------------------------------------------------------------------------
3 changes: 3 additions & 0 deletions work/R45-T018/result.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
command: mvn -B test -Dtest=HierholzerAlgorithmFailureTest -Dsurefire.failIfNoSpecifiedTests=false
tests=3 failures/errors=0 skipped=0 records=7 mvn_exit=0
status=PASS
14 changes: 14 additions & 0 deletions work/R45-T018/tool_versions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
java:
openjdk version "21.0.13" 2026-10-20 LTS
OpenJDK Runtime Environment Temurin-21.0.13+9 (build 21.0.13+9-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.13+9 (build 21.0.13+9-LTS, mixed mode, sharing)

mvn:
Apache Maven 3.10.0 (c43a36b8d67be7e0805a411bc0898af1a51f5472)
Maven home: /opt/homebrew/Cellar/maven/3.10.0/libexec
Java version: 27.0.1.0.3, vendor: Homebrew, runtime: /opt/homebrew/Cellar/openjdk/27.0.1.0.3/libexec/openjdk.jdk/Contents/Home
Default locale: en_IN, platform encoding: UTF-8, time zone: Asia/Kolkata
OS name: "mac os x", version: "27.0", arch: "aarch64", family: "mac"

python:
3.14.7 (v3.14.7:823f0323ee6, Aug 5 2026, 07:07:01) [Clang 21.0.0 (clang-2100.1.1.101)]