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
28 changes: 27 additions & 1 deletion src/main/java/com/thealgorithms/searches/DepthFirstSearch.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,35 @@
import java.util.Optional;

/**
* Depth-First Search (DFS) on generic tree structures.
*
* <p>State & Dependency Contract:
* <ul>
* <li><b>State / Mutation:</b> Maintains an internal list of visited node values traversed during recursive search calls.
* Tree nodes themselves are never mutated.</li>
* <li><b>External Dependencies:</b> None (no live network, time, filesystem, randomness, or GUI dependencies).
* Operates as a deterministic in-memory traversal algorithm.</li>
* <li><b>Dependency Injection:</b> Accepts an optional custom {@link List} sink for tracking visited nodes,
* defaulting to a new {@link ArrayList} to preserve all existing callers.</li>
* <li><b>Repeatability:</b> Given the same tree and target value, {@link #recursiveSearch(Node, Integer)} produces
* identical search outcomes regardless of prior searches.</li>
* </ul>
*
* @author: caos321
* @date: 31 October 2021 (Sunday)
* @wiki: https://en.wikipedia.org/wiki/Depth-first_search
*/
public class DepthFirstSearch<T> {

private final List<T> visited = new ArrayList<>();
private final List<T> visited;

public DepthFirstSearch() {
this(new ArrayList<>());
}

public DepthFirstSearch(final List<T> visited) {
this.visited = visited != null ? visited : new ArrayList<>();
}

public Optional<Node<T>> recursiveSearch(final Node<T> node, final Integer value) {
if (node == null) {
Expand All @@ -29,4 +51,8 @@ public Optional<Node<T>> recursiveSearch(final Node<T> node, final Integer value
public List<T> getVisited() {
return visited;
}

public void clearVisited() {
visited.clear();
}
}
98 changes: 98 additions & 0 deletions src/test/java/com/thealgorithms/searches/DepthFirstSearchTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.thealgorithms.datastructures.Node;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -90,4 +92,100 @@ void testSearchValueThatNotExists() {
// check path is the whole list
assertArrayEquals(expectedPath.toArray(), dfs.getVisited().toArray());
}

/**
* Smallest valid input fixture: null node.
* Preconditions established at:
* - DepthFirstSearch.java lines 41-43 (if node == null return Optional.empty())
*/
@Test
void testSmallestValidInputNullNodeFixture() {
Node<Integer> node = null;
Integer target = 42;

Optional<Node<Integer>> result = dfs.recursiveSearch(node, target);
assertTrue(result.isEmpty(), "Searching null node must return Optional.empty().");
assertTrue(dfs.getVisited().isEmpty(), "Visiting null node must not add elements to visited.");
}

/**
* Ordinary input fixture: tree root with target 4.
* Preconditions established at:
* - DepthFirstSearch.java lines 41-43 (non-null node check)
* - DepthFirstSearch.java line 44 (visited.add)
* - DepthFirstSearch.java line 45 (matching value check)
* - DepthFirstSearch.java line 49 (children recursive stream search)
*/
@Test
void testOrdinaryInputWorkflowFixture() {
Integer target = 4;
Optional<Node<Integer>> result = dfs.recursiveSearch(root, target);

assertTrue(result.isPresent(), "Target node 4 must be found in the tree.");
assertEquals(target, result.get().getValue(), "Found node value must equal 4.");
List<Integer> expectedVisited = List.of(1, 2, 5, 6, 3, 4);
assertEquals(expectedVisited, dfs.getVisited(), "Visited path must follow preorder DFS traversal.");
}

/**
* Repeatability test: run the same ordinary fixture twice in fresh isolated state
* and once after another valid search.
* Asserts that earlier runs cannot alter later search outcomes.
*/
@Test
void testRepeatabilityAndStateIsolation() {
Integer target = 4;
List<Integer> expectedVisited = List.of(1, 2, 5, 6, 3, 4);

// Run 1 in fresh isolated instance
DepthFirstSearch<Integer> freshInstance1 = new DepthFirstSearch<>();
Optional<Node<Integer>> result1 = freshInstance1.recursiveSearch(root, target);
assertTrue(result1.isPresent());
assertEquals(target, result1.get().getValue());
assertEquals(expectedVisited, freshInstance1.getVisited());

// Run 2 in fresh isolated instance
DepthFirstSearch<Integer> freshInstance2 = new DepthFirstSearch<>();
Optional<Node<Integer>> result2 = freshInstance2.recursiveSearch(root, target);
assertTrue(result2.isPresent());
assertEquals(target, result2.get().getValue());
assertEquals(expectedVisited, freshInstance2.getVisited());

// Asserts identical outcomes between fresh instances
assertEquals(result1.get().getValue(), result2.get().getValue(), "Search results across isolated instances must be identical.");
assertEquals(freshInstance1.getVisited(), freshInstance2.getVisited(), "Visited history across isolated instances must be identical.");

// Run 3: execute another valid search (target = 1) on an existing instance, then run the fixture
DepthFirstSearch<Integer> reusedInstance = new DepthFirstSearch<>();
Optional<Node<Integer>> priorSearch = reusedInstance.recursiveSearch(root, 1);
assertTrue(priorSearch.isPresent());
assertEquals(List.of(1), reusedInstance.getVisited());

// Clean accumulator seam
reusedInstance.clearVisited();

Optional<Node<Integer>> result3 = reusedInstance.recursiveSearch(root, target);
assertTrue(result3.isPresent());
assertEquals(target, result3.get().getValue(), "Prior run must not alter subsequent search result.");
assertEquals(expectedVisited, reusedInstance.getVisited(), "Cleaned instance must match isolated visited path.");
}

/**
* Dependency injection seam test: custom visited sink list injection.
* Preconditions established at:
* - DepthFirstSearch.java lines 37-39 (custom constructor dependency seam)
*/
@Test
void testDependencyInjectionSeam() {
List<Integer> customSink = new ArrayList<>();
DepthFirstSearch<Integer> injectedDfs = new DepthFirstSearch<>(customSink);

Optional<Node<Integer>> found = injectedDfs.recursiveSearch(root, 2);
assertTrue(found.isPresent());
assertEquals(2, found.get().getValue());

// Visited values are directly recorded in the injected sink
assertNotNull(customSink);
assertEquals(List.of(1, 2), customSink, "Injected sink must record visited nodes directly.");
}
}
192 changes: 192 additions & 0 deletions work/R45-T005/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""
Focused runner and repeatability verification check for R45-T005:
Make verification repeatable for recursiveSearch (DepthFirstSearch.java).
"""

import json
import os
import re
import subprocess
import sys
from pathlib import Path


def ensure_environment():
java_bin_dir = "/opt/homebrew/opt/openjdk@21/bin"
java_home = "/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home"
current_path = os.environ.get("PATH", "")
if java_bin_dir not in current_path:
os.environ["PATH"] = f"{java_bin_dir}:/opt/homebrew/bin:{current_path}"
if "JAVA_HOME" not in os.environ or not os.environ["JAVA_HOME"]:
os.environ["JAVA_HOME"] = java_home


def get_tool_versions():
ensure_environment()
versions = {}

# Python version
versions["python"] = sys.version.replace("\n", " ")

# Java version
try:
res = subprocess.run(
["java", "-version"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True
)
versions["java"] = (res.stderr or res.stdout).strip().splitlines()[0]
except Exception as e:
versions["java"] = f"Error: {e}"

# Maven version
try:
res = subprocess.run(
["mvn", "-version"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True
)
versions["maven"] = res.stdout.strip().splitlines()[0]
except Exception as e:
versions["maven"] = f"Error: {e}"

return versions


def run_checks():
work_dir = Path(__file__).resolve().parent
repo_root = work_dir.parent.parent

print(f"[*] Running R45-T005 repeatability verification in {repo_root}")

tool_versions = get_tool_versions()
print(f"[*] Python: {tool_versions.get('python')}")
print(f"[*] Java: {tool_versions.get('java')}")
print(f"[*] Maven: {tool_versions.get('maven')}")

test_cmd = ["mvn", "test", "-Dtest=DepthFirstSearchTest"]
actual_command_str = " ".join(test_cmd)
print(f"[*] Executing native check: {actual_command_str}")

proc = subprocess.run(
test_cmd,
cwd=repo_root,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)

combined_output = proc.stdout + "\n" + proc.stderr

# Parse JUnit test count and results
match = re.search(r"Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)", combined_output)
if not match:
print("[!] Failed to locate JUnit test execution summary in maven output!")
print(combined_output)
sys.exit(1)

tests_run = int(match.group(1))
failures = int(match.group(2))
errors = int(match.group(3))
skipped = int(match.group(4))

print(f"[*] Test results: Tests run={tests_run}, Failures={failures}, Errors={errors}, Skipped={skipped}")

if tests_run == 0:
print("[!] Non-zero test count required, but found 0!")
sys.exit(1)

if failures != 0 or errors != 0 or proc.returncode != 0:
print(f"[!] Test failures or errors detected (exit code {proc.returncode})!")
sys.exit(1)

repeatability_fixtures = [
{
"name": "Smallest Valid Input (Null Node)",
"precondition_lines": "DepthFirstSearch.java:41-43 (if node == null return Optional.empty())",
"literal_inputs": "node = null, value = 42",
"expected_results": "Optional.empty(), visited list remains empty []",
"actual_results": "Optional.empty() returned, visited is empty; passed in testSmallestValidInputNullNodeFixture"
},
{
"name": "Ordinary Input (Preorder Tree Search)",
"precondition_lines": "DepthFirstSearch.java:41-43, 44, 45, 49",
"literal_inputs": "Root 1 (children 2, 3, 4; 2 has 5, 6), target = 4",
"expected_results": "Optional.of(node4), visited = [1, 2, 5, 6, 3, 4]",
"actual_results": "Node 4 found, visited matched [1, 2, 5, 6, 3, 4]; passed in testOrdinaryInputWorkflowFixture"
},
{
"name": "Repeatability in Fresh Isolated States",
"precondition_lines": "DepthFirstSearch.java:33-35 (fresh instance constructor), 41-49",
"literal_inputs": "Two distinct fresh instances executing fixture (root, 4)",
"expected_results": "Both instances produce strictly identical search results and identical visited sequences",
"actual_results": "Outcome 1 == Outcome 2, Visited 1 == Visited 2; passed in testRepeatabilityAndStateIsolation"
},
{
"name": "Sequential Run Input Isolation",
"precondition_lines": "DepthFirstSearch.java:41-49, 56-58 (clearVisited seam)",
"literal_inputs": "Instance runs target 1 first, then runs fixture (root, 4)",
"expected_results": "Earlier run does not alter subsequent search result for target 4; clearVisited resets accumulator",
"actual_results": "Result for target 4 is identical to fresh isolated runs; passed in testRepeatabilityAndStateIsolation"
},
{
"name": "Dependency Injection Seam",
"precondition_lines": "DepthFirstSearch.java:37-39 (custom constructor dependency seam)",
"literal_inputs": "new DepthFirstSearch<>(customSinkList), search target 2",
"expected_results": "Node 2 found, visited recorded directly in injected list sink [1, 2]",
"actual_results": "Node 2 found, customSink == [1, 2]; passed in testDependencyInjectionSeam"
}
]

result_data = {
"task": "R45-T005",
"target_file": "src/main/java/com/thealgorithms/searches/DepthFirstSearch.java",
"test_file": "src/test/java/com/thealgorithms/searches/DepthFirstSearchTest.java",
"tool_versions": tool_versions,
"actual_command": actual_command_str,
"nonzero_test_count": tests_run,
"failures": failures,
"errors": errors,
"skipped": skipped,
"status": "SUCCESS",
"external_dependencies": "None (pure deterministic in-memory traversal algorithm; no time, network, filesystem, or UI dependencies)",
"fixtures": repeatability_fixtures
}

results_path = work_dir / "results.json"
with open(results_path, "w", encoding="utf-8") as f:
json.dump(result_data, f, indent=2)
print(f"[*] Saved test results to {results_path}")

evidence_path = work_dir / "evidence.md"
with open(evidence_path, "w", encoding="utf-8") as f:
f.write("# Verification Evidence for R45-T005\n\n")
f.write("- **Target**: `src/main/java/com/thealgorithms/searches/DepthFirstSearch.java`\n")
f.write("- **Test Suite**: `src/test/java/com/thealgorithms/searches/DepthFirstSearchTest.java`\n")
f.write(f"- **Command**: `{actual_command_str}`\n")
f.write(f"- **Tests Run**: {tests_run} (Failures: {failures}, Errors: {errors}, Skipped: {skipped})\n")
f.write("- **Status**: SUCCESS\n")
f.write("- **External Dependencies**: None (pure deterministic tree search)\n\n")
f.write("## Tool Versions\n\n")
for tool, ver in tool_versions.items():
f.write(f"- **{tool.capitalize()}**: `{ver}`\n")
f.write("\n## Fixtures and Repeatability Contracts\n\n")
for fix in repeatability_fixtures:
f.write(f"### {fix['name']}\n")
f.write(f"- **Precondition Lines**: {fix['precondition_lines']}\n")
f.write(f"- **Literal Inputs**: `{fix['literal_inputs']}`\n")
f.write(f"- **Expected Results**: {fix['expected_results']}\n")
f.write(f"- **Actual Results**: {fix['actual_results']}\n\n")
print(f"[*] Saved evidence to {evidence_path}")

print("[*] All checks passed successfully.")
sys.exit(0)


if __name__ == "__main__":
run_checks()
47 changes: 47 additions & 0 deletions work/R45-T005/evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Verification Evidence for R45-T005

- **Target**: `src/main/java/com/thealgorithms/searches/DepthFirstSearch.java`
- **Test Suite**: `src/test/java/com/thealgorithms/searches/DepthFirstSearchTest.java`
- **Command**: `mvn test -Dtest=DepthFirstSearchTest`
- **Tests Run**: 8 (Failures: 0, Errors: 0, Skipped: 0)
- **Status**: SUCCESS
- **External Dependencies**: None (pure deterministic tree search)

## Tool Versions

- **Python**: `3.15.2 (v3.15.2:5e58eaad7c5, Oct 25 2026, 14:19:34) [Clang 21.0.0 (clang-2100.1.1.101)]`
- **Java**: `openjdk version "21.0.13.0.3" 2026-10-24`
- **Maven**: `Apache Maven 3.10.0 (c43a36b8d67be7e0805a411bc0898af1a51f5472)`

## Fixtures and Repeatability Contracts

### Smallest Valid Input (Null Node)
- **Precondition Lines**: DepthFirstSearch.java:41-43 (if node == null return Optional.empty())
- **Literal Inputs**: `node = null, value = 42`
- **Expected Results**: Optional.empty(), visited list remains empty []
- **Actual Results**: Optional.empty() returned, visited is empty; passed in testSmallestValidInputNullNodeFixture

### Ordinary Input (Preorder Tree Search)
- **Precondition Lines**: DepthFirstSearch.java:41-43, 44, 45, 49
- **Literal Inputs**: `Root 1 (children 2, 3, 4; 2 has 5, 6), target = 4`
- **Expected Results**: Optional.of(node4), visited = [1, 2, 5, 6, 3, 4]
- **Actual Results**: Node 4 found, visited matched [1, 2, 5, 6, 3, 4]; passed in testOrdinaryInputWorkflowFixture

### Repeatability in Fresh Isolated States
- **Precondition Lines**: DepthFirstSearch.java:33-35 (fresh instance constructor), 41-49
- **Literal Inputs**: `Two distinct fresh instances executing fixture (root, 4)`
- **Expected Results**: Both instances produce strictly identical search results and identical visited sequences
- **Actual Results**: Outcome 1 == Outcome 2, Visited 1 == Visited 2; passed in testRepeatabilityAndStateIsolation

### Sequential Run Input Isolation
- **Precondition Lines**: DepthFirstSearch.java:41-49, 56-58 (clearVisited seam)
- **Literal Inputs**: `Instance runs target 1 first, then runs fixture (root, 4)`
- **Expected Results**: Earlier run does not alter subsequent search result for target 4; clearVisited resets accumulator
- **Actual Results**: Result for target 4 is identical to fresh isolated runs; passed in testRepeatabilityAndStateIsolation

### Dependency Injection Seam
- **Precondition Lines**: DepthFirstSearch.java:37-39 (custom constructor dependency seam)
- **Literal Inputs**: `new DepthFirstSearch<>(customSinkList), search target 2`
- **Expected Results**: Node 2 found, visited recorded directly in injected list sink [1, 2]
- **Actual Results**: Node 2 found, customSink == [1, 2]; passed in testDependencyInjectionSeam

Loading