Preserve dependencies when uninstalling from the CLI

Previously, the CLI did not keep track of a dependency's users. This
meant that installing two extensions with a common dependency and
then unistalling one extension would break the other extension as the
common dependency would be deleted:

 1. Install a that depends on c
 2. Install b that depends on c
 3. Uninstall b
 4. a is now broken as c has been deleted

This commit updates the CLI to maintain a count for each artifact
that's installed into /lib. An artifact is now only deleted when the
count reaches zero.

As part of this change the code has been
extensively refactored to bring the structure into line with other CLI
commands and to improve testability.

Closes gh-1410
This commit is contained in:
Andy Wilkinson
2014-10-14 14:40:44 +01:00
parent 4e636f069f
commit a2c5b6a7bb
8 changed files with 697 additions and 194 deletions

View File

@@ -0,0 +1,130 @@
/*
* Copyright 2012-2014 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.cli.command.install;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
import org.springframework.boot.cli.compiler.GroovyCompilerScope;
import org.springframework.boot.cli.compiler.RepositoryConfigurationFactory;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link GroovyGrabDependencyResolver}.
*
* @author Andy Wilkinson
*/
public class GroovyGrabDependencyResolverTests {
private DependencyResolver resolver;
@Before
public void setupResolver() {
GroovyCompilerConfiguration configuration = new GroovyCompilerConfiguration() {
@Override
public boolean isGuessImports() {
return true;
}
@Override
public boolean isGuessDependencies() {
return true;
}
@Override
public boolean isAutoconfigure() {
return false;
}
@Override
public GroovyCompilerScope getScope() {
return GroovyCompilerScope.DEFAULT;
}
@Override
public List<RepositoryConfiguration> getRepositoryConfiguration() {
return RepositoryConfigurationFactory
.createDefaultRepositoryConfiguration();
}
@Override
public String[] getClasspath() {
return new String[] { "." };
}
};
this.resolver = new GroovyGrabDependencyResolver(configuration);
}
@Test
public void resolveArtifactWithNoDependencies() throws Exception {
List<File> resolved = this.resolver.resolve(Arrays
.asList("commons-logging:commons-logging:1.1.3"));
assertThat(resolved, hasSize(1));
assertThat(getNames(resolved), hasItems("commons-logging-1.1.3.jar"));
}
@Test
public void resolveArtifactWithDependencies() throws Exception {
List<File> resolved = this.resolver.resolve(Arrays
.asList("org.springframework:spring-core:4.1.1.RELEASE"));
assertThat(resolved, hasSize(2));
assertThat(getNames(resolved),
hasItems("commons-logging-1.1.3.jar", "spring-core-4.1.1.RELEASE.jar"));
}
@SuppressWarnings("unchecked")
@Test
public void resolveShorthandArtifactWithDependencies() throws Exception {
List<File> resolved = this.resolver.resolve(Arrays.asList("spring-core"));
assertThat(resolved, hasSize(2));
assertThat(getNames(resolved),
hasItems(startsWith("commons-logging-"), startsWith("spring-core-")));
}
@Test
public void resolveMultipleArtifacts() throws Exception {
List<File> resolved = this.resolver.resolve(Arrays.asList("junit:junit:4.11",
"commons-logging:commons-logging:1.1.3"));
assertThat(resolved, hasSize(3));
assertThat(
getNames(resolved),
hasItems("junit-4.11.jar", "commons-logging-1.1.3.jar",
"hamcrest-core-1.3.jar"));
}
public Set<String> getNames(Collection<File> files) {
Set<String> names = new HashSet<String>(files.size());
for (File file : files) {
names.add(file.getName());
}
return names;
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright 2012-2014 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.cli.command.install;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.util.FileSystemUtils;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Tests for {@link Installer}
*
* @author Andy Wilkinson
*/
public class InstallerTests {
public DependencyResolver resolver = mock(DependencyResolver.class);
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
private Installer installer;
@Before
public void setUp() throws IOException {
System.setProperty("spring.home", "target");
FileSystemUtils.deleteRecursively(new File("target/lib"));
this.installer = new Installer(this.resolver);
}
@After
public void cleanUp() {
System.clearProperty("spring.home");
}
@Test
public void installNewDependency() throws Exception {
File foo = createTemporaryFile("foo.jar");
when(this.resolver.resolve(Arrays.asList("foo"))).thenReturn(Arrays.asList(foo));
this.installer.install(Arrays.asList("foo"));
assertThat(getNamesOfFilesInLib(), containsInAnyOrder("foo.jar", ".installed"));
}
@Test
public void installAndUninstall() throws Exception {
File foo = createTemporaryFile("foo.jar");
when(this.resolver.resolve(Arrays.asList("foo"))).thenReturn(Arrays.asList(foo));
this.installer.install(Arrays.asList("foo"));
this.installer.uninstall(Arrays.asList("foo"));
assertThat(getNamesOfFilesInLib(), contains(".installed"));
}
@Test
public void installAndUninstallWithCommonDependencies() throws Exception {
File alpha = createTemporaryFile("alpha.jar");
File bravo = createTemporaryFile("bravo.jar");
File charlie = createTemporaryFile("charlie.jar");
when(this.resolver.resolve(Arrays.asList("bravo"))).thenReturn(
Arrays.asList(bravo, alpha));
when(this.resolver.resolve(Arrays.asList("charlie"))).thenReturn(
Arrays.asList(charlie, alpha));
this.installer.install(Arrays.asList("bravo"));
assertThat(getNamesOfFilesInLib(),
containsInAnyOrder("alpha.jar", "bravo.jar", ".installed"));
this.installer.install(Arrays.asList("charlie"));
assertThat(getNamesOfFilesInLib(),
containsInAnyOrder("alpha.jar", "bravo.jar", "charlie.jar", ".installed"));
this.installer.uninstall(Arrays.asList("bravo"));
assertThat(getNamesOfFilesInLib(),
containsInAnyOrder("alpha.jar", "charlie.jar", ".installed"));
this.installer.uninstall(Arrays.asList("charlie"));
assertThat(getNamesOfFilesInLib(), containsInAnyOrder(".installed"));
}
@Test
public void uninstallAll() throws Exception {
File alpha = createTemporaryFile("alpha.jar");
File bravo = createTemporaryFile("bravo.jar");
File charlie = createTemporaryFile("charlie.jar");
when(this.resolver.resolve(Arrays.asList("bravo"))).thenReturn(
Arrays.asList(bravo, alpha));
when(this.resolver.resolve(Arrays.asList("charlie"))).thenReturn(
Arrays.asList(charlie, alpha));
this.installer.install(Arrays.asList("bravo"));
this.installer.install(Arrays.asList("charlie"));
assertThat(getNamesOfFilesInLib(),
containsInAnyOrder("alpha.jar", "bravo.jar", "charlie.jar", ".installed"));
this.installer.uninstallAll();
assertThat(getNamesOfFilesInLib(), containsInAnyOrder(".installed"));
}
private Set<String> getNamesOfFilesInLib() {
Set<String> names = new HashSet<String>();
for (File file : new File("target/lib").listFiles()) {
names.add(file.getName());
}
return names;
}
private File createTemporaryFile(String name) throws IOException {
File temporaryFile = this.tempFolder.newFile(name);
temporaryFile.deleteOnExit();
return temporaryFile;
}
}