Skip to content

Commit

Permalink
Add method to discard current ADB services
Browse files Browse the repository at this point in the history
The connection can then be re-established

Issue: #197
  • Loading branch information
mlopatkin committed Dec 25, 2023
1 parent 244b100 commit 4e021d8
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 5 deletions.
1 change: 1 addition & 0 deletions base/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies {
implementation(libs.guava)
implementation(libs.log4j)

testFixturesApi(libs.test.assertj)
testFixturesApi(libs.test.hamcrest.hamcrest)
testFixturesImplementation(libs.guava)
testFixturesImplementation(libs.test.mockito.core)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2023 the Andlogview authors
*
* 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 name.mlopatkin.andlogview.base.concurrent;

import name.mlopatkin.andlogview.base.MyThrowables;

import com.google.common.base.Preconditions;

import org.assertj.core.api.AbstractCompletableFutureAssert;
import org.assertj.core.api.CompletableFutureAssert;

import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;

/**
* Extended version of AssertJ's {@link CompletableFutureAssert}.
*
* @param <RESULT> the future's result type
*/
public class ExtendedCompletableFutureAssert<RESULT>
extends AbstractCompletableFutureAssert<ExtendedCompletableFutureAssert<RESULT>, RESULT> {
public ExtendedCompletableFutureAssert(CompletableFuture<RESULT> actual) {
super(actual, ExtendedCompletableFutureAssert.class);
}

/**
* Entry point to the completable future assertions (cannot use assertThat to avoid clashing with the existing
* method).
*
* @param future the future to assert
* @param <RESULT> the future's result type
* @return the assertion
*/
public static <RESULT> ExtendedCompletableFutureAssert<RESULT> assertThatCompletableFuture(
CompletableFuture<RESULT> future) {
return new ExtendedCompletableFutureAssert<>(future);
}

/**
* Verifies that the {@link CompletableFuture} is cancelled or failed because one of its upstreams has been
* cancelled.
*
* @return this assertion object
*/
public ExtendedCompletableFutureAssert<RESULT> isCancelledUpstream() {
super.isCompletedExceptionally();
var failureReason = getFailureReason();
if (!(failureReason instanceof CancellationException)) {
failWithMessage("Expected failure reason to be CancellationException but got %s", failureReason);
}
return this;
}


private Throwable getFailureReason() {
Preconditions.checkState(actual.isCompletedExceptionally(), "The future is not yet completed");
try {
actual.get();
throw new AssertionError("Expecting failure to be present");
} catch (ExecutionException | CancellationException | CompletionException e) {
return MyThrowables.unwrapUninteresting(e);
} catch (InterruptedException e) {
throw new AssertionError("Completed future cannot be interrupted");
}
}
}
25 changes: 20 additions & 5 deletions src/name/mlopatkin/andlogview/ui/device/AdbServicesBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ public class AdbServicesBridge implements AdbServicesStatus {
private final GlobalAdbDeviceList adbDeviceList;
private final Subject<Observer> statusObservers = new Subject<>();

// TODO(mlopatkin) My attempt to hide connection replacement logic in the AdbServer failed. What if the connection
// update fails? The server would be unusable and we have to discard the whole component.
private @Nullable CompletableFuture<AdbServices> adbSubcomponent; // This one is three-state. The `null` here
// means that nobody attempted to initialize ADB yet. Non-null holds the current subcomponent if it was created
// successfully or the initialization error if something failed.
Expand All @@ -91,7 +89,7 @@ public class AdbServicesBridge implements AdbServicesStatus {

/**
* Tries to create AdbServices, potentially initializing ADB connection if is it is not ready yet.
* This may fail and show an error dialog.
* This may fail, or may be cancelled.
*
* @return a completable future that will provide {@link AdbServices} when ready
*/
Expand Down Expand Up @@ -122,7 +120,7 @@ private CompletableFuture<AdbServices> initAdbAsync() {
// This is a separate chain, not related to the consumers of getAdbServicesAsync. Therefore, it has a separate
// exception sink to handle runtime errors in the handler.
result.handleAsync(
consumingHandler((r, th) -> onAdbInitFinished(th, stopwatch)),
consumingHandler((r, th) -> onAdbInitFinished(result, th, stopwatch)),
uiExecutor)
.exceptionally(MyFutures::uncaughtException);
return result;
Expand All @@ -133,7 +131,12 @@ private AdbServices buildServices(AdbServer adbServer) {
return adbSubcomponentFactory.build(adbDeviceList);
}

private void onAdbInitFinished(@Nullable Throwable maybeFailure, Stopwatch timeTracing) {
private void onAdbInitFinished(CompletableFuture<?> origin, @Nullable Throwable maybeFailure,
Stopwatch timeTracing) {
if (adbSubcomponent != origin) {
// Our initialization was cancelled by this point. We shouldn't propagate notifications further.
return;
}
logger.info("Initialized adb server in " + timeTracing.elapsed(TimeUnit.MILLISECONDS) + "ms");
if (maybeFailure != null) {
logger.error("Failed to initialize ADB", maybeFailure);
Expand Down Expand Up @@ -181,4 +184,16 @@ private static String getAdbFailureString(Throwable th) {
.map(Throwable::getMessage)
.orElse(MoreObjects.firstNonNull(th.getMessage(), "unknown failure"));
}

/**
* Discards the running adb services subcomponent if any. Any ongoing ADB initialization is cancelled.
*/
public void stopAdb() {
var currentAdb = adbSubcomponent;
if (currentAdb != null) {
adbSubcomponent = null;
notifyStatusChange(getStatus());
currentAdb.cancel(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@

package name.mlopatkin.andlogview.ui.device;

import static name.mlopatkin.andlogview.base.concurrent.ExtendedCompletableFutureAssert.assertThatCompletableFuture;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import name.mlopatkin.andlogview.base.concurrent.TestExecutor;
Expand Down Expand Up @@ -180,6 +184,74 @@ void asyncHandlersSeeProperStatus() {
assertThat(bridgeStatus.get()).isEqualTo(StatusValue.initialized());
}

@Test
void serviceFutureIsCancelledIfAdbStopped() {
var testUiExecutor = new TestExecutor();
var testAdbExecutor = new TestExecutor();
var bridge = createBridge(testUiExecutor, testAdbExecutor);

var serviceFuture = bridge.getAdbServicesAsync();
bridge.stopAdb();

assertThatCompletableFuture(serviceFuture).isCancelledUpstream();
}

@Test
void adbIsNotInitializedAfterStopped() {
var testUiExecutor = new TestExecutor();
var testAdbExecutor = new TestExecutor();
var bridge = createBridge(testUiExecutor, testAdbExecutor);
var observer = mock(AdbServicesStatus.Observer.class);

var firstInit = bridge.getAdbServicesAsync();
bridge.stopAdb();
bridge.asObservable().addObserver(observer);
drainExecutors(testAdbExecutor, testUiExecutor);

assertThat(bridge.getStatus()).isInstanceOf(AdbServicesStatus.NotInitialized.class);

assertThat(firstInit).isCompletedExceptionally();
verifyNoInteractions(observer);
}

@Test
void completingPreviousAdbInitializationAfterStopDoesNotChangeStatus() {
var testUiExecutor = new TestExecutor();
var testAdbExecutor = new TestExecutor();
var bridge = createBridge(testUiExecutor, testAdbExecutor);
var observer = mock(AdbServicesStatus.Observer.class);

var firstInit = bridge.getAdbServicesAsync();
testAdbExecutor.flush();
bridge.stopAdb();
bridge.asObservable().addObserver(observer);
drainExecutors(testAdbExecutor, testUiExecutor);

assertThat(firstInit).isCompletedExceptionally();
assertThat(bridge.getStatus()).isInstanceOf(AdbServicesStatus.NotInitialized.class);
verifyNoInteractions(observer);
}

@Test
void completingNewAdbInitializationAfterStopSendsNoSpuriousNotifications() {
var testUiExecutor = new TestExecutor();
var testAdbExecutor = new TestExecutor();
var bridge = createBridge(testUiExecutor, testAdbExecutor);
var observer = mock(AdbServicesStatus.Observer.class);

var firstInit = bridge.getAdbServicesAsync();
bridge.stopAdb();
var secondInit = bridge.getAdbServicesAsync();
bridge.asObservable().addObserver(observer);
drainExecutors(testAdbExecutor, testUiExecutor);

assertThat(firstInit).isCompletedExceptionally();
assertThat(secondInit).isCompleted().isNotCancelled();

verify(observer).onAdbServicesStatusChanged(isA(AdbServicesStatus.Initialized.class));
verifyNoMoreInteractions(observer);
}

private AdbServicesBridge createBridge() {
return createBridge(MoreExecutors.directExecutor(), MoreExecutors.directExecutor());
}
Expand Down

0 comments on commit 4e021d8

Please sign in to comment.