1 /*
<lambda>null2  * Copyright 2023 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.build.clang
18 
19 import androidx.testutils.assertThrows
20 import com.google.common.truth.Truth.assertThat
21 import com.google.common.truth.Truth.assertWithMessage
22 import java.io.File
23 import org.gradle.api.GradleException
24 import org.gradle.api.file.DirectoryProperty
25 import org.jetbrains.kotlin.konan.file.use
26 import org.jetbrains.kotlin.konan.target.Family
27 import org.jetbrains.kotlin.konan.target.KonanTarget
28 import org.junit.Before
29 import org.junit.Test
30 
31 class KonanBuildServiceTest : BaseClangTest() {
32     private lateinit var buildService: KonanBuildService
33 
34     @Before
35     fun initBuildService() {
36         buildService = KonanBuildService.obtain(project).get()
37     }
38 
39     @Test
40     fun compilationFailure() {
41         val compileParams =
42             createCompileParameters(
43                 "failedCode.c",
44                 """
45             #include <stdio.h>
46             int main() {
47                printf("Hello, World!");
48                return 0 // no ; :)
49             }
50         """
51                     .trimIndent()
52             )
53         assertThrows<GradleException> { buildService.compile(compileParams) }
54             .hasMessageThat()
55             .contains("expected ';' after return statement")
56     }
57 
58     @Test
59     fun compile() {
60         val compileParams = createCompileParameters("code.c", C_HELLO_WORLD)
61         buildService.compile(compileParams)
62         val outputFiles = compileParams.output.getRegularFiles()
63         assertThat(outputFiles).hasSize(1)
64         val outputFile = outputFiles.single()
65         assertThat(outputFile.name).isEqualTo("code.o")
66         val strings = extractStrings(outputFile)
67         assertThat(strings).contains("Hello, World!!")
68         // shouldn't link yet
69         assertThat(strings).doesNotContain("libc")
70     }
71 
72     @Test
73     fun compileWithInclude() {
74         val compileParameters =
75             createCompileParameters(
76                 "code.c",
77                 """
78             #include <stdio.h>
79             #include "dependency.h"
80             int my_function() {
81                return dependency_method();
82             }
83             """
84                     .trimIndent()
85             )
86         val dependency =
87             tmpFolder.newFolder("depSrc").also {
88                 it.resolve("dependency.h")
89                     .writeText(
90                         """
91                     int dependency_method();
92                 """
93                             .trimIndent()
94                     )
95             }
96         compileParameters.includes.from(dependency)
97         buildService.compile(compileParameters)
98         val outputFiles = compileParameters.output.getRegularFiles()
99         val strings = extractStrings(outputFiles.single())
100         assertThat(strings).contains("dependency_method")
101     }
102 
103     @Test
104     fun createSharedLibrary() {
105         val compileParameters = createCompileParameters("code.c", C_HELLO_WORLD)
106         buildService.compile(compileParameters)
107         val sharedLibraryParameters =
108             project.objects.newInstance(ClangSharedLibraryParameters::class.java)
109         sharedLibraryParameters.konanTarget.set(compileParameters.konanTarget)
110         sharedLibraryParameters.objectFiles.from(compileParameters.output)
111         val outputFile = tmpFolder.newFile("code.so")
112         sharedLibraryParameters.outputFile.set(outputFile)
113         buildService.createSharedLibrary(sharedLibraryParameters)
114 
115         val strings = extractStrings(outputFile)
116         assertThat(strings).contains("Hello, World!!")
117         // should link with libc
118         assertThat(strings).contains("libc")
119 
120         // verify shared lib files are aligned to 16Kb boundary for Android targets
121         if (sharedLibraryParameters.konanTarget.get().asKonanTarget.family == Family.ANDROID) {
122             val alignment =
123                 ProcessBuilder("objdump", "-p", outputFile.path)
124                     .start()
125                     .inputStream
126                     .bufferedReader()
127                     .useLines { lines ->
128                         lines
129                             .filter { it.contains("LOAD") }
130                             .map { it.split(" ").last() }
131                             .firstOrNull()
132                     }
133             assertThat(alignment).isEqualTo("2**14")
134         }
135     }
136 
137     @Test
138     fun archive() {
139         val compileParams = createCompileParameters("code.c", C_HELLO_WORLD)
140         buildService.compile(compileParams)
141         val archiveParams = project.objects.newInstance(ClangArchiveParameters::class.java)
142         archiveParams.konanTarget.set(compileParams.konanTarget)
143         archiveParams.objectFiles.from(compileParams.output)
144         val outputFile = tmpFolder.newFile("code.a")
145         archiveParams.outputFile.set(outputFile)
146         buildService.archiveLibrary(archiveParams)
147 
148         val strings = extractStrings(outputFile)
149         assertThat(strings).contains("Hello, World!!")
150         // should not with libc
151         assertThat(strings).doesNotContain("libc")
152     }
153 
154     private fun createCompileParameters(fileName: String, code: String): ClangCompileParameters {
155         val srcDir = tmpFolder.newFolder("src")
156         srcDir.resolve(fileName).writeText(code)
157         val compileParams = project.objects.newInstance(ClangCompileParameters::class.java)
158         compileParams.konanTarget.set(SerializableKonanTarget(KonanTarget.LINUX_X64))
159         compileParams.output.set(tmpFolder.newFolder())
160         compileParams.sources.from(srcDir)
161         return compileParams
162     }
163 
164     private fun DirectoryProperty.getRegularFiles() =
165         get().asFile.walkTopDown().filter { it.isFile }.toList()
166 
167     /**
168      * Extract strings from a binary file so that we can assert output contents.
169      *
170      * We used to use linux strings command here but it stopped working in CI. This implementation
171      * pretty much matches our strings usage and good enough for these tests.
172      * https://man7.org/linux/man-pages/man1/strings.1.html
173      */
174     private fun extractStrings(file: File): String {
175         assertWithMessage("Cannot extract strings from file").that(file.isFile).isTrue()
176         val finalString = StringBuilder()
177         val currentSection = StringBuilder()
178         fun finishSection() {
179             if (currentSection.length > 4) {
180                 finalString.appendLine(currentSection)
181             }
182             currentSection.setLength(0)
183         }
184         file.inputStream().buffered(1024).use { inputStream ->
185             var byte: Int
186             do {
187                 byte = inputStream.read()
188                 // if it is a printable string, add it to the list.
189                 if (byte in 32..127) {
190                     currentSection.append(byte.toChar())
191                 } else {
192                     // cleanup the remaining
193                     finishSection()
194                 }
195             } while (byte != -1)
196         }
197         // one final cleanup
198         finishSection()
199         return finalString.toString()
200     }
201 
202     companion object {
203         private val C_HELLO_WORLD =
204             """
205             #include <stdio.h>
206             int my_function() {
207                printf("Hello, World!!");
208                return 0;
209             }
210         """
211                 .trimIndent()
212     }
213 }
214