|
| 1 | +package python.multiplatform.gradle |
| 2 | + |
| 3 | +import org.gradle.api.GradleException |
| 4 | +import org.gradle.testfixtures.ProjectBuilder |
| 5 | +import java.io.File |
| 6 | +import kotlin.test.Test |
| 7 | +import kotlin.test.assertEquals |
| 8 | +import kotlin.test.assertFailsWith |
| 9 | +import kotlin.test.assertFalse |
| 10 | +import kotlin.test.assertNotEquals |
| 11 | +import kotlin.test.assertTrue |
| 12 | + |
| 13 | +/** Issue #46: per-target CPython include directories as public providers. */ |
| 14 | +class CPythonIncludeDirectoriesTest { |
| 15 | + |
| 16 | + private val version = "3.14.7" |
| 17 | + |
| 18 | + @Test |
| 19 | + fun layoutMatchesTheObservedAcquisitionTree() { |
| 20 | + val l = CPythonIncludeLayout |
| 21 | + assertEquals("macos-aarch64/python/include/python3.14", l.relativePath("macos-aarch64", CPythonFlavour.GIL, version)) |
| 22 | + assertEquals("linux-x86_64-freethreaded/python/include/python3.14t", l.relativePath("linux-x86_64", CPythonFlavour.FREE_THREADED, version)) |
| 23 | + assertEquals("windows-x86_64/python/include", l.relativePath("windows-x86_64", CPythonFlavour.GIL, version)) |
| 24 | + assertEquals("windows-x86_64-freethreaded/python/include", l.relativePath("windows-x86_64", CPythonFlavour.FREE_THREADED, version)) |
| 25 | + assertEquals("android-aarch64/prefix/include/python3.14", l.relativePath("android-aarch64", CPythonFlavour.GIL, version)) |
| 26 | + assertEquals("ios/Python.xcframework/ios-arm64/include/python3.14", l.relativePath("ios-arm64", CPythonFlavour.GIL, version)) |
| 27 | + } |
| 28 | + |
| 29 | + @Test |
| 30 | + fun freeThreadedIsDistinctFromDefaultOnEveryDesktopTarget() { |
| 31 | + for (t in CPythonIncludeLayout.desktopTargets) { |
| 32 | + assertNotEquals( |
| 33 | + CPythonIncludeLayout.relativePath(t, CPythonFlavour.GIL, version), |
| 34 | + CPythonIncludeLayout.relativePath(t, CPythonFlavour.FREE_THREADED, version), |
| 35 | + ) |
| 36 | + } |
| 37 | + } |
| 38 | + |
| 39 | + @Test |
| 40 | + fun freeThreadedDoesNotExistOffDesktop() { |
| 41 | + assertFalse(CPythonIncludeLayout.supports("android-aarch64", CPythonFlavour.FREE_THREADED)) |
| 42 | + assertFalse(CPythonIncludeLayout.supports("ios-arm64", CPythonFlavour.FREE_THREADED)) |
| 43 | + assertFailsWith<IllegalArgumentException> { |
| 44 | + CPythonIncludeLayout.relativePath("ios-arm64", CPythonFlavour.FREE_THREADED, version) |
| 45 | + } |
| 46 | + } |
| 47 | + |
| 48 | + @Test |
| 49 | + fun downloadTaskNamesMatchTheRootBuild() { |
| 50 | + assertEquals("downloadPython_macos_aarch64", CPythonIncludeLayout.downloadTaskName("macos-aarch64")) |
| 51 | + assertEquals("downloadPython_android_x86_64", CPythonIncludeLayout.downloadTaskName("android-x86_64")) |
| 52 | + assertEquals("downloadPython_ios", CPythonIncludeLayout.downloadTaskName("ios-arm64_x86_64-simulator")) |
| 53 | + } |
| 54 | + |
| 55 | + /** Registry over fake extraction tasks that really create `Python.h`; only [configured] desktop flavour is extracted. */ |
| 56 | + private fun fixture(configured: CPythonFlavour): Pair<org.gradle.api.Project, CPythonIncludeDirectories> { |
| 57 | + val project = ProjectBuilder.builder().build() |
| 58 | + val root = project.layout.projectDirectory.dir("extracted/$version") |
| 59 | + val tasks = CPythonIncludeLayout.allTargets.associateWith { target -> |
| 60 | + // Android and iOS are always GIL; desktop follows the configured flavour. |
| 61 | + val flavour = if (target in CPythonIncludeLayout.desktopTargets) configured else CPythonFlavour.GIL |
| 62 | + project.tasks.register("fake_${target.replace('-', '_')}") { |
| 63 | + doLast { |
| 64 | + root.dir(CPythonIncludeLayout.relativePath(target, flavour, version)).asFile |
| 65 | + .also { it.mkdirs() }.resolve("Python.h").writeText("/* $target */") |
| 66 | + } |
| 67 | + } |
| 68 | + } |
| 69 | + val dirs = CPythonIncludeDirectories(root, version) { target, flavour -> |
| 70 | + val extracted = if (target in CPythonIncludeLayout.desktopTargets) configured else CPythonFlavour.GIL |
| 71 | + if (flavour == extracted) tasks[target] else null |
| 72 | + } |
| 73 | + return project to dirs |
| 74 | + } |
| 75 | + |
| 76 | + @Test |
| 77 | + fun everyTargetResolvesToADistinctDirectoryHoldingPythonH() { |
| 78 | + val (project, dirs) = fixture(CPythonFlavour.GIL) |
| 79 | + val seen = mutableSetOf<File>() |
| 80 | + for (t in dirs.targets) { |
| 81 | + val producer = project.tasks.getByName("fake_${t.replace('-', '_')}") |
| 82 | + val provider = dirs.includeDir(t) |
| 83 | + // A consumer that takes the provider as an input must depend on the extraction task. |
| 84 | + val consumer = project.tasks.register("use_${t.replace('-', '_')}") { inputs.dir(provider) }.get() |
| 85 | + assertTrue( |
| 86 | + producer in consumer.taskDependencies.getDependencies(consumer), |
| 87 | + "$t: provider does not carry its extraction task", |
| 88 | + ) |
| 89 | + producer.actions.forEach { it.execute(producer) } |
| 90 | + val dir = provider.get().asFile |
| 91 | + assertTrue(File(dir, "Python.h").isFile, "$t: Python.h missing in $dir") |
| 92 | + assertTrue(seen.add(dir), "$t: duplicate directory $dir") |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + @Test |
| 97 | + fun theOtherFlavourFailsLoudlyNamingTheProperty() { |
| 98 | + val (_, dirs) = fixture(CPythonFlavour.GIL) |
| 99 | + assertFalse(dirs.isAvailable("macos-aarch64", CPythonFlavour.FREE_THREADED)) |
| 100 | + val e = assertFailsWith<GradleException> { dirs.includeDir("macos-aarch64", CPythonFlavour.FREE_THREADED) } |
| 101 | + assertTrue("pythonFreeThreaded" in e.message.orEmpty()) |
| 102 | + assertFailsWith<GradleException> { dirs.includeDir("ios-arm64", CPythonFlavour.FREE_THREADED) } |
| 103 | + } |
| 104 | + |
| 105 | + @Test |
| 106 | + fun configuredFreeThreadedExposesFreeThreadedKey() { |
| 107 | + val (_, dirs) = fixture(CPythonFlavour.FREE_THREADED) |
| 108 | + assertTrue(dirs.isAvailable("linux-x86_64", CPythonFlavour.FREE_THREADED)) |
| 109 | + assertTrue(dirs.includeDir("linux-x86_64", CPythonFlavour.FREE_THREADED).get().asFile.path.endsWith("python3.14t")) |
| 110 | + } |
| 111 | +} |
0 commit comments