Skip to content
Merged
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
2 changes: 1 addition & 1 deletion auto-sdk-java-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>com.applause</groupId>
<artifactId>auto-sdk-java</artifactId>
<version>6.1.0-SNAPSHOT</version>
<version>6.1.1-SNAPSHOT</version>
</parent>
<artifactId>auto-sdk-java-common</artifactId>
<properties>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
*
* Copyright © 2024 Applause App Quality, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.applause.auto.versioning;

import com.google.common.base.Suppliers;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.function.Supplier;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/** Util class to read the SDK version from the VERSION.txt file that we package inside the pom */
public final class SdkVersionReader {

private static final String SDK_VERSION_FILE_NAME = "VERSION.txt";
private static final Logger logger = LogManager.getLogger();

/** Supplies the raw SDK Version from the file */
private static final Supplier<String> rawVersionSupplier =
Suppliers.memoize(SdkVersionReader::loadSdkVersionFromFile);

private SdkVersionReader() { // utility class
}

/**
* Gets the raw SDK Version from the packaged file without performing any network validation.
*
* @return The raw SDK Version String, or null if it cannot be read.
*/
public static String getSdkVersion() {
return rawVersionSupplier.get();
}

/**
* Performs the file I/O to read the version from the classpath.
*
* @return The version string from the file.
*/
private static String loadSdkVersionFromFile() {
try (InputStream fileUrl =
Thread.currentThread().getContextClassLoader().getResourceAsStream(SDK_VERSION_FILE_NAME)) {
if (fileUrl == null) {
logger.error(
"Could not read current Applause Automation SDK version from classpath. Version file not found");
throw new RuntimeException(
"Could not read current Applause Automation SDK version from classpath. Version file not found");
}
return IOUtils.toString(fileUrl, StandardCharsets.UTF_8).trim();
} catch (IOException e) {
logger.fatal("Could not read current Applause Automation SDK version from classpath", e);
throw new RuntimeException(
"Could not read current Applause Automation SDK version from classpath", e);
}
}
}
2 changes: 1 addition & 1 deletion auto-sdk-java-config/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>com.applause</groupId>
<artifactId>auto-sdk-java</artifactId>
<version>6.1.0-SNAPSHOT</version>
<version>6.1.1-SNAPSHOT</version>
</parent>
<artifactId>auto-sdk-java-config</artifactId>
<properties>
Expand Down
2 changes: 1 addition & 1 deletion auto-sdk-java-cucumber/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>com.applause</groupId>
<artifactId>auto-sdk-java</artifactId>
<version>6.1.0-SNAPSHOT</version>
<version>6.1.1-SNAPSHOT</version>
</parent>
<artifactId>auto-sdk-java-cucumber</artifactId>
<properties>
Expand Down
2 changes: 1 addition & 1 deletion auto-sdk-java-framework/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>com.applause</groupId>
<artifactId>auto-sdk-java</artifactId>
<version>6.1.0-SNAPSHOT</version>
<version>6.1.1-SNAPSHOT</version>
</parent>
<artifactId>auto-sdk-java-framework</artifactId>
<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
Expand All @@ -45,6 +46,7 @@ public class KeepAlive {
private Consumer<WebDriver> using = WebDriver::getCurrentUrl;
private Duration every = Duration.ofSeconds(5);
private Duration maxWait = Duration.ofMinutes(20);
@Nullable private ScheduledExecutorService executor;

/**
* Sets the drivers. Accepts a maximum of 10 drivers
Expand Down Expand Up @@ -95,6 +97,18 @@ public KeepAlive withMaxWait(final @NonNull Duration maxWaitDuration) {
return this;
}

/**
* Provides an external ScheduledExecutorService. If provided, the KeepAlive class will not manage
* the lifecycle (i.e., it will not shut down the executor).
*
* @param providedExecutor The executor to use.
* @return The keep alive
*/
public KeepAlive withExecutor(final ScheduledExecutorService providedExecutor) {
this.executor = providedExecutor;
return this;
}

/**
* Polls the keep alive while the provided supplier executes.
*
Expand All @@ -109,27 +123,29 @@ public <T> T executeWhile(final @NonNull Supplier<T> whileFunction)
throws ExecutionException, InterruptedException {
// Since we shut this down every time it is called, set up a new one each time we call this
// function
final ScheduledExecutorService keepAliveExecutors =
new ScheduledThreadPoolExecutor(Math.min(this.keepAliveDrivers.size(), MAX_DRIVERS));
final boolean isExecutorOwned = this.executor == null;
final ScheduledExecutorService executorToUse =
isExecutorOwned
? new ScheduledThreadPoolExecutor(Math.min(this.keepAliveDrivers.size(), MAX_DRIVERS))
: this.executor;

// Start the keep alive threads
this.keepAliveDrivers.forEach(driver -> this.setupKeepAlive(driver, keepAliveExecutors));
this.keepAliveDrivers.forEach(driver -> this.setupKeepAlive(driver, executorToUse));

// Run the execution function
final var executionFuture = this.setupExecutorThread(whileFunction);
try {
// Wait for the execution to finish with a max wait of x milliseconds
return executionFuture.get(maxWait.toMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
// If the function takes too long, then we should inform the user and cancel keep alive
// threads
// This will keep executing the main execution, but stop the keep alive. We can accomplish
// this
// by shutting down the keepAliveExecutors
// If the function takes too long, then we should inform the user.
// If we own the executor, we shut it down to stop the keep-alive threads.
log.warn(
"While function timed out after {} milliseconds. Ending keep alive threads",
maxWait.toMillis());
keepAliveExecutors.shutdown();
if (isExecutorOwned) {
executorToUse.shutdown();
}
return executionFuture.get();
} catch (ExecutionException e) {
// If an exception or error happens during execution, the CompletableFuture throws an
Expand All @@ -145,8 +161,10 @@ public <T> T executeWhile(final @NonNull Supplier<T> whileFunction)
}
throw e;
} finally {
// This is a no-op if we already shut it down
keepAliveExecutors.shutdown();
// Only shut down the executor if this class created it.
if (isExecutorOwned) {
executorToUse.shutdown();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,29 @@
* limitations under the License.
*
*/
package com.applause.auto.framework;
package com.applause.auto.framework.keepalive;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.applause.auto.framework.keepalive.KeepAlive;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.mockito.ArgumentCaptor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.testng.Assert;
import org.testng.annotations.Test;

public class KeepAliveHelperTest {
public class KeepAliveTest {

public WebDriver setupDriver() {
final var mockDriver = mock(FirefoxDriver.class);
Expand All @@ -43,19 +50,32 @@ public WebDriver setupDriver() {
@Test
public void testKeepAlive() throws InterruptedException, ExecutionException {
final WebDriver mockDriver = setupDriver();
final var mockExecutor = mock(ScheduledExecutorService.class);
final var runnableCaptor = ArgumentCaptor.forClass(Runnable.class);

// Use the new public API to inject the mock executor
new KeepAlive()
.forDrivers(mockDriver)
.pollingEvery(Duration.ofSeconds(1))
.executeWhile(
() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
mockDriver.getTitle();
});
.withExecutor(mockExecutor)
.executeWhile(mockDriver::getTitle);

// Verify that the keep-alive task was scheduled
verify(mockExecutor)
.scheduleAtFixedRate(
runnableCaptor.capture(), eq(0L), eq(1000L), eq(TimeUnit.MILLISECONDS));

// Manually run the captured keep-alive task to simulate execution over time
final Runnable keepAliveTask = runnableCaptor.getValue();
for (int i = 0; i < 6; i++) {
keepAliveTask.run();
}

// Verify the results
verify(mockDriver, times(6)).getCurrentUrl();
verify(mockDriver, times(1)).getTitle();
// Verify the injected executor was NOT shut down
verify(mockExecutor, never()).shutdown();
mockDriver.quit();
}

Expand All @@ -64,9 +84,11 @@ public void testKeepAlive() throws InterruptedException, ExecutionException {
expectedExceptionsMessageRegExp = "This Should Propagate as a Runtime Exception")
public void testKeepAliveErrorPropagation() throws InterruptedException, ExecutionException {
final WebDriver mockDriver = setupDriver();
final var mockExecutor = mock(ScheduledExecutorService.class);
new KeepAlive()
.forDrivers(mockDriver)
.pollingEvery(Duration.ofSeconds(1))
.withExecutor(mockExecutor)
.executeWhile(
() -> {
throw new RuntimeException("This Should Propagate as a Runtime Exception");
Expand All @@ -78,62 +100,98 @@ public void testKeepAliveErrorPropagation() throws InterruptedException, Executi
public void testKeepAliveAssertionFailurePropagation()
throws InterruptedException, ExecutionException {
final WebDriver mockDriver = setupDriver();
final var mockExecutor = mock(ScheduledExecutorService.class);
new KeepAlive()
.forDrivers(mockDriver)
.pollingEvery(Duration.ofSeconds(1))
.withExecutor(mockExecutor)
.executeWhile(() -> Assert.fail("This Should Propagate to the main thread"));
}

@Test
public void testMultiDriverKeepAlive() throws InterruptedException, ExecutionException {
final WebDriver mockDriver = setupDriver();
final WebDriver mockDriver2 = setupDriver();
final var mockExecutor = mock(ScheduledExecutorService.class);
final var runnableCaptor = ArgumentCaptor.forClass(Runnable.class);

new KeepAlive()
.forDrivers(mockDriver, mockDriver2)
.pollingEvery(Duration.ofSeconds(1))
.executeWhile(
() -> {
try {
Thread.sleep(5100);
mockDriver.getTitle();
} catch (InterruptedException ignored) {
}
});
.withExecutor(mockExecutor)
.executeWhile(mockDriver::getTitle);

// Verify that keep-alive tasks were scheduled for both drivers
verify(mockExecutor, times(2))
.scheduleAtFixedRate(
runnableCaptor.capture(), eq(0L), eq(1000L), eq(TimeUnit.MILLISECONDS));

// Manually run the captured tasks
final List<Runnable> keepAliveTasks = runnableCaptor.getAllValues();
for (final Runnable task : keepAliveTasks) {
for (int i = 0; i < 6; i++) {
task.run();
}
}

// Verify results for both drivers
verify(mockDriver, times(6)).getCurrentUrl();
verify(mockDriver2, times(6)).getCurrentUrl();
verify(mockDriver, times(1)).getTitle();
// Verify the injected executor was NOT shut down
verify(mockExecutor, never()).shutdown();
mockDriver.quit();
}

@Test
public void testCustomKeepAlive() throws InterruptedException, ExecutionException {
final WebDriver mockDriver = setupDriver();
final var mockExecutor = mock(ScheduledExecutorService.class);
final var runnableCaptor = ArgumentCaptor.forClass(Runnable.class);

new KeepAlive()
.forDrivers(mockDriver)
.pollingEvery(Duration.ofSeconds(1))
.usingKeepAlive(WebDriver::getTitle)
.executeWhile(
() -> {
try {
Thread.sleep(5100);
} catch (InterruptedException ignored) {
}
});
.withExecutor(mockExecutor)
.executeWhile(() -> {}); // Empty runnable

// Verify that the custom keep-alive task was scheduled
verify(mockExecutor)
.scheduleAtFixedRate(
runnableCaptor.capture(), eq(0L), eq(1000L), eq(TimeUnit.MILLISECONDS));

// Manually run the captured keep-alive task
final Runnable keepAliveTask = runnableCaptor.getValue();
for (int i = 0; i < 6; i++) {
keepAliveTask.run();
}

// Verify the results
verify(mockDriver, times(0)).getCurrentUrl();
verify(mockDriver, times(6)).getTitle();
// Verify the injected executor was NOT shut down
verify(mockExecutor, never()).shutdown();
mockDriver.quit();
}

@Test
public void testKeepAliveSupplier() throws InterruptedException, ExecutionException {
final WebDriver mockDriver = setupDriver();
final var mockExecutor = mock(ScheduledExecutorService.class);
final String result =
new KeepAlive()
.forDrivers(mockDriver)
.pollingEvery(Duration.ofSeconds(1))
.usingKeepAlive(WebDriver::getTitle)
.withExecutor(mockExecutor)
.executeWhile(() -> "Done!");

Assert.assertEquals(result, "Done!");
// Verify a keep-alive task was scheduled
verify(mockExecutor).scheduleAtFixedRate(any(Runnable.class), anyLong(), anyLong(), any());
// Verify the injected executor was NOT shut down
verify(mockExecutor, never()).shutdown();
mockDriver.quit();
}
}
Loading