diff --git a/configuration/src/main/kotlin/com/malinskiy/marathon/config/OutputConfiguration.kt b/configuration/src/main/kotlin/com/malinskiy/marathon/config/OutputConfiguration.kt index 30c0c0ec7..3030532b5 100644 --- a/configuration/src/main/kotlin/com/malinskiy/marathon/config/OutputConfiguration.kt +++ b/configuration/src/main/kotlin/com/malinskiy/marathon/config/OutputConfiguration.kt @@ -2,6 +2,12 @@ package com.malinskiy.marathon.config import com.fasterxml.jackson.annotation.JsonProperty +// Value 0 is equivalent to unlimited path length +const val OUTPUT_MAX_PATH = 0 +// Value 0 is equivalent to unlimited filename length +const val OUTPUT_MAX_FILENAME = 255 + data class OutputConfiguration( - @JsonProperty("maxPath") val maxPath: Int = 255 + @JsonProperty("maxPath") val maxPath: Int = OUTPUT_MAX_PATH, + @JsonProperty("maxFilename") val maxFilename: Int = OUTPUT_MAX_FILENAME, ) diff --git a/core/src/main/kotlin/com/malinskiy/marathon/di/Modules.kt b/core/src/main/kotlin/com/malinskiy/marathon/di/Modules.kt index d89a6f5c7..fd80f5271 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/di/Modules.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/di/Modules.kt @@ -38,7 +38,7 @@ val analyticsModule = module { val coreModule = module { single { val configuration = get() - FileManager(configuration.outputConfiguration.maxPath, configuration.outputDir) + FileManager(configuration.outputConfiguration.maxPath, configuration.outputConfiguration.maxFilename, configuration.outputDir) } single { GsonBuilder() diff --git a/core/src/main/kotlin/com/malinskiy/marathon/io/FileManager.kt b/core/src/main/kotlin/com/malinskiy/marathon/io/FileManager.kt index 786fe1f71..3fc75c864 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/io/FileManager.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/io/FileManager.kt @@ -12,97 +12,113 @@ import java.nio.file.Path import java.nio.file.Paths.get import java.util.UUID +/** + * Validation logic should check filename first, then check if the resulting path is within max path len + */ @Suppress("TooManyFunctions") -class FileManager(private val maxPath: Int, private val output: File) { +class FileManager(private val maxPath: Int, private val maxFilename: Int, private val output: File) { val log = MarathonLogging.logger("FileManager") - fun createFile(fileType: FileType, pool: DevicePoolId, device: DeviceInfo, test: Test, testBatchId: String? = null): File { - val directory = createDirectory(fileType, pool, device) - val filename = createFilename(test, fileType, maxPath - (directory.toAbsolutePath().toString().length + 1), testBatchId) + fun createFile( + fileType: FileType, + pool: DevicePoolId, + device: DeviceInfo, + test: Test? = null, + testBatchId: String? = null, + id: String? = null + ): File { + val directory = when { + test != null || testBatchId != null -> createDirectory(fileType, pool, device) + else -> createDirectory(fileType, pool) + } + val filename = when { + test != null -> createTestFilename(test, fileType, testBatchId, id = id) + testBatchId != null -> createBatchFilename(testBatchId, fileType, id = id) + else -> createDeviceFilename(device, fileType, id = id) + } return createFile(directory, filename) } - fun createFile(fileType: FileType, pool: DevicePoolId, device: DeviceInfo, testBatchId: String): File { - val directory = createDirectory(fileType, pool, device) - val filename = createFilename(fileType, testBatchId) - return createFile(directory, filename) - } + fun createFolder(folderType: FolderType, pool: DevicePoolId? = null, device: DeviceInfo? = null): File { + var path = get(output.absolutePath, folderType.dir) + if (pool != null) { + path = path.resolve(pool.name) + } + if (device != null) { + path = path.resolve(device.safeSerialNumber) + } - fun createFile(fileType: FileType, pool: DevicePoolId, device: DeviceInfo): File { - val directory = createDirectory(fileType, pool) - val filename = createFilename(device, fileType) - return createFile(directory, filename) - } + val maybeTooLongPath = path.toFile() + path = if (maxPath > 0 && maybeTooLongPath.absolutePath.length > maxPath) { + val trimmed = maybeTooLongPath.absolutePath.take(maxPath) + log.error { + "Directory path length cannot exceed $maxPath characters and has been trimmed from $maybeTooLongPath to $trimmed and can create a conflict. " + + "This happened because the combination of file path, pool name and device serial is too long." + } + File(trimmed) + } else { + maybeTooLongPath + }.toPath() - fun createScreenshotFile(extension: String, pool: DevicePoolId, device: DeviceInfo, test: Test, testBatchId: String): File { - val directory = createDirectory(FileType.SCREENSHOT, pool, device) - val filename = - createFilename( - test, - FileType.SCREENSHOT, - maxPath - (directory.toAbsolutePath().toString().length + 1), - testBatchId = null, - extension, - UUID.randomUUID().toString() - ) - return createFile(directory, filename) + return createDirectories(path).toFile() } - fun createFolder(folderType: FolderType): File = createDirectories(get(output.absolutePath, folderType.dir)).toFile() - fun createFolder(folderType: FolderType, pool: DevicePoolId, device: DeviceInfo): File = - createDirectories(get(output.absolutePath, folderType.dir, pool.name, device.safeSerialNumber)).toFile() - - fun createFolder(folderType: FolderType, pool: DevicePoolId): File = - createDirectories(get(output.absolutePath, folderType.dir, pool.name)).toFile() - - fun createFolder(folderType: FolderType, device: DeviceInfo): File = - createDirectories(get(output.absolutePath, folderType.dir, device.safeSerialNumber)).toFile() - fun createTestResultFile(filename: String): File { - val resultsFolder = get(output.absolutePath, FileType.TEST_RESULT.dir).toFile() - resultsFolder.mkdirs() - return File(resultsFolder, filename) + val resultsFolder = get(output.absolutePath, FileType.TEST_RESULT.dir) + resultsFolder.toFile().mkdirs() + return createFile(resultsFolder, filename) } - private fun createDirectory(fileType: FileType, pool: DevicePoolId, device: DeviceInfo): Path = - createDirectories(getDirectory(fileType, pool, device)) - - private fun createDirectory(fileType: FileType, pool: DevicePoolId): Path = - createDirectories(getDirectory(fileType, pool)) - - private fun getDirectory(fileType: FileType, pool: DevicePoolId, device: DeviceInfo): Path = - getDirectory(fileType, pool, device.safeSerialNumber) - - private fun getDirectory(fileType: FileType, pool: DevicePoolId, serial: String): Path = - get(output.absolutePath, fileType.dir, pool.name, serial) + private fun createDirectory(fileType: FileType, pool: DevicePoolId, device: DeviceInfo? = null): Path { + return createDirectories(getDirectory(fileType, pool, serial = device?.safeSerialNumber)) + } - private fun getDirectory(fileType: FileType, pool: DevicePoolId): Path = - get(output.absolutePath, fileType.dir, pool.name) + private fun getDirectory(fileType: FileType, pool: DevicePoolId, serial: String? = null): Path { + val path = get(output.absolutePath, fileType.dir, pool.name) + return serial?.let { + path.resolve(serial) + } ?: path + } private fun createFile(directory: Path, filename: String): File { - val maybeTooLongPath = File(directory.toFile(), filename) - return if (maybeTooLongPath.absolutePath.length > maxPath) { + val trimmedFilename = if (maxFilename > 0 && filename.length > maxFilename) { + val safeFilename = filename.take(maxFilename) + log.error { + "File name length cannot exceed $maxFilename characters and has been trimmed to $safeFilename and can create a conflict." + + "This usually happens because the test name is too long." + } + safeFilename + } else { + filename + } + val maybeTooLongPath = File(directory.toFile(), trimmedFilename) + return if (maxPath > 0 && maybeTooLongPath.absolutePath.length > maxPath) { val trimmed = maybeTooLongPath.absolutePath.substring(0 until maxPath) - log.error { "File path length cannot exceed $maxPath characters and has been trimmed to $trimmed and can create a conflict. This happened because the combination of file path, test class name, and test name is too long." } + log.error { + "File path length cannot exceed $maxPath characters and has been trimmed from $maybeTooLongPath to $trimmed and can create a conflict. " + + "This happened because the combination of file path, test class name, and test name is too long." + } File(trimmed) } else { maybeTooLongPath } } - private fun createFilename(fileType: FileType, testBatchId: String): String { + private fun createBatchFilename(testBatchId: String, fileType: FileType, id: String? = null): String { return StringBuilder().apply { append(testBatchId) + if (id != null) { + append("-$id") + } if (fileType.suffix.isNotEmpty()) { append(".$testBatchId") } }.toString() } - private fun createFilename( + private fun createTestFilename( test: Test, fileType: FileType, - limit: Int, testBatchId: String? = null, overrideExtension: String? = null, id: String? = null, @@ -120,24 +136,16 @@ class FileManager(private val maxPath: Int, private val output: File) { append(".${fileType.suffix}") } }.toString() - val rawTestName = test.toTestName().escape() - val testName = when { - limit - testSuffix.length >= 0 -> rawTestName.take(limit - testSuffix.length) - else -> "" - } - val fileName = "$testName$testSuffix" - if (rawTestName.length > testName.length) { - when { - limit >= 0 -> log.error { "File name length cannot exceed $limit characters and has been trimmed to $fileName and can create a conflict. This happened because the combination of file path, test class name, and test name is too long." } - else -> log.error { "Base path for writing a file ${rawTestName}$testSuffix is already maxed out and is ${-limit} characters more than the allowed limit of ${maxPath}." } - } - } - return fileName + val testName = test.toTestName().escape() + return "$testName$testSuffix" } - private fun createFilename(device: DeviceInfo, fileType: FileType): String { + private fun createDeviceFilename(device: DeviceInfo, fileType: FileType, id: String? = null): String { return StringBuilder().apply { append(device.safeSerialNumber) + if (id != null) { + append("-$id") + } if (fileType.suffix.isNotEmpty()) { append(".${fileType.suffix}") } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/io/FileType.kt b/core/src/main/kotlin/com/malinskiy/marathon/io/FileType.kt index 419df9fc6..68dea06a7 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/io/FileType.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/io/FileType.kt @@ -9,6 +9,9 @@ enum class FileType(val dir: String, val suffix: String) { VIDEO("video", "mp4"), SCREENSHOT("screenshot", "gif"), SCREENSHOT_PNG("screenshot", "png"), + SCREENSHOT_JPG("screenshot", "jpg"), + SCREENSHOT_WEBP("screenshot", "jpg"), + SCREENSHOT_GIF("screenshot", "jpg"), XCTESTRUN("xctestrun", "xctestrun"), BILL("bill", "json"), } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/report/bill/BillingReporter.kt b/core/src/main/kotlin/com/malinskiy/marathon/report/bill/BillingReporter.kt index 44ab165f4..f2aebf63a 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/report/bill/BillingReporter.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/report/bill/BillingReporter.kt @@ -57,7 +57,7 @@ internal class BillingReporter( bills.forEach { val json = gson.toJson(it) - fileManager.createFile(FileType.BILL, it.pool, it.device).writeText(json) + fileManager.createFile(FileType.BILL, it.pool, device = it.device).writeText(json) } usageTracker.trackEvent(Event.Devices(bills.size)) diff --git a/core/src/test/kotlin/com/malinskiy/marathon/io/FileManagerTest.kt b/core/src/test/kotlin/com/malinskiy/marathon/io/FileManagerTest.kt index 1d8dd35bb..10e7e7ea5 100644 --- a/core/src/test/kotlin/com/malinskiy/marathon/io/FileManagerTest.kt +++ b/core/src/test/kotlin/com/malinskiy/marathon/io/FileManagerTest.kt @@ -16,10 +16,8 @@ import kotlin.io.path.absolutePathString class FileManagerTest { private val output = Files.createTempDir() - private val fileManager = FileManager(MAX_PATH, output) private companion object { - val MAX_PATH = 255 val poolId = DevicePoolId("testPoolId") val deviceInfo = DeviceInfo( operatingSystem = OperatingSystem("23"), @@ -56,30 +54,32 @@ class FileManagerTest { @Test fun createFilenameNormalLengthTest() { + val fileManager = FileManager(0, 255, output) val file = fileManager.createFile(FileType.LOG, poolId, deviceInfo, shortNameTest, batchId) file.name shouldBeEqualTo "com.example.Clazz#method-batchId.log" } @Test - fun createFilenameLongLengthMethodTest() { + fun createFilenameLongLengthMethodLimitedPathTest() { + val fileManager = FileManager(255, 0, output) val file = fileManager.createFile(FileType.LOG, poolId, deviceInfo, longNameTest, batchId) file.absolutePath.length shouldBeEqualTo 255 val filenameLimit = 255 - file.parentFile.absolutePath.length - File.separator.length - val fqtnLimit = filenameLimit - "-${batchId}.log".length - file.name shouldBeEqualTo "${longNameTest.toTestName().escape().take(fqtnLimit)}-${batchId}.log" + file.name shouldBeEqualTo "${longNameTest.toTestName()}-${batchId}.log".escape().take(filenameLimit) } @Test fun testCreateFilenameNamedParameterizedLong() { + val fileManager = FileManager(255, 0, output) val file = fileManager.createFile(FileType.LOG, poolId, deviceInfo, longNamedParameterizedTest, batchId) file.absolutePath.length shouldBeEqualTo 255 val filenameLimit = 255 - file.parentFile.absolutePath.length - File.separator.length - val fqtnLimit = filenameLimit - "-${batchId}.log".length - file.name shouldBeEqualTo "${longNamedParameterizedTest.toTestName().escape().take(fqtnLimit)}-${batchId}.log" + file.name shouldBeEqualTo "${longNamedParameterizedTest.toTestName()}-${batchId}.log".escape().take(filenameLimit) } @Test fun testDeviceSerialEscaping() { + val fileManager = FileManager(0, 255, output) val file = fileManager.createFile( FileType.LOG, poolId, DeviceInfo( operatingSystem = OperatingSystem("23"), @@ -95,7 +95,26 @@ class FileManagerTest { } @Test - fun testTooLongOutputFolder() { + fun testScreenshotfile() { + val fileManager = FileManager(0, 255, output) + val file = fileManager.createFile( + FileType.SCREENSHOT_PNG, poolId, DeviceInfo( + operatingSystem = OperatingSystem("23"), + serialNumber = "127.0.0.1:5037:emulator-5554", + model = "Android SDK built for x86", + manufacturer = "unknown", + networkState = NetworkState.CONNECTED, + deviceFeatures = listOf(DeviceFeature.SCREENSHOT, DeviceFeature.VIDEO), + healthy = true + ), + test = shortNameTest, + id = "on-device-test", + ) + file.name shouldBeEqualTo "com.example.Clazz#method-on-device-test.png" + } + + @Test + fun testTooLongOutputPathUnlimitedFilename() { val test = com.malinskiy.marathon.test.Test( pkg = "com.xxyyzzxxyy.android.abcdefgh.abcdefghi", clazz = "PackageNameTest", @@ -105,9 +124,12 @@ class FileManagerTest { val tempDir = Files.createTempDir() val proposedPath = Paths.get(tempDir.absolutePath, FileType.LOG.name, poolId.name, deviceInfo.safeSerialNumber) - val additionalPathCharacters = MAX_PATH - proposedPath.absolutePathString().length + val limitedMaxPath = 255 + val additionalPathCharacters = limitedMaxPath - proposedPath.absolutePathString().length val limitedOutputDirectory = File(tempDir, "x".repeat(additionalPathCharacters)) - val limitedFileManager = FileManager(MAX_PATH, limitedOutputDirectory) + val limitedFileManager = FileManager(limitedMaxPath, 0, limitedOutputDirectory) val file = limitedFileManager.createFile(FileType.LOG, poolId, deviceInfo, test, batchId) - } + + file.path.length shouldBeEqualTo 255 + } } diff --git a/docs/runner/intro/configure.md b/docs/runner/intro/configure.md index 282fdb6cb..1f3c26c63 100644 --- a/docs/runner/intro/configure.md +++ b/docs/runner/intro/configure.md @@ -617,18 +617,24 @@ marathon { ### Output configuration -#### Max file path +#### Max file name length -By default, the max file path for any output file is capped at 255 characters due to limitations of some OSs. This is the reason why some -test runs have lots of "File path length cannot exceed" messages in the log. Since there is currently no API to programmatically -establish this limit, it's the user's responsibility to set a larger value if the OS supports this and the user desires it. +By default, the max file name length for any output file is capped at 255 characters due to limitations of most file systems. This is the +reason why some test runs have lots of "File name length cannot exceed" messages in the log. Since there is currently no API to +programmatically establish this limit, it's the user's responsibility to set a larger value if the OS supports this and the user desires it. + +:::info + +Value 0 is equivalent to unlimited file path length + +::: ```yaml outputConfiguration: - maxPath: 1024 + maxFilename: 1024 ``` @@ -637,7 +643,7 @@ outputConfiguration: ```kotlin marathon { outputConfiguration { - maxPath = 1024 + maxFilename = 1024 } } ``` @@ -648,7 +654,7 @@ marathon { ```groovy marathon { outputConfiguration { - maxPath = 1024 + maxFilename = 1024 } } ``` @@ -656,4 +662,49 @@ marathon { +#### Max file path length + +By default, the max file path length for any output file is unlimited which is the default for most file systems. If the user has some +specific constraints then it's possible to limit the max file path length: + +:::info + +Value 0 is equivalent to unlimited file path length + +::: + + + + +```yaml +outputConfiguration: + maxPath: 32767 +``` + + + + +```kotlin +marathon { + outputConfiguration { + maxPath = 32767 + } +} +``` + + + + +```groovy +marathon { + outputConfiguration { + maxPath = 32767 + } +} +``` + + + + + [1]: https://github.com/MarathonLabs/marathon/blob/develop/configuration/src/main/kotlin/com/malinskiy/marathon/config/serialization/ConfigurationFactory.kt diff --git a/marathon-gradle-plugin/src/main/kotlin/com/malinskiy/marathon/gradle/configuration/OutputConfiugration.kt b/marathon-gradle-plugin/src/main/kotlin/com/malinskiy/marathon/gradle/configuration/OutputConfiugration.kt index 388b21e8a..4b7bd0b21 100644 --- a/marathon-gradle-plugin/src/main/kotlin/com/malinskiy/marathon/gradle/configuration/OutputConfiugration.kt +++ b/marathon-gradle-plugin/src/main/kotlin/com/malinskiy/marathon/gradle/configuration/OutputConfiugration.kt @@ -1,13 +1,14 @@ package com.malinskiy.marathon.gradle +import com.malinskiy.marathon.config.OUTPUT_MAX_FILENAME +import com.malinskiy.marathon.config.OUTPUT_MAX_PATH import com.malinskiy.marathon.config.OutputConfiguration open class OutputConfiguration { fun toStrategy(): OutputConfiguration { - return maxPath?.let { - OutputConfiguration(maxPath = it) - } ?: OutputConfiguration() + return OutputConfiguration(maxPath ?: OUTPUT_MAX_PATH, maxFilename ?: OUTPUT_MAX_FILENAME) } var maxPath: Int? = null + var maxFilename: Int? = null } diff --git a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/BaseAndroidDevice.kt b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/BaseAndroidDevice.kt index d4c502f90..1fdec6118 100644 --- a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/BaseAndroidDevice.kt +++ b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/BaseAndroidDevice.kt @@ -236,7 +236,11 @@ abstract class BaseAndroidDevice( testBatch: TestBatch, deferred: CompletableDeferred, ): CompositeTestRunListener { - val fileManager = FileManager(configuration.outputConfiguration.maxPath, configuration.outputDir) + val fileManager = FileManager( + configuration.outputConfiguration.maxPath, + configuration.outputConfiguration.maxFilename, + configuration.outputDir + ) val attachmentProviders = mutableListOf() val features = this.deviceFeatures diff --git a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/filesync/FileSyncTestRunListener.kt b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/filesync/FileSyncTestRunListener.kt index 7006b6878..0fd2a3a0e 100644 --- a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/filesync/FileSyncTestRunListener.kt +++ b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/filesync/FileSyncTestRunListener.kt @@ -100,9 +100,9 @@ class FileSyncTestRunListener( super.afterTestRun() configuration.pull.forEach { entry -> val localFolder = when (entry.aggregationMode) { - AggregationMode.DEVICE_AND_POOL -> fileManager.createFolder(FolderType.DEVICE_FILES, pool, device.toDeviceInfo()) - AggregationMode.DEVICE -> fileManager.createFolder(FolderType.DEVICE_FILES, device.toDeviceInfo()) - AggregationMode.POOL -> fileManager.createFolder(FolderType.DEVICE_FILES, pool) + AggregationMode.DEVICE_AND_POOL -> fileManager.createFolder(FolderType.DEVICE_FILES, pool = pool, device = device.toDeviceInfo()) + AggregationMode.DEVICE -> fileManager.createFolder(FolderType.DEVICE_FILES, device = device.toDeviceInfo()) + AggregationMode.POOL -> fileManager.createFolder(FolderType.DEVICE_FILES, pool = pool) AggregationMode.TEST_RUN -> fileManager.createFolder(FolderType.DEVICE_FILES) } diff --git a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/AdamScreenCaptureTestRunListener.kt b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/AdamScreenCaptureTestRunListener.kt index ebaf3721c..cb3894fc3 100644 --- a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/AdamScreenCaptureTestRunListener.kt +++ b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/AdamScreenCaptureTestRunListener.kt @@ -8,9 +8,11 @@ import com.malinskiy.marathon.device.toDeviceInfo import com.malinskiy.marathon.execution.Attachment import com.malinskiy.marathon.execution.AttachmentType import com.malinskiy.marathon.io.FileManager +import com.malinskiy.marathon.io.FileType import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.report.attachment.AttachmentListener import com.malinskiy.marathon.report.attachment.AttachmentProvider +import java.util.UUID class AdamScreenCaptureTestRunListener( private val pool: DevicePoolId, @@ -27,14 +29,30 @@ class AdamScreenCaptureTestRunListener( val screenshots = testMetrics.filterKeys { it == "com.malinskiy.adam.junit4.android.screencapture.AdamScreenCaptureProcessor.v1" } screenshots.values.forEach { path -> val extension = path.substringAfterLast('.') - val attachmentType = when (extension) { - "jpeg", "jpg" -> AttachmentType.SCREENSHOT_JPEG - "png" -> AttachmentType.SCREENSHOT_PNG - "webp" -> AttachmentType.SCREENSHOT_WEBP - else -> null + var attachmentType: AttachmentType? = null + var fileType: FileType? = null + when (extension) { + "jpeg", "jpg" -> { + attachmentType = AttachmentType.SCREENSHOT_JPEG + fileType = FileType.SCREENSHOT_JPG + } + "png" -> { + attachmentType = AttachmentType.SCREENSHOT_PNG + fileType = FileType.SCREENSHOT_PNG + } + "webp" -> { + attachmentType = AttachmentType.SCREENSHOT_WEBP + fileType = FileType.SCREENSHOT_WEBP + } + "gif" -> { + attachmentType = AttachmentType.SCREENSHOT_GIF + fileType = FileType.SCREENSHOT_GIF + } + else -> Unit } - if (attachmentType != null) { - val localFile = fileManager.createScreenshotFile(extension, pool, device.toDeviceInfo(), test.toTest(), testBatchId) + + if (attachmentType != null && fileType != null) { + val localFile = fileManager.createFile(fileType, pool, device.toDeviceInfo(), test.toTest(), testBatchId = testBatchId, id = UUID.randomUUID().toString()) device.safePullFile(path, localFile.absolutePath) logger.debug { "Received screen capture file $path" } attachmentListeners.forEach { diff --git a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt index 1427493d4..915d05912 100644 --- a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt +++ b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt @@ -105,7 +105,7 @@ class ScreenCapturerTestRunListener( val existingRecording = fileManager.createFile(FileType.SCREENSHOT, pool, device.toDeviceInfo(), id.toTest(), testBatchId) if (existingRecording.length() > 0) { //Moving existing recording for a test as a failure for a batch - existingRecording.renameTo(fileManager.createFile(FileType.SCREENSHOT, pool, device.toDeviceInfo(), testBatchId)) + existingRecording.renameTo(fileManager.createFile(FileType.SCREENSHOT, pool, device.toDeviceInfo(), testBatchId = testBatchId)) } } } diff --git a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/video/ScreenRecorderTestBatchListener.kt b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/video/ScreenRecorderTestBatchListener.kt index c7c78aa8b..709633193 100644 --- a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/video/ScreenRecorderTestBatchListener.kt +++ b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/video/ScreenRecorderTestBatchListener.kt @@ -135,7 +135,7 @@ class ScreenRecorderTestBatchListener( * This can be called both when test times out and device unavailable */ private suspend fun pullLastBatchVideo(remoteFilePath: String) { - val localVideoFile = fileManager.createFile(FileType.VIDEO, pool, device.toDeviceInfo(), testBatchId) + val localVideoFile = fileManager.createFile(FileType.VIDEO, pool, device.toDeviceInfo(), testBatchId = testBatchId) val millis = measureTimeMillis { device.safePullFile(remoteFilePath, localVideoFile.toString()) } diff --git a/vendor/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/executor/listeners/filesync/FileSyncTestRunListenerTest.kt b/vendor/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/executor/listeners/filesync/FileSyncTestRunListenerTest.kt index a9822a1ad..3f826323c 100644 --- a/vendor/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/executor/listeners/filesync/FileSyncTestRunListenerTest.kt +++ b/vendor/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/executor/listeners/filesync/FileSyncTestRunListenerTest.kt @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource +import org.mockito.kotlin.eq import java.io.File import kotlin.io.path.createTempDirectory @@ -63,9 +64,6 @@ class FileSyncTestRunListenerTest { val device = TestDeviceFactory.create(client, configuration, mock()) val poolId = DevicePoolId("testpool") val fileManager = mock() - whenever(fileManager.createFolder(any())).thenReturn(temp) - whenever(fileManager.createFolder(any(), any())).thenReturn(temp) - whenever(fileManager.createFolder(any(), any())).thenReturn(temp) whenever(fileManager.createFolder(any(), any(), any())).thenReturn(temp) val androidConfiguration = configuration.vendorConfiguration as VendorConfiguration.AndroidConfiguration @@ -92,9 +90,9 @@ class FileSyncTestRunListenerTest { AggregationMode.DEVICE_AND_POOL -> verify(fileManager, times(1)).createFolder( FolderType.DEVICE_FILES, poolId, - device.toDeviceInfo() + device = device.toDeviceInfo() ) - AggregationMode.DEVICE -> verify(fileManager, times(1)).createFolder(FolderType.DEVICE_FILES, device.toDeviceInfo()) + AggregationMode.DEVICE -> verify(fileManager, times(1)).createFolder(FolderType.DEVICE_FILES, device = device.toDeviceInfo()) AggregationMode.POOL -> verify(fileManager, times(1)).createFolder(FolderType.DEVICE_FILES, poolId) AggregationMode.TEST_RUN -> verify(fileManager, times(1)).createFolder(FolderType.DEVICE_FILES) } @@ -169,7 +167,7 @@ class FileSyncTestRunListenerTest { ) val device = TestDeviceFactory.create(client, configuration, mock()) val poolId = DevicePoolId("testpool") - val fileManager = FileManager(1024, tempDir) + val fileManager = FileManager(1024, configuration.outputConfiguration.maxFilename, tempDir) val androidConfiguration = configuration.vendorConfiguration as VendorConfiguration.AndroidConfiguration val listener = FileSyncTestRunListener(poolId, device, androidConfiguration.fileSyncConfiguration, fileManager) @@ -222,7 +220,7 @@ class FileSyncTestRunListenerTest { ) val device = TestDeviceFactory.create(client, configuration, mock()) val poolId = DevicePoolId("testpool") - val fileManager = FileManager(1024, tempDir) + val fileManager = FileManager(1024, configuration.outputConfiguration.maxFilename, tempDir) val androidConfiguration = configuration.vendorConfiguration as VendorConfiguration.AndroidConfiguration val listener = FileSyncTestRunListener(poolId, device, androidConfiguration.fileSyncConfiguration, fileManager) diff --git a/vendor/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListenerTest.kt b/vendor/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListenerTest.kt index 27d2cfba4..3269a9e90 100644 --- a/vendor/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListenerTest.kt +++ b/vendor/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListenerTest.kt @@ -105,9 +105,9 @@ class ScreenCapturerTestRunListenerTest { } device.setup() - whenever(fileManager.createFile(FileType.SCREENSHOT, devicePoolId, device.toDeviceInfo(), test1.toTest(), batch.id)) + whenever(fileManager.createFile(FileType.SCREENSHOT, devicePoolId, device.toDeviceInfo(), test = test1.toTest(), testBatchId = batch.id)) .thenReturn(screenshot) - whenever(fileManager.createFile(FileType.SCREENSHOT, devicePoolId, device.toDeviceInfo(), batch.id)) + whenever(fileManager.createFile(FileType.SCREENSHOT, devicePoolId, device.toDeviceInfo(), testBatchId = batch.id)) .thenReturn(screenshotBatch) val listener = ScreenCapturerTestRunListener( @@ -126,8 +126,8 @@ class ScreenCapturerTestRunListenerTest { delay(300) listener.testRunFailed("Problems are all around us") - verify(fileManager, times(2)).createFile(FileType.SCREENSHOT, devicePoolId, device.toDeviceInfo(), test1.toTest(), batch.id) - verify(fileManager, times(1)).createFile(FileType.SCREENSHOT, devicePoolId, device.toDeviceInfo(), batch.id) + verify(fileManager, times(2)).createFile(FileType.SCREENSHOT, devicePoolId, device.toDeviceInfo(), test = test1.toTest(), testBatchId = batch.id) + verify(fileManager, times(1)).createFile(FileType.SCREENSHOT, devicePoolId, device.toDeviceInfo(), testBatchId = batch.id) } } diff --git a/vendor/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/executor/listeners/video/ScreenRecorderTestRunListenerTest.kt b/vendor/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/executor/listeners/video/ScreenRecorderTestRunListenerTest.kt index 7522501d5..c7af1ef59 100644 --- a/vendor/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/executor/listeners/video/ScreenRecorderTestRunListenerTest.kt +++ b/vendor/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/executor/listeners/video/ScreenRecorderTestRunListenerTest.kt @@ -193,7 +193,7 @@ class ScreenRecorderTestRunListenerTest { } device.setup() - whenever(fileManager.createFile(FileType.VIDEO, devicePoolId, device.toDeviceInfo(), batch.id)) + whenever(fileManager.createFile(FileType.VIDEO, devicePoolId, device.toDeviceInfo(), testBatchId = batch.id)) .thenReturn(videoFile) listener.testRunStarted("Testing", 1) diff --git a/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/device/SimulatorFactory.kt b/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/device/SimulatorFactory.kt index 21e365ee3..f886a4db5 100644 --- a/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/device/SimulatorFactory.kt +++ b/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/device/SimulatorFactory.kt @@ -25,7 +25,11 @@ class SimulatorFactory( private val timer: Timer, ) { private val logger = MarathonLogging.logger {} - private val fileManager = FileManager(configuration.outputConfiguration.maxPath, configuration.outputDir) + private val fileManager = FileManager( + configuration.outputConfiguration.maxPath, + configuration.outputConfiguration.maxFilename, + configuration.outputDir + ) suspend fun create( commandExecutor: CommandExecutor, diff --git a/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/executor/listener/ResultBundleRunListener.kt b/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/executor/listener/ResultBundleRunListener.kt index 150f32a02..bceb62c19 100644 --- a/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/executor/listener/ResultBundleRunListener.kt +++ b/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/executor/listener/ResultBundleRunListener.kt @@ -24,7 +24,7 @@ class ResultBundleRunListener( super.afterTestRun() val remotePath = device.remoteFileManager.remoteXcresultFile(batch) if (xcresultConfiguration.pull) { - val localPath = File(fileManager.createFolder(FolderType.DEVICE_FILES, poolId, device.toDeviceInfo()), "xcresult").apply { mkdirs() } + val localPath = File(fileManager.createFolder(FolderType.DEVICE_FILES, poolId, device = device.toDeviceInfo()), "xcresult").apply { mkdirs() } if (!device.pullFolder(remotePath, localPath)) { logger.warn { "failed to pull result bundle" } } else { diff --git a/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/executor/listener/screenshot/ScreenCapturerTestRunListener.kt b/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/executor/listener/screenshot/ScreenCapturerTestRunListener.kt index baf312572..dc1808b43 100644 --- a/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/executor/listener/screenshot/ScreenCapturerTestRunListener.kt +++ b/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/executor/listener/screenshot/ScreenCapturerTestRunListener.kt @@ -84,10 +84,10 @@ class ScreenCapturerTestRunListener( override suspend fun testRunFailed(errorMessage: String, reason: DeviceFailureReason) { supervisorJob?.cancelAndJoin() lastTestIdentifier?.let { id -> - val existingRecording = fileManager.createFile(FileType.SCREENSHOT, pool, device.toDeviceInfo(), id, testBatchId) + val existingRecording = fileManager.createFile(FileType.SCREENSHOT, pool, device.toDeviceInfo(), test = id, testBatchId = testBatchId) if (existingRecording.length() > 0) { //Moving existing recording for a test as a failure for a batch - existingRecording.renameTo(fileManager.createFile(FileType.SCREENSHOT, pool, device.toDeviceInfo(), testBatchId)) + existingRecording.renameTo(fileManager.createFile(FileType.SCREENSHOT, pool, device.toDeviceInfo(), testBatchId = testBatchId)) } } } diff --git a/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/executor/listener/video/ScreenRecordingListener.kt b/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/executor/listener/video/ScreenRecordingListener.kt index 6a447cfbe..750b78ed3 100644 --- a/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/executor/listener/video/ScreenRecordingListener.kt +++ b/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/executor/listener/video/ScreenRecordingListener.kt @@ -158,7 +158,7 @@ class ScreenRecordingListener( } private suspend fun pullLastBatchVideo(remoteFilePath: String) { - val localVideoFile = fileManager.createFile(FileType.VIDEO, pool, device.toDeviceInfo(), testBatchId) + val localVideoFile = fileManager.createFile(FileType.VIDEO, pool, device.toDeviceInfo(), testBatchId = testBatchId) val millis = measureTimeMillis { device.pullFile(remoteFilePath, localVideoFile) } diff --git a/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/MarathonFactory.kt b/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/MarathonFactory.kt index 0ac9fca13..cd39b7689 100644 --- a/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/MarathonFactory.kt +++ b/vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/MarathonFactory.kt @@ -1,6 +1,5 @@ package com.malinskiy.marathon.test.factory -import com.google.gson.Gson import com.google.gson.GsonBuilder import com.malinskiy.marathon.Marathon import com.malinskiy.marathon.config.Configuration @@ -9,7 +8,6 @@ import com.malinskiy.marathon.di.analyticsModule import com.malinskiy.marathon.execution.TestParser import com.malinskiy.marathon.execution.bundle.TestBundleIdentifier import com.malinskiy.marathon.execution.command.parse.MarathonTestParseCommand -import com.malinskiy.marathon.execution.progress.ProgressReporter import com.malinskiy.marathon.io.FileManager import com.malinskiy.marathon.json.FileSerializer import com.malinskiy.marathon.log.MarathonLogConfigurator @@ -40,7 +38,11 @@ class MarathonFactory { val coreTestModule = module { single { val configuration = get() - FileManager(configuration.outputConfiguration.maxPath, configuration.outputDir) + FileManager( + configuration.outputConfiguration.maxPath, + configuration.outputConfiguration.maxFilename, + configuration.outputDir + ) } single { GsonBuilder() .registerTypeAdapter(File::class.java, FileSerializer())