1 /*
2  * Copyright (C) 2024 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 platform.test.screenshot.report
18 
19 import android.content.Context
20 import android.graphics.BitmapFactory
21 import android.graphics.Color
22 import androidx.test.ext.junit.runners.AndroidJUnit4
23 import androidx.test.platform.app.InstrumentationRegistry
24 import com.google.common.truth.Truth.assertThat
25 import java.io.File
26 import org.junit.After
27 import org.junit.Test
28 import org.junit.runner.RunWith
29 import platform.test.screenshot.GoldenPathManager
30 import platform.test.screenshot.PathConfig
31 import platform.test.screenshot.PathElementNoContext
32 import platform.test.screenshot.getDeviceOutputDirectory
33 import platform.test.screenshot.proto.ScreenshotResultProto.DiffResult
34 import platform.test.screenshot.report.OutputFileType.IMAGE_ACTUAL
35 import platform.test.screenshot.report.OutputFileType.IMAGE_DIFF
36 import platform.test.screenshot.report.OutputFileType.IMAGE_EXPECTED
37 import platform.test.screenshot.report.OutputFileType.RESULT_BIN_PROTO
38 import platform.test.screenshot.report.OutputFileType.RESULT_PROTO
39 import platform.test.screenshot.utils.createBitmap
40 
41 @RunWith(AndroidJUnit4::class)
42 class ScubaExportStrategyTest {
43 
44     private val testId = "test-id"
45     private val goldenId = "golden-id"
46 
47     // Helper to avoid repeating constant " testId, goldenId"
getPathOnDeviceFornull48     private fun ExportToScubaStrategy.getPathOnDeviceFor(fileType: OutputFileType) =
49         getPathOnDeviceFor(fileType, goldenId, testId)
50 
51     private val context: Context
52         get() = InstrumentationRegistry.getInstrumentation().context
53 
54     @After
55     fun cleanupFiles() {
56         // Make sure to clean-up the files, since all tests us the same testId/goldenId
57         File(getDeviceOutputDirectory(context)).deleteRecursively()
58     }
59 
60     @Test
getPathOnDeviceFor_emptyPathConfig_filenameIncludesPathElementnull61     fun getPathOnDeviceFor_emptyPathConfig_filenameIncludesPathElement() {
62         val subject = ExportToScubaStrategy(GoldenPathManager(context, pathConfig = PathConfig()))
63 
64         assertThat(subject.getPathOnDeviceFor(IMAGE_ACTUAL).name)
65             .isEqualTo("test-id_actual_golden-id.png")
66         assertThat(subject.getPathOnDeviceFor(IMAGE_EXPECTED).name)
67             .isEqualTo("test-id_expected_golden-id.png")
68         assertThat(subject.getPathOnDeviceFor(IMAGE_DIFF).name)
69             .isEqualTo("test-id_diff_golden-id.png")
70         assertThat(subject.getPathOnDeviceFor(RESULT_PROTO).name)
71             .isEqualTo("test-id_golden-id_goldResult.textproto")
72         assertThat(subject.getPathOnDeviceFor(RESULT_BIN_PROTO).name)
73             .isEqualTo("test-id_golden-id_goldResult.pb")
74     }
75 
76     @Test
getPathOnDeviceFor_directoryElement_filenameIncludesPathElementnull77     fun getPathOnDeviceFor_directoryElement_filenameIncludesPathElement() {
78         val pathConfig = PathConfig(PathElementNoContext("foo", isDir = true) { "bar" })
79         val subject = ExportToScubaStrategy(GoldenPathManager(context, pathConfig = pathConfig))
80 
81         assertThat(subject.getPathOnDeviceFor(IMAGE_ACTUAL).name)
82             .isEqualTo("test-id_actual_bar_golden-id.png")
83         assertThat(subject.getPathOnDeviceFor(IMAGE_EXPECTED).name)
84             .isEqualTo("test-id_expected_bar_golden-id.png")
85         assertThat(subject.getPathOnDeviceFor(IMAGE_DIFF).name)
86             .isEqualTo("test-id_diff_bar_golden-id.png")
87         assertThat(subject.getPathOnDeviceFor(RESULT_PROTO).name)
88             .isEqualTo("test-id_bar_golden-id_goldResult.textproto")
89         assertThat(subject.getPathOnDeviceFor(RESULT_BIN_PROTO).name)
90             .isEqualTo("test-id_bar_golden-id_goldResult.pb")
91     }
92 
93     @Test
getPathOnDeviceFor_nonDirectoryElement_filenameIncludesPathElementnull94     fun getPathOnDeviceFor_nonDirectoryElement_filenameIncludesPathElement() {
95         val pathConfig = PathConfig(PathElementNoContext("foo", isDir = false) { "bar" })
96         val subject = ExportToScubaStrategy(GoldenPathManager(context, pathConfig = pathConfig))
97 
98         assertThat(subject.getPathOnDeviceFor(IMAGE_ACTUAL).name)
99             .isEqualTo("test-id_actual_bar_golden-id.png")
100         assertThat(subject.getPathOnDeviceFor(IMAGE_EXPECTED).name)
101             .isEqualTo("test-id_expected_bar_golden-id.png")
102         assertThat(subject.getPathOnDeviceFor(IMAGE_DIFF).name)
103             .isEqualTo("test-id_diff_bar_golden-id.png")
104         assertThat(subject.getPathOnDeviceFor(RESULT_PROTO).name)
105             .isEqualTo("test-id_bar_golden-id_goldResult.textproto")
106         assertThat(subject.getPathOnDeviceFor(RESULT_BIN_PROTO).name)
107             .isEqualTo("test-id_bar_golden-id_goldResult.pb")
108     }
109 
110     @Test
getPathOnDeviceFor_multiplePathConfigs_filenameIncludesPathElementnull111     fun getPathOnDeviceFor_multiplePathConfigs_filenameIncludesPathElement() {
112         val pathConfig =
113             PathConfig(
114                 PathElementNoContext("foo", isDir = true) { "bar" },
115                 PathElementNoContext("one", isDir = false) { "two" }
116             )
117         val subject = ExportToScubaStrategy(GoldenPathManager(context, pathConfig = pathConfig))
118 
119         assertThat(subject.getPathOnDeviceFor(IMAGE_ACTUAL).name)
120             .isEqualTo("test-id_actual_bar_two_golden-id.png")
121         assertThat(subject.getPathOnDeviceFor(IMAGE_EXPECTED).name)
122             .isEqualTo("test-id_expected_bar_two_golden-id.png")
123         assertThat(subject.getPathOnDeviceFor(IMAGE_DIFF).name)
124             .isEqualTo("test-id_diff_bar_two_golden-id.png")
125         assertThat(subject.getPathOnDeviceFor(RESULT_PROTO).name)
126             .isEqualTo("test-id_bar_two_golden-id_goldResult.textproto")
127         assertThat(subject.getPathOnDeviceFor(RESULT_BIN_PROTO).name)
128             .isEqualTo("test-id_bar_two_golden-id_goldResult.pb")
129     }
130 
131     @Test
reportResult_withoutOptionalArgs_writesActualAndProtonull132     fun reportResult_withoutOptionalArgs_writesActualAndProto() {
133         val subject = ExportToScubaStrategy(GoldenPathManager(context))
134 
135         subject.reportResult(
136             testId,
137             goldenId,
138             status = DiffResult.Status.PASSED,
139             actual = createBitmap(Color.GREEN),
140         )
141 
142         assertThat(subject.getPathOnDeviceFor(IMAGE_ACTUAL).exists()).isTrue()
143         assertThat(subject.getPathOnDeviceFor(RESULT_BIN_PROTO).exists()).isTrue()
144         assertThat(subject.getPathOnDeviceFor(RESULT_PROTO).exists()).isTrue()
145         assertThat(subject.getPathOnDeviceFor(IMAGE_DIFF).exists()).isFalse()
146         assertThat(subject.getPathOnDeviceFor(IMAGE_EXPECTED).exists()).isFalse()
147     }
148 
149     @Test
reportResult_withDiff_writesDiffnull150     fun reportResult_withDiff_writesDiff() {
151         val subject = ExportToScubaStrategy(GoldenPathManager(context))
152 
153         subject.reportResult(
154             testId,
155             goldenId,
156             status = DiffResult.Status.PASSED,
157             actual = createBitmap(Color.GREEN),
158             diff = createBitmap(Color.YELLOW),
159         )
160 
161         assertThat(subject.getPathOnDeviceFor(IMAGE_ACTUAL).exists()).isTrue()
162         assertThat(subject.getPathOnDeviceFor(RESULT_BIN_PROTO).exists()).isTrue()
163         assertThat(subject.getPathOnDeviceFor(RESULT_PROTO).exists()).isTrue()
164         assertThat(subject.getPathOnDeviceFor(IMAGE_DIFF).exists()).isTrue()
165         assertThat(subject.getPathOnDeviceFor(IMAGE_EXPECTED).exists()).isFalse()
166     }
167 
168     @Test
reportResult_withExpected_writesExpectednull169     fun reportResult_withExpected_writesExpected() {
170         val subject = ExportToScubaStrategy(GoldenPathManager(context))
171 
172         subject.reportResult(
173             testId,
174             goldenId,
175             status = DiffResult.Status.PASSED,
176             actual = createBitmap(Color.GREEN),
177             expected = createBitmap(Color.RED),
178         )
179 
180         assertThat(subject.getPathOnDeviceFor(IMAGE_ACTUAL).exists()).isTrue()
181         assertThat(subject.getPathOnDeviceFor(RESULT_BIN_PROTO).exists()).isTrue()
182         assertThat(subject.getPathOnDeviceFor(RESULT_PROTO).exists()).isTrue()
183         assertThat(subject.getPathOnDeviceFor(IMAGE_DIFF).exists()).isFalse()
184         assertThat(subject.getPathOnDeviceFor(IMAGE_EXPECTED).exists()).isTrue()
185     }
186 
187     @Test
reportResult_passed_writesStatusnull188     fun reportResult_passed_writesStatus() {
189         val subject = ExportToScubaStrategy(GoldenPathManager(context))
190 
191         subject.reportResult(
192             testId,
193             goldenId,
194             status = DiffResult.Status.PASSED,
195             actual = createBitmap(Color.GREEN),
196         )
197 
198         assertThat(subject.getPathOnDeviceFor(RESULT_PROTO).readText()).contains("PASSED")
199     }
200 
201     @Test
reportResult_failed_writesStatusnull202     fun reportResult_failed_writesStatus() {
203         val subject = ExportToScubaStrategy(GoldenPathManager(context))
204 
205         subject.reportResult(
206             testId,
207             goldenId,
208             status = DiffResult.Status.FAILED,
209             actual = createBitmap(Color.GREEN),
210         )
211 
212         assertThat(subject.getPathOnDeviceFor(RESULT_PROTO).readText()).contains("FAILED")
213     }
214 
215     @Test
reportResult_writesCorrectImageContentsnull216     fun reportResult_writesCorrectImageContents() {
217         val subject = ExportToScubaStrategy(GoldenPathManager(context))
218 
219         subject.reportResult(
220             testId,
221             goldenId,
222             status = DiffResult.Status.PASSED,
223             actual = createBitmap(Color.RED),
224             diff = createBitmap(Color.GREEN),
225             expected = createBitmap(Color.BLUE),
226         )
227 
228         fun extractColorSample(fileType: OutputFileType): Color {
229             val path = subject.getPathOnDeviceFor(fileType).absolutePath
230             return BitmapFactory.decodeFile(path).getColor(0, 0)
231         }
232 
233         assertThat(extractColorSample(IMAGE_ACTUAL)).isEqualTo(Color.valueOf(Color.RED))
234         assertThat(extractColorSample(IMAGE_DIFF)).isEqualTo(Color.valueOf(Color.GREEN))
235         assertThat(extractColorSample(IMAGE_EXPECTED)).isEqualTo(Color.valueOf(Color.BLUE))
236     }
237 
238     @Test
reportResult_duplicateCall_doesNotOverwriteFilenull239     fun reportResult_duplicateCall_doesNotOverwriteFile() {
240         // Unsure why this is a desired behavior, but there was an explicit test for this before
241         // the refactoring, hence carrying it forward.
242         val subject = ExportToScubaStrategy(GoldenPathManager(context))
243 
244         subject.reportResult(
245             testId,
246             goldenId,
247             status = DiffResult.Status.PASSED,
248             actual = createBitmap(Color.RED),
249             diff = createBitmap(Color.GREEN),
250             expected = createBitmap(Color.BLUE),
251         )
252 
253         subject.reportResult(
254             testId,
255             goldenId,
256             status = DiffResult.Status.FAILED,
257             actual = createBitmap(Color.BLACK),
258             diff = createBitmap(Color.BLACK),
259             expected = createBitmap(Color.BLACK),
260         )
261 
262         fun extractColorSample(fileType: OutputFileType): Color {
263             val path = subject.getPathOnDeviceFor(fileType).absolutePath
264             return BitmapFactory.decodeFile(path).getColor(0, 0)
265         }
266 
267         assertThat(subject.getPathOnDeviceFor(RESULT_PROTO).readText()).contains("PASSED")
268         assertThat(extractColorSample(IMAGE_ACTUAL)).isEqualTo(Color.valueOf(Color.RED))
269         assertThat(extractColorSample(IMAGE_DIFF)).isEqualTo(Color.valueOf(Color.GREEN))
270         assertThat(extractColorSample(IMAGE_EXPECTED)).isEqualTo(Color.valueOf(Color.BLUE))
271     }
272 }
273