Skip to content

Commit

Permalink
#84: Supports multiple sources of sdkmanager
Browse files Browse the repository at this point in the history
Depending on user's setups, there are a variety of places the initial sdkmanager
binary could be located.
  • Loading branch information
quittle committed Oct 16, 2020
1 parent 0dcf38f commit 1fdc1e9
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 8 deletions.
9 changes: 9 additions & 0 deletions android-emulator-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ dependencies {
implementation 'org.apache.logging.log4j:log4j-iostreams:2.13.3'

compileOnly 'com.google.code.findbugs:annotations:3.0.1'
testCompileOnly 'com.google.code.findbugs:annotations:3.0.1'

testImplementation 'io.mockk:mockk:1.10.2'
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.0'
testImplementation 'org.mockito:mockito-junit-jupiter:3.5.13'
}

test {
useJUnitPlatform()
}

tasks.withType(JavaCompile) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,33 @@
import com.google.common.collect.ImmutableMap;
import org.apache.tools.ant.taskdefs.condition.Os;
import org.gradle.api.Project;
import org.gradle.util.VersionNumber;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.List;
import java.util.Map;

@SuppressWarnings("PMD.AvoidDuplicateLiterals")
class EmulatorConfiguration {
/**
* Paths to a folder containing {@code sdkmanager} relative to $SDK_ROOT. {@code null} entries indicate that the
* folder in the path should be a version number and to select the highest revision possible. This
* {@code sdkmanager} will be used to bootstrap the plugin and install a newer version. The earlier entries are
* preferred over later ones.
*/
private static final String[][] POTENTIAL_INITIAL_SDK_MANAGER_PATHS = new String[][] {
// This is used if cmdline-tools is downloaded separately and copied into $SDK_ROOT
new String[] {"cmdline-tools", "tools", "bin"},
// This is used when cmdline-tools;latest is installed via sdkmanager
new String[] {"cmdline-tools", "latest", "bin"},
// This is used when a specific version of the cmdline-tools package is installed via sdkmanager
new String[] {"cmdline-tools", null, "bin"},
// This is used when the sdkmanager is provided by the legacy sdk tools which haven't been updated in years.
new String[] {"tools", "bin"},
};

private final File sdkRoot;
private final File avdRoot;
private final Map<String, String> environmentVariableMap;
Expand Down Expand Up @@ -95,12 +113,28 @@ File sdkFile(String... path) {
return sdkFile(sdkRoot, path);
}

File getSdkManager() {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
return sdkFile(sdkRoot,"tools", "bin", "sdkmanager.bat");
} else {
return sdkFile(sdkRoot,"tools", "bin", "sdkmanager");
/**
* Provides the initial {@code sdkmanager} used to install the latest version, retrieved later by this plugin via
* {@link #getCmdLineToolsSdkManager}.
* @return The {@code sdkmanager} file location, which is guaranteed to exist and be a file if returned.
* @throws RuntimeException if no {@code sdkmanager} to use can be found on disk.
*/
File getSdkManager() throws RuntimeException {
for (final String[] path : POTENTIAL_INITIAL_SDK_MANAGER_PATHS) {
final File basePath = sdkFile(sdkRoot, path);
if (basePath != null) {
final File sdkmanager;
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
sdkmanager = new File(basePath, "sdkmanager.bat");
} else {
sdkmanager = new File(basePath, "sdkmanager");
}
if (sdkmanager.isFile()) {
return sdkmanager;
}
}
}
throw new RuntimeException("Unable to find a valid sdkmanager to use.");
}

File getCmdLineToolsSdkManager() {
Expand Down Expand Up @@ -179,7 +213,30 @@ String getDeviceType() {
return deviceType;
}

private static File sdkFile(final File sdkRoot, final String... path) {
return new File(sdkRoot, String.join(File.separator, path));
private static File sdkFile(final File sdkRoot, final String... pathParts) {
File path = sdkRoot;
for (final String part : pathParts) {
if (part != null) {
path = new File(path, part);
} else if (!path.isDirectory()) {
return null;
} else {
File[] children = path.listFiles();

if (children == null || children.length == 0) {
return null;
}

Arrays.sort(children, (a, b) -> {
final VersionNumber aVersion = VersionNumber.parse(a.getName());
final VersionNumber bVersion = VersionNumber.parse(b.getName());
return aVersion.compareTo(bVersion);
});

path = children[children.length - 1];
}
}

return path;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package com.quittle.androidemulator;

import com.android.build.gradle.BaseExtension;
import com.android.build.gradle.internal.dsl.DefaultConfig;
import io.mockk.impl.annotations.MockK;
import io.mockk.junit5.MockKExtension;
import org.gradle.api.Project;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.io.File;
import java.io.IOException;

import static io.mockk.MockKKt.every;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;

@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "PMD.AvoidDuplicateLiterals"})
@ExtendWith({MockitoExtension.class, MockKExtension.class})
class EmulatorConfigurationTest {
@Mock
private Project mockProject;
@MockK
private BaseExtension mockBaseExtension;
@Mock
private AndroidEmulatorExtension mockAndroidEmulatorExtension;
@Mock
private AndroidEmulatorExtension.EmulatorExtension mockEmulatorExtension;
@Mock
private DefaultConfig mockDefaultConfig;
@TempDir
File tempDir;

private EmulatorConfiguration configuration;

@BeforeEach
void setUp() {
every(_scope -> mockBaseExtension.getSdkDirectory()).returns(tempDir);
every(_scope -> mockBaseExtension.getDefaultConfig()).returns(mockDefaultConfig);
when(mockAndroidEmulatorExtension.getEmulator()).thenReturn(mockEmulatorExtension);
configuration = new EmulatorConfiguration(mockProject, mockBaseExtension, mockAndroidEmulatorExtension);
}

@Test
void testGetSdkManager_emptySdkRoot() {
assertGetSdkManagerThrows();
}

@Test
void testGetSdkManager_unrelatedFolder() {
makeSdkmanagerInTempDirectory("foo");
assertGetSdkManagerThrows();
}

/**
* Commandline tools downloaded standalone and placed in SDK_ROOT
*/
@Test
void testGetSdkManager_cmdlineToolsTools() {
final File file = makeSdkmanagerInTempDirectory("cmdline-tools", "tools", "bin");
assertEquals(file, configuration.getSdkManager());
}

/**
* Commandline tools installed by sdkmanager installed as "latest" version
*/
@Test
void testGetSdkManager_cmdlineToolsLatest() {
final File file = makeSdkmanagerInTempDirectory("cmdline-tools", "latest", "bin");
assertEquals(file, configuration.getSdkManager());
}

/**
* Commandline tools installed by sdkmanager installed as a specific version
*/
@Test
void testGetSdkManager_cmdlineToolsVersion() {
final File file = makeSdkmanagerInTempDirectory("cmdline-tools", "2.1", "bin");
assertEquals(file, configuration.getSdkManager());
}

/**
* Usecase for original sdk tools
*/
@Test
void testGetSdkManager_tools() {
final File file = makeSdkmanagerInTempDirectory("tools", "bin");
assertEquals(file, configuration.getSdkManager());
}

@Test
void testGetSdkManager_allowInvalidVersions() {
File sdkmanager = makeSdkmanagerInTempDirectory("cmdline-tools", "madeupversion", "bin");
assertEquals(sdkmanager, configuration.getSdkManager());
}

@Test
void testGetSdkManager_preferValidVersions() {
// Despite having many options for versions, only the valid version should be chosen
makeSdkmanagerInTempDirectory("cmdline-tools", "invalid", "bin");
makeSdkmanagerInTempDirectory("cmdline-tools", "0invalid", "bin");
makeSdkmanagerInTempDirectory("cmdline-tools", "1invalid", "bin");
makeSdkmanagerInTempDirectory("cmdline-tools", "1.invalid", "bin");
makeSdkmanagerInTempDirectory("cmdline-tools", "1-tagged", "bin");
final File sdkmanager = makeSdkmanagerInTempDirectory("cmdline-tools", "1.0", "bin");
makeSdkmanagerInTempDirectory("cmdline-tools", "2invalid", "bin");

assertEquals(sdkmanager, configuration.getSdkManager());
}

@Test
void testGetSdkManager_versionOrderOfPrecedense() {
final File version1 = makeSdkmanagerInTempDirectory("cmdline-tools", "1", "bin");
final File version2 = makeSdkmanagerInTempDirectory("cmdline-tools", "2.1", "bin");
final File version3 = makeSdkmanagerInTempDirectory("cmdline-tools", "3", "bin");
final File version10 = makeSdkmanagerInTempDirectory("cmdline-tools", "10.0.1", "bin");

// Verify their precedence by deleting them in order
assertEquals(version10, configuration.getSdkManager());
deleteFile(version10);
assertEquals(version3, configuration.getSdkManager());
deleteFile(version3);
assertEquals(version2, configuration.getSdkManager());
deleteFile(version2);
assertEquals(version1, configuration.getSdkManager());
deleteFile(version1);
assertGetSdkManagerThrows();
}

@Test
void testGetSdkManager_orderOfVersionPrecedence() {
// These should each override eachotehr
final File cmdlineTools = makeSdkmanagerInTempDirectory("cmdline-tools", "tools", "bin");
final File cmdlineLatest = makeSdkmanagerInTempDirectory("cmdline-tools", "latest", "bin");
final File cmdlineVersion = makeSdkmanagerInTempDirectory("cmdline-tools", "2.1", "bin");
final File legacy = makeSdkmanagerInTempDirectory("tools", "bin");

// Verify their precedence by deleting them in order
assertEquals(cmdlineTools, configuration.getSdkManager());
deleteFile(cmdlineTools);

assertEquals(cmdlineLatest, configuration.getSdkManager());
deleteFile(cmdlineLatest);

assertEquals(cmdlineVersion, configuration.getSdkManager());
deleteFile(cmdlineVersion);

assertEquals(legacy, configuration.getSdkManager());
deleteFile(legacy);

assertGetSdkManagerThrows();
}

/**
* Asserts that calling {@link EmulatorConfiguration#getSdkManager()} throws an exception.
*/
private void assertGetSdkManagerThrows() {
final RuntimeException exception = assertThrows(RuntimeException.class, () -> configuration.getSdkManager());
assertEquals("Unable to find a valid sdkmanager to use.", exception.getMessage());
}

/**
* Given a folder path, generates a synthetic {@code sdkmanager} relative to
*/
private File makeSdkmanagerInTempDirectory(String... path) {
final File parentFile = new File(tempDir, String.join(File.separator, path));
assertTrue(parentFile.mkdirs());
final File sdkmanager = new File(parentFile, OS.WINDOWS.isCurrentOs() ? "sdkmanager.bat" : "sdkmanager");
try {
assertTrue(sdkmanager.createNewFile());
return sdkmanager;
} catch (final IOException e) {
throw new RuntimeException(e);
}
}

/**
* Deletes a file and its parent directories recursively. Deletes up the tree as long as there are no other
* children.
* @param file The file to delete
*/
private static void deleteFile(final File file) {
assertTrue(file.delete());
final File parent = file.getParentFile();
if (parent != null) {
final String[] children = parent.list();
if (children != null && children.length == 0){
deleteFile(parent);
}
}
}
}

0 comments on commit 1fdc1e9

Please sign in to comment.