Compare commits

...

10 Commits

Author SHA1 Message Date
maddiebaka
26ff1404a1 Refactor in WriteAwareBruteSolver, add javadoc
Refactor WriteAwareBruteSolver constructor to guard against a situation
in which the class could have an inconsistent internal state
2024-03-28 19:13:31 -04:00
maddiebaka
152d9dc3ce Nearest Neighbor solver, basic caching
Add a nearest neighbor solver (WriteAwareBruteSolver) and some basic
caching to the Java API adapter.

Fix a comment typo with an off-by-one error
2024-03-28 17:00:25 -04:00
maddiebaka
8386ace107 Project architecture and naive solver solution
Set up project architecture interfaces and add a naive implementation
of a solver in DumbBruteSolver
2024-03-26 16:26:51 -04:00
Rui Hu
562742206a Upload performance test result to artifact, cat the result to console
As I didn't find a solution to display the txt in pipeline ui.
2024-03-14 16:33:20 +08:00
Rui Hu
6a89285dd6 Add readme for building 2024-03-14 13:35:49 +08:00
Rui Hu
66f05c56bf Add skip sleep flag to performance analyze 2024-03-14 11:34:25 +08:00
Rui Hu
6782050414 Update unit tests and add performance analyze 2024-03-14 11:30:15 +08:00
Rui Hu
72ae4c0136 Move sleep skip to utils class 2024-03-14 10:57:16 +08:00
Rui Hu
499413f09a Add performance counter 2024-03-14 10:52:13 +08:00
Rui Hu
f005187a72 Add option to fast forward sleeping 2024-03-13 17:34:11 +08:00
21 changed files with 704 additions and 62 deletions

3
.gitignore vendored
View File

@ -39,4 +39,5 @@ bin/
.vscode/ .vscode/
### Mac OS ### ### Mac OS ###
.DS_Store .DS_Store
performance.txt

View File

@ -2,8 +2,29 @@ image: azul/zulu-openjdk:8-latest
stages: stages:
- test - test
- analyze
# verify the padlock impl independently
unit-test-padlock-impl: unit-test-padlock-impl:
stage: test stage: test
script: script:
- ./gradlew :padlock-impl:test - ./gradlew :padlock-impl:test
unit-test-all:
stage: test
script:
- ./gradlew test
artifacts:
when: always
reports:
junit: build/test-results/test/**/TEST-*.xml
run-performance-analyze:
stage: analyze
needs:
- job: unit-test-all
script:
- ./gradlew runPerformanceAnalyze
- cat performance.txt
artifacts:
paths: ['performance.txt']

View File

@ -5,7 +5,7 @@
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="" /> <option name="gradleJvm" value="azul-17" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />

View File

@ -48,6 +48,26 @@ You should design the proper architectures for your code.
Last but not least, readability and maintainability are also important. Last but not least, readability and maintainability are also important.
Take this project as a show off to your designing and coding skills. Take this project as a show off to your designing and coding skills.
## Build
This repo uses Gradle to build and test.
There is a unit test boilerplate and a gradle task configured to
automatically test and evaluate the code when you push your commits.
The `SolutionTestBase` is an abstract class for other tests.
It tests the correctness of your solution and don't care about the run time.
See `SolutionTest` for how to use that.
The `PerformanceAnalyze` is not a unit test, but it do analyze roughly how
fast your solution is. You need to fill in the `solve` method before you run it.
Use `./gradlew test` to run all unit test configured in the project,
and use `./gradlew runPerformanceTest` to get an analysis.
> Note: You don't have to have a local gradle installation.
> The `gradlew` script will download one for you.
> Just install a valid jdk (version >= 8) and very thing should be fine.
## Still have unclear problems? ## Still have unclear problems?
Feel free to contact Jeffrey Freeman (jeffrey.freeman@cleverthis.com). Feel free to contact Jeffrey Freeman (jeffrey.freeman@cleverthis.com).

View File

@ -1,3 +1,5 @@
import java.io.FileOutputStream
plugins { plugins {
id("java") id("java")
} }
@ -11,6 +13,7 @@ repositories {
dependencies { dependencies {
implementation(project(":padlock-impl")) implementation(project(":padlock-impl"))
implementation("org.apache.commons", "commons-text", "1.11.0")
// if you ever need import more dependencies, following this format: // if you ever need import more dependencies, following this format:
// implementation("group-id:project-id:version") // implementation("group-id:project-id:version")
// just like the logback classic // just like the logback classic
@ -27,4 +30,15 @@ java {
tasks.test { tasks.test {
useJUnitPlatform() useJUnitPlatform()
jvmArgs = listOf("-Dfast=true")
}
tasks.register<JavaExec>("runPerformanceAnalyze")
tasks.named<JavaExec>("runPerformanceAnalyze") {
dependsOn("testClasses")
group = "verification"
classpath = sourceSets.test.get().runtimeClasspath
mainClass.set("com.cleverthis.interview.PerformanceAnalyze")
jvmArgs("-Dfast=true")
standardOutput = FileOutputStream("performance.txt")
} }

View File

@ -21,4 +21,5 @@ java {
tasks.test { tasks.test {
useJUnitPlatform() useJUnitPlatform()
jvmArgs = listOf("-Dfast=true")
} }

View File

@ -4,6 +4,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import static com.cleverthis.interview.padlock.Utils.ensureSleep; import static com.cleverthis.interview.padlock.Utils.ensureSleep;
@ -27,10 +28,12 @@ import static com.cleverthis.interview.padlock.Utils.ensureSleep;
* After create, the input buffer is empty, you have to initialize. * After create, the input buffer is empty, you have to initialize.
*/ */
public class PadlockImpl { public class PadlockImpl {
private final boolean debug;
private final int numpadSize; private final int numpadSize;
private final Integer[] inputBuffer; private final Integer[] inputBuffer;
private final Integer[] correctPasscode; private final Integer[] correctPasscode;
// performance counter
private final AtomicLong writeCounter = new AtomicLong(0);
private final AtomicLong checkCounter = new AtomicLong(0);
/** /**
* Create a padlock instance. * Create a padlock instance.
@ -38,17 +41,6 @@ public class PadlockImpl {
* @param numpadSize The number of buttons on the numpad of this lock. * @param numpadSize The number of buttons on the numpad of this lock.
*/ */
public PadlockImpl(int numpadSize) { public PadlockImpl(int numpadSize) {
this(numpadSize, false);
}
/**
* Create a padlock instance.
*
* @param numpadSize The number of buttons on the numpad of this lock.
* @param debug Will skip sleep if is true
*/
PadlockImpl(int numpadSize, boolean debug) {
this.debug = debug;
if (numpadSize < 1) throw new IllegalArgumentException("numpadSize must be a positive number"); if (numpadSize < 1) throw new IllegalArgumentException("numpadSize must be a positive number");
this.numpadSize = numpadSize; this.numpadSize = numpadSize;
this.inputBuffer = new Integer[numpadSize]; this.inputBuffer = new Integer[numpadSize];
@ -73,10 +65,11 @@ public class PadlockImpl {
* @return The old value, null if not initialized. * @return The old value, null if not initialized.
*/ */
public synchronized Integer writeInputBuffer(int address, int keyIndex) { public synchronized Integer writeInputBuffer(int address, int keyIndex) {
if (!debug) ensureSleep(1000); ensureSleep(1000);
if (keyIndex < 0 || keyIndex >= numpadSize) if (keyIndex < 0 || keyIndex >= numpadSize)
throw new IllegalArgumentException( throw new IllegalArgumentException(
"keyIndex out of range. Keypad size: " + numpadSize + ", keyIndex: " + keyIndex); "keyIndex out of range. Keypad size: " + numpadSize + ", keyIndex: " + keyIndex);
writeCounter.incrementAndGet();
Integer oldValue = inputBuffer[address]; Integer oldValue = inputBuffer[address];
inputBuffer[address] = keyIndex; inputBuffer[address] = keyIndex;
return oldValue; return oldValue;
@ -98,10 +91,24 @@ public class PadlockImpl {
"Passcode invalid: contain duplicated value. " + Arrays.toString(inputBuffer)); "Passcode invalid: contain duplicated value. " + Arrays.toString(inputBuffer));
uniqueTestArr[i] = true; uniqueTestArr[i] = true;
} }
checkCounter.incrementAndGet();
// if no exception, means: // if no exception, means:
// every digit is unique, and every digit is initialized // every digit is unique, and every digit is initialized
// aka this is a valid code // aka this is a valid code
// now compare with our answer // now compare with our answer
return Arrays.equals(correctPasscode, inputBuffer); return Arrays.equals(correctPasscode, inputBuffer);
} }
}
public long getWriteCounter() {
return writeCounter.get();
}
public long getCheckCounter() {
return checkCounter.get();
}
public void resetCounter() {
writeCounter.set(0);
checkCounter.set(0);
}
}

View File

@ -1,12 +1,24 @@
package com.cleverthis.interview.padlock; package com.cleverthis.interview.padlock;
class Utils { class Utils {
/**
* Check if the sleep is disabled.
* User can use `-Dfast=true` in the jvm args,
* or change it on the fly.
* Might waste sometime on checking this flag, but the effect should be minor.
* */
private static boolean shouldSkipSleep() {
return Boolean.parseBoolean(System.getProperty("fast"));
}
/** /**
* Ensure we will wait a given amount of time even if there are interruptions. * Ensure we will wait a given amount of time even if there are interruptions.
* Property `-Dfast=true` can disable the sleep.
* *
* @param millis The time you want to sleep, measure in millisecond. * @param millis The time you want to sleep, measure in millisecond.
*/ */
public static void ensureSleep(long millis) { public static void ensureSleep(long millis) {
if (shouldSkipSleep()) return;
long endTime = System.currentTimeMillis() + millis; long endTime = System.currentTimeMillis() + millis;
while (endTime > System.currentTimeMillis()) { while (endTime > System.currentTimeMillis()) {
try { try {

View File

@ -22,7 +22,7 @@ class PadlockTest {
@Test @Test
void testInstantiationRest() { void testInstantiationRest() {
PadlockImpl padlock = new PadlockImpl(5, true); PadlockImpl padlock = new PadlockImpl(5);
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
// ensure input buffer is uninitialized // ensure input buffer is uninitialized
// should return null when first set // should return null when first set
@ -34,7 +34,7 @@ class PadlockTest {
@Test @Test
void testRejectInvalidInput() { void testRejectInvalidInput() {
PadlockImpl padlock = new PadlockImpl(5, true); PadlockImpl padlock = new PadlockImpl(5);
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
padlock.writeInputBuffer(i, i); padlock.writeInputBuffer(i, i);
} }
@ -54,7 +54,7 @@ class PadlockTest {
@Test @Test
void testRejectInvalidInputBufferAddressAndValue() { void testRejectInvalidInputBufferAddressAndValue() {
PadlockImpl padlock = new PadlockImpl(5, true); PadlockImpl padlock = new PadlockImpl(5);
// test address // test address
assertThrows(ArrayIndexOutOfBoundsException.class, () -> padlock.writeInputBuffer(-1, 1)); assertThrows(ArrayIndexOutOfBoundsException.class, () -> padlock.writeInputBuffer(-1, 1));
assertThrows(ArrayIndexOutOfBoundsException.class, () -> padlock.writeInputBuffer(-10, 1)); assertThrows(ArrayIndexOutOfBoundsException.class, () -> padlock.writeInputBuffer(-10, 1));

View File

@ -0,0 +1,106 @@
package com.cleverthis.interview;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
/**
* Brute-forces padlock using lexicographically ordered permutation generation
*
* Algorithm documented at: https://en.wikipedia.org/wiki/Permutation#Generation_in_lexicographic_order
*/
public class DumbBruteSolver implements SolverInterface {
/**
* Solves the padlock passed in to the method. The padlock's internal state should be correct after this method
* runs.
* @param padlockAdapter A padlock conforming to the PadlockAdapter contract
*/
@Override
public void solve(PadlockAdapter padlockAdapter) {
int numpadSize = padlockAdapter.getNumpadSize();
Integer[] currentPermutation = new Integer[numpadSize];
for(int i = 0; i < numpadSize; i++) {
currentPermutation[i] = i;
}
while(true) {
boolean isCorrect = checkPermutation(currentPermutation, padlockAdapter);
if (!isCorrect) {
boolean nextPermutationExists = calculateNextPermutation(currentPermutation, numpadSize);
if(!nextPermutationExists) {
return;
}
} else {
return;
}
}
}
/**
* Writes the permutation to the padlock's memory and checks whether this permutation is the correct passcode.
* This is a naive solution that makes no considerations for write-cost.
* @param permutation The permutation to write to the padlock
* @param padlockAdapter The padlock to write to
* @return True if the correct padlock passcode has been found, false otherwise
*/
protected boolean checkPermutation(Integer[] permutation, PadlockAdapter padlockAdapter) {
for(int i = 0; i < padlockAdapter.getNumpadSize(); i++) {
padlockAdapter.writeInputBuffer(i, permutation[i]);
}
return padlockAdapter.isPasscodeCorrect();
}
/**
* Calculates the next permutation in lexicographic order, based on the algorithm linked on wikipedia
* @param currentPermutation The current permutation to run the algorithm on
* @param numpadSize The number of items in the set to be permuted
* @return true if next permutation successfully generated, false if permutations have been exhausted
*/
protected boolean calculateNextPermutation(Integer[] currentPermutation, int numpadSize) {
if(numpadSize < 2) { return false; }
//Integer k, l;
// Find the k and l indices, such that they meet the criteria for the permutation algorithm.
// If such indice values are found, swap them, then reverse the array subset from k+1 to the end of the array
for(int k = (numpadSize - 2); k >= 0; k--) {
if(currentPermutation[k] < currentPermutation[k + 1]) {
for(int l = (numpadSize - 1); l > k; l--) {
if(currentPermutation[k] < currentPermutation[l]) {
// Swap index k value and index l value in permutations array
// TODO: Could be a better swap algorithm
int tempInt = currentPermutation[k];
currentPermutation[k] = currentPermutation[l];
currentPermutation[l] = tempInt;
// Split the currentPermutation array into two slices. The slice happens at index k, with index k
// inclusive to the first slice
Integer[] firstSlice = Arrays.stream(currentPermutation, 0, k + 1).toArray(Integer[]::new);
Integer[] secondSlice = Arrays.stream(currentPermutation, k + 1, numpadSize).toArray(Integer[]::new);
// Reverse the subset of the permutation array from index k+1 to the end of the array
Collections.reverse(Arrays.asList(secondSlice));
// Concat the non-reversed and reversed subarrays into a new permutation
Integer[] newPermutation = Stream.concat(Arrays.stream(firstSlice), Arrays.stream(secondSlice)).toArray(Integer[]::new);
// Copy the new permutation into currentPermutation to return it
System.arraycopy(newPermutation, 0, currentPermutation, 0, numpadSize);
return true;
}
}
}
}
return false;
}
}

View File

@ -0,0 +1,62 @@
package com.cleverthis.interview;
import org.apache.commons.text.similarity.LevenshteinDistance;
/**
* A wrapper class that holds possible padlock passcode permutations in an integer array. This class
* overloads the toString() method and is comparable, using its string value to calculate the levenshtein distance and
* use those distance values for sorting into a data structure.
*/
public class IntegerLevenshtein implements Comparable<IntegerLevenshtein> {
private Integer[] integerData;
private Integer size;
/**
* Creates a new IntegerLevenshtein
* @param size The number of integers (keypad size) of the passcode
*/
public IntegerLevenshtein(int size) {
this.integerData = new Integer[size];
this.size = size;
}
/**
* Sets the integer data array
* @param integerData The new integer data
*/
public void setIntegerData(Integer[] integerData) {
this.integerData = integerData.clone();
}
/**
* Gets the integer data array
* @return the integer data
*/
public Integer[] getIntegerData() {
return this.integerData;
}
/**
* Casts each integer to a string and concatenates them together into a single string.
* For example, an internal integer array of [1,2,3,4] will be returned as the string "1234".
* @return A string representation of the integer array.
*/
public String toString() {
String temp = new String();
for(int i = 0; i < size; i++) {
temp = temp.concat(integerData[i].toString());
}
return temp;
}
/**
* Overriden compareTo method that compares the calculated levenshtein distance between two IntegerLevenshtein objects
* @param otherInteger the object to be compared.
* @return The levenshtein distance between the two objects.
*/
@Override
public int compareTo(IntegerLevenshtein otherInteger) {
LevenshteinDistance levenshtein = LevenshteinDistance.getDefaultInstance();
return levenshtein.apply(this.toString(), otherInteger.toString());
}
}

View File

@ -0,0 +1,47 @@
package com.cleverthis.interview;
/**
* This defines the interface that padlocks must conform to.
* Concrete implementations will be adapted to this interface contract through concrete adapter classes.
*/
public interface PadlockAdapter {
/**
* Get the size of the padlock's physical number pad
*
* @return A count of the physical buttons on the padlock
*/
int getNumpadSize();
/**
* Write key presses to the input buffer of the padlock
* @param address The position / index of the button that is pressed. For example, address 0 is the first button pressed.
* @param keyIndex The value of the button that is pressed. Cannot be greater than the numpad size, as the buttons increment
* sequentially
* @return The old value of keyIndex at the inputted address
*/
Integer writeInputBuffer(int address, int keyIndex);
/**
* Check whether the inputted password is correct
* @return True if password is correct, false otherwise
*/
boolean isPasscodeCorrect();
/**
* Returns the write counter
* @return The number of times a write operation has occurred
*/
long getWriteCounter();
/**
* Returns the check counter
* @return The number of times the password has been checked for correctness
*/
long getCheckCounter();
/**
* Resets both the check and write counters
*/
void resetCounter();
}

View File

@ -0,0 +1,49 @@
package com.cleverthis.interview;
import com.cleverthis.interview.padlock.PadlockImpl;
/**
* The concrete implementation of PadlockAdapter that communicates with the padlock directly through a Java
* class. This implementation also contains a cache that it keeps in sync with the underlying API, as a front-line
* optimization against unnecessary write operations.
*/
public class PadlockJavaAdapter extends PadlockImpl implements PadlockAdapter {
/**
* Intermediate cache between the underlying padlock API and any code that interfaces with the PadlockAdapter
* API.
*/
private final Integer[] inputBufferState;
/**
* Create a padlock instance.
*
* @param numpadSize The number of buttons on the numpad of this lock.
*/
public PadlockJavaAdapter(int numpadSize) {
super(numpadSize);
inputBufferState = new Integer[numpadSize];
for(int i = 0; i < this.getNumpadSize(); i++) {
inputBufferState[i] = i;
super.writeInputBuffer(i, i);
}
}
/**
* Writes to the underlying input buffer, but only if the cache shows that the write is necessary
* @param address The position / index of the button that is pressed. For example, address 0 is the first button pressed.
* @param keyIndex The value of the button that is pressed. Cannot be greater than the numpad size, as the buttons increment
* sequentially
* @return The old value that was replaced, which is the same as keyIndex if a write operation doesn't actually occur.
*/
@Override
public Integer writeInputBuffer(int address, int keyIndex) {
if(inputBufferState[address] != keyIndex) {
inputBufferState[address] = keyIndex;
return super.writeInputBuffer(address, keyIndex);
} else {
return keyIndex;
}
}
}

View File

@ -1,13 +0,0 @@
package com.cleverthis.interview;
import com.cleverthis.interview.padlock.PadlockImpl;
/**
* This is a placeholder class showing a simple boilerplate.
* This class is not required, so you can replace with your own architecture.
*/
public class Solution {
public void solve(PadlockImpl padlock) {
throw new RuntimeException("TODO");
}
}

View File

@ -0,0 +1,13 @@
package com.cleverthis.interview;
/**
* Interface that defines the class signature for solver implementations
*/
public interface SolverInterface {
/**
* Solves the padlock passed in that conforms to the PadlockAdapter interface
* @param padlockAdapter the padlock object to solve
*/
void solve(PadlockAdapter padlockAdapter);
}

View File

@ -0,0 +1,90 @@
package com.cleverthis.interview;
import java.util.Iterator;
import java.util.TreeSet;
import org.apache.commons.text.similarity.LevenshteinDistance;
/**
* This is a write-aware brute solver implementation that uses the levenshtein distance to sort passcode permutations
* into a tree. Walking the tree in-order (Likely a depth-first traversal internally) results in a naive nearest-neighbor
* optimization that generally performs well, but may perform poorly in some cases. Heuristic solutions exist that have
* acceptable performance, and this is a more naive heuristic implementation.
*
* This permutation optimization problem is NP-hard and can be represented as the traveling salesperson problem.
* Representing passcode permutations as vertices and the levenshtein distance as edges in an undirected, weighted graph,
* an optimal solution is the shortest path satisfying a tour of the entire graph.
*
* A more advanced solution (based on Christofides, or another TSP heuristic) may result in more performance gains, but
* the performance of this nearest-neighbor solution is adequate for the keypad sizes under test. A keypad size of 9
* results in a graph of vertex-count 9!. It's unlikely performance gains from a more intelligent TSP solver would
* offset the performance cost of building a graph and optimizing traversal.
*/
public class WriteAwareBruteSolver extends DumbBruteSolver {
private final TreeSet<IntegerLevenshtein> orderedTree;
private Integer numpadSize;
/**
* Creates an ordered tree on instantiation to be used as a cache for subsequent brute-force solves.
* @param numpadSize The size of the padlock's numpad, used in generation of the internal ordered tree
*/
public WriteAwareBruteSolver(int numpadSize) {
orderedTree = new TreeSet<IntegerLevenshtein>();
this.numpadSize = numpadSize;
this.createOrderedTree(numpadSize);
}
/**
* Solves the padlock passed in to the method. The padlock's internal state should be correct after this method
* runs.
* @param padlockAdapter A padlock conforming to the PadlockAdapter contract
*/
@Override
public void solve(PadlockAdapter padlockAdapter) {
int padlockNumpadSize = padlockAdapter.getNumpadSize();
if (this.numpadSize != padlockNumpadSize) {
this.createOrderedTree(padlockNumpadSize);
}
for (IntegerLevenshtein integerLevenshtein : orderedTree) {
if (this.checkPermutation(integerLevenshtein.getIntegerData(), padlockAdapter)) {
return;
}
}
}
/**
* Returns the size of the tree (the number of possible permutations)
* @return the size of the ordered tree
*/
public Integer getTreeSize() {
return this.orderedTree.size();
}
/**
* Creates an ordered tree of possible passcode permutations
* @param size The number of keys on the numpad
*/
protected void createOrderedTree(int size) {
if(this.numpadSize != size) {
this.numpadSize = size;
orderedTree.clear();
}
Integer[] currentPermutation = new Integer[numpadSize];
for(int i = 0; i < numpadSize; i++) {
currentPermutation[i] = i;
}
boolean morePermutationsExist = true;
do {
IntegerLevenshtein levenshteinPermutation = new IntegerLevenshtein(numpadSize);
levenshteinPermutation.setIntegerData(currentPermutation);
orderedTree.add(levenshteinPermutation);
morePermutationsExist = this.calculateNextPermutation(currentPermutation, numpadSize);
} while(morePermutationsExist);
}
}

View File

@ -0,0 +1,102 @@
package com.cleverthis.interview;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for DumbBruteSolver class
*
* Tests that lexicographically ordered permutations are properly calculated, and that the solver works
*/
public class DumbBruteSolverTest extends SolutionTestBase {
/**
* Overridden method that tests in SolutionTestBase.class expect defined and calls.
* Creates a DumbBruteSolver and calls the solve() method.
* @param padlock The padlock to solve
*/
@Override
protected void solve(PadlockAdapter padlock) {
new DumbBruteSolver().solve(padlock);
}
/**
* Check whether a 7-button padlock can be solved
*/
@Test
protected void testSolver() {
DumbBruteSolver dumbSolver = new DumbBruteSolver();
PadlockJavaAdapter padlock = new PadlockJavaAdapter(7);
dumbSolver.solve(padlock);
assertTrue(padlock.isPasscodeCorrect());
}
/**
* Check whether the next permutation is calculated correctly
*/
@Test
protected void testOnePermutation() {
DumbBruteSolver dumbSolver = new DumbBruteSolver();
Integer[] permutation = new Integer[] {1, 2, 3, 4};
Integer[] correctPermutation = new Integer[] {1, 2, 4, 3};
dumbSolver.calculateNextPermutation(permutation, 4);
assertArrayEquals(permutation, correctPermutation);
}
/**
* Check whether two consecutive permutations are correct
*/
@Test
protected void testTwoPermutations() {
DumbBruteSolver dumbSolver = new DumbBruteSolver();
Integer[] permutation = new Integer[] {1, 2, 3, 4};
Integer[] correctPermutation = new Integer[] {1, 3, 2, 4};
for(int i = 0; i < 2; i++) {
dumbSolver.calculateNextPermutation(permutation, 4);
}
assertArrayEquals(permutation, correctPermutation);
}
/**
* Check whether 23 consecutive permutations are correct
*/
@Test
protected void test23Permutations() {
DumbBruteSolver dumbSolver = new DumbBruteSolver();
Integer[] permutation = new Integer[] {1, 2, 3, 4};
Integer[] correctPermutation = new Integer[] {4, 3, 2, 1};
for(int i = 0; i < 23; i++) {
dumbSolver.calculateNextPermutation(permutation, 4);
}
assertArrayEquals(permutation, correctPermutation);
}
/**
* Check whether the 24th permutation returns false, signifying we've exhausted all possible permutations
* for a list of size 4
*/
@Test
protected void test24Permutations() {
DumbBruteSolver dumbSolver = new DumbBruteSolver();
Integer[] permutation = new Integer[] {1, 2, 3, 4};
for(int i = 0; i < 23; i++) {
assertTrue(dumbSolver.calculateNextPermutation(permutation, 4));
}
assertFalse(dumbSolver.calculateNextPermutation(permutation, 4));
}
}

View File

@ -0,0 +1,51 @@
package com.cleverthis.interview;
import com.cleverthis.interview.padlock.PadlockImpl;
/**
* Performance test but not mean to run in unit test.
*/
public class PerformanceAnalyze {
private static final int TOTAL_RUN = 500;
private static final int NUMPAD_SIZE = 9;
/**
* The solver object reference is held between tests, because the data structure used for optimization does not
* change between runs and keeping it in memory greatly increases performance.
*/
static WriteAwareBruteSolver solver = new WriteAwareBruteSolver(NUMPAD_SIZE);
private static void solve(PadlockAdapter padlock) {
solver.solve(padlock);
}
static {
System.out.println("Total run: " + TOTAL_RUN);
System.out.println("Numpad size: " + NUMPAD_SIZE);
}
public static void main(String[] args) {
long timeSum = 0;
long writeSum = 0;
for (int i = 0; i < TOTAL_RUN; i++) {
PadlockAdapter padlock = new PadlockJavaAdapter(NUMPAD_SIZE);
padlock.resetCounter();
long start = System.currentTimeMillis();
solve((PadlockAdapter) padlock);
long end = System.currentTimeMillis();
if (!padlock.isPasscodeCorrect()) throw new IllegalStateException(
"Invalid solution: passcode not correct after return");
long dT = end - start;
timeSum += dT;
writeSum += padlock.getWriteCounter();
System.out.println("Run #" + (i + 1) + ": time: " + dT + "ms; write: " + padlock.getWriteCounter());
}
System.out.println("Run time sum: " + timeSum + "ms");
System.out.println("Write sum: " + writeSum);
double avgTime = timeSum / (double) TOTAL_RUN;
double avgWrite = writeSum / (double) TOTAL_RUN;
System.out.println("Avg run time: " + avgTime + "ms");
System.out.println("Avg write: " + avgWrite);
System.out.println("Calculated estimate avg run time: " + (avgTime / 1000 + avgTime) + "s");
}
}

View File

@ -1,30 +0,0 @@
package com.cleverthis.interview;
import com.cleverthis.interview.padlock.PadlockImpl;
import org.junit.jupiter.api.Test;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.*;
/**
* This is a simple placeholder to show how unit test works.
* You can replace it with your own test.
*/
class SolutionTest {
private void solve(PadlockImpl padlock) {
new Solution().solve(padlock);
}
@Test
void verify(){
Random random = new Random();
PadlockImpl padlock = new PadlockImpl(random.nextInt(1, 8));
long startTime = System.currentTimeMillis();
solve(padlock);
long endTime = System.currentTimeMillis();
assertTrue(padlock.isPasscodeCorrect());
System.out.println("Time usage: " + (endTime - startTime) + "ms");
}
}

View File

@ -0,0 +1,34 @@
package com.cleverthis.interview;
import com.cleverthis.interview.padlock.PadlockImpl;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* This is a base class for verifying the correctness of the solution.
*/
public abstract class SolutionTestBase {
/**
* Implement your solution in this function.
* */
protected abstract void solve(PadlockAdapter padlock);
protected void verify(int numpadSize) {
PadlockAdapter padlock = new PadlockJavaAdapter(numpadSize);
solve(padlock);
assertTrue(padlock.isPasscodeCorrect());
}
/**
* Tests padlocks with numpad sizes of 1 to 7. This test runs for every class that extends this SolutionTestBase
* abstract class.
*/
@Test
void verify1to7() {
for (int i = 1; i <= 7; i++) {
verify(i);
}
}
}

View File

@ -0,0 +1,55 @@
package com.cleverthis.interview;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests for WriteAwareBruteSolver class
*
* These tests do not keep a long-lived reference to WriteAwareBruteSolver, instead opting
* to reconstruct the permutation tree for every test. This is a trade-off between performance
* and avoiding any inadvertent behavior changes from state persisting between tests.
*/
public class WriteAwareBruteSolverTest extends SolutionTestBase {
/**
* Overridden method that tests in SolutionTestBase.class expect defined and calls.
* Creates a WriteAwareBruteSolver and calls the solve() method.
* @param padlock The padlock to solve
*/
@Override
protected void solve(PadlockAdapter padlock) {
new WriteAwareBruteSolver(padlock.getNumpadSize()).solve(padlock);
}
/**
* Tests whether the solver can brute-force a 7-numpad padlock.
*/
@Test
protected void testSolver() {
Integer numpadSize = 7;
WriteAwareBruteSolver writeAwareSolver = new WriteAwareBruteSolver(numpadSize);
PadlockJavaAdapter padlock = new PadlockJavaAdapter(numpadSize);
writeAwareSolver.solve(padlock);
assertTrue(padlock.isPasscodeCorrect());
}
/**
* Tests whether a numpad-size of 4 is correctly expanded to 24 possible permutations in the underlying
* tree
*/
@Test
protected void testTreeSize() {
Integer numpadSize = 4;
WriteAwareBruteSolver writeAwareSolver = new WriteAwareBruteSolver(numpadSize);
PadlockJavaAdapter padlock = new PadlockJavaAdapter(4);
writeAwareSolver.solve(padlock);
assertEquals(writeAwareSolver.getTreeSize(), 24);
}
}