diff --git a/build.gradle.kts b/build.gradle.kts index b238d85..aa161c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,12 +28,17 @@ kotlin { } } +configure { + nodeVersion = "12.18.3" +} + val String.v: String get() = rootProject.extra["$this.version"] as String dependencies { + implementation(project(":lib")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${"kotlinx-coroutines".v}") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:${"kotlinx-serialization".v}") - implementation(project(":lib")) + implementation("org.jetbrains:kotlin-extensions:${"kotlin-wrappers".v}-kotlin-${"kotlin".v}") testImplementation(kotlin("test-js")) } diff --git a/gradle.properties b/gradle.properties index d905527..2165e89 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,3 +8,4 @@ kotlin.code.style=official kotlinx-coroutines.version=1.3.8-1.4.0-rc kotlinx-serialization.version=1.0-M1-1.4.0-rc +kotlin-wrappers.version=1.0.1-pre.110 diff --git a/src/main/kotlin/com/github/burrunan/gradle/FsExtensions.kt b/src/main/kotlin/com/github/burrunan/gradle/FsExtensions.kt index cdb7a84..d591f08 100644 --- a/src/main/kotlin/com/github/burrunan/gradle/FsExtensions.kt +++ b/src/main/kotlin/com/github/burrunan/gradle/FsExtensions.kt @@ -39,6 +39,12 @@ suspend fun removeFiles(files: List) { } } +suspend fun mkdir(path: String) { + if (!exists(path)) { + fs2.promises.mkdir(path) + } +} + suspend fun exists(path: String) = suspendCoroutine { cont -> fs.exists(path.normalizedPath) { diff --git a/src/main/kotlin/com/github/burrunan/gradle/JsExtensions.kt b/src/main/kotlin/com/github/burrunan/gradle/JsExtensions.kt index 2c612e1..31c510c 100644 --- a/src/main/kotlin/com/github/burrunan/gradle/JsExtensions.kt +++ b/src/main/kotlin/com/github/burrunan/gradle/JsExtensions.kt @@ -20,9 +20,6 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -inline fun jsObject(builder: T.() -> Unit = {}): T = - (js("({})") as T).apply(builder) - suspend inline fun suspendWithCallback(crossinline block: ((Error?) -> Unit) -> Unit) = suspendCoroutine { cont -> block.invoke { error -> diff --git a/src/main/kotlin/com/github/burrunan/gradle/Main.kt b/src/main/kotlin/com/github/burrunan/gradle/Main.kt index ccd3a82..3430a6d 100644 --- a/src/main/kotlin/com/github/burrunan/gradle/Main.kt +++ b/src/main/kotlin/com/github/burrunan/gradle/Main.kt @@ -19,6 +19,7 @@ package com.github.burrunan.gradle import com.github.burrunan.gradle.github.env.ActionsEnvironment import com.github.burrunan.gradle.github.event.currentTrigger import github.actions.core.info +import kotlinext.js.jsObject import process internal fun getInput(name: String, required: Boolean = false): String = diff --git a/src/main/kotlin/com/github/burrunan/gradle/StreamExtensions.kt b/src/main/kotlin/com/github/burrunan/gradle/StreamExtensions.kt index b28c0fd..eaf6af9 100644 --- a/src/main/kotlin/com/github/burrunan/gradle/StreamExtensions.kt +++ b/src/main/kotlin/com/github/burrunan/gradle/StreamExtensions.kt @@ -18,6 +18,7 @@ package com.github.burrunan.gradle import NodeJS.ReadableStream import NodeJS.WritableStream +import kotlinext.js.jsObject import stream.Duplex import stream.internal diff --git a/src/main/kotlin/com/github/burrunan/gradle/cache/Cache.kt b/src/main/kotlin/com/github/burrunan/gradle/cache/Cache.kt index 410f712..7e55895 100644 --- a/src/main/kotlin/com/github/burrunan/gradle/cache/Cache.kt +++ b/src/main/kotlin/com/github/burrunan/gradle/cache/Cache.kt @@ -26,6 +26,10 @@ interface Cache { sealed class RestoreType { data class Exact(val path: String) : RestoreType() data class Partial(val path: String) : RestoreType() - object None : RestoreType() - object Unknown : RestoreType() + object None : RestoreType() { + override fun toString() = "None" + } + object Unknown : RestoreType() { + override fun toString() = "Unknown" + } } diff --git a/src/main/kotlin/github/actions/cache/CacheExtensions.kt b/src/main/kotlin/github/actions/cache/CacheExtensions.kt index 65717c1..be05434 100644 --- a/src/main/kotlin/github/actions/cache/CacheExtensions.kt +++ b/src/main/kotlin/github/actions/cache/CacheExtensions.kt @@ -19,6 +19,7 @@ package github.actions.cache import com.github.burrunan.gradle.cache.RestoreType import github.actions.core.info import github.actions.core.warning +import kotlinext.js.jsObject import kotlinx.coroutines.await suspend fun restoreAndLog( @@ -44,8 +45,8 @@ suspend fun restoreAndLog( } } } - if (result != null) { - return if (result == primaryKey) RestoreType.Exact(result) else RestoreType.Partial(result) + result?.removePrefix(version)?.let { + return if (it.endsWith(primaryKey)) RestoreType.Exact(it) else RestoreType.Partial(it) } info("Cache was not found for $primaryKey, restore keys: ${restoreKeys.joinToString(", ")}") return RestoreType.None diff --git a/src/main/kotlin/github/actions/exec/functions.kt b/src/main/kotlin/github/actions/exec/functions.kt index 1bce50a..272708a 100644 --- a/src/main/kotlin/github/actions/exec/functions.kt +++ b/src/main/kotlin/github/actions/exec/functions.kt @@ -16,7 +16,7 @@ */ package github.actions.exec -import com.github.burrunan.gradle.jsObject +import kotlinext.js.jsObject import kotlinx.coroutines.await class ExecResult( diff --git a/src/test/kotlin/com/github/burrunan/gradle/CacheServerTest.kt b/src/test/kotlin/com/github/burrunan/gradle/CacheServerTest.kt new file mode 100644 index 0000000..4f99714 --- /dev/null +++ b/src/test/kotlin/com/github/burrunan/gradle/CacheServerTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2020 Vladimir Sitnikov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.github.burrunan.gradle + +import com.github.burrunan.gradle.cache.CacheService +import com.github.burrunan.gradle.cache.RestoreType +import github.actions.cache.restoreAndLog +import github.actions.cache.saveAndLog +import kotlinx.coroutines.await +import kotlin.test.Test +import kotlin.test.assertEquals + +class CacheServerTest { + val cacheService = CacheService() + + @Test + fun saveCache() = runTest { + val dir = "saveCache" + mkdir(dir) + val file = "$dir/cached.txt" + val contents = "hello, world" + fs2.promises.writeFile(file, contents, "utf8").await() + val patterns = listOf("$dir/**") + + val primaryKey = "linux-gradle-123123" + + cacheService { + saveAndLog(patterns, primaryKey, "1-") + + fs2.promises.unlink(file) + + assertEquals( + RestoreType.Exact(primaryKey), + restoreAndLog( + patterns, + primaryKey, + restoreKeys = listOf("linux-gradle-", "linux-"), + version = "1-", + ), + "Cache restored from exact match", + ) + + assertEquals( + fs2.promises.readFile(file, "utf8").await(), + contents, + "Contents after restore should match", + ) + + assertEquals( + RestoreType.Partial(primaryKey), + restoreAndLog( + patterns, + "asdf$primaryKey", + restoreKeys = listOf("linux-gradle-", "linux-"), + version = "1-", + ), + "PK not found => restored from restoreKeys", + ) + + assertEquals( + fs2.promises.readFile(file, "utf8").await(), + contents, + "Contents after restore should match", + ) + } + } +} diff --git a/src/test/kotlin/com/github/burrunan/gradle/GlobTest.kt b/src/test/kotlin/com/github/burrunan/gradle/GlobTest.kt index 2dce82e..3a0425f 100644 --- a/src/test/kotlin/com/github/burrunan/gradle/GlobTest.kt +++ b/src/test/kotlin/com/github/burrunan/gradle/GlobTest.kt @@ -1,9 +1,23 @@ +/* + * Copyright 2020 Vladimir Sitnikov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ package com.github.burrunan.gradle import com.github.burrunan.gradle.github.formatBytes import com.github.burrunan.gradle.hashing.hashFilesDetailed -import kotlinx.coroutines.await -import runTest import kotlin.test.Test import kotlin.test.assertEquals @@ -11,17 +25,14 @@ class GlobTest { @Test fun glob() = runTest { val dirName = "globTest" - if (!exists(dirName)) { - fs2.promises.mkdir(dirName).await() - } + mkdir(dirName) fs2.promises.writeFile("$dirName/good.txt", "a", "utf8") fs2.promises.writeFile("$dirName/bad.txt", "a", "utf8") val hash = hashFilesDetailed( - "**/*.txt", - "!**/*bad**", + "$dirName/**/*.txt", + "!$dirName/**/*bad**", ) - println("${hash.info.totalBytes.formatBytes()} ${hash.info.totalFiles} files") val actual = hash.contents.files.entries.joinToString { (file, details) -> "${details.fileSize.formatBytes()} ${details.hash} $file" } diff --git a/src/test/kotlin/com/github/burrunan/gradle/StreamExtensions.kt b/src/test/kotlin/com/github/burrunan/gradle/StreamExtensions.kt new file mode 100644 index 0000000..d395ccb --- /dev/null +++ b/src/test/kotlin/com/github/burrunan/gradle/StreamExtensions.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Vladimir Sitnikov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.github.burrunan.gradle + +import Buffer +import NodeJS.ReadableStream + +suspend fun ReadableStream.readJson(): T = JSON.parse(readToBuffer().toString("utf8")) + +suspend fun ReadableStream.readToBuffer(): Buffer { + val data = mutableListOf() + use { + it.on("data") { chunk: Any -> + data += chunk as Buffer + } + } + return Buffer.concat(data.toTypedArray()) +} diff --git a/src/test/kotlin/com/github/burrunan/gradle/cache/CacheContract.kt b/src/test/kotlin/com/github/burrunan/gradle/cache/CacheContract.kt new file mode 100644 index 0000000..70ea0ac --- /dev/null +++ b/src/test/kotlin/com/github/burrunan/gradle/cache/CacheContract.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Vladimir Sitnikov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.github.burrunan.gradle.cache + +external interface GetCacheParams { + val keys: String + val version: String +} + +external interface ReserveCacheRequest { + val key: String + val version: String? +} + +external interface ReserveCacheResponse { + var cacheId: Number +} + +external interface ArtifactCacheEntry { + var cacheKey: String? + var scope: String? + var creationTime: String? + var archiveLocation: String? +} + +external interface CommitCacheRequest { + val size: Number +} + +external interface InternalCacheOptions { + val compressionMethod: CompressionMethod? +} + +external enum class CompressionMethod { + Gzip, + + // Long range mode was added to zstd in v1.3.2. + // This enum is for earlier version of zstd that does not have --long support + ZstdWithoutLong, + Zstd +} diff --git a/src/test/kotlin/com/github/burrunan/gradle/cache/CacheService.kt b/src/test/kotlin/com/github/burrunan/gradle/cache/CacheService.kt new file mode 100644 index 0000000..6db7ca2 --- /dev/null +++ b/src/test/kotlin/com/github/burrunan/gradle/cache/CacheService.kt @@ -0,0 +1,174 @@ +/* + * Copyright 2020 Vladimir Sitnikov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.github.burrunan.gradle.cache + +import com.github.burrunan.gradle.exists +import com.github.burrunan.gradle.readJson +import com.github.burrunan.gradle.readToBuffer +import com.github.burrunan.gradle.suspendWithCallback +import github.actions.core.debug +import http.IncomingMessage +import http.ServerResponse +import kotlinext.js.jsObject +import process +import url.UrlWithParsedQuery +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class CacheService { + companion object { + const val ARCHIVE_DOWNLOAD_URL = "_apis/artifactcache/get" + } + + private val storage = CacheStorage() + + private val server = http.createServer { req, res -> + val query = url.parse(req.url, true) + val path = query.pathname ?: "" + res.handle { + when { + path == "/_apis/artifactcache/caches" && req.method == "POST" -> + reserveCache(req, res) + path == "/_apis/artifactcache/cache" && req.method == "GET" -> + getCache(query, res) + path.endsWith(ARCHIVE_DOWNLOAD_URL) && req.method == "GET" -> + getContents(query, res) + path.startsWith("/_apis/artifactcache/caches/") -> + cacheOp(path.substringAfter("/_apis/artifactcache/caches/").toInt(), req, res) + else -> HttpException.notImplemented("Path: $path") + } + } + } + + private fun getContents(query: UrlWithParsedQuery, res: ServerResponse) { + val key = query.query["key"] as String + val entry = storage.getValue(key) + res.writeHead( + 200, "Ok", + jsObject { + this["content-length"] = entry.value.length + }, + ) + res.write(entry.value) + } + + private suspend fun cacheOp(cacheId: Number, req: IncomingMessage, res: ServerResponse) = when (req.method) { + "PATCH" -> uploadCache(cacheId, req, res) + "POST" -> commitCache(cacheId, req, res) + else -> throw HttpException.notImplemented("Unknown method: ${req.method}") + } + + private fun getCache(query: UrlWithParsedQuery, res: ServerResponse) { + val request = query.query.unsafeCast() + var resultKey: String? = null + var resultEntry: CacheEntry? = null + for ((index, key) in request.keys.split(',').withIndex()) { + if (index == 0) { + val entry = storage[key] ?: continue + if (entry.version != request.version) { + debug("Entry version differs for key $key. Requested: ${request.version}, actual: ${entry.version}") + } + resultKey = key + resultEntry = entry + break + } + val entry = storage.find(key, request.version) ?: continue + resultKey = entry.key + resultEntry = entry.value + } + if (resultKey == null) { + throw HttpException.noContent("No entries found") + } + + res.writeHead(200, "Ok") + + res.write( + JSON.stringify( + jsObject { + cacheKey = resultKey + scope = "refs/origin/main" + creationTime = resultEntry?.creationTime?.toString() + archiveLocation = + "${process.env["ACTIONS_CACHE_URL"]}$ARCHIVE_DOWNLOAD_URL?key=$resultKey".takeIf { resultEntry != null } + }, + ), + ) + } + + private suspend fun uploadCache(cacheId: Number, req: IncomingMessage, res: ServerResponse) { + val contentRange = req.headers.asDynamic()["content-range"] as String + val (_, start, end) = contentRange.match("bytes (\\d+)-(\\d+)") ?: arrayOf("", "", "") + if (start.isEmpty()) { + throw HttpException.notImplemented("Unknown content-range: $contentRange") + } + storage.update(cacheId, start.toInt(), end.toInt(), req.readToBuffer()) + res.writeHead(200, "OK") + } + + private suspend fun commitCache(cacheId: Number, req: IncomingMessage, res: ServerResponse) { + storage.commitCache(cacheId, req.readJson().size) + res.writeHead(200, "OK") + } + + private suspend fun reserveCache(req: IncomingMessage, res: ServerResponse) { + if (req.method != "POST") { + throw HttpException.badRequest("Expecting POST method, got ${req.method}") + } + val request = req.readJson() + + val cacheId = storage.reserveCache(request.key, request.version!!) + ?: throw HttpException.badRequest("Cache entry already exists") + res.writeHead(200, "Reserve Cache OK") + res.write( + JSON.stringify( + jsObject { + this.cacheId = cacheId + }, + ), + ) + } + + suspend inline operator fun invoke(block: () -> T): T { + start() + try { + return block() + } finally { + stop() + } + } + + suspend fun start() { + suspendCoroutine { cont -> + server.listen(0) { + cont.resume(null) + } + } + + val runnerTemp = "runner_temp" + if (!exists(runnerTemp)) { + fs2.promises.mkdir(runnerTemp) + } + + process.env["ACTIONS_RUNTIME_TOKEN"] = "42" + process.env["RUNNER_TEMP"] = process.cwd() + "/" + runnerTemp + process.env["ACTIONS_CACHE_URL"] = "http://localhost:${server.address().port}/" + } + + suspend fun stop() { + suspendWithCallback { server.close(it) } + } +} diff --git a/src/test/kotlin/com/github/burrunan/gradle/cache/CacheStorage.kt b/src/test/kotlin/com/github/burrunan/gradle/cache/CacheStorage.kt new file mode 100644 index 0000000..40c7fbc --- /dev/null +++ b/src/test/kotlin/com/github/burrunan/gradle/cache/CacheStorage.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2020 Vladimir Sitnikov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.github.burrunan.gradle.cache + +import Buffer + +class CacheStorage { + private val storage = mutableMapOf() + private val reservations = mutableMapOf() + private val caches = mutableMapOf() + private var nextId = 0 + + fun reserveCache(key: String, version: String): Number? { + if (key in storage || key in reservations) { + return null + } + if (reservations[key]?.version?.equals(version) == false) { + return null + } + nextId += 1 + reservations[key] = CacheReservation(nextId, version) + caches[nextId] = TemporaryCache(key) + return nextId + } + + operator fun set(key: String, value: CacheEntry) { + storage[key] = value + } + + operator fun get(key: String) = storage[key] + + fun getValue(key: String) = storage.getValue(key) + + fun find(prefix: String, version: String) = + storage.filterKeys { it.startsWith(prefix) } + .filterValues { it.version == version } + .maxByOrNull { it.value.creationTime.toDouble() } + + fun update(cacheId: Number, start: Int, end: Int, buffer: Buffer) { + caches.getValue(cacheId).parts.add(UploadPart(start, end, buffer)) + } + + fun commitCache(cacheId: Number, size: Number) { + val cache = caches.remove(cacheId) + ?: throw HttpException.noContent("Cache $cacheId is not found") + val reservation = reservations.remove(cache.key) + ?: throw HttpException.noContent("Reservation ${cache.key} is not found for cache $cacheId") + + val parts = cache.parts + val result = if (parts.size == 1 && parts[0].contents.length == size) { + parts[0].contents + } else { + Buffer.alloc(size).also { + for (part in parts) { + part.contents.copy(it, part.start, 0, part.end) + } + } + } + set(cache.key, CacheEntry(reservation.version, result, cacheId)) + } +} + +class CacheEntry(val version: String, val value: Buffer, val creationTime: Number) + +class CacheReservation(val number: Number, val version: String) + +class TemporaryCache(val key: String) { + val parts = mutableListOf() +} + +class UploadPart(val start: Int, val end: Int, val contents: Buffer) diff --git a/src/test/kotlin/com/github/burrunan/gradle/cache/HttpException.kt b/src/test/kotlin/com/github/burrunan/gradle/cache/HttpException.kt new file mode 100644 index 0000000..2725b76 --- /dev/null +++ b/src/test/kotlin/com/github/burrunan/gradle/cache/HttpException.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Vladimir Sitnikov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.github.burrunan.gradle.cache + +class HttpException(val code: Int, message: String) : Throwable(message) { + companion object { + fun noContent(message: String) = HttpException(204, message) + fun notImplemented(message: String) = HttpException(501, message) + fun notFound(message: String) = HttpException(404, message) + fun badRequest(message: String) = HttpException(400, message) + } +} diff --git a/src/test/kotlin/com/github/burrunan/gradle/cache/HttpExtensions.kt b/src/test/kotlin/com/github/burrunan/gradle/cache/HttpExtensions.kt new file mode 100644 index 0000000..c83d547 --- /dev/null +++ b/src/test/kotlin/com/github/burrunan/gradle/cache/HttpExtensions.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Vladimir Sitnikov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.github.burrunan.gradle.cache + +import http.ServerResponse +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope + +fun ServerResponse.handle(action: suspend CoroutineScope.() -> Unit) = + GlobalScope.launch { + try { + supervisorScope { + action() + } + } catch (e: HttpException) { + writeHead(e.code, e.message ?: "no message") + } finally { + end() + } + } diff --git a/src/test/kotlin/com/github/burrunan/gradle/runTest.kt b/src/test/kotlin/com/github/burrunan/gradle/runTest.kt index dde369c..4f1240e 100644 --- a/src/test/kotlin/com/github/burrunan/gradle/runTest.kt +++ b/src/test/kotlin/com/github/burrunan/gradle/runTest.kt @@ -1,5 +1,24 @@ +/* + * Copyright 2020 Vladimir Sitnikov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.github.burrunan.gradle + import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.promise +import process fun runTest(block: suspend () -> Unit): dynamic = GlobalScope.promise { process.env["RUNNER_OS"] = "macos" diff --git a/webpack.config.d/config.js b/webpack.config.d/config.js index b5ee1b7..464b5ca 100644 --- a/webpack.config.d/config.js +++ b/webpack.config.d/config.js @@ -2,6 +2,8 @@ config.output = config.output || {} config.output.globalObject = "this" config.target = "node" +config.resolve.modules.unshift("src/test/resources") + const TerserPlugin = require('terser-webpack-plugin'); // keep_classnames is required to workaround node-fetch Expected signal to be an instanceof AbortSignal