Skip to content

Commit

Permalink
Add tests for autosyncing on file changes
Browse files Browse the repository at this point in the history
Refactors QuerySyncAsyncFileListener to facilitate testing. Factory methods are added to the listener and sync requester, which also handle subscribing to the relevant message system, so the classes themselves no longer depend on a disposable.

QuerySyncAsyncFileListener now takes a SyncRequester, and callbacks for determining project paths and auto-sync settings. Factory methods derive these from relevant components. Path checking is moved into the requiresSync method for clarity.

Adds unit tests for verifying sync requests for file events.

PiperOrigin-RevId: 591048704
  • Loading branch information
Googler authored and copybara-github committed Dec 14, 2023
1 parent 3b7014d commit 041f740
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
*/
package com.google.idea.blaze.base.qsync;

import static com.google.common.collect.ImmutableList.toImmutableList;

import com.google.common.collect.ImmutableList;
import com.google.common.annotations.VisibleForTesting;
import com.google.idea.blaze.base.lang.buildfile.language.BuildFileType;
import com.google.idea.blaze.base.logging.utils.querysync.QuerySyncActionStatsScope;
import com.google.idea.blaze.base.qsync.QuerySyncManager.TaskOrigin;
Expand All @@ -30,36 +28,60 @@
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.AsyncFileListener;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent;
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
import com.intellij.openapi.vfs.newvfs.events.VFileMoveEvent;
import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.Nullable;

/** {@link AsyncFileListener} for monitoring project changes requiring a re-sync */
public class QuerySyncAsyncFileListener implements AsyncFileListener {
private final Project project;

private final SyncRequester syncRequester;
private final Function<Path, Boolean> includedPathChecker;
private final Supplier<Boolean> autoSyncOnFileChanges;

@VisibleForTesting
public QuerySyncAsyncFileListener(
SyncRequester syncRequester,
Function<Path, Boolean> includedPathChecker,
Supplier<Boolean> autoSyncOnFileChanges) {
this.syncRequester = syncRequester;
this.includedPathChecker = includedPathChecker;
this.autoSyncOnFileChanges = autoSyncOnFileChanges;
}

private static QuerySyncAsyncFileListener create(Project project, Disposable parentDisposable) {
SyncRequester syncRequester = QueueingSyncRequester.create(project, parentDisposable);
return new QuerySyncAsyncFileListener(
syncRequester,
path ->
QuerySyncManager.getInstance(project)
.getLoadedProject()
.map(p -> p.containsPath(path))
.orElse(false),
() -> QuerySyncSettings.getInstance().syncOnFileChanges());
}

public QuerySyncAsyncFileListener(Project project, Disposable parentDisposable) {
this.project = project;
this.syncRequester = new SyncRequester(project, parentDisposable);
public static void createAndListen(Project project, Disposable parentDisposable) {
VirtualFileManager.getInstance()
.addAsyncFileListener(create(project, parentDisposable), parentDisposable);
}

@Override
@Nullable
public ChangeApplier prepareChange(List<? extends VFileEvent> events) {
if (!QuerySyncSettings.getInstance().syncOnFileChanges()) {
if (!autoSyncOnFileChanges.get()) {
return null;
}

ImmutableList<? extends VFileEvent> projectEvents = filterForProject(events);

if (projectEvents.stream().anyMatch(this::requiresSync)) {
if (events.stream().anyMatch(this::requiresSync)) {
return new ChangeApplier() {
@Override
public void afterVfsChange() {
Expand All @@ -70,19 +92,10 @@ public void afterVfsChange() {
return null;
}

private ImmutableList<? extends VFileEvent> filterForProject(
List<? extends VFileEvent> rawEvents) {
QuerySyncProject querySyncProject =
QuerySyncManager.getInstance(project).getLoadedProject().orElse(null);
if (querySyncProject == null) {
return ImmutableList.of();
}
return rawEvents.stream()
.filter(event -> querySyncProject.containsPath(Path.of(event.getPath())))
.collect(toImmutableList());
}

private boolean requiresSync(VFileEvent event) {
if (!includedPathChecker.apply(Path.of(event.getPath()))) {
return false;
}
if (event instanceof VFileCreateEvent || event instanceof VFileMoveEvent) {
return true;
} else if (event instanceof VFilePropertyChangeEvent
Expand All @@ -102,35 +115,46 @@ private boolean requiresSync(VFileEvent event) {
return false;
}

/** Interface for requesting project syncs. */
public interface SyncRequester {
void requestSync();
}

/**
* Utility for requesting partial syncs, with a listener to retry requests if a sync is already in
* progress.
* {link @SyncRequester} that can listen to sync events and request a sync later if changes are
* added during a sync.
*/
private static class SyncRequester {
private static class QueueingSyncRequester implements SyncRequester {
private final Project project;

private final AtomicBoolean changePending = new AtomicBoolean(false);

public SyncRequester(Project project, Disposable parentDisposable) {
public QueueingSyncRequester(Project project) {
this.project = project;
}

static QueueingSyncRequester create(Project project, Disposable parentDisposable) {
QueueingSyncRequester requester = new QueueingSyncRequester(project);
ApplicationManager.getApplication()
.getExtensionArea()
.getExtensionPoint(SyncListener.EP_NAME)
.registerExtension(
new SyncListener() {
@Override
public void afterQuerySync(Project project1, BlazeContext context) {
if (SyncRequester.this.project != project1) {
public void afterQuerySync(Project project, BlazeContext context) {
if (!requester.project.equals(project)) {
return;
}
if (changePending.get()) {
requestSyncInternal();
if (requester.changePending.get()) {
requester.requestSyncInternal();
}
}
},
parentDisposable);
return requester;
}

@Override
public void requestSync() {
if (changePending.compareAndSet(false, true)) {
if (!BlazeSyncStatus.getInstance(project).syncInProgress()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.psi.PsiFile;
import com.intellij.serviceContainer.NonInjectable;
import com.intellij.util.concurrency.AppExecutorUtil;
Expand Down Expand Up @@ -129,8 +128,7 @@ public QuerySyncManager(Project project, @Nullable ProjectLoader loader) {
this.project = project;
this.loader = loader != null ? loader : createProjectLoader(executor, project);
this.syncStatus = new QuerySyncStatus(project);
VirtualFileManager.getInstance()
.addAsyncFileListener(new QuerySyncAsyncFileListener(project, this), this);
QuerySyncAsyncFileListener.createAndListen(project, this);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright 2023 The Bazel Authors. All rights reserved.
*
* 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.google.idea.blaze.base.qsync;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import com.google.idea.blaze.base.qsync.QuerySyncAsyncFileListener.SyncRequester;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.testFramework.LightPlatformTestCase;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase4;
import java.nio.file.Path;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class QuerySyncAsyncFileListenerTest extends LightJavaCodeInsightFixtureTestCase4 {

private static final Path SOURCE_ROOT = Path.of("my/project");

private SyncRequester mockSyncRequester;

@Before
public void setup() throws Exception {
getFixture()
.addFileToProject(
SOURCE_ROOT.resolve("java/com/example/Class1.java").toString(),
"package com.example;public class Class1 {}");
getFixture()
.addFileToProject(
SOURCE_ROOT.resolve("BUILD").toString(), "java_library(name=\"java\",srcs=[])");

getFixture()
.getTempDirFixture()
.findOrCreateDir(SOURCE_ROOT.resolve("submodule/java/com/example").toString());
mockSyncRequester = mock(SyncRequester.class);
}

private void setupAutoSyncListener() {
setupListener(true);
}

private void setupNoAutoSyncListener() {
setupListener(false);
}

private void setupListener(boolean autoSync) {
Path projectSourceRoot =
Path.of(LightPlatformTestCase.getSourceRoot().getPath()).resolve(SOURCE_ROOT);
QuerySyncAsyncFileListener fileListener =
new QuerySyncAsyncFileListener(
mockSyncRequester, path -> path.startsWith(projectSourceRoot), () -> autoSync);
VirtualFileManager.getInstance()
.addAsyncFileListener(fileListener, getFixture().getTestRootDisposable());
}

@Test
public void projectFileAdded_requestsSync() {
setupAutoSyncListener();

getFixture()
.addFileToProject(
SOURCE_ROOT.resolve("java/com/example/Class2.java").toString(),
"package com.example;public class Class2{}");
verify(mockSyncRequester, atLeastOnce()).requestSync();
}

@Test
public void projectFileAdded_autoSyncDisabled_neverRequestsSync() {
setupNoAutoSyncListener();

getFixture()
.addFileToProject(
SOURCE_ROOT.resolve("java/com/example/Class2.java").toString(),
"package com.example;public class Class2{}");
verify(mockSyncRequester, never()).requestSync();
}

@Test
public void nonProjectFileAdded_neverRequestsSync() {
setupAutoSyncListener();

getFixture()
.addFileToProject(
"some/other/path/Class1.java", "package some.other.path;public class Class1{}");
verify(mockSyncRequester, never()).requestSync();
}

@Test
public void projectFileMoved_requestsSync() {
setupAutoSyncListener();

WriteAction.runAndWait(
() ->
getFixture()
.moveFile(
SOURCE_ROOT.resolve("java/com/example/Class1.java").toString(),
SOURCE_ROOT.resolve("submodule/java/com/example").toString()));
verify(mockSyncRequester, atLeastOnce()).requestSync();
}

@Test
public void projectFileModified_nonBuildFile_doesNotRequestSync() throws Exception {
setupAutoSyncListener();

VirtualFile vf =
getFixture()
.findFileInTempDir(SOURCE_ROOT.resolve("java/com/example/Class1.java").toString());

WriteAction.runAndWait(
() ->
vf.setBinaryContent(
"/**LICENSE-TEXT*/package com.example;public class Class1{}".getBytes(UTF_8)));

verify(mockSyncRequester, never()).requestSync();
}

@Test
public void projectFileModified_buildFile_requestsSync() throws Exception {
setupAutoSyncListener();

VirtualFile vf = getFixture().findFileInTempDir(SOURCE_ROOT.resolve("BUILD").toString());

WriteAction.runAndWait(
() ->
vf.setBinaryContent(
"/**LICENSE-TEXT*/java_library(name=\"javalib\",srcs=[])".getBytes(UTF_8)));

verify(mockSyncRequester, atLeastOnce()).requestSync();
}
}

0 comments on commit 041f740

Please sign in to comment.