Skip to content

Commit

Permalink
Merge pull request #79 from ppi-ag/bug-sampler-fixture
Browse files Browse the repository at this point in the history
Bug sampler fixture issue #61
  • Loading branch information
chb-ppi committed Sep 3, 2021
2 parents 2758d09 + eb0d185 commit d48ecc5
Show file tree
Hide file tree
Showing 18 changed files with 456 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package de.ppi.deepsampler.core.internal;

import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

public class FuzzySearchUtility {

/**
* Private constructor to emphasize the utility-nature of this class. It is not meant to be instantiated.
*/
private FuzzySearchUtility() {

}


/**
* Searches for wantedKey in candidates. The search tries to find the String that has the most similarity,
* perfect equality is not necessary.
*
* @param wantedKey The String that is searched in candidates
* @param candidates A {@link List} of Strings that might be equal, or similar to wantedKey.
* @return A pair containing the best matching candidate and a percentage value that shows the similarity.
*/
public static Match<String> findClosestString(String wantedKey, List<String> candidates) {
return findClosestObject(wantedKey, candidates, String::toString);
}

/**
* Searches for wantedKey in candidates. candidates may be a {@link List} of arbitrary objects. candidateKeyProvider()
* is used to get a searchable String from each candidate, that is used for comparison.
* The search tries to find the String that has the most similarity, perfect equality is not necessary.
*
* @param wantedKey The String that is searched in candidates
* @param candidates A {@link List} of arbitrary Objects, that might have a String that is equal, or similar to wantedKey.
* @param candidateKeyProvider A functional interface, that should provide the String from a candidate, that is used for the comparison.
* @return A pair containing the best matching candidate and a percentage value that shows the similarity, or null if candidates is empty.
*/
public static <T> Match<T> findClosestObject(String wantedKey, List<T> candidates, Function<T, String> candidateKeyProvider) {
if (candidates.isEmpty()) {
return null;
}

List<Match<T>> matchedCandidates = candidates.stream()
.map(candidate -> new Match<>(candidate, calcEquality(candidateKeyProvider.apply(candidate), wantedKey)))
.sorted(Comparator.comparingDouble(Match<T>::getEquality))
.collect(Collectors.toList());

return matchedCandidates.get(matchedCandidates.size() - 1);
}


/**
* Calculates how different the two Strings left and right are.
*
* @param left One of the two Strings that are compared.
* @param right The other of two Strings that are compared.
* @return A value between 0 and 1 where 0 means the Strings are completely different and 1 means, that both Strings are equal.
* @see <a href="https://stackoverflow.com/questions/955110/similarity-string-comparison-in-java"/>
*/
public static double calcEquality(String left, String right) {
String longer = left;
String shorter = right;

if (left.length() < right.length()) { // longer should always have greater length
longer = right;
shorter = left;
}

int longerLength = longer.length();

if (longerLength == 0) {
return 1.0; // both strings are zero length
}

return (longerLength - calcEditDistance(longer, shorter)) / (double) longerLength;
}


/**
* Calculates the difference between two Strings using the "Levenshtein Edit Distance" algorithm.
*
* @param longer One of the two Strings that are compared.
* @param shorter The other of two Strings that are compared.
* @return the cost of converting shorter into longer. This can be used to measure the difference between shorter and longer.
* @see <a href="https://stackoverflow.com/questions/955110/similarity-string-comparison-in-java"/>
*/
private static int calcEditDistance(String longer, String shorter) {
longer = longer.toLowerCase();
shorter = shorter.toLowerCase();

int[] costs = new int[shorter.length() + 1];
for (int i = 0; i <= longer.length(); i++) {
int lastValue = i;
for (int j = 0; j <= shorter.length(); j++) {
if (i == 0) {
costs[j] = j;
} else if (j > 0) {
int newValue = costs[j - 1];

if (longer.charAt(i - 1) != shorter.charAt(j - 1)) {
newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
}

costs[j - 1] = lastValue;
lastValue = newValue;
}
}

if (i > 0) {
costs[shorter.length()] = lastValue;
}
}

return costs[shorter.length()];
}

/**
* Describes a String that matches to another String. How similar the compared Strings are is expressed by
* {@link Match#getEquality()}.
*/
public static class Match<T> {
private final double equality;
private final T matchedObject;

public Match(T matchedObject, double equality) {
this.equality = equality;
this.matchedObject = matchedObject;
}

/**
* The extend of equality. A 0 means no equality at all and a 1 means perfect equality.
*
* @return The extend of equality.
*/
public double getEquality() {
return equality;
}

public T getMatchedObject() {
return matchedObject;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ public void add(final SampleDefinition sampleDefinition) {
* @param mergedPersistentSamples The {@link SampleDefinition}s that are inserted at i.
*/
public void replace(int i, List<SampleDefinition> mergedPersistentSamples) {
samples.addAll(i + 1, mergedPersistentSamples);
samples.remove(i);
samples.addAll(i, mergedPersistentSamples);
samples.remove(i + mergedPersistentSamples.size());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package de.ppi.deepsampler.core.internal;

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class FuzzySearchUtilityTest {

@Test
void findClosestString() {
// GIVEN
List<String> candidates = Arrays.asList("X", "ABAB", "C", "D", "");
List<String> emptyCandidates = new ArrayList<>();

// WHEN
FuzzySearchUtility.Match<String> match = FuzzySearchUtility.findClosestString("AB", candidates);
FuzzySearchUtility.Match<String> matchEmptyString = FuzzySearchUtility.findClosestString("", candidates);
FuzzySearchUtility.Match<String> emptyMatch = FuzzySearchUtility.findClosestString("AB", emptyCandidates);

// THEN
assertNotNull(match);
assertEquals("ABAB", match.getMatchedObject());
assertEquals("", matchEmptyString.getMatchedObject());
assertNull(emptyMatch);
}

@Test
void similarity() {
assertEquals(1.0, FuzzySearchUtility.calcEquality("A", "A"));
assertEquals(0.0, FuzzySearchUtility.calcEquality("A", "B"));
assertEquals(0.5, FuzzySearchUtility.calcEquality("AB", "B"));
assertEquals(0.5, FuzzySearchUtility.calcEquality("BA", "B"));
assertEquals(0.25, FuzzySearchUtility.calcEquality("ABAC", "B"));
assertEquals(0.5, FuzzySearchUtility.calcEquality("ABAB", "CBCB"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,29 @@ public static void injectSamplers(final Object targetObject) {
.forEach(field -> JUnitPluginUtils.assignNewSamplerToField(targetObject, field));
}

public static void applyTestFixture(final Method testMethod) {
final UseSamplerFixture fixture = testMethod.getAnnotation(UseSamplerFixture.class);
/**
* The anntation {@link UseSamplerFixture} references a {@link SamplerFixture} that can be used to define Samplers in a
* reusable manner. A test can use a {@link SamplerFixture} if the testMethod or the class that declares testMethod is annotated
* with {@link UseSamplerFixture}. The annotation at method-level overrides the annotation at class-level.
*
* @param testMethod the test-method that should be initialized with a {@link SamplerFixture}
*/
public static void applySamplerFixture(final Method testMethod) {
final UseSamplerFixture fixtureOnMethod = testMethod.getAnnotation(UseSamplerFixture.class);
final UseSamplerFixture fixtureOnClass = testMethod.getDeclaringClass().getAnnotation(UseSamplerFixture.class);

if (fixture == null) {
if (fixtureOnMethod == null && fixtureOnClass == null) {
return;
}

final Class<? extends SamplerFixture> samplerFixtureClass = fixture.value();

final Class<? extends SamplerFixture> samplerFixtureClass;

if (fixtureOnMethod != null) {
samplerFixtureClass = fixtureOnMethod.value();
} else {
samplerFixtureClass = fixtureOnClass.value();
}

try {
final Constructor<? extends SamplerFixture> samplerFixtureClassConstructor = samplerFixtureClass.getConstructor();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
import java.lang.annotation.Target;

/**
* {@link SamplerFixture}s are a convenient way to share a set of Samplers with multiple test cases. If a method within a Junit-Test
* is annotated with {@link UseSamplerFixture}, the associated {@link SamplerFixture} and all Samplers that are defined by
* the {@link SamplerFixture} are prepared before the test method is executed.
* {@link SamplerFixture}s are a convenient way to share a set of Samplers with multiple test cases. If a method within a Junit-Test,
* or the test-class itself, is annotated with {@link UseSamplerFixture}, the associated {@link SamplerFixture} and all
* Samplers that are defined by the {@link SamplerFixture} are prepared before the test method is executed.
*
* If a class is annotated with {@link UseSamplerFixture} the {@link SamplerFixture} is applied to all test-methods in that class.
* Annotations on methods override the annotation on classes.
*
* This Annotation is used by the DeepSamplerRule (junit4) and the DeepSamplerExtension (junit5).
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2020 PPI AG (Hamburg, Germany)
* This program is made available under the terms of the MIT License.
*/

package de.ppi.deepsampler.junit;

import de.ppi.deepsampler.core.api.PersistentSample;

@SuppressWarnings("unused")
public class GetSomeStringTestSampleFixture implements SamplerFixture {

@PrepareSampler
private TestBean testBeanSampler;

@Override
public void defineSamplers() {
PersistentSample.of(testBeanSampler.getSomeString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ private JUnitTestUtility() {

/**
* Proves that {@link TestBean} has a Sampler in {@link SampleRepository} and that the Sample for the {@link TestBean#getSomeInt()} method
* is 42. this value should be provided by an arbitrary Sampler, since the default implementarion would return 0.
* is 42. this value should be provided by an arbitrary Sampler, since the default implementation would return 0.
*
* @throws Exception the generic call to an {@link Answer#call(StubMethodInvocation)} may yield an Exception of any kind if the concrete
* implementation decides that this is necessary.
*/
public static void assertTestBeanHasBeenStubbed() throws Throwable {
public static void assertTestBeanHasStubbedInt() throws Throwable {
final SampleRepository sampleRepository = SampleRepository.getInstance();

assertFalse(sampleRepository.isEmpty());
Expand All @@ -45,6 +45,24 @@ public static void assertTestBeanHasBeenStubbed() throws Throwable {
assertEquals(42, getSomeInt.getAnswer().call(null));
}

/**
* Proves that {@link TestBean} has a Sampler in {@link SampleRepository} and that the Sample for the {@link TestBean#getSomeString()} method
* is "42". this value should be provided by an arbitrary Sampler.
*
* @throws Throwable the generic call to an {@link Answer#call(StubMethodInvocation)} may yield an {@link Throwable} of any kind if the concrete
* implementation decides that this was necessary.
*/
public static void assertTestBeanHasStubbedString() throws Throwable {
final SampleRepository sampleRepository = SampleRepository.getInstance();

assertFalse(sampleRepository.isEmpty());

final SampledMethod expectedSampledMethod = new SampledMethod(TestBean.class, TestBean.class.getMethod("getSomeString"));
final SampleDefinition getSomeString = sampleRepository.findAllForMethod(expectedSampledMethod).get(0);

assertEquals("42", getSomeString.getAnswer().call(null));
}

/**
* Proves that path does not exist. However, if it exists, it is deleted.
* @param path the path of the file that must must not exist.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public void evaluate() throws Throwable {
Sampler.clear();

JUnitPluginUtils.injectSamplers(target);
JUnitPluginUtils.applyTestFixture(method.getMethod());
JUnitPluginUtils.applySamplerFixture(method.getMethod());
JUnitPluginUtils.loadSamples(method.getMethod());

base.evaluate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;

import static de.ppi.deepsampler.junit.JUnitTestUtility.assertTestBeanHasBeenStubbed;
import static de.ppi.deepsampler.junit.JUnitTestUtility.assertTestBeanHasStubbedInt;
import static de.ppi.deepsampler.junit.JUnitTestUtility.assertThatFileDoesNotExistOrOtherwiseDeleteIt;
import static org.junit.Assert.assertTrue;

Expand All @@ -35,7 +35,7 @@ public class PersistentSamplerTest {
@UseSamplerFixture(TestSampleFixture.class)
@LoadSamples
public void aSamplerCanBeLoadedFromFile() throws Throwable {
assertTestBeanHasBeenStubbed();
assertTestBeanHasStubbedInt();
}

@Test
Expand Down Expand Up @@ -84,14 +84,14 @@ public void gSamplerHasBeenSavedInSpecificWithSpecificBuilderFileByPriorTestMeth
@UseSamplerFixture(TestSampleFixture.class)
@LoadSamples(file = LOAD_SPECIFIC_FILE_JSON)
public void fSamplerCanBeLoadedFromSpecificFile() throws Throwable {
assertTestBeanHasBeenStubbed();
assertTestBeanHasStubbedInt();
}

@Test
@UseSamplerFixture(TestSampleFixture.class)
@LoadSamples(classPath = LOAD_SPECIFIC_FILE_FROM_CLASSPATH_JSON)
public void gSamplerCanBeLoadedFromSpecificClasspathResource() throws Throwable {
assertTestBeanHasBeenStubbed();
assertTestBeanHasStubbedInt();
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2020 PPI AG (Hamburg, Germany)
* This program is made available under the terms of the MIT License.
*/

package de.ppi.deepsampler.junit4;

import de.ppi.deepsampler.junit.*;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runners.MethodSorters;

import static de.ppi.deepsampler.junit.JUnitTestUtility.assertTestBeanHasStubbedInt;
import static de.ppi.deepsampler.junit.JUnitTestUtility.assertTestBeanHasStubbedString;
import static org.junit.Assert.assertTrue;

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@UseSamplerFixture(TestSampleFixture.class)
public class SamplerFixtureTest {

@Rule
public DeepSamplerRule deepSamplerRule = new DeepSamplerRule();

@Test
@LoadSamples
public void samplerFixtureAtClassLevelShouldBeUsed() throws Throwable {
assertTestBeanHasStubbedInt();
}

@Test
@LoadSamples
@UseSamplerFixture(GetSomeStringTestSampleFixture.class)
public void samplerFixtureAtMethodLevelShouldBeUsed() throws Throwable {
assertTestBeanHasStubbedString();
}
}
Loading

0 comments on commit d48ecc5

Please sign in to comment.