It is possible to share code between this two test modes in Android Studio?
I have a set of Mock Utils class's that I need to access in both of the test modes.
Finally I found the solution (workaround) thanks to a blog post from Dan Lew (http://blog.danlew.net/2015/11/02/sharing-code-between-unit-tests-and-instrumentation-tests-on-android/).
The solution I've come up with is to leverage source sets to define common code. First, I put my shared test code into src/sharedTest/java1 .
android {
sourceSets {
String sharedTestDir = 'src/sharedTest/java'
test {
java.srcDir sharedTestDir
}
androidTest {
java.srcDir sharedTestDir
}
}
}
What it's doing above is adding my shared code directory to both the test and androidTest source sets. Now, in addition to their default Java sources, they'll also include the shared code.
Edit (20022-08-15): This no longer works, for a discussion of why, and the (now) recommneded way to achieve sharing of code, see this question and the accepted answer: Shared srcDirs between test and androidTest, unresolved references after upgrade to Android Studio Chipmunk (IntelliJ 2021.2.1)
Sharing code via common folder and sourceSets no longer works as of Android Studio chipmunk. BUT, there is another method to make it work:
Basically, create an android library (sharedTestCode), depend on it in your app via testImplementation and androidTestImplementation. In the sharedTestCode build.gradle file, depend on the app. You should now be able to create shared test data and reference them in both types of tests.
Here is a sample project with this setup working:
https://drive.google.com/file/d/1I2CZhTxHGRgCN9UCEjIuWUfFnGxTF_Cv/view?usp=sharing
For multiple modules project
sourceSets {
test.java.srcDirs += ["${project(':module-name').projectDir}/src/sharedTest/java",
"src/test/java"]
}
The accepted answer does not seem to work with Android Studio Chipmunk anymore...
Here is one (not ideal) workaround:
android {
sourceSets.main.java.srcDirs += "$projectDir/src/testShared/java"
}
Try assigning the sourceSets manually:
sourceSets {
val sharedTestDir = file("src/sharedTest/java")
getByName("test").java.srcDirs(sharedTestDir)
getByName("androidTest").java.srcDirs(sharedTestDir)
}
Related
On Gradle 6.1.1, how to go around adding sourceSets for android project?
The answer on other questions doesn't work anymore, getByName("name") returns error with SourceSet with name 'main' not found.
The official document said to use
sourceSets {
main {
java {
srcDir("thirdParty/src/main/java")
}
}
}
However, there are over 20 main that has to be imported and I'm not sure which one is correct.
I'm using gradle 6.5.1, however documentation suggest, that is should also work for you, try:
sourceSets {
named("main") {
java.srcDir("../buildSrc/src/main/java")
}
}
It's also works for build types (debug/release), flavours etc.
Reason for this is that groovy can somehow interpret itself and knows main etc., but on gradle kts, you have call it using named for already existing, or getByName, create etc. base on need.
Similiar situation is for implement and api in groovy you can just use implementationDebug to attach it only for debug version, but in kotlin dsl you have to call it as a string "implementationDebug", because there is no such function
----- PS -----
If named, getByName not works for you, then try to experiment with findByName and create
What I'm trying to achieve
I'm trying to generate my REST API client for Android using OpenAPI Generator from the build.gradle script. That way, I wouldn't have to run the generator command line every time the specs change. Ideally, this would be generated when I build/assemble my app, and the sources would end up in the java (generated) folder, where generated sources are then accessible from the code (this is what happens with the BuildConfig.java file for example).
What I've tried so far
Following this link from their official GitHub, here's the build.gradle file I ended up with:
apply plugin: 'com.android.application'
apply plugin: 'org.openapi.generator'
...
openApiValidate {
inputSpec = "$rootDir/app/src/main/openapi/my-api.yaml"
recommend = true
}
openApiGenerate {
generatorName = "java"
inputSpec = "$rootDir/app/src/main/openapi/my-api.yaml"
outputDir = "$buildDir/generated/openapi"
groupId = "$project.group"
id = "$project.name-openapi"
version = "$project.version"
apiPackage = "com.example.mypackage.api"
invokerPackage = "com.example.mypackage.invoker"
modelPackage = "com.example.mypackage.model"
configOptions = [
java8 : "true",
dateLibrary : "java8",
library : "retrofit2"
]
}
...
First, I've never managed to get the API generated with the build/assemble task, even when I tried adding:
compileJava.dependsOn tasks.openApiGenerate
or
assemble.dependsOn tasks.openApiGenerate
The only way I could generate the sources was by manually triggering the openApiGenerate task:
Then, when I do generate my sources this way, they end up in the build folder but aren't accessible from my code, and aren't visible in the java (generated) folder:
I then have to manually copy/paste the generated source files to my project sources in order to use the API.
Even though I'm able to work around these issues by adding manual procedures, it would be way more maintainable if the whole process was simply automatic. I was able to achieve a similar result with another tool, Protobuf. Indeed, my gradle task gets triggered every time I build the app, and the sources end up in the java (generated) folder, so I don't have to do any additional work. The task is much simpler though, so I assume the main work that I'm not able to replicate with OpenAPI Generator is handled by the Protobuf plugin itself.
You have to specify path to the generated sources as a custom source set for your Gradle module, which is app in this case, as described here – https://developer.android.com/studio/build/build-variants#configure-sourcesets. That way Gradle will treat your sources as accessible from your code.
Something like this:
android {
...
sourceSets {
main {
java.srcDirs = ['build/generated/openapi/src/main/java']
}
}
...
}
I solved the issue you described like this, I'm using gradle.kts however.
See my build.gradle.kts
plugins {
// Your other plugins
id("org.openapi.generator") version "5.3.0"
}
openApiGenerate {
generatorName.set("kotlin")
inputSpec.set("$rootDir/app/src/main/openapi/my-api.yaml")
outputDir.set("$buildDir/generated/api")
// Your other specification
}
application {
// Your other code
sourceSets {
main {
java {
// TODO: Set this path according to what was generated for you
srcDir("$buildDir/generated/api/src/main/kotlin")
}
}
}
}
tasks.compileKotlin {
dependsOn(tasks.openApiGenerate)
}
You need to build the application at least once for the IDE to detect the library (at least this is the case for me in Intellij)
Your build should automatically generate the open api classes , to refer the generated classes in your java project you should add the generated class path to your source directory like it was mentioned in the other answers
https://developer.android.com/studio/build/build-variants#configure-sourcesets
As far as the task dependency goes , in android tasks are generated after configuration thus for gradle to recognize the task , wrap it inside afterEvaluate block like
afterEvaluate {
tasks.compileDebugJavaWithJavac.dependsOn(tasks.openApiGenerate)
}
I had this issue, and this answer https://stackoverflow.com/a/55646891/14111809 led me to a more informative error:
error: incompatible types: Object cannot be converted to Annotation
#java.lang.Object()
Taking a look at the generated files that were causing this error, noticed:
import com.squareup.moshi.Json;
After including a Moshi in the app build.gradle, the build succeeded and the generated code was accessible.
implementation("com.squareup.moshi:moshi-kotlin:1.13.0")
TL;DR;
How to add two or more kotlin native modules on an iOS project without getting duplicate symbols error?
The detailed question
Let's assume a multi-module KMP project as a follow where there exists a native app for Android and a native app for iOS and two common modules to hold shared kotlin code.
.
├── android
│ └── app
├── common
│ ├── moduleA
│ └── moduleB
├── ios
│ └── app
The module A contains a data class HelloWorld and has no module dependencies:
package hello.world.modulea
data class HelloWorld(
val message: String
)
Module B contains an extension function for HelloWorld class so it depends on module A:
package hello.world.moduleb
import hello.world.modulea.HelloWorld
fun HelloWorld.egassem() = message.reversed()
The build.gradle configuration of the modules are:
Module A
apply plugin: "org.jetbrains.kotlin.multiplatform"
apply plugin: "org.jetbrains.kotlin.native.cocoapods"
…
kotlin {
targets {
jvm("android")
def iosClosure = {
binaries {
framework("moduleA")
}
}
if (System.getenv("SDK_NAME")?.startsWith("iphoneos")) {…}
}
cocoapods {…}
sourceSets {
commonMain.dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72"
}
androidMain.dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72"
}
iosMain.dependencies {
}
}
}
Module B
apply plugin: "org.jetbrains.kotlin.multiplatform"
apply plugin: "org.jetbrains.kotlin.native.cocoapods"
…
kotlin {
targets {
jvm("android")
def iosClosure = {
binaries {
framework("moduleB")
}
}
if (System.getenv("SDK_NAME")?.startsWith("iphoneos")) {…}
}
cocoapods {…}
sourceSets {
commonMain.dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72"
implementation project(":common:moduleA")
}
androidMain.dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72"
}
iosMain.dependencies {
}
}
}
It looks pretty straightforward and it even works on android if I configure the android build gradle dependencies as a following:
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72"
implementation project(":common:moduleA")
implementation project(":common:moduleB")
}
However, this does not seem to be the correct way to organize multi modules on iOS, because running the ./gradlew podspec I get a BUILD SUCCESSFUL as expected with the following pods:
pod 'moduleA', :path => '…/HelloWorld/common/moduleA'
pod 'moduleB', :path => '…/HelloWorld/common/moduleB'
Even running a pod install I get a success output Pod installation complete! There are 2 dependencies from the Podfile and 2 total pods installed. whats looks correctly once the Xcode shows the module A and module B on the Pods section.
However, if I try to build the iOS project I get the following error:
Ld …/Hello_World-…/Build/Products/Debug-iphonesimulator/Hello\ World.app/Hello\ World normal x86_64 (in target 'Hello World' from project 'Hello World')
cd …/HelloWorld/ios/app
…
duplicate symbol '_ktypew:kotlin.Any' in:
…/HelloWorld/common/moduleA/build/cocoapods/framework/moduleA.framework/moduleA(result.o)
…/HelloWorld/common/moduleB/build/cocoapods/framework/moduleB.framework/moduleB(result.o)
… a lot of duplicate symbol more …
duplicate symbol '_kfun:kotlin.throwOnFailure$stdlib#kotlin.Result<#STAR>.()' in:
…/HelloWorld/common/moduleA/build/cocoapods/framework/moduleA.framework/moduleA(result.o)
…/HelloWorld/common/moduleB/build/cocoapods/framework/moduleB.framework/moduleB(result.o)
ld: 9928 duplicate symbols for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
My knowledge in iOS is not that much, so to my untrained eyes, it looks like each module is adding its own version of the things instead of using some resolutions strategy to share it.
If I use only the module A the code works and run as expected, so I know the code itself is correct, the problem is how to manage more than 1 module, so that the question, how to add both (module A and module B) on iOS and make things works?
P.S
I did reduce the code as much as I could, trying to keep only the parts that I guess is the source of the problem, however, the complete code is available here if you want to check anything missing in the snippets, or if you want to run and try to solve the problem…
Multiple Kotlin frameworks can be tricky, but should be working as of 1.3.70 which I see you have.
The issue seems to be that both frameworks are static, which is currently an issue in 1.3.70 so it isn't working. (This should be updated by 1.40). It looks like by default the cocoapods plugin sets the frameworks to be static which won't work. I'm unaware of a way to change cocoapods to set it as dynamic but I've tested building without cocoapods and using the isStatic variable in a gradle task, and have gotten an iOS project to compile. Something like:
binaries {
framework("moduleA"){
isStatic = false
}
}
For now you can work around the issue using this method by using the code above and creating a task to build the frameworks(here's an example)
Another thing worth noting is that on the iOS side, the HelloWorld classes will appear as two separate classes despite both coming from moduleA. It's another strange situation with multiple Kotlin frameworks, but I think the extension will still work in this case since you're returning a string.
I actually just wrote up a blog post about multiple Kotlin frameworks that may help with some other questions if you'd like to take a look. https://touchlab.co/multiple-kotlin-frameworks-in-application/
EDIT: Looks like cocoapodsext also has an isStatic variable, so set it to isStatic = false
tl:dr You currently can't have more than one static Kotlin frameworks in the same iOS project. Set them to not be static using isStatic = false.
However, if I try to build the iOS project I get the following error:
This particular error is a known issue. Multiple debug static frameworks are incompatible with compiler caches.
So to workaround the issue you can either disable compiler caches by putting the following line into your gradle.properties:
kotlin.native.cacheKind=none
or make the frameworks dynamic by adding the following snippet to your Gradle build script:
kotlin {
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
binaries.withType<org.jetbrains.kotlin.gradle.plugin.mpp.Framework> {
isStatic = false
}
}
}
See https://youtrack.jetbrains.com/issue/KT-42254 for more details.
I guess current behaviour for multiple frameworks doesn't make much sense for the original topic starter, I'm just putting my answer here for anyone who might encounter the same issue.
My knowledge in iOS is not that much, so to my untrained eyes, it looks like each module is adding its own version of the things instead of using some resolutions strategy to share it.
This is exactly how it is supposed to work at this moment. But "versions of the things" in each of the frameworks are put into the separate independent namespaces, so there should be no linkage errors, and the one you've encountered is a bug.
Currently, Kotlin is compiling .class files into a different output directory than Java:
Java:
<module>/build/intermediates/classes/<flavor>/<buildType>
Kotlin:
<module>/build/tmp/kotlin-classes/<buildVariant>
This is causing me a headache with some post compilation tasks. How do you configure Kotlin to generate class files into a different directory?
The following should be what you are looking for. Keep in mind you will have to reference the proper source sets.
gradle.build:
sourceSets {
main {
//if you truly want to override the defaults:
output.resourcesDir = file('out/bin')
// Compiled Java classes should use this directory
java.outputDir = file('out/bin')
}
}
Another nice thing here is also pointing the kotlin/java source directories through the gradle such as:
sourceSets {
main.kotlin.srcDirs += 'src/main/myKotlin'
main.java.srcDirs += 'src/main/myJava'
}
Of course these can be combined in one sourceSet block
Source:
https://docs.gradle.org/current/dsl/org.gradle.api.tasks.SourceSetOutput.html
The Kotlin plugin doesn't use outputDir in the same way the Java plugin does.
Try this workaround:
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
destinationDir = new File(buildDir, "work/program")
}
See this other SO answer: Kotlin gradle plugin - how to use custom output directory?
And the Kotlin plugin issue filed as a result: https://youtrack.jetbrains.com/issue/KT-23807
My app is being tested by junit, robolectric and gradle build.
The test code follows below structure.
approot/mytest/com/xx/pkg/*.java
test {
java {
srcDir 'mytest'
}
}
This includes all the files under test pakage, needlessly including some of the utility files.
I have all the test files name xxxxxTest.java, yyyyTest.java etc.
So I thought to change srcDir to mytest/**/*Test.java or /mytest/com/xx/pkg/*Test.java, build does not compile any test code. However /mytest/com/xx/pkg/*.java picks all test source files.
Any pointers?
Thanks
You can use TestFilter to achieve this. Try the following:
test {
filter {
includeTestsMatching "*Test"
}
}