Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/sources/test
diff options
context:
space:
mode:
authorLeitwolf2026-11-12 22:36:11 +0300
committerGitHub2026-11-12 22:36:11 +0300
commit86214c96285f73d352f934b0da33d231150898f0 (patch)
treeaffc0bfef110459bc8fcf3e8f197ed95cc46f6bb /sources/test
parent95cdafcf1aa01b5d32171c56e6a94137402643b9 (diff)
parent7cb809c6560fc4ed29f41e4e4199cd91af6cbed1 (diff)
downloadtazel-main.tar.gz
Merge pull request #5 from Progmasoft/feature/build-c-familyHEADmain
Build and run C, C++, Objective-C, Objective-C++ and Zig targets
Diffstat (limited to 'sources/test')
-rw-r--r--sources/test/kotlin/com/progmasoft/tazel/cli/BuildCommandTest.kt165
-rw-r--r--sources/test/kotlin/com/progmasoft/tazel/driver/DriverTest.kt113
2 files changed, 278 insertions, 0 deletions
diff --git a/sources/test/kotlin/com/progmasoft/tazel/cli/BuildCommandTest.kt b/sources/test/kotlin/com/progmasoft/tazel/cli/BuildCommandTest.kt
new file mode 100644
index 0000000..36e1323
--- /dev/null
+++ b/sources/test/kotlin/com/progmasoft/tazel/cli/BuildCommandTest.kt
@@ -0,0 +1,165 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+ * SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
+ */
+
+package com.progmasoft.tazel.cli
+
+import com.progmasoft.tazel.driver.GatheredRun
+import com.progmasoft.tazel.driver.Processes
+import com.progmasoft.tazel.driver.Programs
+import com.progmasoft.tazel.driver.SystemProcesses
+import com.progmasoft.tazel.dsl.TargetTriple
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.test.Test
+import kotlin.test.assertContains
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+/**
+ * Builds and runs a small module with the C compiler of this machine. Without Clang or GCC on PATH
+ * there is nothing to build with, and the tests return early.
+ */
+class BuildCommandTest {
+ private val os = TargetTriple.host().os
+ private val programs = Programs(os)
+ private val compiler = programs.find("clang") ?: programs.find("gcc")
+
+ /** Real processes; what a program `tazel run` starts prints is kept instead of shown. */
+ private class Recording : Processes by SystemProcesses {
+ var printed = ""
+
+ override fun runWith(
+ command: List<String>,
+ directory: Path,
+ environment: Map<String, String>,
+ ): Int {
+ val result: GatheredRun = SystemProcesses.runGatheredWith(command, directory, environment)
+ printed += result.output
+ return result.exitCode
+ }
+ }
+
+ private fun module(): Path {
+ val root = Files.createTempDirectory("tazel-build-").toRealPath()
+ fun write(path: String, text: String) {
+ val file = root.resolve(path)
+ Files.createDirectories(file.parent)
+ Files.writeString(file, text.trimIndent() + "\n")
+ }
+ write("MODULE.kts", """module { name = "sample" }""")
+ write(
+ "core/BUILD.kts",
+ """
+ isPublic = true
+ c.staticlib("core") {
+ sources(glob("src/*.c"))
+ headers("include")
+ stripIncludePrefix = "include"
+ includePrefix = "sample"
+ defines("SAMPLE_OFFSET=1")
+ }
+ """,
+ )
+ write("core/include/core.h", "int sample_add(int a, int b);")
+ write(
+ "core/src/core.c",
+ "#include <sample/core.h>\nint sample_add(int a, int b) { return a + b + SAMPLE_OFFSET - 1; }",
+ )
+ write(
+ "app/BUILD.kts",
+ """
+ c.bin("app") {
+ sources("main.c")
+ depends("//core/:core")
+ args("--given")
+ env("SAMPLE_GREETING" to "hi")
+ }
+ """,
+ )
+ write(
+ "app/main.c",
+ """
+ #include <stdio.h>
+ #include <stdlib.h>
+ #include <sample/core.h>
+ int main(int argc, char **argv) {
+ printf("%s %d", getenv("SAMPLE_GREETING"), sample_add(2, 3));
+ for (int i = 1; i < argc; ++i) printf(" %s", argv[i]);
+ printf("\n");
+ return 0;
+ }
+ """,
+ )
+ return root
+ }
+
+ private fun tazel(
+ root: Path,
+ directory: String,
+ processes: Processes,
+ vararg arguments: String,
+ ): Pair<Int, Recorder> {
+ val recorder = Recorder()
+ val code =
+ run(
+ arguments.toList(),
+ recorder.out,
+ recorder.err,
+ Environment(root.resolve(directory), os, programs, processes),
+ )
+ return code to recorder
+ }
+
+ @Test
+ fun buildsRunsAndRebuildsOnlyWhatChanged() {
+ if (compiler == null) return
+ val root = module()
+ val processes = Recording()
+
+ val (first, firstOutput) = tazel(root, ".", processes, "build", "//...")
+ assertEquals(ExitCode.SUCCESS, first, firstOutput.stderr() + firstOutput.stdout())
+ assertContains(firstOutput.stdout(), "4 command(s) run")
+
+ val (again, againOutput) = tazel(root, ".", processes, "build", "//...")
+ assertEquals(ExitCode.SUCCESS, again, againOutput.stderr())
+ assertContains(againOutput.stdout(), "0 command(s) run")
+
+ // A header change recompiles what includes it, and relinks.
+ val header = root.resolve("core/include/core.h")
+ Files.writeString(header, "int sample_add(int a, int b); /* changed */\n")
+ Files.setLastModifiedTime(
+ header,
+ java.nio.file.attribute.FileTime.fromMillis(System.currentTimeMillis() + 5_000),
+ )
+ val (changed, changedOutput) = tazel(root, ".", processes, "build", "//...")
+ assertEquals(ExitCode.SUCCESS, changed, changedOutput.stderr())
+ assertContains(changedOutput.stdout(), "4 command(s) run")
+
+ val (ran, ranOutput) = tazel(root, "app", processes, "run", "--", "extra")
+ assertEquals(ExitCode.SUCCESS, ran, ranOutput.stderr())
+ assertEquals("hi 5 --given extra", processes.printed.trim())
+ }
+
+ @Test
+ fun aCompilerErrorFailsTheBuildWithItsMessage() {
+ if (compiler == null) return
+ val root = module()
+ Files.writeString(root.resolve("app/main.c"), "int main(void) { return undeclared; }\n")
+ val (code, output) = tazel(root, ".", Recording(), "build", "//app/:app")
+ assertEquals(ExitCode.FAILURE, code)
+ assertContains(output.stdout(), "undeclared")
+ assertContains(output.stderr(), "//app/:app")
+ }
+
+ @Test
+ fun commandLineMistakesAreUsageErrors() {
+ val root = module()
+ assertEquals(ExitCode.USAGE, tazel(root, ".", Recording(), "build", "--fast").first)
+ assertEquals(ExitCode.USAGE, tazel(root, ".", Recording(), "build", "//core").first)
+ val (code, output) = tazel(root, ".", Recording(), "run", "//core/:core")
+ assertEquals(ExitCode.FAILURE, code)
+ assertTrue("no bin target" in output.stderr(), output.stderr())
+ }
+}
diff --git a/sources/test/kotlin/com/progmasoft/tazel/driver/DriverTest.kt b/sources/test/kotlin/com/progmasoft/tazel/driver/DriverTest.kt
new file mode 100644
index 0000000..5f7d831
--- /dev/null
+++ b/sources/test/kotlin/com/progmasoft/tazel/driver/DriverTest.kt
@@ -0,0 +1,113 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+ * SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
+ */
+
+package com.progmasoft.tazel.driver
+
+import com.progmasoft.tazel.dsl.FilePath
+import com.progmasoft.tazel.dsl.Glob
+import com.progmasoft.tazel.dsl.ProjectException
+import com.progmasoft.tazel.dsl.TargetTriple
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.attribute.FileTime
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class DriverTest {
+ @Test
+ fun depfilesAreReadWithTheirEscapes() {
+ val text = "obj/a.c.o: src/a.c include/a\\ b.h \\\n include/c.h \\\r\n /usr/include/stdio.h\n"
+ assertEquals(
+ listOf("src/a.c", "include/a b.h", "include/c.h", "/usr/include/stdio.h"),
+ depfileInputs(text),
+ )
+ assertEquals(listOf("C:\\src\\a.c"), depfileInputs("C:\\out\\a.obj: C:\\src\\a.c\n"))
+ }
+
+ @Test
+ fun anOutputIsCurrentUntilItsCommandOrAnInputChanges() {
+ val directory = Files.createTempDirectory("tazel-up-to-date")
+ val input = Files.writeString(directory.resolve("a.c"), "int a;")
+ val output = Files.writeString(directory.resolve("a.o"), "object")
+ Files.setLastModifiedTime(input, FileTime.fromMillis(1_000_000))
+ Files.setLastModifiedTime(output, FileTime.fromMillis(2_000_000))
+ val command = listOf("cc", "-c", "a.c")
+ assertFalse(UpToDate.check(output, command, listOf(input)), "never recorded")
+ UpToDate.record(output, command)
+ assertTrue(UpToDate.check(output, command, listOf(input)))
+ assertFalse(UpToDate.check(output, command + "-O2", listOf(input)), "another command")
+ Files.setLastModifiedTime(input, FileTime.fromMillis(3_000_000))
+ assertFalse(UpToDate.check(output, command, listOf(input)), "a newer input")
+ Files.setLastModifiedTime(input, FileTime.fromMillis(1_000_000))
+ assertFalse(
+ UpToDate.check(output, command, listOf(directory.resolve("gone.h"))),
+ "a missing input",
+ )
+ UpToDate.forget(output)
+ assertFalse(UpToDate.check(output, command, listOf(input)), "forgotten")
+ }
+
+ @Test
+ fun globsTakeFilesBelowThePackageAndExcludesRemoveThem() {
+ val root = Files.createTempDirectory("tazel-globs")
+ for (file in
+ listOf("src/a.c", "src/deep/b.c", "src/experimental/x.c", "src/a.h", "other/c.c", "top.c")) {
+ Files.createDirectories(root.resolve(file).parent)
+ Files.writeString(root.resolve(file), "")
+ }
+ val sets = FileSets(ignoreCase = false)
+ fun names(files: List<Path>) = files.map { root.relativize(it).toString().replace('\\', '/') }
+ assertEquals(
+ listOf("src/a.c", "src/deep/b.c", "src/experimental/x.c"),
+ names(sets.expand(root, listOf(Glob(listOf("src/**/*.c"))))),
+ )
+ assertEquals(
+ listOf("src/a.c", "src/deep/b.c"),
+ names(
+ sets.expand(
+ root,
+ listOf(Glob(listOf("src/**/*.c"))),
+ listOf(Glob(listOf("src/experimental"))),
+ )
+ ),
+ )
+ assertEquals(listOf("src/a.c"), names(sets.expand(root, listOf(Glob(listOf("src/*.c"))))))
+ assertEquals(
+ listOf("top.c", "other/c.c"),
+ names(sets.expand(root, listOf(FilePath("top.c"), Glob(listOf("other/?.c"))))),
+ )
+ val sub = root.resolve("src/deep")
+ assertEquals(listOf("other/c.c"), names(sets.expand(sub, listOf(FilePath("../../other/c.c")))))
+ assertFailsWith<ProjectException> { sets.expand(root, listOf(FilePath("missing.c"))) }
+ assertEquals(emptyList(), sets.expand(root, listOf(Glob(listOf("nowhere/*.c")))))
+ }
+
+ @Test
+ fun zigSpellsTriplesItsOwnWay() {
+ fun zig(text: String) = zigTriple(TargetTriple.parse(text))
+ assertEquals("x86_64-linux-gnu", zig("x86_64-unknown-linux-gnu"))
+ assertEquals("aarch64-macos", zig("aarch64-apple-darwin"))
+ assertEquals("aarch64-macos", zig("arm64-apple-macosx14.0"))
+ assertEquals("x86_64-windows-msvc", zig("x86_64-pc-windows-msvc"))
+ assertEquals("x86_64-freebsd", zig("x86_64-unknown-freebsd14"))
+ }
+
+ @Test
+ fun outputNamesFollowTheSystemOfTheTriple() {
+ val linux = TargetTriple.parse("x86_64-unknown-linux-gnu").os
+ val windows = TargetTriple.parse("x86_64-pc-windows-msvc").os
+ val mac = TargetTriple.parse("aarch64-apple-darwin").os
+ assertEquals("libcore.a", OutputNames.staticLibrary("core", linux))
+ assertEquals("core.lib", OutputNames.staticLibrary("core", windows))
+ assertEquals("libcore.so.1", OutputNames.sharedLibrary("core", linux, "1"))
+ assertEquals("libcore.dylib", OutputNames.sharedLibrary("core", mac, null))
+ assertEquals("core.dll", OutputNames.sharedLibrary("core", windows, "1"))
+ assertEquals("app.exe", OutputNames.executable("app", windows))
+ assertEquals("app", OutputNames.executable("app", mac))
+ }
+}