Please explain the Android build.gradle groovy syntax - groovy

What does the following groovy syntax really mean?
The Gradle docs tout how the build.gradle is just groovy. The Android team has simplified the default build.gradle to the point that it doesn't look like code (to me at least). Please explain what this is doing in terms of groovy syntax. For example, are these global variable declarations that the Android plugin uses?
Bonus points if you include references to http://groovy-lang.org/syntax.html as part of your explanation.
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion "21.1.2"
defaultConfig {
applicationId "com.crittercism"
minSdkVersion 15
targetSdkVersion 21
versionCode 5
versionName "5.0"
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}

You can think of a gradle build script as some code which is delegated to an object which can respond to method calls written in it.
The script uses a lot of Groovy syntactic sugar, so removing them, it should look like this:
apply( [plugin: 'com.android.application'] );
android({
compileSdkVersion( 21 );
buildToolsVersion( "21.1.2" );
defaultConfig({
applicationId( "com.crittercism" );
minSdkVersion( 15 );
targetSdkVersion( 21 );
versionCode( 5 );
versionName( "5.0" );
});
});
dependencies({
compile( fileTree([dir: 'libs', include: ['*.jar']]) );
});
So the script is really a bunch of method calls:
def apply(Map)
def android(Closure)
def dependencies(Closure)
This android(Closure) will receive a closure and will delegate the methods called in it to an object which can respond to these methods:
def compileSdkVersion(Integer)
def buildToolsVersion(String)
...
Given that, we can parse the script, delegate it to some object and then execute it.
Delegating using DelegatingBaseScript is one way to do it (not sure if Gradle does it this way). Here is a dumbed down working version:
import org.codehaus.groovy.control.CompilerConfiguration
gradleScript = '''
apply plugin: 'com.android.application'
android({
compileSdkVersion( 21 )
buildToolsVersion( "21.1.2" )
})'''
class PocketGradle {
def config = [apply:[]].withDefault { [:] }
def apply(map) {
config.apply << map.plugin
}
def android(Closure closure) {
closure.delegate = new Expando(
compileSdkVersion: { Integer version ->
config.android.compileSdkVersion = version
},
buildToolsVersion : { String version ->
config.android.buildToolsVersion = version
},
)
closure()
}
}
def compiler = new CompilerConfiguration(scriptBaseClass: DelegatingScript.class.name)
shell = new GroovyShell(this.class.classLoader, new Binding(), compiler)
script = shell.parse gradleScript
script.setDelegate( gradle = new PocketGradle() )
script.run()
assert gradle.config == [
apply: ['com.android.application'],
android: [
compileSdkVersion: 21,
buildToolsVersion: '21.1.2'
]
]
You can execute the script in Groovy Web Console (click "Edit in console" and then "Execute script").
Most of the syntax explanation are in the DSL section:
Command chains
Groovy lets you omit parentheses around the arguments of a method call for top-level statements. "command chain" feature extends this by allowing us to chain such parentheses-free method calls, requiring neither parentheses around arguments, nor dots between the chained calls.
There is also Groovy ConfigSlurper, but i'm not sure if it can go as far as Gradle wants to.

Thanks to AndroidGuy for supplying the excellent video that informed me of the information below. The video is 35 minutes long, so here's the TL;DR.
Most of this syntax is a mixture of method calls and closures. The closures are represented by curly braces. Also note that method calls do not require parenthesis.
apply plugin: 'com.android.application'
This is calling the apply method on the project object with a single named parameter "plugin". The project object is the top level object supplied by Gradle.
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}
This is setting the dependencies property of the project object. Groovy properties are basically shorthand for getters and setters. The dependencies property is a Closure object that delegates to DependencyHandler. Groovy delegation is essentially a way to augment the scope resolution of a closure. The dependencies closure contains a single method call to compile, which takes a FileTree positional parameter. The FileTree is generated by the fileTree method which is defined in the project object. The compile method is still a bit nebulous to me. It appears to come from the Java plugin, but it isn't explicitly documented there. The 'compile' part is still a bit magical to me.
android {
...
}
I'll leave the 'android' section as an exercise to the reader. The Android Gradle Domain Specific Language (DSL) is not available on the web. You have to download it.

Related

Unresolved reference for button id in MainActivity.kt

when I try to directly type the button id in main activity of kotlin , it does not pick it up automatically, instead it says unresolved reference, what is wrong with Android Studio??
In my case I had the same problem and have solved it this way with Android Studio 4.x:
Put this in your apps build.gradle or build.gradle.kts. The latter in my case because my project is made with Kotlin KMM.
buildFeatures {
viewBinding = true
}
Here is my complete build.gradle.kts so you can see where it belongs to:
plugins {
id("com.android.application")
kotlin("android")
id("androidx.navigation.safeargs.kotlin")
}
dependencies {
implementation(project(":shared"))
implementation("com.google.android.material:material:1.3.0")
implementation("androidx.appcompat:appcompat:1.2.0")
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
implementation("androidx.navigation:navigation-fragment-ktx:2.3.4")
implementation("androidx.navigation:navigation-ui-ktx:2.3.4")
}
android {
compileSdkVersion(30)
defaultConfig {
applicationId = "************************"
minSdkVersion(24)
targetSdkVersion(30)
versionCode = 1
versionName = "1.0"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
buildFeatures {
viewBinding = true
}
}
Here you can read all about it. Also if you are using Android Studio lower than 4.x how it has to be done.
https://developer.android.com/topic/libraries/view-binding
Don't forget to sync your project. It will only appear after that step. You can check it by having a look at dataBinding folder which you will find at that path.
build > generated > data_binding_base_class_source_out > debug > out > rest is app specific
and the last folder will be "dataBinding" folder.
Have fun!
It's hard to advise anything without being given any code.
I'm guessing you're using Kotlin synthetic import? Then you would also need to import something like this import kotlinx.android.synthetic.main.activity_main.* to be able to use view elements without findById. Also, make su you have integrated Kotlin extensions plugin.
Also, I can't rule out this possibility, since I can't see any of your code, but maybe check if the view element you're trying to access in MainActivity is in it's layout?

Objenesis dependency causes instantiation error

Just starting a new Gradle project.
This test passes:
def 'Launcher.main should call App.launch'(){
given:
GroovyMock(Application, global: true)
when:
Launcher.main()
then:
1 * Application.launch( App, null ) >> null
}
... until, to get another test using a (Java) Mock to work, I have to add these dependencies:
testImplementation 'net.bytebuddy:byte-buddy:1.10.8'
testImplementation 'org.objenesis:objenesis:3.1'
(NB I assume these versions are OK for Groovy 3.+, which I'm now using ... both are the most up-to-date available at Maven Repo).
With these dependencies the above test fails:
java.lang.InstantiationError: javafx.application.Application
at org.objenesis.instantiator.sun.SunReflectionFactoryInstantiator.newInstance(SunReflectionFactoryInstantiator.java:48)
at org.objenesis.ObjenesisBase.newInstance(ObjenesisBase.java:73)
at org.objenesis.ObjenesisHelper.newInstance(ObjenesisHelper.java:44)
at org.spockframework.mock.runtime.MockInstantiator$ObjenesisInstantiator.instantiate(MockInstantiator.java:45)
at org.spockframework.mock.runtime.MockInstantiator.instantiate(MockInstantiator.java:31)
at org.spockframework.mock.runtime.GroovyMockFactory.create(GroovyMockFactory.java:57)
at org.spockframework.mock.runtime.CompositeMockFactory.create(CompositeMockFactory.java:42)
at org.spockframework.lang.SpecInternals.createMock(SpecInternals.java:47)
at org.spockframework.lang.SpecInternals.createMockImpl(SpecInternals.java:298)
at org.spockframework.lang.SpecInternals.createMockImpl(SpecInternals.java:288)
at org.spockframework.lang.SpecInternals.GroovyMockImpl(SpecInternals.java:215)
at core.AppSpec.Launcher.main should call App.launch(first_tests.groovy:30)
I confess that I have only the sketchiest notion of what "bytebuddy" and "objenesis" actually do, although I assume it is fiendishly clever. Edit: having just visited their respective home pages my notion is now slightly less sketchy, and yes, it is fiendishly clever.
If an orthodox solution to this is not available, is it by any chance possible to turn off the use of these dependencies for an individual feature (i.e. test)? Possibly using some annotation maybe?
Edit
This is an MCVE:
Specs: Java 11.0.5, OS Linux Mint 18.3.
build.gradle:
plugins {
id 'groovy'
id 'java'
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.8'
}
repositories { mavenCentral() }
javafx {
version = "11.0.2"
modules = [ 'javafx.controls', 'javafx.fxml' ]
}
dependencies {
implementation 'org.codehaus.groovy:groovy:3.+'
testImplementation 'junit:junit:4.12'
testImplementation 'org.spockframework:spock-core:2.0-M2-groovy-3.0'
testImplementation 'net.bytebuddy:byte-buddy:1.10.8'
testImplementation 'org.objenesis:objenesis:3.1'
// in light of kriegaex's comments:
implementation group: 'cglib', name: 'cglib', version: '3.3.0'
}
test { useJUnitPlatform() }
application {
mainClassName = 'core.Launcher'
}
installDist{}
main.groovy:
class Launcher {
static void main(String[] args) {
Application.launch(App, null )
}
}
class App extends Application {
void start(Stage primaryStage) {
}
}
first_tests.groovy:
class AppSpec extends Specification {
def 'Launcher.main should call App.launch'(){
given:
GroovyMock(Application, global: true)
when:
Launcher.main()
then:
1 * Application.launch( App, null ) >> null
}
}
The reason why this project needs something to call the Application subclass is explained here: it's so that it is possible to do an installDist which bundles in JavaFX.
Don't we have to use a global GroovyMock?
If you want to check the interaction, yes. But actually you are testing the JavaFX launcher rather than your application. So I doubt that there is any benefit. I would focus on testing the App class instead. Also imagine for a moment that you would write the classes with main methods in Java instead of Groovy. Groovy mocks would not work when called from Java code, especially not global ones. Then you would end up testing via Powermockito from Spock, which would also work but still you would test the JavaFX launcher rather than your application.
Also isn't it slightly extreme to say any use of Groovy mocks is wrong?
I did not say that. I said: "probably something is wrong with your application design". The reason I said that is because the use of Groovy mocks and things like mocking static methods are test code smells. You can check the smell and then decide it is okay, which IMO in most cases it is not. Besides, instead of application design the problem can also be in the test itself, which in this case I would say it is. But that is arguable, so I am going to present a solution to you further below.
In this case technically the global Application mock is your only way if you do insist to test the JavaFX launcher because even a global mock on App would not work as the launcher uses reflection in order to call the App constructor and that is not intercepted by the mock framework.
you say that Spock spock-core:2.0-M2-groovy-3.0 is a "pre-release". I can't see anything on this page (...) which says that. How do you know?
You found out already by checking out the GitHub repository, but I was just seeing it in the unusual version number containing "M2" like "milestone 2" which is similar to "RC" (or "CR") for release candidates (or candidate releases).
As for the technical problem, you can either not declare Objenesis in your Gradle script because it is an optional dependency, then the test compiles and runs fine, as you already noticed yourself. But assuming you need optional dependencies like Objenesis, CGLIB (actually cglib-nodep), Bytebuddy and ASM for other tests in your suite, you can just tell Spock not to use Objenesis in this case. So assuming you have a Gradle build file like this:
plugins {
id 'groovy'
id 'java'
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.8'
}
repositories { mavenCentral() }
javafx {
version = "11.0.2"
modules = ['javafx.controls', 'javafx.fxml']
}
dependencies {
implementation 'org.codehaus.groovy:groovy:3.+'
testImplementation 'org.spockframework:spock-core:2.0-M2-groovy-3.0'
// Optional Spock dependencies, versions matching the ones listed at
// https://mvnrepository.com/artifact/org.spockframework/spock-core/2.0-M2-groovy-3.0
testImplementation 'net.bytebuddy:byte-buddy:1.9.11'
testImplementation 'org.objenesis:objenesis:3.0.1'
testImplementation 'cglib:cglib-nodep:3.2.10'
testImplementation 'org.ow2.asm:asm:7.1'
}
test { useJUnitPlatform() }
application {
mainClassName = 'de.scrum_master.app.Launcher'
}
installDist {}
My version of your MCVE would looks like this (sorry, I added my own package names and also imports because otherwise it is not really an MCVE):
package de.scrum_master.app
import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
import javafx.stage.Stage
class App extends Application {
#Override
void start(Stage stage) {
def javaVersion = System.getProperty("java.version")
def javafxVersion = System.getProperty("javafx.version")
Label l = new Label("Hello, JavaFX $javafxVersion, running on Java $javaVersion.")
Scene scene = new Scene(new StackPane(l), 640, 480)
stage.setScene(scene)
stage.show()
}
}
package de.scrum_master.app
import javafx.application.Application
class Launcher {
static void main(String[] args) {
Application.launch(App, null)
}
}
package de.scrum_master.app
import javafx.application.Application
import spock.lang.Specification
class AppSpec extends Specification {
def 'Launcher.main should call App.launch'() {
given:
GroovyMock(Application, global: true, useObjenesis: false)
when:
Launcher.main()
then:
1 * Application.launch(App, null)
}
}
The decisive detail here is the useObjenesis: false parameter.
Update: Just for reference, this is how you would do it with a launcher class implemented in Java using PowerMockito.
Attention, this solution needs the Sputnik runner from Spock 1.x which was removed in 2.x. So in Spock 2 this currently does not work because it is based on JUnit 5 and can no longer use #RunWith(PowerMockRunner) and #PowerMockRunnerDelegate(Sputnik) because PowerMock currently does not support JUnit 5. But I tested it with Spock 1.3-groovy-2.5 and Groovy 2.5.8.
package de.scrum_master.app
import javafx.application.Application
import org.junit.runner.RunWith
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import org.powermock.modules.junit4.PowerMockRunnerDelegate
import org.spockframework.runtime.Sputnik
import spock.lang.Specification
import static org.mockito.Mockito.*
import static org.powermock.api.mockito.PowerMockito.*
#RunWith(PowerMockRunner)
#PowerMockRunnerDelegate(Sputnik)
#PrepareForTest(Application)
class JavaAppSpec extends Specification {
def 'JavaLauncher.main should launch JavaApp'() {
given:
mockStatic(Application)
when:
JavaLauncher.main()
then:
verifyStatic(Application, times(1))
Application.launch(JavaApp)
}
}

Gradle task is never up-to-date

In the configuration phase of the task I register some dir as builtBy: thisTask. I expect gradle to automatically detect if sources were changed, but the task always being executed.
Here is the task:
subprojects {
def srcMainMirah = file('src/main/mirah')
if (srcMainMirah.exists()) {
idea.module.sourceDirs += srcMainMirah
task compileMirah {
def classesMirahMain = file("$buildDir/classes-mirah/main")
inputs.sourceDir srcMainMirah
def thisTask = delegate
sourceSets.main {
output.dir(classesMirahMain, builtBy: thisTask)
java.srcDir srcMainMirah
}
dependsOn tasks.compileJava
doFirst {
def classpath = files("$buildDir/classes/main").plus(configurations.compile)
mirahc(srcMainMirah, classesMirahMain, classpath)
}
}
}
}
It is for compiling sources in mirah language, which produces *.class files just like java compiler does.
Declaring inputs alone for a task is insufficient to determine if the task is up-to-date. You are required to also declared task.outputs
A task with no defined outputs will never be considered up-to-date. For scenarios where the outputs of a task are not files, or for more complex scenarios, the TaskOutputs.upToDateWhen() method allows you to calculate programmatically if the tasks outputs should be considered up to date.
A task with only outputs defined will be considered up-to-date if those outputs are unchanged since the previous build.
From section 17.9.1 here.

How to access a Gradle configurations object correctly

First off, this is my first foray into Gradle/Groovy (using Gradle 1.10). I'm setting up a multi-project environment where I'm creating a jar artifact in one project and then want to define an Exec task, in another project, which depends on the created jar. I'm setting it up something like this:
// This is from the jar building project
jar {
...
}
configurations {
loaderJar
}
dependencies {
loaderJar files(jar.archivePath)
...
}
// From the project which consumes the built jar
configurations {
loaderJar
}
dependencies {
loaderJar project(path: ":gfxd-demo-loader", configuration: "loaderJar")
}
// This is my test task
task foo << {
configurations.loaderJar.each { println it }
println configurations.loaderJar.collect { it }[0]
// The following line breaks!!!
println configurations.loaderJar[0]
}
When executing the foo task it fails with:
> Could not find method getAt() for arguments [0] on configuration ':loaderJar'.
In my foo task I'm just testing to see how to access the jar. So the question is, why does the very last println fail? if a Configuration object is a Collection/Iterable then surely I should be able to index into it?
Configuration is-a java.util.Iterable, but not a java.util.Collection. As can be seen in the Groovy GDK docs, the getAt method (which corresponds to the [] operator) is defined on Collection, but not on Iterable. Hence, you can't index into a Configuration.

How to Report Results to Sauce Labs using Geb/Spock?

I want to use the Sauce Labs Java REST API to send Pass/Fail status back to the Sauce Labs dashboard. I am using Geb+Spock, and my Gradle build creates a test results directory where results are output in XML. My problem is that the results XML file doesn't seem to be generated until after the Spock specification's cleanupSpec() exits. This causes my code to report the results of the previous test run, rather than the current one. Clearly not what I want!
Is there some way to get to the results from within cleanupSpec() without relying on the XML? Or a way to get the results to file earlier? Or some alternative that will be much better than either of those?
Some code:
In build.gradle, I specify the testResultsDir. This is where the XML file is written after the Spock specifications exit:
drivers.each { driver ->
task "${driver}Test"(type: Test) {
cleanTest
systemProperty "geb.env", driver
testResultsDir = file("$buildDir/test-results/${driver}")
systemProperty "proj.test.resultsDir", testResultsDir
}
}
Here is the setupSpec() and cleanupSpec() in my LoginSpec class:
class LoginSpec extends GebSpec {
#Shared def SauceREST client = new SauceREST("redactedName", "redactedKey")
#Shared def sauceJobID
#Shared def allSpecsPass = true
def setupSpec() {
sauceJobID = driver.getSessionId().toString()
}
def cleanupSpec() {
def String specResultsDir = System.getProperty("proj.test.resultsDir") ?: "./build/test-results"
def String specResultsFile = this.getClass().getName()
def String specResultsXML = "${specResultsDir}/TEST-${specResultsFile}.xml"
def testsuiteResults = new XmlSlurper().parse( new File( specResultsXML ))
// read error and failure counts from the XML
def errors = testsuiteResults.#errors.text()?.toInteger()
def failures = testsuiteResults.#failures.text()?.toInteger()
if ( (errors + failures) > 0 ) { allSpecsPass = false }
if ( allSpecsPass ) {
client.jobPassed(sauceJobID)
} else {
client.jobFailed(sauceJobID)
}
}
}
The rest of this class contains login specifications that do not interact with SauceLabs. When I read the XML, it turns out that it was written at the end of the previous LoginSpec run. I need a way to get to the values of the current run.
Thanks!
Test reports are generated after a Specification has finished execution and the generation is performed by the build system, so in your case by Gradle. Spock has no knowledge of that so you are unable to get that information from within the test.
You can on the other hand quite easily get that information from Gradle. Test task has two methods that might be of interest to you here: addTestListener() and afterSuite(). It seems that the cleaner solution here would be to use the first method, implement a test listener and put your logic in afterSuite() of the listener (and not the task configuration). You would probably need to put that listener implementation in buildSrc as it looks like you have a dependency on SauceREST and you would need to build and compile your listener class before being able to use it as an argument to addTestListener() in build.gradle of your project.
Following on from erdi's suggestion, I've created a Sauce Gradle helper library, which provides a Test Listener that parses the test XML output and invokes the Sauce REST API to set the pass/fail status.
The library can be included by adding the following to your build.gradle file:
import com.saucelabs.gradle.SauceListener
buildscript {
repositories {
mavenCentral()
maven {
url "https://repository-saucelabs.forge.cloudbees.com/release"
}
}
dependencies {
classpath group: 'com.saucelabs', name: 'saucerest', version: '1.0.2'
classpath group: 'com.saucelabs', name: 'sauce_java_common', version: '1.0.14'
classpath group: 'com.saucelabs.gradle', name: 'sauce-gradle-plugin', version: '0.0.1'
}
}
gradle.addListener(new SauceListener("YOUR_SAUCE_USERNAME", "YOUR_SAUCE_ACCESS_KEY"))
You will also need to output the Selenium session id for each test, so that the SauceListener can associate the Sauce Job with the pass/fail status. To do this, include the following output in the stdout:
SauceOnDemandSessionID=SELENIUM_SESSION_ID

Resources