1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.navigation.safeargs.gradle
18 
19 import org.gradle.testkit.runner.BuildResult
20 import org.gradle.testkit.runner.GradleRunner
21 import org.gradle.testkit.runner.TaskOutcome
22 import org.hamcrest.CoreMatchers.`is`
23 import org.hamcrest.CoreMatchers.not
24 import org.hamcrest.MatcherAssert.assertThat
25 import org.junit.Before
26 import org.junit.Rule
27 import org.junit.Test
28 import org.junit.rules.TemporaryFolder
29 import org.junit.runner.RunWith
30 import org.junit.runners.JUnit4
31 import java.io.File
32 import java.util.Properties
33 
34 private const val MAIN_DIR = "androidx/navigation/testapp"
35 
36 private const val NEXT_DIRECTIONS = "$MAIN_DIR/NextFragmentDirections.java"
37 private const val MAIN_DIRECTIONS = "$MAIN_DIR/MainFragmentDirections.java"
38 private const val MODIFIED_NEXT_DIRECTIONS = "$MAIN_DIR/ModifiedNextFragmentDirections.java"
39 private const val ADDITIONAL_DIRECTIONS = "$MAIN_DIR/AdditionalFragmentDirections.java"
40 private const val FOO_DIRECTIONS = "safe/gradle/test/app/foo/FooFragmentDirections.java"
41 
42 private const val NAV_RESOURCES = "src/main/res/navigation"
43 private const val SEC = 1000L
44 
45 // Does not work in the Android Studio
46 @RunWith(JUnit4::class)
47 class PluginTest {
48 
49     @Suppress("MemberVisibilityCanBePrivate")
50     @get:Rule
51     val testProjectDir = TemporaryFolder()
52 
53     private var buildFile: File = File("")
54     private var compileSdkVersion = ""
55     private var buildToolsVersion = ""
56 
projectRootnull57     private fun projectRoot(): File = testProjectDir.root
58 
59     private fun assertGenerated(name: String) = assertExists(name, true)
60 
61     private fun assertNotGenerated(name: String) = assertExists(name, false)
62 
63     private fun assertExists(name: String, ex: Boolean): File {
64         val generatedFile = File(projectRoot(), "build/$GENERATED_PATH/$name")
65         assertThat(generatedFile.exists(), `is`(ex))
66         return generatedFile
67     }
68 
navResourcenull69     private fun navResource(name: String) = File(projectRoot(), "$NAV_RESOURCES/$name")
70 
71     private fun gradleBuilder(vararg args: String) = GradleRunner.create()
72             .withProjectDir(projectRoot()).withPluginClasspath().withArguments(*args)
73 
74     private fun runGradle(vararg args: String) = gradleBuilder(*args).build()
75     private fun runAndFailGradle(vararg args: String) = gradleBuilder(*args).buildAndFail()
76 
77     @Before
78     fun setup() {
79         projectRoot().mkdirs()
80         buildFile = File(projectRoot(), "build.gradle")
81         buildFile.createNewFile()
82         // copy local.properties
83         val appToolkitProperties = File("../../app-toolkit/local.properties")
84         if (appToolkitProperties.exists()) {
85             appToolkitProperties.copyTo(File(projectRoot(), "local.properties"), overwrite = true)
86         } else {
87             File("../../local.properties").copyTo(
88                     File(projectRoot(), "local.properties"), overwrite = true)
89         }
90         val stream = PluginTest::class.java.classLoader.getResourceAsStream("sdk.prop")
91         val properties = Properties()
92         properties.load(stream)
93         compileSdkVersion = properties.getProperty("compileSdkVersion")
94         buildToolsVersion = properties.getProperty("buildToolsVersion")
95         testData("app-project").copyRecursively(projectRoot())
96     }
97 
setupSimpleBuildGradlenull98     private fun setupSimpleBuildGradle() {
99         buildFile.writeText("""
100             plugins {
101                 id('com.android.application')
102                 id('androidx.navigation.safeargs')
103             }
104 
105             android {
106                 compileSdkVersion $compileSdkVersion
107                 buildToolsVersion "$buildToolsVersion"
108             }
109         """.trimIndent())
110     }
111 
112     @Test
runGenerateTasknull113     fun runGenerateTask() {
114         buildFile.writeText("""
115             plugins {
116                 id('com.android.application')
117                 id('androidx.navigation.safeargs')
118             }
119 
120             android {
121                 compileSdkVersion $compileSdkVersion
122                 buildToolsVersion "$buildToolsVersion"
123                 flavorDimensions "mode"
124                 productFlavors {
125                     foo {
126                         dimension "mode"
127                         applicationIdSuffix ".foo"
128                     }
129                     notfoo {
130                         dimension "mode"
131                     }
132 
133                 }
134             }
135         """.trimIndent())
136 
137         runGradle("generateSafeArgsNotfooDebug", "generateSafeArgsFooDebug")
138                 .assertSuccessfulTask("generateSafeArgsNotfooDebug")
139                 .assertSuccessfulTask("generateSafeArgsFooDebug")
140 
141         assertGenerated("notfoo/debug/$NEXT_DIRECTIONS")
142         assertNotGenerated("foo/debug/$NEXT_DIRECTIONS")
143         assertGenerated("foo/debug/$FOO_DIRECTIONS")
144     }
145 
146     @Test
incrementalAddnull147     fun incrementalAdd() {
148         setupSimpleBuildGradle()
149         runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
150         val nextLastMod = assertGenerated("debug/$NEXT_DIRECTIONS").lastModified()
151 
152         testData("incremental-test-data/add_nav.xml").copyTo(navResource("add_nav.xml"))
153 
154         // lastModified has one second precision on certain platforms and jdk versions
155         // so sleep for a second
156         Thread.sleep(SEC)
157         runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
158         assertGenerated("debug/$ADDITIONAL_DIRECTIONS")
159         val newNextLastMod = assertGenerated("debug/$NEXT_DIRECTIONS").lastModified()
160         assertThat(newNextLastMod, `is`(nextLastMod))
161     }
162 
163     @Test
incrementalModifynull164     fun incrementalModify() {
165         setupSimpleBuildGradle()
166         testData("incremental-test-data/add_nav.xml").copyTo(navResource("add_nav.xml"))
167 
168         runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
169         val mainLastMod = assertGenerated("debug/$MAIN_DIRECTIONS").lastModified()
170         val additionalLastMod = assertGenerated("debug/$ADDITIONAL_DIRECTIONS").lastModified()
171         assertGenerated("debug/$NEXT_DIRECTIONS")
172 
173         testData("incremental-test-data/modified_nav.xml").copyTo(navResource("nav_test.xml"), true)
174 
175         // lastModified has one second precision on certain platforms and jdk versions
176         // so sleep for a second
177         Thread.sleep(SEC)
178         runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
179         val newMainLastMod = assertGenerated("debug/$MAIN_DIRECTIONS").lastModified()
180         // main directions were regenerated
181         assertThat(newMainLastMod, not(mainLastMod))
182 
183         // but additional directions weren't touched
184         val newAdditionalLastMod = assertGenerated("debug/$ADDITIONAL_DIRECTIONS").lastModified()
185         assertThat(newAdditionalLastMod, `is`(additionalLastMod))
186 
187         assertGenerated("debug/$MODIFIED_NEXT_DIRECTIONS")
188         assertNotGenerated("debug/$NEXT_DIRECTIONS")
189     }
190 
191     @Test
incrementalRemovenull192     fun incrementalRemove() {
193         setupSimpleBuildGradle()
194         testData("incremental-test-data/add_nav.xml").copyTo(navResource("add_nav.xml"))
195 
196         runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
197         val mainLastMod = assertGenerated("debug/$MAIN_DIRECTIONS").lastModified()
198         assertGenerated("debug/$ADDITIONAL_DIRECTIONS")
199 
200         val wasRemoved = navResource("add_nav.xml").delete()
201         assertThat(wasRemoved, `is`(true))
202 
203         // lastModified has one second precision on certain platforms and jdk versions
204         // so sleep for a second
205         Thread.sleep(SEC)
206         runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
207         val newMainLastMod = assertGenerated("debug/$MAIN_DIRECTIONS").lastModified()
208         // main directions weren't touched
209         assertThat(newMainLastMod, `is`(mainLastMod))
210 
211         // but additional directions are removed
212         assertNotGenerated("debug/$ADDITIONAL_DIRECTIONS")
213     }
214 
215     @Test
invalidModifynull216     fun invalidModify() {
217         setupSimpleBuildGradle()
218         testData("incremental-test-data/add_nav.xml").copyTo(navResource("add_nav.xml"))
219         runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
220         val step1MainLastMod = assertGenerated("debug/$MAIN_DIRECTIONS").lastModified()
221         val step1AdditionalLastMod = assertGenerated("debug/$ADDITIONAL_DIRECTIONS").lastModified()
222         assertGenerated("debug/$NEXT_DIRECTIONS")
223 
224         testData("invalid/failing_nav.xml").copyTo(navResource("nav_test.xml"), true)
225         Thread.sleep(SEC)
226         runAndFailGradle("generateSafeArgsDebug").assertFailingTask("generateSafeArgsDebug")
227         val step2MainLastMod = assertGenerated("debug/$MAIN_DIRECTIONS").lastModified()
228         // main directions were regenerated
229         assertThat(step2MainLastMod, not(step1MainLastMod))
230 
231         // but additional directions weren't touched
232         val step2AdditionalLastMod = assertGenerated("debug/$ADDITIONAL_DIRECTIONS").lastModified()
233         assertThat(step2AdditionalLastMod, `is`(step1AdditionalLastMod))
234 
235         val step2ModifiedTime = assertGenerated("debug/$MODIFIED_NEXT_DIRECTIONS").lastModified()
236         assertNotGenerated("debug/$NEXT_DIRECTIONS")
237 
238         testData("incremental-test-data/modified_nav.xml").copyTo(navResource("nav_test.xml"), true)
239         Thread.sleep(SEC)
240         runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
241 
242         // additional directions are touched because once task failed,
243         // gradle next time makes full run
244         val step3AdditionalLastMod = assertGenerated("debug/$ADDITIONAL_DIRECTIONS").lastModified()
245         assertThat(step3AdditionalLastMod, not(step2AdditionalLastMod))
246 
247         val step3ModifiedTime = assertGenerated("debug/$MODIFIED_NEXT_DIRECTIONS").lastModified()
248         assertThat(step2ModifiedTime, not(step3ModifiedTime))
249     }
250 }
251 
testDatanull252 private fun testData(name: String) = File("src/test/test-data", name)
253 
254 private fun BuildResult.assertSuccessfulTask(name: String): BuildResult {
255     assertThat(task(":$name")!!.outcome, `is`(TaskOutcome.SUCCESS))
256     return this
257 }
258 
BuildResultnull259 private fun BuildResult.assertFailingTask(name: String): BuildResult {
260     assertThat(task(":$name")!!.outcome, `is`(TaskOutcome.FAILED))
261     return this
262 }