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
17 changes: 16 additions & 1 deletion src/main/java/com/thealgorithms/ciphers/Autokey.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,22 @@
*/
public class Autokey {

// Encrypts the plaintext using the Autokey cipher
/**
* Encrypts the plaintext using the Autokey cipher.
*
* <p>Protected conditions:
* <ul>
* <li>Normalizes {@code plaintext} to uppercase and strips non-letter characters
* ({@code [^A-Z]}) before indexing, and uppercases {@code keyword}.</li>
* <li>Appends the sanitized {@code plaintext} to {@code keyword} to form the autokey stream,
* covering all {@code 0 <= i < plaintext.length()} characters.</li>
* <li>Wraps character sums modulo 26 over {@code 'A'..'Z'}.</li>
* </ul>
*
* @param plaintext input message to encrypt
* @param keyword initial cipher keyword
* @return uppercase ciphertext over {@code 'A'..'Z'}
*/
public String encrypt(String plaintext, String keyword) {
plaintext = plaintext.toUpperCase().replaceAll("[^A-Z]", ""); // Sanitize input
keyword = keyword.toUpperCase();
Expand Down
62 changes: 62 additions & 0 deletions src/test/java/com/thealgorithms/ciphers/AutokeyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,66 @@ void autokeyDecryptTest() {
// then
assertEquals("MEETATDAWN", plainText);
}

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

/**
* Mutation-resistant regression for {@link Autokey#encrypt(String, String)}.
*
* <p>Source preconditions ({@code Autokey.java}):
* <ul>
* <li>lines 31-32: uppercases {@code plaintext} and strips {@code [^A-Z]} (line 31);
* uppercases {@code keyword} (line 32)</li>
* <li>lines 34-35: {@code extendedKey = keyword + sanitizedPlaintext}</li>
* <li>line 39: loop runs for {@code 0 <= i < plaintext.length()}</li>
* <li>line 43: {@code (plainChar - 'A' + keyChar - 'A') % 26 + 'A'} wraps modulo 26</li>
* </ul>
*/
@Test
void encryptSmallestOrdinaryAndBoundaryInputsDetectMutants() {
// Smallest valid inputs: empty after sanitization (line 31 & line 39) and 1-char minimal
String emptyActual = autokeyCipher.encrypt("", "A");
logCase("smallest-empty", "(\"\", \"A\")", "", emptyActual);
assertEquals("", emptyActual);

String strippedEmptyActual = autokeyCipher.encrypt("123!", "A");
logCase("smallest-nonalpha", "(\"123!\", \"A\")", "", strippedEmptyActual);
assertEquals("", strippedEmptyActual);

// Minimal 1-char plaintext protects loop bound `i < plaintext.length()` (line 39)
String minCharActual = autokeyCipher.encrypt("A", "B");
logCase("smallest-one-char", "(\"A\", \"B\")", "B", minCharActual);
assertEquals("B", minCharActual);

// Wrap-around boundary: ('Z' - 'A' + 'B' - 'A') % 26 + 'A' = 26 % 26 + 'A' = 'A' (line 43)
String wrapActual = autokeyCipher.encrypt("Z", "B");
logCase("boundary-mod-26-wrap", "(\"Z\", \"B\")", "A", wrapActual);
assertEquals("A", wrapActual);

// Minimal autokey extension: keyword "D" (len 1) + plaintext "BC" -> extendedKey "DBC" (line 35)
// i=0: B(1)+D(3)=E(4); i=1: C(2)+B(1)=D(3)
String autokeyActual = autokeyCipher.encrypt("BC", "D");
logCase("boundary-autokey-extension", "(\"BC\", \"D\")", "ED", autokeyActual);
assertEquals("ED", autokeyActual);

// Ordinary input with lowercase, spaces, and 12-char plaintext > 5-char keyword:
// "attack at dawn" -> "ATTACKATDAWN", key "queen" -> "QUEENATTACKATDAWN" -> "QNXEPKTMDCGN"
String origPlain = "attack at dawn";
String origKey = "queen";
String ordinaryActual = autokeyCipher.encrypt(origPlain, origKey);
logCase("ordinary-lowercase-spaced", "(\"attack at dawn\", \"queen\")",
"QNXEPKTMDCGN", ordinaryActual);
assertEquals("QNXEPKTMDCGN", ordinaryActual);
assertEquals(12, ordinaryActual.length());

// Fresh equivalent sanitized call produces identical output; inputs remain unmutated
String freshEquivalent = autokeyCipher.encrypt("ATTACKATDAWN", "QUEEN");
assertEquals("QNXEPKTMDCGN", freshEquivalent);
assertEquals("attack at dawn", origPlain);
assertEquals("queen", origKey);
assertEquals("ATTACKATDAWN", autokeyCipher.decrypt(ordinaryActual, origKey));
}
}
70 changes: 70 additions & 0 deletions work/R45-T020/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Runner for task R45-T020: mutation-resistant checks for Autokey.encrypt.

Runs the focused JUnit test through Maven, requires a nonzero test count,
saves inputs, expected versus actual results, and tool versions into work/R45-T020/,
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 = "AutokeyCipherTest"
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-T020]")]
(HERE / "comparisons.txt").write_text(
("\n".join(records) + "\n") if records else "No regression 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())
6 changes: 6 additions & 0 deletions work/R45-T020/comparisons.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[R45-T020] smallest-empty | input=("", "A") | expected= | actual=
[R45-T020] smallest-nonalpha | input=("123!", "A") | expected= | actual=
[R45-T020] smallest-one-char | input=("A", "B") | expected=B | actual=B
[R45-T020] boundary-mod-26-wrap | input=("Z", "B") | expected=A | actual=A
[R45-T020] boundary-autokey-extension | input=("BC", "D") | expected=ED | actual=ED
[R45-T020] ordinary-lowercase-spaced | input=("attack at dawn", "queen") | expected=QNXEPKTMDCGN | actual=QNXEPKTMDCGN
55 changes: 55 additions & 0 deletions work/R45-T020/mvn_output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[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 796 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.ciphers.AutokeyCipherTest
[R45-T020] smallest-empty | input=("", "A") | expected= | actual=
[R45-T020] smallest-nonalpha | input=("123!", "A") | expected= | actual=
[R45-T020] smallest-one-char | input=("A", "B") | expected=B | actual=B
[R45-T020] boundary-mod-26-wrap | input=("Z", "B") | expected=A | actual=A
[R45-T020] boundary-autokey-extension | input=("BC", "D") | expected=ED | actual=ED
[R45-T020] ordinary-lowercase-spaced | input=("attack at dawn", "queen") | expected=QNXEPKTMDCGN | actual=QNXEPKTMDCGN
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.045 s -- in com.thealgorithms.ciphers.AutokeyCipherTest
[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.787 s
[INFO] Finished at: 2026-11-11T20:29:56+05:30
[INFO] ------------------------------------------------------------------------
3 changes: 3 additions & 0 deletions work/R45-T020/result.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
command: mvn -B test -Dtest=AutokeyCipherTest -Dsurefire.failIfNoSpecifiedTests=false
tests=3 failures/errors=0 skipped=0 records=6 mvn_exit=0
status=PASS
14 changes: 14 additions & 0 deletions work/R45-T020/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)]