Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug sampler fixture issue #61 #79

Merged
merged 12 commits into from
Sep 3, 2021
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.
*/
JanSchankin marked this conversation as resolved.
Show resolved Hide resolved
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"/>
*/
JanSchankin marked this conversation as resolved.
Show resolved Hide resolved
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"));
}
JanSchankin marked this conversation as resolved.
Show resolved Hide resolved
}
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