Skip to content

Commit

Permalink
Enhance Playback Record Naming (Azure#14801)
Browse files Browse the repository at this point in the history
* Add enhanced playback record names

* Fixed incorrect conditional and added missing Javadoc

* Changes based an feedback, updating loading of all HttpClients on the class path
  • Loading branch information
alzimmermsft authored Sep 10, 2020
1 parent 6eb0b5d commit 3d9d990
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
import com.azure.core.http.HttpClient;
import com.azure.core.util.Configuration;
import com.azure.core.util.CoreUtils;
import org.junit.jupiter.params.provider.Arguments;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.params.provider.Arguments;

import static com.azure.core.test.TestBase.AZURE_TEST_SERVICE_VERSIONS_VALUE_ALL;
import static com.azure.core.test.TestBase.getHttpClients;
Expand All @@ -30,11 +31,9 @@ static Stream<Arguments> getTestParameters() {
// when this issues is closed, the newer version of junit will have better support for
// cartesian product of arguments - https://github.com/junit-team/junit5/issues/1427
List<Arguments> argumentsList = new ArrayList<>();
getHttpClients()
.forEach(httpClient -> {
Arrays.stream(ConfigurationServiceVersion.values()).filter(TestHelper::shouldServiceVersionBeTested)
.forEach(serviceVersion -> argumentsList.add(Arguments.of(httpClient, serviceVersion)));
});
getHttpClients().forEach(httpClient -> Arrays.stream(ConfigurationServiceVersion.values())
.filter(TestHelper::shouldServiceVersionBeTested)
.forEach(serviceVersion -> argumentsList.add(Arguments.of(httpClient, serviceVersion))));
return argumentsList.stream();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.azure.core.test.models.NetworkCallRecord;
import com.azure.core.test.models.RecordedData;
import com.azure.core.test.policy.RecordNetworkCallPolicy;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
Expand Down Expand Up @@ -39,10 +40,12 @@
*/
public class InterceptorManager implements AutoCloseable {
private static final String RECORD_FOLDER = "session-records/";
private static final ObjectMapper RECORD_MAPPER = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);

private final ClientLogger logger = new ClientLogger(InterceptorManager.class);
private final Map<String, String> textReplacementRules;
private final String testName;
private final String playbackRecordName;
private final TestMode testMode;
private final boolean allowedToReadRecordedValues;
private final boolean allowedToRecordValues;
Expand Down Expand Up @@ -72,7 +75,7 @@ public class InterceptorManager implements AutoCloseable {
*/
@Deprecated
public InterceptorManager(String testName, TestMode testMode) {
this(testName, testMode, false);
this(testName, testName, testMode, false);
}

/**
Expand All @@ -96,13 +99,15 @@ public InterceptorManager(String testName, TestMode testMode) {
* @throws NullPointerException If {@code testName} is {@code null}.
*/
public InterceptorManager(TestContextManager testContextManager) {
this(testContextManager.getTestName(), testContextManager.getTestMode(), testContextManager.doNotRecordTest());
this(testContextManager.getTestName(), testContextManager.getTestPlaybackRecordingName(),
testContextManager.getTestMode(), testContextManager.doNotRecordTest());
}

private InterceptorManager(String testName, TestMode testMode, boolean doNotRecord) {
private InterceptorManager(String testName, String playbackRecordName, TestMode testMode, boolean doNotRecord) {
Objects.requireNonNull(testName, "'testName' cannot be null.");

this.testName = testName;
this.playbackRecordName = CoreUtils.isNullOrEmpty(playbackRecordName) ? testName : playbackRecordName;
this.testMode = testMode;
this.textReplacementRules = new HashMap<>();

Expand All @@ -119,8 +124,8 @@ private InterceptorManager(String testName, TestMode testMode, boolean doNotReco
}

/**
* Creates a new InterceptorManager that replays test session records. It takes a set of
* {@code textReplacementRules}, that can be used by {@link PlaybackClient} to replace values in a {@link
* Creates a new InterceptorManager that replays test session records. It takes a set of {@code
* textReplacementRules}, that can be used by {@link PlaybackClient} to replace values in a {@link
* NetworkCallRecord#getResponse()}.
*
* The test session records are read from: "<i>session-records/{@code testName}.json</i>"
Expand All @@ -135,12 +140,12 @@ private InterceptorManager(String testName, TestMode testMode, boolean doNotReco
*/
@Deprecated
public InterceptorManager(String testName, Map<String, String> textReplacementRules) {
this(testName, textReplacementRules, false);
this(testName, textReplacementRules, false, testName);
}

/**
* Creates a new InterceptorManager that replays test session records. It takes a set of
* {@code textReplacementRules}, that can be used by {@link PlaybackClient} to replace values in a {@link
* Creates a new InterceptorManager that replays test session records. It takes a set of {@code
* textReplacementRules}, that can be used by {@link PlaybackClient} to replace values in a {@link
* NetworkCallRecord#getResponse()}.
*
* The test session records are read from: "<i>session-records/{@code testName}.json</i>"
Expand All @@ -152,12 +157,36 @@ public InterceptorManager(String testName, Map<String, String> textReplacementRu
* @throws UncheckedIOException An existing test session record could not be located or the data could not be
* deserialized into an instance of {@link RecordedData}.
* @throws NullPointerException If {@code testName} or {@code textReplacementRules} is {@code null}.
* @deprecated Use {@link #InterceptorManager(String, Map, boolean, String)} instead.
*/
@Deprecated
public InterceptorManager(String testName, Map<String, String> textReplacementRules, boolean doNotRecord) {
this(testName, textReplacementRules, doNotRecord, testName);
}

/**
* Creates a new InterceptorManager that replays test session records. It takes a set of {@code
* textReplacementRules}, that can be used by {@link PlaybackClient} to replace values in a {@link
* NetworkCallRecord#getResponse()}.
*
* The test session records are read from: "<i>session-records/{@code testName}.json</i>"
*
* @param testName Name of the test.
* @param textReplacementRules A set of rules to replace text in {@link NetworkCallRecord#getResponse()} when
* playing back network calls.
* @param doNotRecord Flag indicating whether network calls should be record or played back.
* @param playbackRecordName Full name of the test including its iteration, used as the playback record name.
* @throws UncheckedIOException An existing test session record could not be located or the data could not be
* deserialized into an instance of {@link RecordedData}.
* @throws NullPointerException If {@code testName} or {@code textReplacementRules} is {@code null}.
*/
public InterceptorManager(String testName, Map<String, String> textReplacementRules, boolean doNotRecord,
String playbackRecordName) {
Objects.requireNonNull(testName, "'testName' cannot be null.");
Objects.requireNonNull(textReplacementRules, "'textReplacementRules' cannot be null.");

this.testName = testName;
this.playbackRecordName = CoreUtils.isNullOrEmpty(playbackRecordName) ? testName : playbackRecordName;
this.testMode = TestMode.PLAYBACK;
this.allowedToReadRecordedValues = !doNotRecord;
this.allowedToRecordValues = false;
Expand Down Expand Up @@ -223,19 +252,19 @@ public HttpClient getPlaybackClient() {
public void close() {
if (allowedToRecordValues) {
try {
writeDataToFile();
} catch (IOException e) {
logger.error("Unable to write data to playback file.", e);
RECORD_MAPPER.writeValue(createRecordFile(playbackRecordName), recordedData);
} catch (IOException ex) {
throw logger.logExceptionAsError(
new UncheckedIOException("Unable to write data to playback file.", ex));
}
}
}

private RecordedData readDataFromFile() {
File recordFile = getRecordFile(testName);
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
File recordFile = getRecordFile();

try {
return mapper.readValue(recordFile, RecordedData.class);
return RECORD_MAPPER.readValue(recordFile, RecordedData.class);
} catch (IOException ex) {
throw logger.logExceptionAsWarning(new UncheckedIOException(ex));
}
Expand All @@ -252,23 +281,24 @@ private File getRecordFolder() {
/*
* Attempts to retrieve the playback file, if it is not found an exception is thrown as playback can't continue.
*/
private File getRecordFile(String testName) {
File playbackFile = new File(getRecordFolder(), testName + ".json");
private File getRecordFile() {
File recordFolder = getRecordFolder();
File playbackFile = new File(recordFolder, playbackRecordName + ".json");
File oldPlaybackFile = new File(recordFolder, testName + ".json");

if (!playbackFile.exists()) {
if (!playbackFile.exists() && !oldPlaybackFile.exists()) {
throw logger.logExceptionAsError(new RuntimeException(String.format(
"Missing playback file. File path: %s. ", playbackFile.getPath())));
"Missing both new and old playback files. Files are %s and %s.", playbackFile.getPath(),
oldPlaybackFile.getPath())));
}

logger.info("==> Playback file path: " + playbackFile.getPath());
return playbackFile;
}

private void writeDataToFile() throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
File recordFile = createRecordFile(testName);
mapper.writeValue(recordFile, recordedData);
if (playbackFile.exists()) {
logger.info("==> Playback file path: {}", playbackFile.getPath());
return playbackFile;
} else {
logger.info("==> Playback file path: {}", oldPlaybackFile.getPath());
return oldPlaybackFile;
}
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,29 @@
package com.azure.core.test;

import com.azure.core.http.HttpClient;
import com.azure.core.implementation.http.HttpClientProviders;
import com.azure.core.http.HttpClientProvider;
import com.azure.core.test.utils.TestResourceNamer;
import com.azure.core.util.Configuration;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;
import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.io.UncheckedIOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Locale;
import java.util.ServiceLoader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
* Base class for running live and playback tests using {@link InterceptorManager}.
Expand All @@ -33,6 +38,8 @@ public abstract class TestBase implements BeforeEachCallback {
public static final String AZURE_TEST_HTTP_CLIENTS_VALUE_NETTY = "NettyAsyncHttpClient";
public static final String AZURE_TEST_SERVICE_VERSIONS_VALUE_ALL = "ALL";

private static final Pattern TEST_ITERATION_PATTERN = Pattern.compile("test-template-invocation:#(\\d+)");

private static TestMode testMode;

private final ClientLogger logger = new ClientLogger(TestBase.class);
Expand All @@ -43,6 +50,9 @@ public abstract class TestBase implements BeforeEachCallback {

private ExtensionContext extensionContext;

@RegisterExtension
final TestIterationContext testIterationContext = new TestIterationContext();

/**
* Before tests are executed, determines the test mode by reading the {@link TestBase#AZURE_TEST_MODE} environment
* variable. If it is not set, {@link TestMode#PLAYBACK}
Expand All @@ -66,6 +76,9 @@ public void beforeEach(ExtensionContext extensionContext) {
@BeforeEach
public void setupTest(TestInfo testInfo) {
this.testContextManager = new TestContextManager(testInfo.getTestMethod().get(), testMode);
if (testIterationContext != null) {
testContextManager.setTestIteration(testIterationContext.testIteration);
}
logger.info("Test Mode: {}, Name: {}", testMode, testContextManager.getTestName());

try {
Expand All @@ -81,6 +94,7 @@ public void setupTest(TestInfo testInfo) {

/**
* Disposes of {@link InterceptorManager} and its inheriting class' resources.
*
* @param testInfo the injected testInfo
*/
@AfterEach
Expand All @@ -103,11 +117,11 @@ public TestMode getTestMode() {
/**
* Gets the name of the current test being run.
*
* @return The name of the current test.
* @deprecated This method is deprecated as JUnit 5 provides a simpler mechanism to get the test method name through
* {@link TestInfo}. Keeping this for backward compatability of other client libraries that still override this
* method. This method can be deleted when all client libraries remove this method. See {@link
* #setupTest(TestInfo)}.
* @return The name of the current test.
*/
@Deprecated
protected String getTestName() {
Expand Down Expand Up @@ -137,12 +151,16 @@ protected void afterTest() {
* @return A list of {@link HttpClient HttpClients} to be tested.
*/
public static Stream<HttpClient> getHttpClients() {
if (testMode == TestMode.PLAYBACK) {
// Call to @MethodSource method happens @BeforeEach call, so the interceptorManager is
// not yet initialized. So, playbackClient will not be available until later.
return Stream.of(new HttpClient[]{null});
}
return HttpClientProviders.getAllHttpClients().stream().filter(TestBase::shouldClientBeTested);
/*
* In PLAYBACK mode PlaybackClient is used, so there is no need to load HttpClient instances from the classpath.
* In LIVE or RECORD mode load all HttpClient instances and let the test run determine which HttpClient
* implementation it will use.
*/
return (testMode == TestMode.PLAYBACK)
? Stream.of(new HttpClient[]{null})
: StreamSupport.stream(ServiceLoader.load(HttpClientProvider.class).spliterator(), false)
.map(HttpClientProvider::createInstance)
.filter(TestBase::shouldClientBeTested);
}

/**
Expand Down Expand Up @@ -209,4 +227,16 @@ protected void sleepIfRunningAgainstService(long millis) {
throw logger.logExceptionAsWarning(new IllegalStateException(ex));
}
}

private static final class TestIterationContext implements BeforeEachCallback {
Integer testIteration;

@Override
public void beforeEach(ExtensionContext extensionContext) {
Matcher matcher = TEST_ITERATION_PATTERN.matcher(extensionContext.getUniqueId());
if (matcher.find()) {
testIteration = Integer.valueOf(matcher.group(1));
}
}
}
}
Loading

0 comments on commit 3d9d990

Please sign in to comment.