diff --git a/model/src/main/java/com/microsoft/java/bs/gradle/model/GradleSourceSet.java b/model/src/main/java/com/microsoft/java/bs/gradle/model/GradleSourceSet.java index 465b2783..cf5f57e6 100644 --- a/model/src/main/java/com/microsoft/java/bs/gradle/model/GradleSourceSet.java +++ b/model/src/main/java/com/microsoft/java/bs/gradle/model/GradleSourceSet.java @@ -86,9 +86,9 @@ public interface GradleSourceSet extends Serializable { Set getSourceOutputDirs(); /** - * The resource output directory of this source set. + * The resource output directories of this source set. */ - public File getResourceOutputDir(); + public Set getResourceOutputDirs(); /** * Any archive files created from the output of this source set to the output dirs. diff --git a/model/src/main/java/com/microsoft/java/bs/gradle/model/actions/GetSourceSetsAction.java b/model/src/main/java/com/microsoft/java/bs/gradle/model/actions/GetSourceSetsAction.java index e655ef09..852a876b 100644 --- a/model/src/main/java/com/microsoft/java/bs/gradle/model/actions/GetSourceSetsAction.java +++ b/model/src/main/java/com/microsoft/java/bs/gradle/model/actions/GetSourceSetsAction.java @@ -46,12 +46,12 @@ private Collection fetchIncludedBuilds(BuildController buildControl Map builds = new HashMap<>(); GradleBuild build = buildController.getBuildModel(); String rootProjectName = build.getRootProject().getName(); - fetchIncludedBuilds(buildController, build, builds, rootProjectName); + fetchIncludedBuilds(build, builds, rootProjectName); return builds.values(); } - private void fetchIncludedBuilds(BuildController buildController, GradleBuild build, - Map builds, String rootProjectName) { + private void fetchIncludedBuilds(GradleBuild build, Map builds, String rootProjectName) { if (builds.containsKey(rootProjectName)) { return; } @@ -75,7 +75,7 @@ private void fetchIncludedBuilds(BuildController buildController, GradleBuild bu if (moreBuilds != null) { for (GradleBuild includedBuild : moreBuilds) { String includedBuildName = includedBuild.getRootProject().getName(); - fetchIncludedBuilds(buildController, includedBuild, builds, includedBuildName); + fetchIncludedBuilds(includedBuild, builds, includedBuildName); } } } @@ -87,7 +87,7 @@ private void fetchIncludedBuilds(BuildController buildController, GradleBuild bu * @param builds The Gradle build models representing the build and included builds. */ private List fetchModels(BuildController buildController, - Collection builds) { + Collection builds) { List projectActions = new ArrayList<>(); for (GradleBuild build : builds) { @@ -101,7 +101,7 @@ private List fetchModels(BuildController buildController, // populate source set dependencies. List sourceSets = buildController.run(projectActions).stream() .flatMap(ss -> ss.getGradleSourceSets().stream()) - .map(ss -> new DefaultGradleSourceSet(ss)) + .map(DefaultGradleSourceSet::new) .collect(Collectors.toList()); populateInterProjectInfo(sourceSets); @@ -140,8 +140,10 @@ private void populateInterProjectInfo(List sourceSets) { outputsToSourceSet.put(file, sourceSet); } } - if (sourceSet.getResourceOutputDir() != null) { - outputsToSourceSet.put(sourceSet.getResourceOutputDir(), sourceSet); + if (sourceSet.getResourceOutputDirs() != null) { + for (File file : sourceSet.getResourceOutputDirs()) { + outputsToSourceSet.put(file, sourceSet); + } } if (sourceSet.getArchiveOutputFiles() != null) { for (Map.Entry> archive : sourceSet.getArchiveOutputFiles().entrySet()) { diff --git a/model/src/main/java/com/microsoft/java/bs/gradle/model/impl/DefaultGradleSourceSet.java b/model/src/main/java/com/microsoft/java/bs/gradle/model/impl/DefaultGradleSourceSet.java index 97151aae..45ffb04f 100644 --- a/model/src/main/java/com/microsoft/java/bs/gradle/model/impl/DefaultGradleSourceSet.java +++ b/model/src/main/java/com/microsoft/java/bs/gradle/model/impl/DefaultGradleSourceSet.java @@ -49,7 +49,7 @@ public class DefaultGradleSourceSet implements GradleSourceSet { private Set resourceDirs; - private File resourceOutputDir; + private Set resourceOutputDirs; private Map> archiveOutputFiles; @@ -85,7 +85,7 @@ public DefaultGradleSourceSet(GradleSourceSet gradleSourceSet) { this.generatedSourceDirs = gradleSourceSet.getGeneratedSourceDirs(); this.sourceOutputDirs = gradleSourceSet.getSourceOutputDirs(); this.resourceDirs = gradleSourceSet.getResourceDirs(); - this.resourceOutputDir = gradleSourceSet.getResourceOutputDir(); + this.resourceOutputDirs = gradleSourceSet.getResourceOutputDirs(); this.archiveOutputFiles = gradleSourceSet.getArchiveOutputFiles(); this.compileClasspath = gradleSourceSet.getCompileClasspath(); this.moduleDependencies = gradleSourceSet.getModuleDependencies().stream() @@ -234,12 +234,12 @@ public void setResourceDirs(Set resourceDirs) { } @Override - public File getResourceOutputDir() { - return resourceOutputDir; + public Set getResourceOutputDirs() { + return resourceOutputDirs; } - public void setResourceOutputDir(File resourceOutputDir) { - this.resourceOutputDir = resourceOutputDir; + public void setResourceOutputDirs(Set resourceOutputDirs) { + this.resourceOutputDirs = resourceOutputDirs; } @Override @@ -300,7 +300,7 @@ public void setExtensions(Map extensions) { public int hashCode() { return Objects.hash(gradleVersion, displayName, projectName, projectPath, projectDir, rootDir, sourceSetName, classesTaskName, cleanTaskName, taskNames, sourceDirs, - generatedSourceDirs, sourceOutputDirs, resourceDirs, resourceOutputDir, archiveOutputFiles, + generatedSourceDirs, sourceOutputDirs, resourceDirs, resourceOutputDirs, archiveOutputFiles, compileClasspath, moduleDependencies, buildTargetDependencies, hasTests, extensions); } @@ -331,7 +331,7 @@ public boolean equals(Object obj) { && Objects.equals(generatedSourceDirs, other.generatedSourceDirs) && Objects.equals(sourceOutputDirs, other.sourceOutputDirs) && Objects.equals(resourceDirs, other.resourceDirs) - && Objects.equals(resourceOutputDir, other.resourceOutputDir) + && Objects.equals(resourceOutputDirs, other.resourceOutputDirs) && Objects.equals(archiveOutputFiles, other.archiveOutputFiles) && Objects.equals(compileClasspath, other.compileClasspath) && Objects.equals(moduleDependencies, other.moduleDependencies) diff --git a/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/SourceSetsModelBuilder.java b/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/SourceSetsModelBuilder.java index 5b20f596..ef351d25 100644 --- a/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/SourceSetsModelBuilder.java +++ b/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/SourceSetsModelBuilder.java @@ -16,6 +16,8 @@ import java.util.Set; import java.util.stream.Collectors; +import com.microsoft.java.bs.gradle.plugin.utils.AndroidUtils; +import com.microsoft.java.bs.gradle.plugin.utils.SourceSetUtils; import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.Task; @@ -33,7 +35,6 @@ import com.microsoft.java.bs.gradle.model.GradleSourceSet; import com.microsoft.java.bs.gradle.model.GradleSourceSets; import com.microsoft.java.bs.gradle.model.LanguageExtension; -import com.microsoft.java.bs.gradle.model.SupportedLanguages; import com.microsoft.java.bs.gradle.model.impl.DefaultGradleSourceSet; import com.microsoft.java.bs.gradle.model.impl.DefaultGradleSourceSets; import com.microsoft.java.bs.gradle.plugin.dependency.DependencyCollector; @@ -47,17 +48,25 @@ public boolean canBuild(String modelName) { return modelName.equals(GradleSourceSets.class.getName()); } + @SuppressWarnings("NullableProblems") @Override public Object buildAll(String modelName, Project project) { // mapping Gradle source set to our customized model. - List sourceSets = getSourceSetContainer(project).stream() - .map(ss -> getSourceSet(project, ss)).collect(Collectors.toList()); + List sourceSets; + + // Fetch source sets depending on the project type + if (AndroidUtils.isAndroidProject(project)) { + sourceSets = AndroidUtils.getBuildVariantsAsGradleSourceSets(project); + } else { + sourceSets = getSourceSetContainer(project).stream() + .map(ss -> getSourceSet(project, ss)).collect(Collectors.toList()); + } excludeSourceDirsFromModules(sourceSets); return new DefaultGradleSourceSets(sourceSets); } - + private DefaultGradleSourceSet getSourceSet(Project project, SourceSet sourceSet) { DefaultGradleSourceSet gradleSourceSet = new DefaultGradleSourceSet(); // dependencies are populated by the GradleSourceSetsAction. Make sure not null. @@ -69,13 +78,14 @@ private DefaultGradleSourceSet getSourceSet(Project project, SourceSet sourceSet gradleSourceSet.setProjectDir(project.getProjectDir()); gradleSourceSet.setRootDir(project.getRootDir()); gradleSourceSet.setSourceSetName(sourceSet.getName()); - String classesTaskName = getFullTaskName(projectPath, sourceSet.getClassesTaskName()); + String classesTaskName = + SourceSetUtils.getFullTaskName(projectPath, sourceSet.getClassesTaskName()); gradleSourceSet.setClassesTaskName(classesTaskName); - String cleanTaskName = getFullTaskName(projectPath, "clean"); + String cleanTaskName = SourceSetUtils.getFullTaskName(projectPath, "clean"); gradleSourceSet.setCleanTaskName(cleanTaskName); Set taskNames = new HashSet<>(); gradleSourceSet.setTaskNames(taskNames); - String projectName = stripPathPrefix(projectPath); + String projectName = SourceSetUtils.stripPathPrefix(projectPath); if (projectName.isEmpty()) { projectName = project.getName(); } @@ -90,11 +100,13 @@ private DefaultGradleSourceSet getSourceSet(Project project, SourceSet sourceSet Set srcDirs = new HashSet<>(); Set generatedSrcDirs = new HashSet<>(); Set sourceOutputDirs = new HashSet<>(); - for (LanguageModelBuilder languageModelBuilder : getSupportedLanguages()) { + for (LanguageModelBuilder languageModelBuilder + : SourceSetUtils.getSupportedLanguageModelBuilders()) { LanguageExtension extension = languageModelBuilder.getExtensionFor(project, sourceSet, gradleSourceSet.getModuleDependencies()); if (extension != null) { - String compileTaskName = getFullTaskName(projectPath, extension.getCompileTaskName()); + String compileTaskName = + SourceSetUtils.getFullTaskName(projectPath, extension.getCompileTaskName()); taskNames.add(compileTaskName); srcDirs.addAll(extension.getSourceDirs()); @@ -124,16 +136,18 @@ private DefaultGradleSourceSet getSourceSet(Project project, SourceSet sourceSet // resource output dir File resourceOutputDir = sourceSet.getOutput().getResourcesDir(); + Set resourceOutputDirs = new HashSet<>(); if (resourceOutputDir != null) { - gradleSourceSet.setResourceOutputDir(resourceOutputDir); + resourceOutputDirs.add(resourceOutputDir); } + gradleSourceSet.setResourceOutputDirs(resourceOutputDirs); // archive output dirs Map> archiveOutputFiles = getArchiveOutputFiles(project, sourceSet); gradleSourceSet.setArchiveOutputFiles(archiveOutputFiles); // tests - if (sourceOutputDirs != null) { + if (!sourceOutputDirs.isEmpty()) { Set testTasks = tasksWithType(project, Test.class); for (Test testTask : testTasks) { if (GradleVersion.current().compareTo(GradleVersion.version("4.0")) >= 0) { @@ -161,7 +175,7 @@ private DefaultGradleSourceSet getSourceSet(Project project, SourceSet sourceSet break; } } catch (NoSuchMethodException | SecurityException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException e) { + | IllegalArgumentException | InvocationTargetException e) { // ignore } } @@ -171,22 +185,6 @@ private DefaultGradleSourceSet getSourceSet(Project project, SourceSet sourceSet return gradleSourceSet; } - private List getSupportedLanguages() { - List results = new LinkedList<>(); - String supportedLanguagesProps = System.getProperty("bsp.gradle.supportedLanguages"); - if (supportedLanguagesProps != null) { - String[] supportedLanguages = supportedLanguagesProps.split(","); - for (String language : supportedLanguages) { - if (language.equalsIgnoreCase(SupportedLanguages.JAVA.getBspName())) { - results.add(new JavaLanguageModelBuilder()); - } else if (language.equalsIgnoreCase(SupportedLanguages.SCALA.getBspName())) { - results.add(new ScalaLanguageModelBuilder()); - } - } - } - return results; - } - private Set tasksWithType(Project project, Class clazz) { // Gradle gives concurrentmodification exceptions if multiple threads resolve // the tasks concurrently, which happens on multi-project builds @@ -199,6 +197,7 @@ private Set tasksWithType(Project project, Class clazz) { * get all archive tasks for this project and maintain the archive file * to source set mapping. */ + @SuppressWarnings("deprecation") private Map> getArchiveOutputFiles(Project project, SourceSet sourceSet) { // get all archive tasks for this project and find the dirs that are included in the archive Set archiveTasks = tasksWithType(project, AbstractArchiveTask.class); @@ -213,8 +212,7 @@ private Map> getArchiveOutputFiles(Project project, SourceSet s } else { archiveFile = archiveTask.getArchivePath(); } - List sourceSetOutputs = new LinkedList<>(); - sourceSetOutputs.addAll(sourceSet.getOutput().getFiles()); + List sourceSetOutputs = new LinkedList<>(sourceSet.getOutput().getFiles()); archiveOutputFiles.put(archiveFile, sourceSetOutputs); } } @@ -243,8 +241,8 @@ private void excludeSourceDirsFromModules(List sourceSets) { if (sourceSet.getSourceOutputDirs() != null) { exclusions.addAll(sourceSet.getSourceOutputDirs()); } - if (sourceSet.getResourceOutputDir() != null) { - exclusions.add(sourceSet.getResourceOutputDir()); + if (sourceSet.getResourceOutputDirs() != null) { + exclusions.addAll(sourceSet.getResourceOutputDirs()); } if (sourceSet.getArchiveOutputFiles() != null) { exclusions.addAll(sourceSet.getArchiveOutputFiles().keySet()); @@ -256,7 +254,7 @@ private void excludeSourceDirsFromModules(List sourceSets) { for (GradleSourceSet sourceSet : sourceSets) { Set filteredModuleDependencies = sourceSet.getModuleDependencies() .stream().filter(mod -> mod.getArtifacts() - .stream().anyMatch(art -> !exclusionUris.contains(art.getUri()))) + .stream().anyMatch(art -> !exclusionUris.contains(art.getUri()))) .collect(Collectors.toSet()); if (sourceSet instanceof DefaultGradleSourceSet) { ((DefaultGradleSourceSet) sourceSet).setModuleDependencies(filteredModuleDependencies); @@ -267,7 +265,7 @@ private void excludeSourceDirsFromModules(List sourceSets) { private Collection getSourceSetContainer(Project project) { if (GradleVersion.current().compareTo(GradleVersion.version("5.0")) >= 0) { SourceSetContainer sourceSetContainer = project.getExtensions() - .findByType(SourceSetContainer.class); + .findByType(SourceSetContainer.class); if (sourceSetContainer != null) { return sourceSetContainer; } @@ -287,37 +285,12 @@ private Collection getSourceSetContainer(Project project) { return (SourceSetContainer) getSourceSetsMethod.invoke(pluginConvention); } } catch (NoSuchMethodException | SecurityException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException e) { + | IllegalArgumentException | InvocationTargetException e) { // ignore } return new LinkedList<>(); } - /** - * Return a project task name - [project path]:[task]. - */ - private String getFullTaskName(String modulePath, String taskName) { - if (taskName == null) { - return null; - } - if (taskName.isEmpty()) { - return taskName; - } - - if (modulePath == null || modulePath.equals(":")) { - // must be prefixed with ":" as taskPaths are reported back like that in progress messages - return ":" + taskName; - } - return modulePath + ":" + taskName; - } - - private String stripPathPrefix(String projectPath) { - if (projectPath.startsWith(":")) { - return projectPath.substring(1); - } - return projectPath; - } - private Set getArchiveSourcePaths(CopySpec copySpec) { Set sourcePaths = new HashSet<>(); if (copySpec instanceof DefaultCopySpec) { @@ -340,7 +313,7 @@ private Set getArchiveSourcePaths(CopySpec copySpec) { } } } catch (NoSuchMethodException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException e) { + | IllegalArgumentException | InvocationTargetException e) { // cannot get archive information } } diff --git a/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/dependency/AndroidDependencyCollector.java b/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/dependency/AndroidDependencyCollector.java new file mode 100644 index 00000000..960d2e27 --- /dev/null +++ b/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/dependency/AndroidDependencyCollector.java @@ -0,0 +1,202 @@ +package com.microsoft.java.bs.gradle.plugin.dependency; + +import com.microsoft.java.bs.gradle.model.Artifact; +import com.microsoft.java.bs.gradle.model.GradleModuleDependency; +import com.microsoft.java.bs.gradle.model.impl.DefaultArtifact; +import com.microsoft.java.bs.gradle.model.impl.DefaultGradleModuleDependency; +import com.microsoft.java.bs.gradle.plugin.utils.AndroidUtils; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.component.ComponentArtifactIdentifier; +import org.gradle.api.artifacts.result.ArtifactResolutionResult; +import org.gradle.api.artifacts.result.ArtifactResult; +import org.gradle.api.artifacts.result.ComponentArtifactsResult; +import org.gradle.api.artifacts.result.ResolvedArtifactResult; +import org.gradle.api.specs.Specs; +import org.gradle.internal.component.external.model.ModuleComponentArtifactIdentifier; +import org.gradle.internal.component.local.model.ComponentFileArtifactIdentifier; +import org.gradle.internal.component.local.model.OpaqueComponentArtifactIdentifier; +import org.gradle.jvm.JvmLibrary; +import org.gradle.language.base.artifact.SourcesArtifact; +import org.gradle.language.java.artifact.JavadocArtifact; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Collects dependencies from an Android build variant. + */ +public class AndroidDependencyCollector { + + private static final String UNKNOWN = "unknown"; + + /** + * Resolve and collect dependencies from an Android variant. + */ + public static Set getModuleDependencies(Project project, Object variant) { + Set dependencies = new HashSet<>(); + + try { + // Retrieve and process compile configuration + Configuration compileConfiguration = + (Configuration) AndroidUtils.getProperty(variant, "compileConfiguration"); + dependencies.addAll(resolveConfigurationDependencies( + project, + compileConfiguration) + ); + + // Retrieve and process runtime configuration + Configuration runtimeConfiguration = + (Configuration) AndroidUtils.getProperty(variant, "runtimeConfiguration"); + dependencies.addAll(resolveConfigurationDependencies( + project, + runtimeConfiguration) + ); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + + return dependencies; + } + + /** + * Resolve and collect dependencies from a given configuration. + */ + private static Set resolveConfigurationDependencies( + Project project, + Configuration configuration + ) { + return configuration.getIncoming() + .artifactView(viewConfiguration -> { + viewConfiguration.lenient(true); + viewConfiguration.componentFilter(Specs.satisfyAll()); + }) + .getArtifacts() + .getArtifacts() + .stream() + .map(artifactResult -> getArtifact(project, artifactResult)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + private static DefaultGradleModuleDependency getArtifact( + Project project, + ResolvedArtifactResult artifactResult + ) { + ComponentArtifactIdentifier id = artifactResult.getId(); + File artifactFile = artifactResult.getFile(); + if (id instanceof ModuleComponentArtifactIdentifier) { + return getModuleArtifactDependency( + project, + (ModuleComponentArtifactIdentifier) id, + artifactFile + ); + } + if (id instanceof OpaqueComponentArtifactIdentifier) { + return getFileArtifactDependency((OpaqueComponentArtifactIdentifier) id, artifactFile); + } + if (id instanceof ComponentFileArtifactIdentifier) { + return getFileArtifactDependency((ComponentFileArtifactIdentifier) id, artifactFile); + } + return null; + } + + @SuppressWarnings({"unchecked", "UnstableApiUsage"}) + private static DefaultGradleModuleDependency getModuleArtifactDependency( + Project project, + ModuleComponentArtifactIdentifier artifactIdentifier, + File resolvedArtifactFile + ) { + + ArtifactResolutionResult resolutionResult = project.getDependencies() + .createArtifactResolutionQuery() + .forComponents(artifactIdentifier.getComponentIdentifier()) + .withArtifacts( + JvmLibrary.class /* componentType */, + JavadocArtifact.class, SourcesArtifact.class /*artifactTypes*/ + ) + .execute(); + + List artifacts = new LinkedList<>(); + if (resolvedArtifactFile != null) { + artifacts.add(new DefaultArtifact(resolvedArtifactFile.toURI(), null)); + } + + Set resolvedComponents = resolutionResult.getResolvedComponents(); + File sourceJar = getNonClassesArtifact(resolvedComponents, SourcesArtifact.class); + if (sourceJar != null) { + artifacts.add(new DefaultArtifact(sourceJar.toURI(), "sources")); + } + + File javaDocJar = getNonClassesArtifact(resolvedComponents, JavadocArtifact.class); + if (javaDocJar != null) { + artifacts.add(new DefaultArtifact(javaDocJar.toURI(), "javadoc")); + } + + return new DefaultGradleModuleDependency( + artifactIdentifier.getComponentIdentifier().getGroup(), + artifactIdentifier.getComponentIdentifier().getModule(), + artifactIdentifier.getComponentIdentifier().getVersion(), + artifacts + ); + + } + + private static File getNonClassesArtifact( + Set resolvedComponents, + Class artifactClass + ) { + for (ComponentArtifactsResult component : resolvedComponents) { + Set artifacts = component.getArtifacts(artifactClass); + for (ArtifactResult artifact : artifacts) { + if (artifact instanceof ResolvedArtifactResult) { + return ((ResolvedArtifactResult) artifact).getFile(); + } + } + } + return null; + } + + private static DefaultGradleModuleDependency getFileArtifactDependency( + ComponentFileArtifactIdentifier artifactIdentifier, + File resolvedArtifactFile + ) { + return getFileArtifactDependency( + artifactIdentifier.getCapitalizedDisplayName(), + resolvedArtifactFile + ); + } + + private static DefaultGradleModuleDependency getFileArtifactDependency( + OpaqueComponentArtifactIdentifier artifactIdentifier, + File resolvedArtifactFile + ) { + return getFileArtifactDependency( + artifactIdentifier.getCapitalizedDisplayName(), + resolvedArtifactFile + ); + } + + private static DefaultGradleModuleDependency getFileArtifactDependency( + String displayName, + File resolvedArtifactFile + ) { + List artifacts = new LinkedList<>(); + if (resolvedArtifactFile != null) { + artifacts.add(new DefaultArtifact(resolvedArtifactFile.toURI(), null)); + } + + return new DefaultGradleModuleDependency( + UNKNOWN, + displayName, + UNKNOWN, + artifacts + ); + } +} diff --git a/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/dependency/DependencyCollector.java b/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/dependency/DependencyCollector.java index b952831a..cc6a7b01 100644 --- a/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/dependency/DependencyCollector.java +++ b/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/dependency/DependencyCollector.java @@ -4,6 +4,7 @@ package com.microsoft.java.bs.gradle.plugin.dependency; import java.io.File; +import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -18,7 +19,6 @@ import org.gradle.api.artifacts.ResolvedArtifact; import org.gradle.api.artifacts.ResolvedConfiguration; import org.gradle.api.artifacts.component.ComponentArtifactIdentifier; -import org.gradle.api.artifacts.component.ComponentIdentifier; import org.gradle.api.artifacts.result.ArtifactResolutionResult; import org.gradle.api.artifacts.result.ArtifactResult; import org.gradle.api.artifacts.result.ComponentArtifactsResult; @@ -111,20 +111,18 @@ private static DefaultGradleModuleDependency getArtifact(Project project, } private static List getConfigurationArtifacts(Configuration config) { - return config.getIncoming() - .artifactView(viewConfiguration -> { - viewConfiguration.lenient(true); - viewConfiguration.componentFilter(Specs.satisfyAll()); - }) - .getArtifacts() // get ArtifactCollection from ArtifactView. - .getArtifacts() // get a set of ResolvedArtifactResult from ArtifactCollection. - .stream() - .collect(Collectors.toList()); + return new ArrayList<>(config.getIncoming() + .artifactView(viewConfiguration -> { + viewConfiguration.lenient(true); + viewConfiguration.componentFilter(Specs.satisfyAll()); + }) + .getArtifacts() // get ArtifactCollection from ArtifactView. + .getArtifacts()); } private static DefaultGradleModuleDependency getModuleArtifactDependency(Project project, ModuleComponentArtifactIdentifier artifactIdentifier, File resolvedArtifactFile) { - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "UnstableApiUsage"}) ArtifactResolutionResult resolutionResult = project.getDependencies() .createArtifactResolutionQuery() .forComponents(artifactIdentifier.getComponentIdentifier()) 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 new file mode 100644 index 00000000..c551b592 --- /dev/null +++ b/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/utils/AndroidUtils.java @@ -0,0 +1,530 @@ +package com.microsoft.java.bs.gradle.plugin.utils; + +import com.microsoft.java.bs.gradle.model.Artifact; +import com.microsoft.java.bs.gradle.model.GradleModuleDependency; +import com.microsoft.java.bs.gradle.model.GradleSourceSet; +import com.microsoft.java.bs.gradle.model.LanguageExtension; +import com.microsoft.java.bs.gradle.model.SupportedLanguages; +import com.microsoft.java.bs.gradle.model.impl.DefaultJavaExtension; +import com.microsoft.java.bs.gradle.model.impl.DefaultArtifact; +import com.microsoft.java.bs.gradle.model.impl.DefaultGradleModuleDependency; +import com.microsoft.java.bs.gradle.model.impl.DefaultGradleSourceSet; +import com.microsoft.java.bs.gradle.plugin.dependency.AndroidDependencyCollector; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.file.RegularFile; +import org.gradle.api.internal.tasks.compile.DefaultJavaCompileSpec; +import org.gradle.api.internal.tasks.compile.JavaCompilerArgumentsBuilder; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.compile.CompileOptions; +import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.util.GradleVersion; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.LinkedList; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Utility class for android related operations. + */ +public class AndroidUtils { + + private AndroidUtils() { + } + + /** + * Checks if the given project is an Android project. + * + * @param project Gradle project to check + */ + public static boolean isAndroidProject(Project project) { + return getAndroidExtension(project) != null; + } + + /** + * Extracts build variants from the given Android project and converts + * them into list of GradleSourceSets. + * + * @param project Gradle project for extracting the build variants + */ + @SuppressWarnings("unchecked") + public static List getBuildVariantsAsGradleSourceSets(Project project) { + + List sourceSets = new LinkedList<>(); + + Object androidExtension = getAndroidExtension(project); + if (androidExtension == null) { + return sourceSets; + } + + AndroidProjectType type = getProjectType(project); + if (type == null) { + return sourceSets; + } + + String methodName; + switch (type) { + case APPLICATION: + case DYNAMIC_FEATURE: + methodName = "getApplicationVariants"; + break; + case LIBRARY: + methodName = "getLibraryVariants"; + break; + case INSTANT_APP_FEATURE: + methodName = "getFeatureVariants"; + break; + case ANDROID_TEST: + methodName = "getTestVariants"; + break; + default: + methodName = ""; + } + + try { + Set variants = (Set) invokeMethod(androidExtension, methodName); + for (Object variant : variants) { + GradleSourceSet sourceSet = convertVariantToGradleSourceSet(project, variant); + if (sourceSet == null) { + continue; + } + sourceSets.add(sourceSet); + } + } catch (IllegalAccessException | NoSuchMethodException + | InvocationTargetException | ClassCastException e) { + // do nothing + } + + return sourceSets; + + } + + /** + * Returns a GradleSourceSet populated with the given Android build variant data. + * + * @param project Gradle project to populate GradleSourceSet properties + * @param variant Android Build Variant object to populate GradleSourceSet properties + */ + @SuppressWarnings("unchecked") + private static GradleSourceSet convertVariantToGradleSourceSet(Project project, Object variant) { + + try { + + DefaultGradleSourceSet gradleSourceSet = new DefaultGradleSourceSet(); + gradleSourceSet.setBuildTargetDependencies(new HashSet<>()); + + gradleSourceSet.setGradleVersion(project.getGradle().getGradleVersion()); + gradleSourceSet.setProjectName(project.getName()); + String projectPath = project.getPath(); + gradleSourceSet.setProjectPath(projectPath); + gradleSourceSet.setProjectDir(project.getProjectDir()); + gradleSourceSet.setRootDir(project.getRootDir()); + + String variantName = (String) invokeMethod(variant, "getName"); + gradleSourceSet.setSourceSetName(variantName); + + // classes task equivalent in android (assembleRelease) + gradleSourceSet.setClassesTaskName( + SourceSetUtils.getFullTaskName(projectPath, "assemble" + capitalize(variantName)) + ); + + gradleSourceSet.setCleanTaskName(SourceSetUtils.getFullTaskName(projectPath, "clean")); + + // compile task in android (compileReleaseJavaWithJavac) + HashSet tasks = new HashSet<>(); + String compileTaskName = "compile" + capitalize(variantName) + "JavaWithJavac"; + tasks.add(SourceSetUtils.getFullTaskName(projectPath, compileTaskName)); + gradleSourceSet.setTaskNames(tasks); + + String projectName = SourceSetUtils.stripPathPrefix(projectPath); + if (projectName.isEmpty()) { + projectName = project.getName(); + } + String displayName = projectName + " [" + variantName + ']'; + gradleSourceSet.setDisplayName(displayName); + + // module dependencies + Set moduleDependencies = + AndroidDependencyCollector.getModuleDependencies(project, variant); + // add Android SDK + Object androidComponents = getAndroidComponentExtension(project); + if (androidComponents != null) { + Object sdkComponents = getProperty(androidComponents, "sdkComponents"); + Object bootClasspath = + ((Provider) getProperty(sdkComponents, "bootclasspathProvider")).get(); + try { + List bootClasspathFiles = + (List) invokeMethod(bootClasspath, "get"); + List sdkClasspath = + bootClasspathFiles.stream().map(RegularFile::getAsFile).collect(Collectors.toList()); + for (File file : sdkClasspath) { + moduleDependencies.add(mockModuleDependency(file.toURI())); + } + } catch (IllegalStateException | InvocationTargetException e) { + // failed to retrieve android sdk classpath + // do nothing + } + } + // add R.jar file + String taskName = "process" + capitalize(variantName) + "Resources"; + Task processResourcesTask = project.getTasks().findByName(taskName); + if (processResourcesTask != null) { + Object output = invokeMethod(processResourcesTask, "getRClassOutputJar"); + RegularFile file = (RegularFile) invokeMethod(output, "get"); + File jarFile = file.getAsFile(); + if (jarFile.exists()) { + moduleDependencies.add(mockModuleDependency(jarFile.toURI())); + } + } + gradleSourceSet.setModuleDependencies(moduleDependencies); + + // source and resource + 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); + } + } + gradleSourceSet.setSourceDirs(sourceDirs); + gradleSourceSet.setResourceDirs(resourceDirs); + + // resource outputs + Set resourceOutputs = new HashSet<>(); + Provider resourceProvider = + (Provider) getProperty(variant, "processJavaResourcesProvider"); + if (resourceProvider != null) { + Task resTask = resourceProvider.get(); + 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); + } + gradleSourceSet.setResourceOutputDirs(resourceOutputs); + + // generated sources and source outputs + Set generatedSources = new HashSet<>(); + Set sourceOutputs = new HashSet<>(); + Provider javaCompileProvider = + (Provider) getProperty(variant, "javaCompileProvider"); + List compilerArgs = new ArrayList<>(); + if (javaCompileProvider != null) { + Task javaCompileTask = javaCompileProvider.get(); + + compilerArgs.addAll(getCompilerArgs((JavaCompile) javaCompileTask)); + + File outputDir = (File) invokeMethod(javaCompileTask, "getDestinationDir"); + sourceOutputs.add(outputDir); + + Object source = invokeMethod(javaCompileTask, "getSource"); + Set compileSources = (Set) invokeMethod(source, "getFiles"); + + // generated = compile source - source + for (File compileSource : compileSources) { + boolean inSourceDir = sourceDirs.stream() + .anyMatch(dir -> compileSource.getAbsolutePath().startsWith(dir.getAbsolutePath())); + if (inSourceDir) { + continue; + } + boolean inGeneratedSourceDir = generatedSources.stream() + .anyMatch(dir -> compileSource.getAbsolutePath().startsWith(dir.getAbsolutePath())); + if (inGeneratedSourceDir) { + continue; + } + generatedSources.add(compileSource); + } + } + gradleSourceSet.setGeneratedSourceDirs(generatedSources); + gradleSourceSet.setSourceOutputDirs(sourceOutputs); + + // classpath + Object compileConfig = invokeMethod(variant, "getCompileConfiguration"); + Set classpathFiles = (Set) invokeMethod(compileConfig, "getFiles"); + gradleSourceSet.setCompileClasspath(new LinkedList<>(classpathFiles)); + + // 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); + + // 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(); + + extension.setCompilerArgs(compilerArgs); + extension.setSourceCompatibility(getSourceCompatibility(compilerArgs)); + extension.setTargetCompatibility(getTargetCompatibility(compilerArgs)); + + extensions.put(SupportedLanguages.JAVA.getBspName(), extension); + } + gradleSourceSet.setExtensions(extensions); + + return gradleSourceSet; + + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + return null; + } + + } + + /** + * Extracts the AndroidExtension from the given project. + * + * @param project Gradle project to extract the AndroidExtension object. + */ + private static Object getAndroidExtension(Project project) { + return getExtension(project, "android"); + } + + /** + * Extracts the AndroidComponentsExtension from the given project. + * + * @param project Gradle project to extract the AndroidComponentsExtension object. + */ + private static Object getAndroidComponentExtension(Project project) { + return getExtension(project, "androidComponents"); + } + + /** + * Extracts the given extension from the given project. + * + * @param project Gradle project to extract the extension object. + * @param extensionName Name of the extension to extract. + */ + private static Object getExtension(Project project, String extensionName) { + Object extension = null; + + try { + Object convention = invokeMethod(project, "getConvention"); + Object extensionMap = invokeMethod(convention, "getAsMap"); + extension = extensionMap.getClass() + .getMethod("get", Object.class).invoke(extensionMap, extensionName); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + // do nothing + } + + return extension; + } + + /** + * Returns the AndroidProjectType based on the plugin applied to the given project. + * + * @param project Gradle project to check for plugin and return the corresponding project type. + */ + private static AndroidProjectType getProjectType(Project project) { + + if (getAndroidExtension(project) == null) { + return null; + } + + AndroidProjectType projectType = null; + + if (project.getPluginManager().hasPlugin("com.android.application")) { + projectType = AndroidProjectType.APPLICATION; + } else if (project.getPluginManager().hasPlugin("com.android.library")) { + projectType = AndroidProjectType.LIBRARY; + } else if (project.getPluginManager().hasPlugin("com.android.dynamic-feature")) { + projectType = AndroidProjectType.DYNAMIC_FEATURE; + } else if (project.getPluginManager().hasPlugin("com.android.feature")) { + projectType = AndroidProjectType.INSTANT_APP_FEATURE; + } else if (project.getPluginManager().hasPlugin("com.android.test")) { + projectType = AndroidProjectType.ANDROID_TEST; + } + + return projectType; + + } + + /** + * Extracts the given property from the given object with {@code getProperty} method. + * + * @param obj object from which the property is to be extracted + * @param propertyName name of the property to be extracted + */ + public static Object getProperty(Object obj, String propertyName) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return obj.getClass().getMethod("getProperty", String.class).invoke(obj, propertyName); + } + + /** + * Enum class representing different types of Android projects. + */ + private enum AndroidProjectType { + APPLICATION, + LIBRARY, + DYNAMIC_FEATURE, + INSTANT_APP_FEATURE, + ANDROID_TEST + } + + /** + * Returns the given string with its first letter capitalized. + * + * @param s String to capitalize + */ + private static String capitalize(String s) { + return s.substring(0, 1).toUpperCase() + s.substring(1); + } + + /** + * Mocks GradleModuleDependency with a single artifact. + * + * @param jarUri Uri for the artifact to include in the ModuleDependency object. + */ + private static GradleModuleDependency mockModuleDependency(URI jarUri) { + + final String unknown = "UNKNOWN"; + + List artifacts = new LinkedList<>(); + artifacts.add(new DefaultArtifact(jarUri, null)); + + return new DefaultGradleModuleDependency( + unknown, + unknown, + unknown, + artifacts + ); + + } + + private static Object invokeMethod(Object object, String methodName) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return object.getClass().getMethod(methodName).invoke(object); + } + + // region TODO: Duplicate code from JavaLanguageModelBuilder + /** + * Get the compilation arguments of the build variant. + */ + private static List getCompilerArgs(JavaCompile javaCompile) { + CompileOptions options = javaCompile.getOptions(); + + try { + DefaultJavaCompileSpec specs = getJavaCompileSpec(javaCompile); + + JavaCompilerArgumentsBuilder builder = new JavaCompilerArgumentsBuilder(specs) + .includeMainOptions(true) + .includeClasspath(false) + .includeSourceFiles(false) + .includeLauncherOptions(false); + return builder.build(); + } catch (Exception e) { + // DefaultJavaCompileSpec and JavaCompilerArgumentsBuilder are internal so may not exist. + // Fallback to returning just the compiler arguments the build has specified. + // This will miss a lot of arguments derived from the CompileOptions e.g. sourceCompatibilty + // Arguments must be cast and converted to String because Groovy can use GStringImpl + // which then throws IllegalArgumentException when passed back over the tooling connection. + List compilerArgs = new LinkedList<>(options.getCompilerArgs()); + return compilerArgs + .stream() + .map(Object::toString) + .collect(Collectors.toList()); + } + } + + private static DefaultJavaCompileSpec getJavaCompileSpec(JavaCompile javaCompile) { + CompileOptions options = javaCompile.getOptions(); + + DefaultJavaCompileSpec specs = new DefaultJavaCompileSpec(); + specs.setCompileOptions(options); + + // check the project hasn't already got the target or source defined in the + // compiler args so they're not overwritten below + List originalArgs = options.getCompilerArgs(); + String argsSourceCompatibility = getSourceCompatibility(originalArgs); + String argsTargetCompatibility = getTargetCompatibility(originalArgs); + + if (!argsSourceCompatibility.isEmpty() && !argsTargetCompatibility.isEmpty()) { + return specs; + } + + if (GradleVersion.current().compareTo(GradleVersion.version("6.6")) >= 0) { + if (options.getRelease().isPresent()) { + specs.setRelease(options.getRelease().get()); + return specs; + } + } + if (argsSourceCompatibility.isEmpty() && specs.getSourceCompatibility() == null) { + String sourceCompatibility = javaCompile.getSourceCompatibility(); + if (sourceCompatibility != null) { + specs.setSourceCompatibility(sourceCompatibility); + } + } + if (argsTargetCompatibility.isEmpty() && specs.getTargetCompatibility() == null) { + String targetCompatibility = javaCompile.getTargetCompatibility(); + if (targetCompatibility != null) { + specs.setTargetCompatibility(targetCompatibility); + } + } + return specs; + } + + /** + * Get the source compatibility level of the build variant. + */ + private static String getSourceCompatibility(List compilerArgs) { + return findFirstCompilerArgMatch(compilerArgs, + Stream.of("-source", "--source", "--release")) + .orElse(""); + } + + /** + * Get the target compatibility level of the build variant. + */ + private static String getTargetCompatibility(List compilerArgs) { + return findFirstCompilerArgMatch(compilerArgs, + Stream.of("-target", "--target", "--release")) + .orElse(""); + } + + private static Optional findCompilerArg(List compilerArgs, String arg) { + int idx = compilerArgs.indexOf(arg); + if (idx >= 0 && idx < compilerArgs.size() - 1) { + return Optional.of(compilerArgs.get(idx + 1)); + } + return Optional.empty(); + } + + private static Optional findFirstCompilerArgMatch(List compilerArgs, + Stream args) { + return args.map(arg -> findCompilerArg(compilerArgs, arg)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } + // endregion + +} diff --git a/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/utils/SourceSetUtils.java b/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/utils/SourceSetUtils.java new file mode 100644 index 00000000..d425b516 --- /dev/null +++ b/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/utils/SourceSetUtils.java @@ -0,0 +1,78 @@ +package com.microsoft.java.bs.gradle.plugin.utils; + +import com.microsoft.java.bs.gradle.model.SupportedLanguages; +import com.microsoft.java.bs.gradle.plugin.JavaLanguageModelBuilder; +import com.microsoft.java.bs.gradle.plugin.LanguageModelBuilder; +import com.microsoft.java.bs.gradle.plugin.ScalaLanguageModelBuilder; + +import java.util.LinkedList; +import java.util.List; + +/** + * Utility class for common source set operations. + */ +public class SourceSetUtils { + + private SourceSetUtils() { + } + + /** + * Returns the gradle project path without the initial {@code :}. + * + * @param projectPath project path to operate upon + */ + public static String stripPathPrefix(String projectPath) { + if (projectPath.startsWith(":")) { + return projectPath.substring(1); + } + return projectPath; + } + + /** + * Return a project task name - [project path]:[task]. + * + * @param modulePath path of project module + * @param taskName name of gradle task + */ + public static String getFullTaskName(String modulePath, String taskName) { + if (taskName == null) { + return null; + } + if (taskName.isEmpty()) { + return taskName; + } + + if (modulePath == null || modulePath.equals(":")) { + // must be prefixed with ":" as taskPaths are reported back like that in progress messages + return ":" + taskName; + } + return modulePath + ":" + taskName; + } + + /** + * Returns a list of LanguageModelBuilder for the supported languages. + */ + public static List getSupportedLanguageModelBuilders() { + List results = new LinkedList<>(); + for (String language : getSupportedLanguages()) { + if (language.equalsIgnoreCase(SupportedLanguages.JAVA.getBspName())) { + results.add(new JavaLanguageModelBuilder()); + } else if (language.equalsIgnoreCase(SupportedLanguages.SCALA.getBspName())) { + results.add(new ScalaLanguageModelBuilder()); + } + } + return results; + } + + /** + * Returns a list of bsp names for the supported languages. + */ + public static String[] getSupportedLanguages() { + String supportedLanguagesProps = System.getProperty("bsp.gradle.supportedLanguages"); + if (supportedLanguagesProps != null) { + return supportedLanguagesProps.split(","); + } + return new String[]{}; + } + +} diff --git a/plugin/src/test/java/com/microsoft/java/bs/gradle/plugin/GradleBuildServerPluginTest.java b/plugin/src/test/java/com/microsoft/java/bs/gradle/plugin/GradleBuildServerPluginTest.java index 7c75c4ce..51ebb2f7 100644 --- a/plugin/src/test/java/com/microsoft/java/bs/gradle/plugin/GradleBuildServerPluginTest.java +++ b/plugin/src/test/java/com/microsoft/java/bs/gradle/plugin/GradleBuildServerPluginTest.java @@ -185,7 +185,7 @@ void testModelBuilder(GradleVersion gradleVersion) throws IOException { } assertEquals(1, gradleSourceSet.getResourceDirs().size()); assertNotNull(gradleSourceSet.getSourceOutputDirs()); - assertNotNull(gradleSourceSet.getResourceOutputDir()); + assertNotNull(gradleSourceSet.getResourceOutputDirs()); assertNotNull(gradleSourceSet.getBuildTargetDependencies()); assertNotNull(gradleSourceSet.getModuleDependencies()); @@ -486,7 +486,7 @@ void testScala2ModelBuilder(GradleVersion gradleVersion) throws IOException { assertNotNull(gradleSourceSet.getBuildTargetDependencies()); assertNotNull(gradleSourceSet.getModuleDependencies()); assertNotNull(gradleSourceSet.getSourceOutputDirs()); - assertNotNull(gradleSourceSet.getResourceOutputDir()); + assertNotNull(gradleSourceSet.getResourceOutputDirs()); JavaExtension javaExtension = SupportedLanguages.JAVA.getExtension(gradleSourceSet); assertNotNull(javaExtension); @@ -573,7 +573,7 @@ void testScala3ModelBuilder(GradleVersion gradleVersion) throws IOException { } assertEquals(1, gradleSourceSet.getResourceDirs().size()); assertNotNull(gradleSourceSet.getSourceOutputDirs()); - assertNotNull(gradleSourceSet.getResourceOutputDir()); + assertNotNull(gradleSourceSet.getResourceOutputDirs()); assertNotNull(gradleSourceSet.getBuildTargetDependencies()); assertNotNull(gradleSourceSet.getModuleDependencies()); JavaExtension javaExtension = SupportedLanguages.JAVA.getExtension(gradleSourceSet); diff --git a/server/src/main/java/com/microsoft/java/bs/core/internal/services/BuildTargetService.java b/server/src/main/java/com/microsoft/java/bs/core/internal/services/BuildTargetService.java index 50f2c39a..095c573f 100644 --- a/server/src/main/java/com/microsoft/java/bs/core/internal/services/BuildTargetService.java +++ b/server/src/main/java/com/microsoft/java/bs/core/internal/services/BuildTargetService.java @@ -247,18 +247,20 @@ public OutputPathsResult getBuildTargetOutputPaths(OutputPathsParams params) { if (sourceOutputDirs != null) { for (File sourceOutputDir : sourceOutputDirs) { outputPaths.add(new OutputPathItem( - sourceOutputDir.toURI().toString() + "?kind=source", + sourceOutputDir.toURI() + "?kind=source", OutputPathItemKind.DIRECTORY )); } } - File resourceOutputDir = sourceSet.getResourceOutputDir(); - if (resourceOutputDir != null) { - outputPaths.add(new OutputPathItem( - resourceOutputDir.toURI().toString() + "?kind=resource", - OutputPathItemKind.DIRECTORY - )); + Set resourceOutputDirs = sourceSet.getResourceOutputDirs(); + if (resourceOutputDirs != null) { + for (File resourceOutputDir : resourceOutputDirs) { + outputPaths.add(new OutputPathItem( + resourceOutputDir.toURI() + "?kind=resource", + OutputPathItemKind.DIRECTORY + )); + } } OutputPathsItem item = new OutputPathsItem(btId, outputPaths); @@ -286,7 +288,7 @@ public DependencySourcesResult getBuildTargetDependencySources(DependencySources List artifacts = dep.getArtifacts().stream() .filter(a -> "sources".equals(a.getClassifier())) .map(a -> a.getUri().toString()) - .collect(Collectors.toList()); + .toList(); sources.addAll(artifacts); } @@ -488,68 +490,19 @@ public TestResult buildTargetTest(TestParams params) { TestResult testResult = new TestResult(StatusCode.OK); testResult.setOriginId(params.getOriginId()); // running tests can trigger compilation that must be reported on - CompileProgressReporter compileProgressReporter = new CompileProgressReporter(client, - params.getOriginId(), getFullTaskPathMap()); + CompileProgressReporter compileProgressReporter = + new CompileProgressReporter(client, params.getOriginId(), getFullTaskPathMap()); Map> groupedTargets = groupBuildTargetsByRootDir(params.getTargets()); + for (Map.Entry> entry : groupedTargets.entrySet()) { // TODO ideally BSP would have a jvmTestEnv style testkind for executing tests, not scala. StatusCode statusCode; if (TestParamsDataKind.SCALA_TEST.equals(params.getDataKind())) { - // ScalaTestParams is for a list of classes only - ScalaTestParams testParams = JsonUtils.toModel(params.getData(), ScalaTestParams.class); - Map>> testClasses = new HashMap<>(); - for (ScalaTestClassesItem testClassesItem : testParams.getTestClasses()) { - Map> classesMethods = new HashMap<>(); - for (String classNames : testClassesItem.getClasses()) { - classesMethods.put(classNames, null); - } - testClasses.put(testClassesItem.getTarget(), classesMethods); - } - statusCode = connector.runTests(entry.getKey(), testClasses, testParams.getJvmOptions(), - params.getArguments(), null, client, params.getOriginId(), - compileProgressReporter); + // existing logic for scala test (class level) + statusCode = runScalaTests(entry, params, compileProgressReporter); } else if ("scala-test-suites-selection".equals(params.getDataKind())) { - // ScalaTestSuites is for a list of classes + methods - // Since it doesn't supply the specific BuildTarget we require a single - // build target in the params and reject any request that doesn't match this - if (params.getTargets().size() != 1) { - LOGGER.warning("Test params with Test Data Kind " + params.getDataKind() - + " must contain only 1 build target"); - statusCode = StatusCode.ERROR; - } else { - ScalaTestSuites testSuites = JsonUtils.toModel(params.getData(), ScalaTestSuites.class); - Map envVars = null; - boolean argsValid = true; - if (testSuites.getEnvironmentVariables() != null) { - // arg is of the form KEY=VALUE - List splitArgs = testSuites.getEnvironmentVariables() - .stream() - .map(arg -> arg.split("=")) - .collect(Collectors.toList()); - argsValid = splitArgs.stream().allMatch(arg -> arg.length == 2); - if (argsValid) { - envVars = splitArgs.stream().collect(Collectors.toMap(arg -> arg[0], arg -> arg[1])); - } - } - if (!argsValid) { - LOGGER.warning("Test params arguments must each be in the form KEY=VALUE. " - + testSuites.getEnvironmentVariables()); - statusCode = StatusCode.ERROR; - } else { - Map> classesMethods = new HashMap<>(); - for (ScalaTestSuiteSelection testSuiteSelection : testSuites.getSuites()) { - Set methods = classesMethods - .computeIfAbsent(testSuiteSelection.getClassName(), k -> new HashSet<>()); - methods.addAll(testSuiteSelection.getTests()); - } - Map>> testClasses = new HashMap<>(); - testClasses.put(params.getTargets().get(0), classesMethods); - statusCode = connector.runTests(entry.getKey(), testClasses, testSuites.getJvmOptions(), - params.getArguments(), envVars, client, params.getOriginId(), - compileProgressReporter); - } - } + statusCode = runScalaTestSuitesSelection(entry, params, compileProgressReporter); } else { LOGGER.warning("Test Data Kind " + params.getDataKind() + " not supported"); statusCode = StatusCode.ERROR; @@ -559,9 +512,77 @@ public TestResult buildTargetTest(TestParams params) { testResult.setStatusCode(statusCode); } } + return testResult; } + private StatusCode runScalaTests( + Map.Entry> entry, + TestParams params, + CompileProgressReporter compileProgressReporter + ) { + // ScalaTestParams is for a list of classes only + ScalaTestParams testParams = JsonUtils.toModel(params.getData(), ScalaTestParams.class); + Map>> testClasses = new HashMap<>(); + for (ScalaTestClassesItem testClassesItem : testParams.getTestClasses()) { + Map> classesMethods = new HashMap<>(); + for (String classNames : testClassesItem.getClasses()) { + classesMethods.put(classNames, null); + } + testClasses.put(testClassesItem.getTarget(), classesMethods); + } + return connector.runTests(entry.getKey(), testClasses, testParams.getJvmOptions(), + params.getArguments(), null, client, params.getOriginId(), + compileProgressReporter); + } + + private StatusCode runScalaTestSuitesSelection( + Map.Entry> entry, + TestParams params, + CompileProgressReporter compileProgressReporter + ) { + // ScalaTestSuites is for a list of classes + methods + // Since it doesn't supply the specific BuildTarget we require a single + // build target in the params and reject any request that doesn't match this + if (params.getTargets().size() != 1) { + LOGGER.warning("Test params with Test Data Kind " + params.getDataKind() + + " must contain only 1 build target"); + return StatusCode.ERROR; + } else { + ScalaTestSuites testSuites = JsonUtils.toModel(params.getData(), ScalaTestSuites.class); + Map envVars = null; + boolean argsValid = true; + if (testSuites.getEnvironmentVariables() != null) { + // arg is of the form KEY=VALUE + List splitArgs = testSuites.getEnvironmentVariables() + .stream() + .map(arg -> arg.split("=")) + .toList(); + argsValid = splitArgs.stream().allMatch(arg -> arg.length == 2); + if (argsValid) { + envVars = splitArgs.stream().collect(Collectors.toMap(arg -> arg[0], arg -> arg[1])); + } + } + if (!argsValid) { + LOGGER.warning("Test params arguments must each be in the form KEY=VALUE. " + + testSuites.getEnvironmentVariables()); + return StatusCode.ERROR; + } else { + Map> classesMethods = new HashMap<>(); + for (ScalaTestSuiteSelection testSuiteSelection : testSuites.getSuites()) { + Set methods = classesMethods + .computeIfAbsent(testSuiteSelection.getClassName(), k -> new HashSet<>()); + methods.addAll(testSuiteSelection.getTests()); + } + Map>> testClasses = new HashMap<>(); + testClasses.put(params.getTargets().get(0), classesMethods); + return connector.runTests(entry.getKey(), testClasses, testSuites.getJvmOptions(), + params.getArguments(), envVars, client, params.getOriginId(), + compileProgressReporter); + } + } + } + /** * Group the build targets by the project root directory, * projects with the same root directory can run their tasks 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 25f2de89..9e676335 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 @@ -22,6 +22,7 @@ import com.microsoft.java.bs.core.Launcher; import com.microsoft.java.bs.core.internal.managers.PreferenceManager; import com.microsoft.java.bs.core.internal.model.Preferences; +import com.microsoft.java.bs.gradle.model.GradleModuleDependency; import com.microsoft.java.bs.gradle.model.GradleSourceSet; import com.microsoft.java.bs.gradle.model.GradleSourceSets; import com.microsoft.java.bs.gradle.model.ScalaExtension; @@ -92,6 +93,38 @@ void testGetGradleSourceSets() { "junit5-jupiter-starter-gradle [test]").getSourceSetName()); } + @Test + void testAndroidSourceSets() { + File projectDir = projectPath.resolve("android-test").toFile(); + PreferenceManager preferenceManager = new PreferenceManager(); + preferenceManager.setPreferences(new Preferences()); + GradleApiConnector connector = new GradleApiConnector(preferenceManager); + GradleSourceSets gradleSourceSets = connector.getGradleSourceSets(projectDir.toURI(), null); + assertEquals(4, gradleSourceSets.getGradleSourceSets().size()); + findSourceSet(gradleSourceSets, "app [debug]"); + findSourceSet(gradleSourceSets, "app [release]"); + findSourceSet(gradleSourceSets, "mylibrary [debug]"); + findSourceSet(gradleSourceSets, "mylibrary [release]"); + 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. + // Specifically R file and Android Components. For eg: + // 1. When android-test project has not been or doesn't have the resources compiled + // 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); + } + private GradleSourceSet findSourceSet(GradleSourceSets gradleSourceSets, String displayName) { GradleSourceSet sourceSet = gradleSourceSets.getGradleSourceSets().stream() .filter(ss -> ss.getDisplayName().equals(displayName)) diff --git a/server/src/test/java/com/microsoft/java/bs/core/internal/server/BuildTargetServiceIntegrationTest.java b/server/src/test/java/com/microsoft/java/bs/core/internal/server/BuildTargetServiceIntegrationTest.java index 0ba95a69..c412b29f 100644 --- a/server/src/test/java/com/microsoft/java/bs/core/internal/server/BuildTargetServiceIntegrationTest.java +++ b/server/src/test/java/com/microsoft/java/bs/core/internal/server/BuildTargetServiceIntegrationTest.java @@ -7,6 +7,8 @@ import ch.epfl.scala.bsp4j.CompileParams; import ch.epfl.scala.bsp4j.CompileReport; import ch.epfl.scala.bsp4j.CompileResult; +import ch.epfl.scala.bsp4j.DependencyModule; +import ch.epfl.scala.bsp4j.DependencyModulesItem; import ch.epfl.scala.bsp4j.DependencyModulesParams; import ch.epfl.scala.bsp4j.DependencyModulesResult; import ch.epfl.scala.bsp4j.DependencySourcesParams; @@ -29,6 +31,8 @@ import ch.epfl.scala.bsp4j.extended.TestFinishEx; import ch.epfl.scala.bsp4j.extended.TestName; import ch.epfl.scala.bsp4j.extended.TestStartEx; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.microsoft.java.bs.core.internal.utils.JsonUtils; import org.junit.jupiter.api.Test; @@ -128,7 +132,7 @@ void testCompilingSingleProjectServer() { .buildTargetDependencySources(dependencySourcesParams).join(); assertEquals(2, dependencySourcesResult.getItems().size()); List allSources = dependencySourcesResult.getItems().stream() - .flatMap(item -> item.getSources().stream()).collect(Collectors.toList()); + .flatMap(item -> item.getSources().stream()).toList(); assertTrue(allSources.stream().anyMatch(source -> source.endsWith("-sources.jar"))); // check dependency modules @@ -143,7 +147,7 @@ void testCompilingSingleProjectServer() { MavenDependencyModule.class)) .flatMap(mavenDependencyModule -> mavenDependencyModule.getArtifacts().stream()) .filter(artifact -> "sources".equals(artifact.getClassifier())) - .collect(Collectors.toList()); + .toList(); assertTrue(allArtifacts.stream() .anyMatch(artifact -> artifact.getUri().endsWith("-sources.jar"))); @@ -1745,4 +1749,44 @@ void testExtraConfiguration() { }); } + @Test + void testAndroidBuildTargets() { + + // NOTE: Requires Android SDK to be configured via ANDROID_HOME property + + withNewTestServer("android-test", (buildServer, client) -> { + + WorkspaceBuildTargetsResult buildTargetsResult = buildServer.workspaceBuildTargets().join(); + List btIds = buildTargetsResult.getTargets().stream() + .map(BuildTarget::getId) + .collect(Collectors.toList()); + + DependencyModulesResult dependencyModulesResultBefore = buildServer + .buildTargetDependencyModules(new DependencyModulesParams(btIds)).join(); + + for (DependencyModulesItem item : dependencyModulesResultBefore.getItems()) { + assertTrue(containsAndroidArtifactPath(item.getModules())); + } + + }); + + } + + private boolean containsAndroidArtifactPath(List modules) { + + for (DependencyModule module : modules) { + Object data = module.getData(); + if (data instanceof JsonObject) { + for (JsonElement artifact : ((JsonObject) data).getAsJsonArray("artifacts")) { + if (artifact.getAsJsonObject().get("uri").getAsString().endsWith("android.jar")) { + return true; + } + } + } + } + + return false; + + } + } diff --git a/server/src/test/java/com/microsoft/java/bs/core/internal/services/BuildTargetServiceTest.java b/server/src/test/java/com/microsoft/java/bs/core/internal/services/BuildTargetServiceTest.java index dbf5a735..edded74e 100644 --- a/server/src/test/java/com/microsoft/java/bs/core/internal/services/BuildTargetServiceTest.java +++ b/server/src/test/java/com/microsoft/java/bs/core/internal/services/BuildTargetServiceTest.java @@ -190,7 +190,9 @@ void testGetBuildTargetOutputPaths() { sourceOutputDirs.add(sourceOutputDir); when(gradleSourceSet.getSourceOutputDirs()).thenReturn(sourceOutputDirs); File resourceOutputDir = new File(("resourceOutputDir")); - when(gradleSourceSet.getResourceOutputDir()).thenReturn(resourceOutputDir); + Set resourceOutputDirs = new HashSet<>(); + resourceOutputDirs.add(resourceOutputDir); + when(gradleSourceSet.getResourceOutputDirs()).thenReturn(resourceOutputDirs); BuildTargetService buildTargetService = new BuildTargetService(buildTargetManager, connector, preferenceManager); diff --git a/testProjects/android-test/.gitignore b/testProjects/android-test/.gitignore new file mode 100644 index 00000000..aa724b77 --- /dev/null +++ b/testProjects/android-test/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/testProjects/android-test/app/.gitignore b/testProjects/android-test/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/testProjects/android-test/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/testProjects/android-test/app/build.gradle b/testProjects/android-test/app/build.gradle new file mode 100644 index 00000000..7f4c082a --- /dev/null +++ b/testProjects/android-test/app/build.gradle @@ -0,0 +1,38 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace 'com.example.androidtest' + compileSdk 34 + + defaultConfig { + applicationId "com.example.androidtest" + minSdk 26 + targetSdk 34 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation libs.appcompat + implementation libs.material + implementation libs.activity + implementation libs.constraintlayout + testImplementation libs.junit + androidTestImplementation libs.ext.junit + androidTestImplementation libs.espresso.core +} \ No newline at end of file diff --git a/testProjects/android-test/app/src/androidTest/java/com/example/androidtest/ExampleInstrumentedTest.java b/testProjects/android-test/app/src/androidTest/java/com/example/androidtest/ExampleInstrumentedTest.java new file mode 100644 index 00000000..9451b02e --- /dev/null +++ b/testProjects/android-test/app/src/androidTest/java/com/example/androidtest/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.androidtest; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.androidtest", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/testProjects/android-test/app/src/main/AndroidManifest.xml b/testProjects/android-test/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..49cdb934 --- /dev/null +++ b/testProjects/android-test/app/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testProjects/android-test/app/src/main/java/com/example/androidtest/MainActivity.java b/testProjects/android-test/app/src/main/java/com/example/androidtest/MainActivity.java new file mode 100644 index 00000000..1315acc0 --- /dev/null +++ b/testProjects/android-test/app/src/main/java/com/example/androidtest/MainActivity.java @@ -0,0 +1,24 @@ +package com.example.androidtest; + +import android.os.Bundle; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_main); + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + } +} \ No newline at end of file diff --git a/testProjects/android-test/app/src/main/res/layout/activity_main.xml b/testProjects/android-test/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..9affce0f --- /dev/null +++ b/testProjects/android-test/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/testProjects/android-test/app/src/test/java/com/example/androidtest/ExampleUnitTest.java b/testProjects/android-test/app/src/test/java/com/example/androidtest/ExampleUnitTest.java new file mode 100644 index 00000000..b24cae29 --- /dev/null +++ b/testProjects/android-test/app/src/test/java/com/example/androidtest/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.example.androidtest; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/testProjects/android-test/build.gradle b/testProjects/android-test/build.gradle new file mode 100644 index 00000000..41b070ae --- /dev/null +++ b/testProjects/android-test/build.gradle @@ -0,0 +1,5 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false +} \ No newline at end of file diff --git a/testProjects/android-test/gradle.properties b/testProjects/android-test/gradle.properties new file mode 100644 index 00000000..4387edc2 --- /dev/null +++ b/testProjects/android-test/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/testProjects/android-test/gradle/libs.versions.toml b/testProjects/android-test/gradle/libs.versions.toml new file mode 100644 index 00000000..abf6cd39 --- /dev/null +++ b/testProjects/android-test/gradle/libs.versions.toml @@ -0,0 +1,27 @@ +[versions] +agp = "8.5.1" +junit = "4.13.2" +junitVersion = "1.2.1" +espressoCore = "3.6.1" +appcompat = "1.7.0" +material = "1.12.0" +activity = "1.9.0" +constraintlayout = "2.1.4" +runner = "1.0.2" +espressoCoreVersion = "3.0.2" + +[libraries] +junit = { group = "junit", name = "junit", version.ref = "junit" } +ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } +constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +runner = { group = "com.android.support.test", name = "runner", version.ref = "runner" } +android-espresso-core = { group = "com.android.support.test.espresso", name = "espresso-core", version.ref = "espressoCoreVersion" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } + diff --git a/testProjects/android-test/gradle/wrapper/gradle-wrapper.properties b/testProjects/android-test/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..7f026d4d --- /dev/null +++ b/testProjects/android-test/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jul 16 19:40:56 IST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/testProjects/android-test/gradlew b/testProjects/android-test/gradlew new file mode 100644 index 00000000..4f906e0c --- /dev/null +++ b/testProjects/android-test/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/testProjects/android-test/gradlew.bat b/testProjects/android-test/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/testProjects/android-test/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/testProjects/android-test/mylibrary/.gitignore b/testProjects/android-test/mylibrary/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/testProjects/android-test/mylibrary/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/testProjects/android-test/mylibrary/build.gradle b/testProjects/android-test/mylibrary/build.gradle new file mode 100644 index 00000000..f6e082ee --- /dev/null +++ b/testProjects/android-test/mylibrary/build.gradle @@ -0,0 +1,30 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace 'com.example.mylibrary' + compileSdk 34 + + defaultConfig { + minSdk 26 + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + testImplementation libs.junit + androidTestImplementation libs.runner + androidTestImplementation libs.android.espresso.core +} \ No newline at end of file diff --git a/testProjects/android-test/mylibrary/src/androidTest/java/com/example/mylibrary/ExampleInstrumentedTest.java b/testProjects/android-test/mylibrary/src/androidTest/java/com/example/mylibrary/ExampleInstrumentedTest.java new file mode 100644 index 00000000..d29708b8 --- /dev/null +++ b/testProjects/android-test/mylibrary/src/androidTest/java/com/example/mylibrary/ExampleInstrumentedTest.java @@ -0,0 +1,25 @@ +package com.example.mylibrary; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.mylibrary.test", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/testProjects/android-test/mylibrary/src/main/AndroidManifest.xml b/testProjects/android-test/mylibrary/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/testProjects/android-test/mylibrary/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/testProjects/android-test/mylibrary/src/main/java/com/example/mylibrary/Greeting.java b/testProjects/android-test/mylibrary/src/main/java/com/example/mylibrary/Greeting.java new file mode 100644 index 00000000..21ff32c4 --- /dev/null +++ b/testProjects/android-test/mylibrary/src/main/java/com/example/mylibrary/Greeting.java @@ -0,0 +1,9 @@ +package com.example.mylibrary; + +public class Greeting { + + public void greet(String name) { + System.out.println("Hello " + name); + } + +} diff --git a/testProjects/android-test/mylibrary/src/test/java/com/example/mylibrary/ExampleUnitTest.java b/testProjects/android-test/mylibrary/src/test/java/com/example/mylibrary/ExampleUnitTest.java new file mode 100644 index 00000000..0c3b0847 --- /dev/null +++ b/testProjects/android-test/mylibrary/src/test/java/com/example/mylibrary/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.example.mylibrary; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/testProjects/android-test/settings.gradle b/testProjects/android-test/settings.gradle new file mode 100644 index 00000000..be72bbfa --- /dev/null +++ b/testProjects/android-test/settings.gradle @@ -0,0 +1,24 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "AndroidTest" +include ':app' +include ':mylibrary'