From 4c37ea13cd8d888e35db79a90a2c85c96315d63f Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Sat, 17 Oct 2026 04:00:57 +0300 Subject: [PATCH] Save campaign file artifacts before stdout publication --- .../usvm/ts/pbt/cli/CampaignFileRequest.kt | 7 ++- .../usvm/ts/pbt/cli/CampaignFileCliTest.kt | 54 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/usvm-ts-fast-check/src/main/kotlin/org/usvm/ts/pbt/cli/CampaignFileRequest.kt b/usvm-ts-fast-check/src/main/kotlin/org/usvm/ts/pbt/cli/CampaignFileRequest.kt index 4b26e54ff1..52427582cc 100644 --- a/usvm-ts-fast-check/src/main/kotlin/org/usvm/ts/pbt/cli/CampaignFileRequest.kt +++ b/usvm-ts-fast-check/src/main/kotlin/org/usvm/ts/pbt/cli/CampaignFileRequest.kt @@ -55,8 +55,11 @@ internal fun runCampaignFile( ) val json = PropertyManifestJson.json.encodeToUtf8SafeString(artifact) - output.appendLine(json) - Files.writeString(Path.of(args[1]), json) + try { + Files.writeString(Path.of(args[1]), json) + } finally { + output.appendLine(json) + } campaignExitCode(artifact.result) } catch (error: IllegalArgumentException) { diff --git a/usvm-ts-fast-check/src/test/kotlin/org/usvm/ts/pbt/cli/CampaignFileCliTest.kt b/usvm-ts-fast-check/src/test/kotlin/org/usvm/ts/pbt/cli/CampaignFileCliTest.kt index 3d858edabc..543e9df775 100644 --- a/usvm-ts-fast-check/src/test/kotlin/org/usvm/ts/pbt/cli/CampaignFileCliTest.kt +++ b/usvm-ts-fast-check/src/test/kotlin/org/usvm/ts/pbt/cli/CampaignFileCliTest.kt @@ -1,5 +1,6 @@ package org.usvm.ts.pbt.cli +import kotlinx.serialization.decodeFromString import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import org.usvm.ts.pbt.backend.PropertyRunConfiguration @@ -13,6 +14,8 @@ import org.usvm.ts.pbt.model.PropertyInput import org.usvm.ts.pbt.model.TypeScriptEntryPoint import org.usvm.ts.pbt.model.encodeToUtf8SafeString import org.usvm.ts.pbt.testResourcesRoot +import java.io.IOException +import java.io.Writer import java.nio.file.Files import java.nio.file.Path import kotlin.test.assertEquals @@ -61,6 +64,57 @@ class CampaignFileCliTest { assertFalse(Files.exists(destination)) } + @Test + fun `file artifact is readable before failing stdout publication`() { + val request = request(mode = PropertyCampaignMode.PBT_ONLY) + val path = directory.resolve("request.json") + val destination = directory.resolve("artifact.json") + Files.writeString(path, PropertyManifestJson.json.encodeToUtf8SafeString(request)) + val errors = StringBuilder() + var stdoutAttempted = false + val output = object : Writer() { + override fun write(buffer: CharArray, offset: Int, length: Int) { + stdoutAttempted = true + val artifact = PropertyCampaignArtifact.read(destination) + assertEquals(PropertyCampaignMode.PBT_ONLY, artifact.result.mode) + + throw IOException("stdout closed") + } + + override fun flush() = Unit + + override fun close() = Unit + } + + val exit = runCampaignFile(arrayOf(path.toString(), destination.toString()), output, errors) + val artifact = PropertyCampaignArtifact.read(destination) + + assertEquals(2, exit) + assertTrue(stdoutAttempted) + assertTrue(errors.contains("stdout closed")) + assertEquals(1, artifact.result.concrete?.numRuns) + assertEquals(37, artifact.configuration.seed) + } + + @Test + fun `file write failure retains the full stdout artifact and original error`() { + val request = request(mode = PropertyCampaignMode.PBT_ONLY) + val path = directory.resolve("request.json") + val destination = directory.resolve("missing-parent").resolve("artifact.json") + Files.writeString(path, PropertyManifestJson.json.encodeToUtf8SafeString(request)) + val output = StringBuilder() + val errors = StringBuilder() + + val exit = runCampaignFile(arrayOf(path.toString(), destination.toString()), output, errors) + val artifact = PropertyManifestJson.json.decodeFromString(output.toString()) + + assertEquals(2, exit) + assertEquals(destination.toString(), errors.toString().trim()) + assertFalse(Files.exists(destination)) + assertEquals(1, artifact.result.concrete?.numRuns) + assertEquals(37, artifact.configuration.seed) + } + private fun request(mode: PropertyCampaignMode) = CampaignFileRequest( sourceRoot = testResourcesRoot().resolve("properties/campaign").toString(), property = PropertyDefinition(