Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build tests once and copy to other platforms to run #650

Merged
merged 28 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 46 additions & 19 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,57 +13,70 @@ env:
GRADLE_OPTS: "-Dkotlin.incremental=false -Dorg.gradle.logging.stacktrace=full"

jobs:
zig:
terminal:
runs-on: macos-15
steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version-file: .github/.java-version

- uses: goto-bus-stop/setup-zig@v2
with:
version: 0.13.0

- run: zig build -p src/jvmMain/resources/jni
working-directory: mosaic-terminal

- run: ./gradlew :mosaic-terminal:zipJvmTestDistribution :mosaic-terminal:linkNativeDebugTests

- uses: actions/upload-artifact@v4
with:
name: jni-binaries
path: mosaic-terminal/src/jvmMain/resources/jni
if-no-files-found: error

- uses: actions/upload-artifact@v4
with:
name: mosaic-terminal-jvm-tests
path: mosaic-terminal/build/dist/mosaic-terminal-jvm-*-tests.zip
if-no-files-found: error

- uses: actions/upload-artifact@v4
with:
name: mosaic-terminal-native-tests
path: mosaic-terminal/build/bin
if-no-files-found: error

tests:
needs:
- zig
- terminal
strategy:
fail-fast: false
matrix:
platform:
- os: macOS-13
task: macosX64Test
target: macosX64
- os: macOS-14
task: macosArm64Test
target: macosArm64
- os: macOS-15
task: macosArm64Test
target: macosArm64
- os: ubuntu-20.04
task: linuxX64Test
target: linuxX64
- os: ubuntu-22.04
task: linuxX64Test
target: linuxX64
- os: ubuntu-24.04
task: linuxX64Test
target: linuxX64
- os: windows-2019
task: mingwX64Test
target: mingwX64
- os: windows-2022
task: mingwX64Test
target: mingwX64

runs-on: ${{ matrix.platform.os }}

steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: jni-binaries
path: mosaic-terminal/src/jvmMain/resources/jni

- uses: actions/setup-java@v4
with:
distribution: 'zulu'
Expand All @@ -77,12 +90,26 @@ jobs:
distribution: 'zulu'
java-version-file: .github/.java-version

- run: ./gradlew --continue allJvmTests ${{ matrix.platform.task }}
- uses: actions/download-artifact@v4
with:
name: mosaic-terminal-jvm-tests

- uses: actions/download-artifact@v4
with:
name: mosaic-terminal-native-tests

- run: ls -lhAR
- run: mosaic-terminal-native-tests/${{ matrix.platform.target }}/debugTest/test.*
- run: mosaic-terminal-jvm-tests/bin/mosaic-terminal-test
# TODO Run JVM 8 tests
# TODO Run JVM 11 tests
# TODO Run JVM 17 tests
# TODO Run JVM 21 tests

binaries:
runs-on: macos-15
needs:
- zig
- terminal
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
Expand All @@ -101,7 +128,7 @@ jobs:
checks:
runs-on: macos-15
needs:
- zig
- terminal
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
Expand Down
46 changes: 46 additions & 0 deletions build-support/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile

buildscript {
dependencies {
classpath libs.kotlin.plugin.core
}
repositories {
mavenCentral()
}
}

apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'java-gradle-plugin'

gradlePlugin {
plugins {
mosaicBuild {
id = 'com.jakewharton.mosaic.build'
displayName = "Mosaic Build plugin"
description = "Gradle plugin for Mosaic build things"
implementationClass = "com.jakewharton.mosaic.buildsupport.MosaicBuildPlugin"
}
}
}

dependencies {
compileOnly(libs.kotlin.plugin.core)
}

kotlin {
explicitApi()
}

repositories {
mavenCentral()
}

tasks.withType(JavaCompile).configureEach {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

tasks.withType(KotlinJvmCompile).configureEach {
compilerOptions.jvmTarget = JvmTarget.JVM_1_8
}
9 changes: 9 additions & 0 deletions build-support/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
rootProject.name = 'build-support'

dependencyResolutionManagement {
versionCatalogs {
libs {
from(files('../gradle/libs.versions.toml'))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.jakewharton.mosaic.buildsupport

public interface MosaicBuildExtension {
public fun jvmTestDistribution()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package com.jakewharton.mosaic.buildsupport

import java.io.File
import org.gradle.api.Project
import org.gradle.api.file.FileTree
import org.gradle.api.internal.file.FileCollectionFactory
import org.gradle.api.internal.tasks.testing.TestClassProcessor
import org.gradle.api.internal.tasks.testing.TestClassRunInfo
import org.gradle.api.internal.tasks.testing.TestResultProcessor
import org.gradle.api.internal.tasks.testing.detection.ClassFileExtractionManager
import org.gradle.api.internal.tasks.testing.detection.DefaultTestClassScanner
import org.gradle.api.internal.tasks.testing.junit.JUnitDetector
import org.gradle.api.plugins.BasePluginExtension
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.application.CreateStartScripts
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.bundling.Zip
import org.gradle.internal.Factory
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.TEST_COMPILATION_NAME
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType

internal class MosaicBuildExtensionImpl(
private val project: Project,
) : MosaicBuildExtension {
override fun jvmTestDistribution() {
var gotMpp = false
project.afterEvaluate {
check(gotMpp) {
"JVM test distribution requires the Kotlin multiplatform plugin"
}
}
project.pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") {
gotMpp = true

val gradleSupport: GradleSupport = Gradle_8_10_Support()

val base = project.extensions.getByType(BasePluginExtension::class.java)
val kotlin = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
kotlin.targets.configureEach { target ->
if (target.platformType != KotlinPlatformType.jvm) return@configureEach

val name = target.name + "Test"
val nameUpper = name.replaceFirstChar(Char::uppercase)

val mainJarProvider = project.tasks.named(target.artifactsTaskName)

val testCompilation = target.compilations.named(TEST_COMPILATION_NAME)
val testClassesProvider = testCompilation.map { it.output.allOutputs }
val testDependenciesProvider = testCompilation.map {
it.runtimeDependencyFiles?.filter { it.isFile }
?: FileCollectionFactory.empty()
}

val testJarProvider = project.tasks.register("jar$nameUpper", Jar::class.java) {
it.from(testClassesProvider)
it.archiveAppendix.set(target.name)
it.archiveClassifier.set("tests")
}

val testScriptsProvider = project.tasks.register("scripts$nameUpper", CreateStartScripts::class.java) {
it.outputDir = project.layout.buildDirectory.dir("scripts/$name").get().asFile
it.applicationName = base.archivesName.get() + "-test"

// The classpath property is not lazy, so we need explicit dependencies here.
it.dependsOn(mainJarProvider)
it.dependsOn(testJarProvider)
it.dependsOn(testDependenciesProvider)
// However, this 'plus' result will be live, and can still be set at configuration time.
val classpath = mainJarProvider.get().outputs.files
.plus(testJarProvider.get().outputs.files)
.plus(testDependenciesProvider.get())
it.classpath = classpath

it.mainClass.set(
testClassesProvider.zip(testDependenciesProvider) { testClasses, testDependencies ->
val testFqcns = gradleSupport.detectTestClassNames(
testClasses.asFileTree,
testClasses.files.toList(),
testDependencies.files.toList()
)
"org.junit.runner.JUnitCore ${testFqcns.joinToString(" ") { """"$it"""" }}"
}
)
}

val installProvider = project.tasks.register("install${nameUpper}Distribution", Copy::class.java) {
it.group = "distribution"
it.description = "Installs $name as a distribution as-is."

it.into("bin") {
it.from(testScriptsProvider)
}
it.into("lib") {
it.from(testJarProvider)
it.from(mainJarProvider)
it.from(testDependenciesProvider)
}
it.destinationDir = project.layout.buildDirectory.dir("install/$name").get().asFile
}

project.tasks.register("zip${nameUpper}Distribution", Zip::class.java) {
it.group = "distribution"
it.description = "Bundles $name as a distribution."

it.from(installProvider)
it.destinationDirectory.set(project.layout.buildDirectory.dir("dist"))
it.archiveAppendix.set(target.name)
it.archiveClassifier.set("tests")
}

// TODO add to archives?
}
}
}

interface GradleSupport {
fun detectTestClassNames(
testClasses: FileTree,
testClassDirectories: List<File>,
testClasspath: List<File>,
): List<String>
}

class Gradle_8_10_Support : GradleSupport {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're on Gradle 8.12 now, but when I originally wrote this it worked on Gradle 8.10...

override fun detectTestClassNames(
testClasses: FileTree,
testClassDirectories: List<File>,
testClasspath: List<File>,
): List<String> {
val detector = JUnitDetector(ClassFileExtractionManager(object : Factory<File> {
override fun create() = File.createTempFile("gradle", "test-class-detection").apply {
deleteOnExit()
}
}))
detector.setTestClasses(testClassDirectories)
detector.setTestClasspath(testClasspath)

val testFqcns = mutableListOf<String>()
val testClassProcessor = object : TestClassProcessor {
override fun processTestClass(testClass: TestClassRunInfo) {
testFqcns += testClass.testClassName
}
override fun startProcessing(resultProcessor: TestResultProcessor) {}
override fun stop() {}
override fun stopNow() {}
}

DefaultTestClassScanner(testClasses, detector, testClassProcessor).run()

return testFqcns
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.jakewharton.mosaic.buildsupport

import org.gradle.api.Plugin
import org.gradle.api.Project

@Suppress("unused") // Invoked reflectively by Gradle.
public class MosaicBuildPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.extensions.add(
MosaicBuildExtension::class.java,
"mosaicBuild",
MosaicBuildExtensionImpl(target),
)
}
}

1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ buildscript {
classpath libs.spotless.gradlePlugin
classpath libs.binary.compatibility.validator.gradlePlugin
classpath libs.cklib.gradlePlugin
classpath 'com.jakewharton.mosaic.build:gradle-plugin'
}
repositories {
mavenCentral()
Expand Down
10 changes: 10 additions & 0 deletions mosaic-terminal/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ apply from: "$rootDir/publish.gradle"
apply plugin: 'co.touchlab.cklib'
apply plugin: 'com.jakewharton.cite'
apply plugin: 'dev.drewhamilton.poko'
apply plugin: 'com.jakewharton.mosaic.build'

mosaicBuild {
jvmTestDistribution()
}

configurations {
proGuard
Expand Down Expand Up @@ -135,13 +140,18 @@ kotlin {
}
}

def linkNativeDebugTests = tasks.register('linkNativeDebugTests')

targets.withType(KotlinNativeTarget).configureEach {
compilations.main.cinterops {
create('mosaic') {
header(file('src/c/mosaic.h'))
packageName('com.jakewharton.mosaic.terminal')
}
}
linkNativeDebugTests.configure {
it.dependsOn(compilations.test.binariesTaskName)
}
}

compilerOptions.freeCompilerArgs.add('-Xexpect-actual-classes')
Expand Down
Loading
Loading