From e06306a6ce58587b68d1ba618a817896ac8e4f0a Mon Sep 17 00:00:00 2001 From: Tanish Ranjan <62828604+Tanish-Ranjan@users.noreply.github.com> Date: Fri, 13 Sep 2024 06:26:33 +0530 Subject: [PATCH] feat - Android test variant support (#194) Changes: - Updated AndroidUtils to populate all fields individually so failure in retrieval of one property won't lead to the collapse of whole source set. - Added android resource directories only if the variant is not a unit test - Updated GradleApiConnectorTest#testAndroidSourceSets to consider the all the test variants and their dependencies - AndroidUtils#convertVariantToGradleSourceSet now returns a list of source sets rather than a single source set. - AndroidUtils#addTests now returns a list of test source sets. --- .../bs/gradle/plugin/utils/AndroidUtils.java | 331 +++++++++++++----- .../gradle/GradleApiConnectorTest.java | 17 +- 2 files changed, 258 insertions(+), 90 deletions(-) diff --git a/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/utils/AndroidUtils.java b/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/utils/AndroidUtils.java index c551b592..df35d5ed 100644 --- a/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/utils/AndroidUtils.java +++ b/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/utils/AndroidUtils.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.LinkedList; @@ -59,7 +60,6 @@ public static boolean isAndroidProject(Project project) { * * @param project Gradle project for extracting the build variants */ - @SuppressWarnings("unchecked") public static List getBuildVariantsAsGradleSourceSets(Project project) { List sourceSets = new LinkedList<>(); @@ -69,42 +69,43 @@ public static List getBuildVariantsAsGradleSourceSets(Project p return sourceSets; } - AndroidProjectType type = getProjectType(project); - if (type == null) { + AndroidProjectType androidProjectType = getProjectType(project); + if (androidProjectType == null) { return sourceSets; } - String methodName; - switch (type) { + List variants = new LinkedList<>(); + switch (androidProjectType) { case APPLICATION: case DYNAMIC_FEATURE: - methodName = "getApplicationVariants"; + variants = getVariants(androidExtension, "getApplicationVariants", "getTestVariants"); break; case LIBRARY: - methodName = "getLibraryVariants"; + variants = getVariants(androidExtension, "getLibraryVariants", "getTestVariants"); break; case INSTANT_APP_FEATURE: - methodName = "getFeatureVariants"; + variants = getVariants(androidExtension, "getFeatureVariants", "getTestVariants"); break; case ANDROID_TEST: - methodName = "getTestVariants"; + variants = getVariants(androidExtension, "getTestVariants"); break; default: - methodName = ""; } - try { - Set variants = (Set) invokeMethod(androidExtension, methodName); - for (Object variant : variants) { - GradleSourceSet sourceSet = convertVariantToGradleSourceSet(project, variant); - if (sourceSet == null) { - continue; - } + for (Object variant : variants) { + GradleSourceSet sourceSet = convertVariantToGradleSourceSet(project, variant, false); + if (sourceSet != null) { sourceSets.add(sourceSet); } - } catch (IllegalAccessException | NoSuchMethodException - | InvocationTargetException | ClassCastException e) { - // do nothing + } + + if (androidProjectType != AndroidProjectType.ANDROID_TEST) { + for (Object variant : getVariants(androidExtension, "getUnitTestVariants")) { + GradleSourceSet sourceSet = convertVariantToGradleSourceSet(project, variant, true); + if (sourceSet != null) { + sourceSets.add(sourceSet); + } + } } return sourceSets; @@ -112,13 +113,38 @@ public static List getBuildVariantsAsGradleSourceSets(Project p } /** - * Returns a GradleSourceSet populated with the given Android build variant data. + * Returns a list of variants extracted with the listed method names from the given + * android extension. + * + * @param androidExtension AndroidExtension object from which the variants are to be extracted. + * @param methodNames name of different methods to invoke to get all the variants. + */ + @SuppressWarnings("unchecked") + private static List getVariants(Object androidExtension, String... methodNames) { + List variants = new LinkedList<>(); + for (String methodName : methodNames) { + try { + variants.addAll((Collection) invokeMethod(androidExtension, methodName)); + } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { + // do nothing + } + } + return variants; + } + + /** + * Returns a GradleSourceSet which has been populated with respective + * Android build variant data. * * @param project Gradle project to populate GradleSourceSet properties * @param variant Android Build Variant object to populate GradleSourceSet properties + * @param isUnitTest Indicates if the given variant is a unit test variant */ - @SuppressWarnings("unchecked") - private static GradleSourceSet convertVariantToGradleSourceSet(Project project, Object variant) { + private static GradleSourceSet convertVariantToGradleSourceSet( + Project project, + Object variant, + boolean isUnitTest + ) { try { @@ -156,8 +182,59 @@ private static GradleSourceSet convertVariantToGradleSourceSet(Project project, gradleSourceSet.setDisplayName(displayName); // module dependencies - Set moduleDependencies = - AndroidDependencyCollector.getModuleDependencies(project, variant); + addModuleDependencies(gradleSourceSet, project, variant); + + // source and resource + addSourceAndResources(gradleSourceSet, variant, isUnitTest); + + // resource outputs + addResourceOutputs(gradleSourceSet, variant, isUnitTest); + + List compilerArgs = new ArrayList<>(); + + // generated sources and source outputs + addGeneratedSourceAndSourceOutputs(gradleSourceSet, variant, compilerArgs); + + // classpath + addClasspath(gradleSourceSet, variant); + + // Archive output dirs (not relevant in case of android build variants) + gradleSourceSet.setArchiveOutputFiles(new HashMap<>()); + + // has tests + gradleSourceSet.setHasTests((boolean) hasProperty(variant, "testedVariant")); + + // extensions + addExtensions(gradleSourceSet, compilerArgs); + + return gradleSourceSet; + + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ignored) { + // do nothing + } + + return null; + + } + + /** + * Add module dependencies to the given GradleSourceSet. + * + * @param gradleSourceSet Instance of DefaultGradleSourceSet + * @param project Instance of Project + * @param variant Instance of Build Variant + */ + @SuppressWarnings("unchecked") + private static void addModuleDependencies( + DefaultGradleSourceSet gradleSourceSet, + Project project, + Object variant + ) { + + Set moduleDependencies = + AndroidDependencyCollector.getModuleDependencies(project, variant); + + try { // add Android SDK Object androidComponents = getAndroidComponentExtension(project); if (androidComponents != null) { @@ -178,7 +255,7 @@ private static GradleSourceSet convertVariantToGradleSourceSet(Project project, } } // add R.jar file - String taskName = "process" + capitalize(variantName) + "Resources"; + String taskName = "process" + capitalize(gradleSourceSet.getSourceSetName()) + "Resources"; Task processResourcesTask = project.getTasks().findByName(taskName); if (processResourcesTask != null) { Object output = invokeMethod(processResourcesTask, "getRClassOutputJar"); @@ -188,30 +265,70 @@ private static GradleSourceSet convertVariantToGradleSourceSet(Project project, moduleDependencies.add(mockModuleDependency(jarFile.toURI())); } } - gradleSourceSet.setModuleDependencies(moduleDependencies); + } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { + // do nothing + } - // source and resource + gradleSourceSet.setModuleDependencies(moduleDependencies); + + } + + /** + * Add source and resource directories to the given GradleSourceSet. + * + * @param gradleSourceSet Instance of DefaultGradleSourceSet + * @param variant Instance of Build Variant + * @param isUnitTest Indicates if the given variant is a unit test variant + */ + @SuppressWarnings("unchecked") + private static void addSourceAndResources( + DefaultGradleSourceSet gradleSourceSet, + Object variant, + boolean isUnitTest + ) { + + Set sourceDirs = new HashSet<>(); + Set resourceDirs = new HashSet<>(); + + try { Object sourceSets = getProperty(variant, "sourceSets"); - Set sourceDirs = new HashSet<>(); - Set resourceDirs = new HashSet<>(); if (sourceSets instanceof Iterable) { for (Object sourceSet : (Iterable) sourceSets) { Set javaDirectories = (Set) getProperty(sourceSet, "javaDirectories"); - Set resDirectories = - (Set) getProperty(sourceSet, "resDirectories"); - Set resourceDirectories = - (Set) getProperty(sourceSet, "resourcesDirectories"); sourceDirs.addAll(javaDirectories); - resourceDirs.addAll(resDirectories); - resourceDirs.addAll(resourceDirectories); + if (!isUnitTest) { + resourceDirs.addAll((Set) getProperty(sourceSet, "resDirectories")); + } + resourceDirs.addAll((Set) getProperty(sourceSet, "resourcesDirectories")); } } - gradleSourceSet.setSourceDirs(sourceDirs); - gradleSourceSet.setResourceDirs(resourceDirs); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + // do nothing + } - // resource outputs - Set resourceOutputs = new HashSet<>(); + gradleSourceSet.setSourceDirs(sourceDirs); + gradleSourceSet.setResourceDirs(resourceDirs); + + } + + /** + * Add resource output directories to the given GradleSourceSet. + * + * @param gradleSourceSet Instance of DefaultGradleSourceSet + * @param variant Instance of Build Variant + * @param isUnitTest Indicates if the given variant is a unit test variant + */ + @SuppressWarnings("unchecked") + private static void addResourceOutputs( + DefaultGradleSourceSet gradleSourceSet, + Object variant, + boolean isUnitTest + ) { + + Set resourceOutputs = new HashSet<>(); + + try { Provider resourceProvider = (Provider) getProperty(variant, "processJavaResourcesProvider"); if (resourceProvider != null) { @@ -219,22 +336,45 @@ private static GradleSourceSet convertVariantToGradleSourceSet(Project project, File outputDir = (File) invokeMethod(resTask, "getDestinationDir"); resourceOutputs.add(outputDir); } - Provider resProvider = - (Provider) getProperty(variant, "mergeResourcesProvider"); - if (resProvider != null) { - Task resTask = resProvider.get(); - Object outputDir = invokeMethod(resTask, "getOutputDir"); - File output = ((Provider) invokeMethod(outputDir, "getAsFile")).get(); - resourceOutputs.add(output); + + if (!isUnitTest) { + Provider resProvider = + (Provider) getProperty(variant, "mergeResourcesProvider"); + if (resProvider != null) { + Task resTask = resProvider.get(); + Object outputDir = invokeMethod(resTask, "getOutputDir"); + File output = ((Provider) invokeMethod(outputDir, "getAsFile")).get(); + resourceOutputs.add(output); + } } - gradleSourceSet.setResourceOutputDirs(resourceOutputs); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + // do nothing + } - // generated sources and source outputs - Set generatedSources = new HashSet<>(); - Set sourceOutputs = new HashSet<>(); + gradleSourceSet.setResourceOutputDirs(resourceOutputs); + + } + + /** + * Add source output and generated source output directories to the given GradleSourceSet. + * + * @param gradleSourceSet Instance of DefaultGradleSourceSet + * @param variant Instance of Build Variant + * @param compilerArgs List to be populated from the java compiler arguments. + */ + @SuppressWarnings("unchecked") + private static void addGeneratedSourceAndSourceOutputs( + DefaultGradleSourceSet gradleSourceSet, + Object variant, + List compilerArgs + ) { + + Set generatedSources = new HashSet<>(); + Set sourceOutputs = new HashSet<>(); + + try { Provider javaCompileProvider = (Provider) getProperty(variant, "javaCompileProvider"); - List compilerArgs = new ArrayList<>(); if (javaCompileProvider != null) { Task javaCompileTask = javaCompileProvider.get(); @@ -248,7 +388,7 @@ private static GradleSourceSet convertVariantToGradleSourceSet(Project project, // generated = compile source - source for (File compileSource : compileSources) { - boolean inSourceDir = sourceDirs.stream() + boolean inSourceDir = gradleSourceSet.getSourceDirs().stream() .anyMatch(dir -> compileSource.getAbsolutePath().startsWith(dir.getAbsolutePath())); if (inSourceDir) { continue; @@ -261,43 +401,60 @@ private static GradleSourceSet convertVariantToGradleSourceSet(Project project, generatedSources.add(compileSource); } } - gradleSourceSet.setGeneratedSourceDirs(generatedSources); - gradleSourceSet.setSourceOutputDirs(sourceOutputs); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + // do nothing + } - // classpath - Object compileConfig = invokeMethod(variant, "getCompileConfiguration"); - Set classpathFiles = (Set) invokeMethod(compileConfig, "getFiles"); - gradleSourceSet.setCompileClasspath(new LinkedList<>(classpathFiles)); + gradleSourceSet.setGeneratedSourceDirs(generatedSources); + gradleSourceSet.setSourceOutputDirs(sourceOutputs); - // Archive output dirs (not relevant in case of android build variants) - gradleSourceSet.setArchiveOutputFiles(new HashMap<>()); + } - // has tests - Object unitTestVariant = invokeMethod(variant, "getUnitTestVariant"); - Object testVariant = invokeMethod(variant, "getTestVariant"); - gradleSourceSet.setHasTests(unitTestVariant != null || testVariant != null); + /** + * Add classpath files to the given GradleSourceSet. + * + * @param gradleSourceSet Instance of DefaultGradleSourceSet + * @param variant Instance of Build Variant + */ + @SuppressWarnings("unchecked") + private static void addClasspath(DefaultGradleSourceSet gradleSourceSet, Object variant) { - // extensions - Map extensions = new HashMap<>(); - boolean isJavaSupported = Arrays.stream(SourceSetUtils.getSupportedLanguages()) - .anyMatch(l -> Objects.equals(l, SupportedLanguages.JAVA.getBspName())); - if (isJavaSupported) { - DefaultJavaExtension extension = new DefaultJavaExtension(); + Set classpathFiles = new HashSet<>(); - extension.setCompilerArgs(compilerArgs); - extension.setSourceCompatibility(getSourceCompatibility(compilerArgs)); - extension.setTargetCompatibility(getTargetCompatibility(compilerArgs)); + try { + Object compileConfig = invokeMethod(variant, "getCompileConfiguration"); + classpathFiles.addAll((Set) invokeMethod(compileConfig, "getFiles")); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + // do nothing + } - extensions.put(SupportedLanguages.JAVA.getBspName(), extension); - } - gradleSourceSet.setExtensions(extensions); + gradleSourceSet.setCompileClasspath(new LinkedList<>(classpathFiles)); - return gradleSourceSet; + } - } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - return null; + /** + * Add language extension to the given GradleSourceSet. + * + * @param gradleSourceSet Instance of DefaultGradleSourceSet + * @param compilerArgs List of compiler arguments needed to build the language extension. + */ + private static void addExtensions( + DefaultGradleSourceSet gradleSourceSet, + List compilerArgs + ) { + Map extensions = new HashMap<>(); + boolean isJavaSupported = Arrays.stream(SourceSetUtils.getSupportedLanguages()) + .anyMatch(l -> Objects.equals(l, SupportedLanguages.JAVA.getBspName())); + if (isJavaSupported) { + DefaultJavaExtension extension = new DefaultJavaExtension(); + + extension.setCompilerArgs(compilerArgs); + extension.setSourceCompatibility(getSourceCompatibility(compilerArgs)); + extension.setTargetCompatibility(getTargetCompatibility(compilerArgs)); + + extensions.put(SupportedLanguages.JAVA.getBspName(), extension); } - + gradleSourceSet.setExtensions(extensions); } /** @@ -379,6 +536,17 @@ public static Object getProperty(Object obj, String propertyName) return obj.getClass().getMethod("getProperty", String.class).invoke(obj, propertyName); } + /** + * Checks if the given property exists in the given object with {@code hasProperty} method. + * + * @param obj object from which the property is to be extracted + * @param propertyName name of the property to be extracted + */ + public static Object hasProperty(Object obj, String propertyName) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return obj.getClass().getMethod("hasProperty", String.class).invoke(obj, propertyName); + } + /** * Enum class representing different types of Android projects. */ @@ -426,6 +594,7 @@ private static Object invokeMethod(Object object, String methodName) } // region TODO: Duplicate code from JavaLanguageModelBuilder + /** * Get the compilation arguments of the build variant. */ diff --git a/server/src/test/java/com/microsoft/java/bs/core/internal/gradle/GradleApiConnectorTest.java b/server/src/test/java/com/microsoft/java/bs/core/internal/gradle/GradleApiConnectorTest.java index 9e676335..ea99d4a5 100644 --- a/server/src/test/java/com/microsoft/java/bs/core/internal/gradle/GradleApiConnectorTest.java +++ b/server/src/test/java/com/microsoft/java/bs/core/internal/gradle/GradleApiConnectorTest.java @@ -100,18 +100,19 @@ void testAndroidSourceSets() { preferenceManager.setPreferences(new Preferences()); GradleApiConnector connector = new GradleApiConnector(preferenceManager); GradleSourceSets gradleSourceSets = connector.getGradleSourceSets(projectDir.toURI(), null); - assertEquals(4, gradleSourceSets.getGradleSourceSets().size()); + assertEquals(10, gradleSourceSets.getGradleSourceSets().size()); findSourceSet(gradleSourceSets, "app [debug]"); + findSourceSet(gradleSourceSets, "app [debugUnitTest]"); + findSourceSet(gradleSourceSets, "app [debugAndroidTest]"); findSourceSet(gradleSourceSets, "app [release]"); + findSourceSet(gradleSourceSets, "app [releaseUnitTest]"); findSourceSet(gradleSourceSets, "mylibrary [debug]"); + findSourceSet(gradleSourceSets, "mylibrary [debugUnitTest]"); + findSourceSet(gradleSourceSets, "mylibrary [debugAndroidTest]"); findSourceSet(gradleSourceSets, "mylibrary [release]"); + findSourceSet(gradleSourceSets, "mylibrary [releaseUnitTest]"); Set combinedModuleDependencies = new HashSet<>(); for (GradleSourceSet sourceSet : gradleSourceSets.getGradleSourceSets()) { - assertEquals(2, sourceSet.getSourceDirs().size()); - assertEquals(4, sourceSet.getResourceDirs().size()); - assertEquals(0, sourceSet.getExtensions().size()); - assertEquals(0, sourceSet.getArchiveOutputFiles().size()); - assertTrue(sourceSet.hasTests()); combinedModuleDependencies.addAll(sourceSet.getModuleDependencies()); } // This test can vary depending on the environment due to generated files. @@ -120,9 +121,7 @@ void testAndroidSourceSets() { // the R.jar files don't exist for the build targets and are not included. // 2. ANDROID_HOME is not configured in which case the Android Component classpath // is not added to module dependencies. - - // 57 is the number of actual project module dependencies without test variant dependencies - assertTrue(combinedModuleDependencies.size() >= 57); + assertTrue(combinedModuleDependencies.size() >= 82); } private GradleSourceSet findSourceSet(GradleSourceSets gradleSourceSets, String displayName) {