/*
* Copyright 2012-2017 the original author or 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 org.springframework.boot.devtools.filewatch;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.devtools.filewatch.ChangedFile.Type;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link FileSystemWatcher}.
*
* @author Phillip Webb
*/
public class FileSystemWatcherTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private FileSystemWatcher watcher;
private List<Set<ChangedFiles>> changes = Collections
.synchronizedList(new ArrayList<Set<ChangedFiles>>());
@Rule
public TemporaryFolder temp = new TemporaryFolder();
@Before
public void setup() throws Exception {
setupWatcher(20, 10);
}
@Test
public void pollIntervalMustBePositive() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("PollInterval must be positive");
new FileSystemWatcher(true, 0, 1);
}
@Test
public void quietPeriodMustBePositive() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("QuietPeriod must be positive");
new FileSystemWatcher(true, 1, 0);
}
@Test
public void pollIntervalMustBeGreaterThanQuietPeriod() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("PollInterval must be greater than QuietPeriod");
new FileSystemWatcher(true, 1, 1);
}
@Test
public void listenerMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("FileChangeListener must not be null");
this.watcher.addListener(null);
}
@Test
public void cannotAddListenerToStartedListener() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("FileSystemWatcher already started");
this.watcher.start();
this.watcher.addListener(mock(FileChangeListener.class));
}
@Test
public void sourceFolderMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Folder must not be null");
this.watcher.addSourceFolder(null);
}
@Test
public void sourceFolderMustExist() throws Exception {
File folder = new File("does/not/exist");
assertThat(folder.exists()).isFalse();
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage(
"Folder '" + folder + "' must exist and must be a directory");
this.watcher.addSourceFolder(folder);
}
@Test
public void sourceFolderMustBeADirectory() throws Exception {
File folder = new File("pom.xml");
assertThat(folder.isFile()).isTrue();
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Folder 'pom.xml' must exist and must be a directory");
this.watcher.addSourceFolder(new File("pom.xml"));
}
@Test
public void cannotAddSourceFolderToStartedListener() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("FileSystemWatcher already started");
this.watcher.start();
this.watcher.addSourceFolder(this.temp.newFolder());
}
@Test
public void addFile() throws Exception {
File folder = startWithNewFolder();
File file = touch(new File(folder, "test.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
ChangedFile expected = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFiles.getFiles()).contains(expected);
}
@Test
public void addNestedFile() throws Exception {
File folder = startWithNewFolder();
File file = touch(new File(new File(folder, "sub"), "text.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
ChangedFile expected = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFiles.getFiles()).contains(expected);
}
@Test
public void waitsForPollingInterval() throws Exception {
setupWatcher(10, 1);
File folder = startWithNewFolder();
touch(new File(folder, "test1.txt"));
while (this.changes.size() != 1) {
Thread.sleep(10);
}
touch(new File(folder, "test2.txt"));
this.watcher.stopAfter(1);
assertThat(this.changes.size()).isEqualTo(2);
}
@Test
public void waitsForQuietPeriod() throws Exception {
setupWatcher(300, 200);
File folder = startWithNewFolder();
for (int i = 0; i < 10; i++) {
touch(new File(folder, i + "test.txt"));
Thread.sleep(100);
}
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
assertThat(changedFiles.getFiles().size()).isEqualTo(10);
}
@Test
public void withExistingFiles() throws Exception {
File folder = this.temp.newFolder();
touch(new File(folder, "test.txt"));
this.watcher.addSourceFolder(folder);
this.watcher.start();
File file = touch(new File(folder, "test2.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
ChangedFile expected = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFiles.getFiles()).contains(expected);
}
@Test
public void multipleSources() throws Exception {
File folder1 = this.temp.newFolder();
File folder2 = this.temp.newFolder();
this.watcher.addSourceFolder(folder1);
this.watcher.addSourceFolder(folder2);
this.watcher.start();
File file1 = touch(new File(folder1, "test.txt"));
File file2 = touch(new File(folder2, "test.txt"));
this.watcher.stopAfter(1);
Set<ChangedFiles> change = getSingleOnChange();
assertThat(change.size()).isEqualTo(2);
for (ChangedFiles changedFiles : change) {
if (changedFiles.getSourceFolder().equals(folder1)) {
ChangedFile file = new ChangedFile(folder1, file1, Type.ADD);
assertThat(changedFiles.getFiles()).containsOnly(file);
}
else {
ChangedFile file = new ChangedFile(folder2, file2, Type.ADD);
assertThat(changedFiles.getFiles()).containsOnly(file);
}
}
}
@Test
public void multipleListeners() throws Exception {
File folder = this.temp.newFolder();
final Set<ChangedFiles> listener2Changes = new LinkedHashSet<>();
this.watcher.addSourceFolder(folder);
this.watcher.addListener(new FileChangeListener() {
@Override
public void onChange(Set<ChangedFiles> changeSet) {
listener2Changes.addAll(changeSet);
}
});
this.watcher.start();
File file = touch(new File(folder, "test.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
ChangedFile expected = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFiles.getFiles()).contains(expected);
assertThat(listener2Changes).isEqualTo(this.changes.get(0));
}
@Test
public void modifyDeleteAndAdd() throws Exception {
File folder = this.temp.newFolder();
File modify = touch(new File(folder, "modify.txt"));
File delete = touch(new File(folder, "delete.txt"));
this.watcher.addSourceFolder(folder);
this.watcher.start();
FileCopyUtils.copy("abc".getBytes(), modify);
delete.delete();
File add = touch(new File(folder, "add.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
Set<ChangedFile> actual = changedFiles.getFiles();
Set<ChangedFile> expected = new HashSet<>();
expected.add(new ChangedFile(folder, modify, Type.MODIFY));
expected.add(new ChangedFile(folder, delete, Type.DELETE));
expected.add(new ChangedFile(folder, add, Type.ADD));
assertThat(actual).isEqualTo(expected);
}
@Test
public void withTriggerFilter() throws Exception {
File folder = this.temp.newFolder();
File file = touch(new File(folder, "file.txt"));
File trigger = touch(new File(folder, "trigger.txt"));
this.watcher.addSourceFolder(folder);
this.watcher.setTriggerFilter(new FileFilter() {
@Override
public boolean accept(File file) {
return file.getName().equals("trigger.txt");
}
});
this.watcher.start();
FileCopyUtils.copy("abc".getBytes(), file);
Thread.sleep(100);
assertThat(this.changes.size()).isEqualTo(0);
FileCopyUtils.copy("abc".getBytes(), trigger);
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
Set<ChangedFile> actual = changedFiles.getFiles();
Set<ChangedFile> expected = new HashSet<>();
expected.add(new ChangedFile(folder, file, Type.MODIFY));
assertThat(actual).isEqualTo(expected);
}
private void setupWatcher(long pollingInterval, long quietPeriod) {
this.watcher = new FileSystemWatcher(false, pollingInterval, quietPeriod);
this.watcher.addListener(new FileChangeListener() {
@Override
public void onChange(Set<ChangedFiles> changeSet) {
FileSystemWatcherTests.this.changes.add(changeSet);
}
});
}
private File startWithNewFolder() throws IOException {
File folder = this.temp.newFolder();
this.watcher.addSourceFolder(folder);
this.watcher.start();
return folder;
}
private ChangedFiles getSingleChangedFiles() {
Set<ChangedFiles> singleChange = getSingleOnChange();
assertThat(singleChange.size()).isEqualTo(1);
return singleChange.iterator().next();
}
private Set<ChangedFiles> getSingleOnChange() {
assertThat(this.changes.size()).isEqualTo(1);
return this.changes.get(0);
}
private File touch(File file) throws FileNotFoundException, IOException {
file.getParentFile().mkdirs();
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.close();
return file;
}
}