From 7afcbfb9b7facea4ae24849dc8c62ee03b5089b7 Mon Sep 17 00:00:00 2001 From: ritwickrajmakhal Date: Wed, 8 Oct 2025 19:50:14 +0530 Subject: [PATCH 1/4] Add upload progress tracking feature - Introduced `showUploadProgress` parameter in relevant classes to enable upload progress tracking. - Updated `README.md` to document the new feature. - Enhanced `UploaderUtil` to support progress tracking during file uploads. - Implemented `ProgressRequestBody` to handle progress updates and display in the console. --- README.md | 3 + build.gradle | 2 +- .../github/lambdatest/gradle/AppUploader.java | 19 ++- .../lambdatest/gradle/LambdaTestTask.java | 12 +- .../lambdatest/gradle/LambdaUploaderTask.java | 12 +- .../gradle/ProgressRequestBody.java | 130 ++++++++++++++++++ .../lambdatest/gradle/TestSuiteUploader.java | 23 +++- .../lambdatest/gradle/UploaderUtil.java | 62 ++++++++- 8 files changed, 252 insertions(+), 11 deletions(-) create mode 100644 src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java diff --git a/README.md b/README.md index 0c63ab5..1ab039f 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ runLambdaTest { isFlutter = true //if you are running flutter dart tests appId = "lt//1234343" //provide this only if you have already uploaded the app testSuiteId = "lt//1223444" //provide this only if you have already uploaded the app + showUploadProgress = true //enable upload progress tracking in console } ``` @@ -50,6 +51,7 @@ uploadApkToLambdaTest { accessKey = 'yourLambdaTestAccessKey' appFilePath = 'pathToYourAppFile' testSuiteFilePath = 'pathToYourTestSuite' + showUploadProgress = true //enable upload progress tracking in console } ``` @@ -68,6 +70,7 @@ The following capabilities are supported: - `build`: Set the name of the Espresso test build. Example: My Espresso Build. - `geoLocation`: Set the geolocation country code if you want to enable the same in your test. Example - FR. - `tunnel`, `tunnelName`: Set tunnel as true and provide the tunnelName such as NewTunnel as needed if you are running a tunnel. +- `showUploadProgress`: Display real-time upload progress in the console with percentage and data transferred. Example: true. - `appFilePath` : Path of your app file (this will be uploaded to LambdaTest) diff --git a/build.gradle b/build.gradle index 97afcfb..f6e4de2 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id 'com.diffplug.spotless' version '6.25.0' } -version = '1.0.6' +version = '1.0.7' group = 'io.github.lambdatest' repositories { diff --git a/src/main/java/io/github/lambdatest/gradle/AppUploader.java b/src/main/java/io/github/lambdatest/gradle/AppUploader.java index 4ce322d..475a63e 100644 --- a/src/main/java/io/github/lambdatest/gradle/AppUploader.java +++ b/src/main/java/io/github/lambdatest/gradle/AppUploader.java @@ -19,6 +19,7 @@ public class AppUploader { private String username; private String accessKey; private String appFilePath; + private boolean showProgress; /** * Creates a new AppUploader instance with the specified credentials and file path. @@ -28,6 +29,20 @@ public class AppUploader { * @param appFilePath The path to the application file to be uploaded */ public AppUploader(String username, String accessKey, String appFilePath) { + this(username, accessKey, appFilePath, false); + } + + /** + * Creates a new AppUploader instance with the specified credentials, file path, and progress + * tracking option. + * + * @param username The LambdaTest account username + * @param accessKey The LambdaTest account access key + * @param appFilePath The path to the application file to be uploaded + * @param showProgress Whether to display upload progress in the console + */ + public AppUploader( + String username, String accessKey, String appFilePath, boolean showProgress) { if (username == null) throw new IllegalArgumentException("Username cannot be null"); if (accessKey == null) throw new IllegalArgumentException("Access Key cannot be null"); if (appFilePath == null) throw new IllegalArgumentException("App File Path cannot be null"); @@ -35,6 +50,7 @@ public AppUploader(String username, String accessKey, String appFilePath) { this.username = username; this.accessKey = accessKey; this.appFilePath = appFilePath; + this.showProgress = showProgress; } /** @@ -49,7 +65,8 @@ public CompletableFuture uploadAppAsync() { () -> { try { String appId = - UploaderUtil.uploadAndGetId(username, accessKey, appFilePath); + UploaderUtil.uploadAndGetId( + username, accessKey, appFilePath, showProgress, "App"); logger.info("Uploaded app ID: {}", appId); return appId; } catch (IOException e) { diff --git a/src/main/java/io/github/lambdatest/gradle/LambdaTestTask.java b/src/main/java/io/github/lambdatest/gradle/LambdaTestTask.java index 44af815..f90a88a 100644 --- a/src/main/java/io/github/lambdatest/gradle/LambdaTestTask.java +++ b/src/main/java/io/github/lambdatest/gradle/LambdaTestTask.java @@ -45,6 +45,7 @@ public class LambdaTestTask extends DefaultTask { private String appId; private String testSuiteId; private Integer queueTimeout; + private Boolean showUploadProgress; /** * Executes the LambdaTest task, which includes uploading the application and test suite, @@ -64,16 +65,19 @@ public void runLambdaTest() { CompletableFuture appIdFuture = null; CompletableFuture testSuiteIdFuture = null; + boolean progressEnabled = showUploadProgress != null && showUploadProgress; + if (appId == null && appFilePath != null) { logger.info("Uploading app..."); - AppUploader appUploader = new AppUploader(username, accessKey, appFilePath); + AppUploader appUploader = + new AppUploader(username, accessKey, appFilePath, progressEnabled); appIdFuture = appUploader.uploadAppAsync(); } if (testSuiteId == null && testSuiteFilePath != null) { logger.info("Uploading test suite..."); TestSuiteUploader testSuiteUploader = - new TestSuiteUploader(username, accessKey, testSuiteFilePath); + new TestSuiteUploader(username, accessKey, testSuiteFilePath, progressEnabled); testSuiteIdFuture = testSuiteUploader.uploadTestSuiteAsync(); } @@ -217,4 +221,8 @@ public void setTestSuiteId(String testSuiteId) { this.testSuiteId = testSuiteId; } } + + public void setShowUploadProgress(Boolean showUploadProgress) { + this.showUploadProgress = showUploadProgress; + } } diff --git a/src/main/java/io/github/lambdatest/gradle/LambdaUploaderTask.java b/src/main/java/io/github/lambdatest/gradle/LambdaUploaderTask.java index dc65649..bed52dc 100644 --- a/src/main/java/io/github/lambdatest/gradle/LambdaUploaderTask.java +++ b/src/main/java/io/github/lambdatest/gradle/LambdaUploaderTask.java @@ -21,6 +21,7 @@ public class LambdaUploaderTask extends DefaultTask { private String accessKey; private String appFilePath; private String testSuiteFilePath; + private Boolean showUploadProgress; @TaskAction public void uploadApkToLambdaTest() { @@ -31,16 +32,19 @@ public void uploadApkToLambdaTest() { CompletableFuture testSuiteIdFuture = null; logger.lifecycle("Starting LambdaTest APK Uploader task..."); + boolean progressEnabled = showUploadProgress != null && showUploadProgress; + if (appFilePath != null) { logger.lifecycle("Uploading app ..."); - AppUploader appUploader = new AppUploader(username, accessKey, appFilePath); + AppUploader appUploader = + new AppUploader(username, accessKey, appFilePath, progressEnabled); appIdFuture = appUploader.uploadAppAsync(); } if (testSuiteFilePath != null) { logger.lifecycle("Uploading test suite ..."); TestSuiteUploader testSuiteUploader = - new TestSuiteUploader(username, accessKey, testSuiteFilePath); + new TestSuiteUploader(username, accessKey, testSuiteFilePath, progressEnabled); testSuiteIdFuture = testSuiteUploader.uploadTestSuiteAsync(); } @@ -80,4 +84,8 @@ public void setAppFilePath(String appFilePath) { public void setTestSuiteFilePath(String testSuiteFilePath) { this.testSuiteFilePath = testSuiteFilePath; } + + public void setShowUploadProgress(Boolean showUploadProgress) { + this.showUploadProgress = showUploadProgress; + } } diff --git a/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java b/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java new file mode 100644 index 0000000..208e4c7 --- /dev/null +++ b/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java @@ -0,0 +1,130 @@ +package io.github.lambdatest.gradle; + +import java.io.File; +import java.io.IOException; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.Buffer; +import okio.BufferedSink; +import okio.ForwardingSink; +import okio.Okio; +import okio.Sink; + +/** + * A RequestBody wrapper that tracks upload progress and displays it in the console. This class uses + * OkHttp's native Sink and BufferedSink APIs to monitor the upload progress. + */ +public class ProgressRequestBody extends RequestBody { + + private final RequestBody delegate; + private final File file; + private final ProgressCallback progressCallback; + + /** Interface for progress callbacks. */ + public interface ProgressCallback { + void onProgress(long bytesWritten, long totalBytes, float percentage); + } + + /** + * Creates a new ProgressRequestBody that wraps the given RequestBody. + * + * @param delegate The original RequestBody to wrap + * @param file The file being uploaded (used to get total size) + * @param progressCallback Callback to receive progress updates + */ + public ProgressRequestBody(RequestBody delegate, File file, ProgressCallback progressCallback) { + this.delegate = delegate; + this.file = file; + this.progressCallback = progressCallback; + } + + @Override + public MediaType contentType() { + return delegate.contentType(); + } + + @Override + public long contentLength() throws IOException { + return delegate.contentLength(); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + long contentLength = contentLength(); + ProgressSink progressSink = new ProgressSink(sink, contentLength, progressCallback); + BufferedSink bufferedSink = Okio.buffer(progressSink); + + delegate.writeTo(bufferedSink); + bufferedSink.flush(); + } + + /** + * Custom Sink implementation that tracks progress while forwarding data to the original sink. + */ + private static class ProgressSink extends ForwardingSink { + private final long totalBytes; + private final ProgressCallback progressCallback; + private long bytesWritten = 0L; + private long lastLoggedPercentage = -1L; + private long lastUpdateTime = System.currentTimeMillis(); + + public ProgressSink(Sink delegate, long totalBytes, ProgressCallback progressCallback) { + super(delegate); + this.totalBytes = totalBytes; + this.progressCallback = progressCallback; + } + + @Override + public void write(Buffer source, long byteCount) throws IOException { + super.write(source, byteCount); + bytesWritten += byteCount; + + if (progressCallback != null) { + float percentage = totalBytes > 0 ? (bytesWritten * 100.0f) / totalBytes : 0f; + long currentTime = System.currentTimeMillis(); + + // Update every 1% or every 250ms for more real-time updates + long currentPercentage = Math.round(percentage); + boolean timeBased = (currentTime - lastUpdateTime) >= 250; // 250ms intervals + + if (currentPercentage != lastLoggedPercentage || timeBased) { + progressCallback.onProgress(bytesWritten, totalBytes, percentage); + lastLoggedPercentage = currentPercentage; + lastUpdateTime = currentTime; + } + } + } + } + + /** + * Creates a console-based progress callback that displays upload progress. + * + * @param fileName The name of the file being uploaded + * @return A ProgressCallback that logs to console + */ + public static ProgressCallback createConsoleCallback(String fileName) { + return (bytesWritten, totalBytes, percentage) -> { + String formattedBytes = formatBytes(bytesWritten); + String formattedTotal = formatBytes(totalBytes); + + // Use println with newlines to avoid conflicts between concurrent uploads + System.out.printf( + "\u001B[33mUploading %s: %.1f%% (%s / %s)\u001B[0m\n", + fileName, percentage, formattedBytes, formattedTotal); + System.out.flush(); // Force immediate output to bypass buffering + }; + } + + /** + * Formats bytes into human-readable format. + * + * @param bytes The number of bytes to format + * @return Formatted string (e.g., "1.5 MB", "512 KB") + */ + public static String formatBytes(long bytes) { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0); + if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); + return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0)); + } +} diff --git a/src/main/java/io/github/lambdatest/gradle/TestSuiteUploader.java b/src/main/java/io/github/lambdatest/gradle/TestSuiteUploader.java index a91983e..89a315c 100644 --- a/src/main/java/io/github/lambdatest/gradle/TestSuiteUploader.java +++ b/src/main/java/io/github/lambdatest/gradle/TestSuiteUploader.java @@ -16,6 +16,7 @@ public class TestSuiteUploader { private String username; private String accessKey; private String testSuiteFilePath; + private boolean showProgress; /** * Creates a new TestSuiteUploader instance with the specified credentials and file path. @@ -25,9 +26,24 @@ public class TestSuiteUploader { * @param testSuiteFilePath The path to the test suite file to be uploaded */ public TestSuiteUploader(String username, String accessKey, String testSuiteFilePath) { + this(username, accessKey, testSuiteFilePath, false); + } + + /** + * Creates a new TestSuiteUploader instance with the specified credentials, file path, and + * progress tracking option. + * + * @param username The LambdaTest account username + * @param accessKey The LambdaTest account access key + * @param testSuiteFilePath The path to the test suite file to be uploaded + * @param showProgress Whether to display upload progress in the console + */ + public TestSuiteUploader( + String username, String accessKey, String testSuiteFilePath, boolean showProgress) { this.username = username; this.accessKey = accessKey; this.testSuiteFilePath = testSuiteFilePath; + this.showProgress = showProgress; } /** @@ -42,7 +58,12 @@ public CompletableFuture uploadTestSuiteAsync() { () -> { try { String testSuiteId = - UploaderUtil.uploadAndGetId(username, accessKey, testSuiteFilePath); + UploaderUtil.uploadAndGetId( + username, + accessKey, + testSuiteFilePath, + showProgress, + "Test Suite"); logger.info("Uploaded test suite ID: {}", testSuiteId); return testSuiteId; } catch (IOException e) { diff --git a/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java b/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java index d692d42..870e569 100644 --- a/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java +++ b/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java @@ -40,6 +40,50 @@ private UploaderUtil() { */ public static String uploadAndGetId(String username, String accessKey, String filePath) throws IOException { + return uploadAndGetId(username, accessKey, filePath, false, null); + } + + /** + * Uploads a file to LambdaTest and returns its ID with optional progress tracking. + * + * @implNote This method sends the file to {@link Constants#API_URL} and handles the multipart + * form data construction and response parsing. When showProgress is true, it uses {@link + * ProgressRequestBody} to track and display upload progress. + * @param username The LambdaTest account username + * @param accessKey The LambdaTest account access key + * @param filePath The path to the file to be uploaded + * @param showProgress Whether to display upload progress in the console + * @return The ID of the uploaded file + * @throws IOException if there's an error during file upload or response parsing + */ + public static String uploadAndGetId( + String username, String accessKey, String filePath, boolean showProgress) + throws IOException { + return uploadAndGetId(username, accessKey, filePath, showProgress, null); + } + + /** + * Uploads a file to LambdaTest and returns its ID with optional progress tracking and custom + * prefix. + * + * @implNote This method sends the file to {@link Constants#API_URL} and handles the multipart + * form data construction and response parsing. When showProgress is true, it uses {@link + * ProgressRequestBody} to track and display upload progress. + * @param username The LambdaTest account username + * @param accessKey The LambdaTest account access key + * @param filePath The path to the file to be uploaded + * @param showProgress Whether to display upload progress in the console + * @param progressPrefix Optional prefix for progress display (e.g., "App", "Test Suite") + * @return The ID of the uploaded file + * @throws IOException if there's an error during file upload or response parsing + */ + public static String uploadAndGetId( + String username, + String accessKey, + String filePath, + boolean showProgress, + String progressPrefix) + throws IOException { OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(1, TimeUnit.MINUTES) // Increase connection timeout @@ -47,16 +91,26 @@ public static String uploadAndGetId(String username, String accessKey, String fi .writeTimeout(0, TimeUnit.MILLISECONDS) // Increase write timeout .build(); + File file = new File(filePath); MediaType mediaType = MediaType.parse("application/octet-stream"); + RequestBody fileRequestBody = RequestBody.create(file, mediaType); + RequestBody body = new MultipartBody.Builder() .setType(MultipartBody.FORM) - .addFormDataPart( - "appFile", - filePath, - RequestBody.create(new File(filePath), mediaType)) + .addFormDataPart("appFile", filePath, fileRequestBody) .addFormDataPart("type", "espresso-android") .build(); + + // Wrap the entire multipart body with progress tracking if requested + if (showProgress) { + String fileName = file.getName(); + String displayName = + progressPrefix != null ? progressPrefix + " - " + fileName : fileName; + ProgressRequestBody.ProgressCallback callback = + ProgressRequestBody.createConsoleCallback(displayName); + body = new ProgressRequestBody(body, file, callback); + } Request request = new Request.Builder() .url(Constants.API_URL) From 180906cd5472ec2060ffe7f2e1898ac101c7ffa6 Mon Sep 17 00:00:00 2001 From: ritwickrajmakhal Date: Thu, 9 Oct 2025 17:49:13 +0530 Subject: [PATCH 2/4] Enhance upload progress tracking with console output and cleanup functionality --- .../lambdatest/gradle/LambdaTestTask.java | 40 ++++- .../lambdatest/gradle/LambdaUploaderTask.java | 54 ++++-- .../gradle/ProgressRequestBody.java | 32 ++-- .../lambdatest/gradle/ProgressTracker.java | 165 ++++++++++++++++++ .../lambdatest/gradle/UploaderUtil.java | 5 +- 5 files changed, 266 insertions(+), 30 deletions(-) create mode 100644 src/main/java/io/github/lambdatest/gradle/ProgressTracker.java diff --git a/src/main/java/io/github/lambdatest/gradle/LambdaTestTask.java b/src/main/java/io/github/lambdatest/gradle/LambdaTestTask.java index f90a88a..8c23636 100644 --- a/src/main/java/io/github/lambdatest/gradle/LambdaTestTask.java +++ b/src/main/java/io/github/lambdatest/gradle/LambdaTestTask.java @@ -59,23 +59,29 @@ public class LambdaTestTask extends DefaultTask { */ @TaskAction public void runLambdaTest() { - logger.info("Starting LambdaTest task..."); + boolean progressEnabled = showUploadProgress != null && showUploadProgress; + + if (!progressEnabled) { + logger.info("Starting LambdaTest task..."); + } // Upload app CompletableFuture appIdFuture = null; CompletableFuture testSuiteIdFuture = null; - boolean progressEnabled = showUploadProgress != null && showUploadProgress; - if (appId == null && appFilePath != null) { - logger.info("Uploading app..."); + if (!progressEnabled) { + logger.info("Uploading app..."); + } AppUploader appUploader = new AppUploader(username, accessKey, appFilePath, progressEnabled); appIdFuture = appUploader.uploadAppAsync(); } if (testSuiteId == null && testSuiteFilePath != null) { - logger.info("Uploading test suite..."); + if (!progressEnabled) { + logger.info("Uploading test suite..."); + } TestSuiteUploader testSuiteUploader = new TestSuiteUploader(username, accessKey, testSuiteFilePath, progressEnabled); testSuiteIdFuture = testSuiteUploader.uploadTestSuiteAsync(); @@ -85,14 +91,34 @@ public void runLambdaTest() { try { if (appIdFuture != null) { appId = appIdFuture.join(); - logger.info("App uploaded successfully with ID: {}", appId); + if (!progressEnabled) { + logger.info("App uploaded successfully with ID: {}", appId); + } } if (testSuiteIdFuture != null) { testSuiteId = testSuiteIdFuture.join(); - logger.info("Test suite uploaded successfully with ID: {}", testSuiteId); + if (!progressEnabled) { + logger.info("Test suite uploaded successfully with ID: {}", testSuiteId); + } + } + + // Clear progress display if enabled + if (progressEnabled) { + ProgressTracker.cleanup(); + // Show success messages after progress cleanup + if (appIdFuture != null) { + logger.info("App uploaded successfully with ID: {}", appId); + } + if (testSuiteIdFuture != null) { + logger.info("Test suite uploaded successfully with ID: {}", testSuiteId); + } } } catch (CompletionException e) { + // Cleanup progress display on error + if (progressEnabled) { + ProgressTracker.cleanup(); + } logger.error("Failed to execute tasks: {}", e); throw new RuntimeException(e); } diff --git a/src/main/java/io/github/lambdatest/gradle/LambdaUploaderTask.java b/src/main/java/io/github/lambdatest/gradle/LambdaUploaderTask.java index bed52dc..927b779 100644 --- a/src/main/java/io/github/lambdatest/gradle/LambdaUploaderTask.java +++ b/src/main/java/io/github/lambdatest/gradle/LambdaUploaderTask.java @@ -26,23 +26,31 @@ public class LambdaUploaderTask extends DefaultTask { @TaskAction public void uploadApkToLambdaTest() { // Generated after upload of app and test suite - String appId; - String testSuiteId; + String appId = null; + String testSuiteId = null; CompletableFuture appIdFuture = null; CompletableFuture testSuiteIdFuture = null; - logger.lifecycle("Starting LambdaTest APK Uploader task..."); boolean progressEnabled = showUploadProgress != null && showUploadProgress; + // Only log to lifecycle if progress is disabled + if (!progressEnabled) { + logger.lifecycle("Starting LambdaTest APK Uploader task..."); + } + if (appFilePath != null) { - logger.lifecycle("Uploading app ..."); + if (!progressEnabled) { + logger.lifecycle("Uploading app ..."); + } AppUploader appUploader = new AppUploader(username, accessKey, appFilePath, progressEnabled); appIdFuture = appUploader.uploadAppAsync(); } if (testSuiteFilePath != null) { - logger.lifecycle("Uploading test suite ..."); + if (!progressEnabled) { + logger.lifecycle("Uploading test suite ..."); + } TestSuiteUploader testSuiteUploader = new TestSuiteUploader(username, accessKey, testSuiteFilePath, progressEnabled); testSuiteIdFuture = testSuiteUploader.uploadTestSuiteAsync(); @@ -51,21 +59,47 @@ public void uploadApkToLambdaTest() { try { if (appIdFuture != null) { appId = appIdFuture.join(); - logger.lifecycle("\u001B[32mApp uploaded successfully with ID: {}\u001B[0m", appId); + if (!progressEnabled) { + logger.lifecycle( + "\u001B[32mApp uploaded successfully with ID: {}\u001B[0m", appId); + } } if (testSuiteIdFuture != null) { testSuiteId = testSuiteIdFuture.join(); - logger.lifecycle( - "\u001B[32mTest suite uploaded successfully with ID: {}\u001B[0m", - testSuiteId); + if (!progressEnabled) { + logger.lifecycle( + "\u001B[32mTest suite uploaded successfully with ID: {}\u001B[0m", + testSuiteId); + } + } + + // Clear progress display if enabled + if (progressEnabled) { + ProgressTracker.cleanup(); + // Show success messages after progress cleanup + if (appIdFuture != null) { + logger.lifecycle( + "\u001B[32mApp uploaded successfully with ID: {}\u001B[0m", appId); + } + if (testSuiteIdFuture != null) { + logger.lifecycle( + "\u001B[32mTest suite uploaded successfully with ID: {}\u001B[0m", + testSuiteId); + } } } catch (CompletionException e) { + // Cleanup progress display on error + if (progressEnabled) { + ProgressTracker.cleanup(); + } logger.error("Failed to execute LambdaTest APK Uploader task : {}", e); throw new RuntimeException(e); } - logger.lifecycle("Completed LambdaTest APK Uploader task ..."); + if (!progressEnabled) { + logger.lifecycle("Completed LambdaTest APK Uploader task ..."); + } } // Setter functions for the task diff --git a/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java b/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java index 208e4c7..df06587 100644 --- a/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java +++ b/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java @@ -97,24 +97,36 @@ public void write(Buffer source, long byteCount) throws IOException { } /** - * Creates a console-based progress callback that displays upload progress. + * Creates a console-based progress callback that displays upload progress using the + * ProgressTracker for clean, fixed-line output. * + * @param uploadId The unique identifier for this upload (e.g., "App", "Test Suite") * @param fileName The name of the file being uploaded * @return A ProgressCallback that logs to console */ - public static ProgressCallback createConsoleCallback(String fileName) { + public static ProgressCallback createConsoleCallback(String uploadId, String fileName) { return (bytesWritten, totalBytes, percentage) -> { - String formattedBytes = formatBytes(bytesWritten); - String formattedTotal = formatBytes(totalBytes); - - // Use println with newlines to avoid conflicts between concurrent uploads - System.out.printf( - "\u001B[33mUploading %s: %.1f%% (%s / %s)\u001B[0m\n", - fileName, percentage, formattedBytes, formattedTotal); - System.out.flush(); // Force immediate output to bypass buffering + ProgressTracker.updateProgress( + uploadId, fileName, percentage, bytesWritten, totalBytes); + + if (percentage >= 100.0f) { + ProgressTracker.completeUpload(uploadId); + } }; } + /** + * Creates a console-based progress callback (legacy version for backward compatibility). + * + * @param fileName The name of the file being uploaded + * @return A ProgressCallback that logs to console + * @deprecated Use {@link #createConsoleCallback(String, String)} instead + */ + @Deprecated + public static ProgressCallback createConsoleCallback(String fileName) { + return createConsoleCallback("Upload", fileName); + } + /** * Formats bytes into human-readable format. * diff --git a/src/main/java/io/github/lambdatest/gradle/ProgressTracker.java b/src/main/java/io/github/lambdatest/gradle/ProgressTracker.java new file mode 100644 index 0000000..4c4609e --- /dev/null +++ b/src/main/java/io/github/lambdatest/gradle/ProgressTracker.java @@ -0,0 +1,165 @@ +package io.github.lambdatest.gradle; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Manages progress tracking for multiple concurrent uploads with clean, fixed-line console output. + * This class ensures that concurrent uploads don't clutter the terminal by maintaining fixed + * positions for each upload's progress line. + */ +public class ProgressTracker { + private static final Map uploadLines = new ConcurrentHashMap<>(); + private static final AtomicInteger nextLineNumber = new AtomicInteger(0); + private static final Object consoleLock = new Object(); + + /** + * Registers a new upload and allocates a line number for its progress display. + * + * @param uploadId Unique identifier for the upload (e.g., "App", "Test Suite") + * @return The allocated line number for this upload + */ + private static int registerUpload(String uploadId) { + return uploadLines.computeIfAbsent( + uploadId, + k -> { + int lineNum = nextLineNumber.getAndIncrement(); + // Print a newline to reserve space for this upload + System.out.println(); + System.out.flush(); + return lineNum; + }); + } + + /** + * Updates the progress display for a specific upload. + * + * @param uploadId The unique identifier for the upload + * @param fileName The name of the file being uploaded + * @param percentage The upload percentage (0-100) + * @param bytesWritten Bytes uploaded so far + * @param totalBytes Total bytes to upload + */ + public static void updateProgress( + String uploadId, + String fileName, + float percentage, + long bytesWritten, + long totalBytes) { + synchronized (consoleLock) { + // Register upload if not already registered (reserves a line) + int lineNumber = registerUpload(uploadId); + int totalLines = uploadLines.size(); + + // Move cursor to the appropriate line + if (lineNumber < totalLines - 1) { + // Not the last line - need to move up + System.out.print(String.format("\u001B[%dA", totalLines - lineNumber - 1)); + } + + // Clear the line and print progress + System.out.print("\r\u001B[K"); // Clear line + String progressBar = createProgressBar(percentage); + String formattedBytes = ProgressRequestBody.formatBytes(bytesWritten); + String formattedTotal = ProgressRequestBody.formatBytes(totalBytes); + + System.out.printf( + "\u001B[33mUploading %-15s %s %.1f%% (%s / %s)\u001B[0m", + uploadId, progressBar, percentage, formattedBytes, formattedTotal); + + // Move cursor back to bottom + if (lineNumber < totalLines - 1) { + System.out.print(String.format("\u001B[%dB", totalLines - lineNumber - 1)); + } + + System.out.flush(); + } + } + + /** + * Creates a visual progress bar. + * + * @param percentage The completion percentage (0-100) + * @return A string representing the progress bar + */ + private static String createProgressBar(float percentage) { + int barLength = 10; + int filled = (int) (percentage / 100.0 * barLength); + StringBuilder bar = new StringBuilder("["); + for (int i = 0; i < barLength; i++) { + bar.append(i < filled ? "#" : "-"); + } + bar.append("]"); + return bar.toString(); + } + + /** + * Completes the progress for an upload and moves to a new line. + * + * @param uploadId The unique identifier for the upload + */ + public static void completeUpload(String uploadId) { + synchronized (consoleLock) { + Integer lineNumber = uploadLines.get(uploadId); + if (lineNumber != null && lineNumber == uploadLines.size() - 1) { + // Only add newline if this is the last upload + System.out.println(); + System.out.flush(); + } + } + } + + /** + * Cleans up all progress lines from the console and resets the tracker. This should be called + * after all uploads are complete to clear the progress display. + */ + public static void cleanup() { + synchronized (consoleLock) { + int totalLines = uploadLines.size(); + if (totalLines > 0) { + // Move to the first line + System.out.print(String.format("\u001B[%dA", totalLines - 1)); + // Clear all progress lines + for (int i = 0; i < totalLines; i++) { + System.out.print("\r\u001B[K"); // Clear current line + if (i < totalLines - 1) { + System.out.println(); // Move to next line + } + } + // Move back to start position + System.out.print(String.format("\u001B[%dA", totalLines - 1)); + System.out.flush(); + } + reset(); + } + } + + /** Resets the progress tracker. Should be called when starting a new set of uploads. */ + public static void reset() { + synchronized (consoleLock) { + uploadLines.clear(); + nextLineNumber.set(0); + } + } + + /** + * Initializes the display with placeholder lines for all uploads. + * + * @param uploadIds Array of upload IDs to register + */ + public static void initializeDisplay(String... uploadIds) { + synchronized (consoleLock) { + reset(); + for (String uploadId : uploadIds) { + registerUpload(uploadId); + System.out.println(); // Reserve a line for each upload + } + // Move cursor back to the beginning + if (uploadIds.length > 0) { + System.out.print(String.format("\u001B[%dA", uploadIds.length)); + System.out.flush(); + } + } + } +} diff --git a/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java b/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java index 870e569..db8ae67 100644 --- a/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java +++ b/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java @@ -105,10 +105,9 @@ public static String uploadAndGetId( // Wrap the entire multipart body with progress tracking if requested if (showProgress) { String fileName = file.getName(); - String displayName = - progressPrefix != null ? progressPrefix + " - " + fileName : fileName; + String uploadId = progressPrefix != null ? progressPrefix : "Upload"; ProgressRequestBody.ProgressCallback callback = - ProgressRequestBody.createConsoleCallback(displayName); + ProgressRequestBody.createConsoleCallback(uploadId, fileName); body = new ProgressRequestBody(body, file, callback); } Request request = From 726cb0d379949a825dcfa8b419bac8846c64e410 Mon Sep 17 00:00:00 2001 From: ritwickrajmakhal Date: Thu, 9 Oct 2025 18:03:49 +0530 Subject: [PATCH 3/4] Refactor ProgressRequestBody to remove file parameter and update related classes for improved upload progress tracking --- .../gradle/ProgressRequestBody.java | 11 +++----- .../lambdatest/gradle/ProgressTracker.java | 28 +++---------------- .../lambdatest/gradle/UploaderUtil.java | 2 +- 3 files changed, 9 insertions(+), 32 deletions(-) diff --git a/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java b/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java index df06587..802db1e 100644 --- a/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java +++ b/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java @@ -1,6 +1,5 @@ package io.github.lambdatest.gradle; -import java.io.File; import java.io.IOException; import okhttp3.MediaType; import okhttp3.RequestBody; @@ -9,6 +8,7 @@ import okio.ForwardingSink; import okio.Okio; import okio.Sink; +import org.jetbrains.annotations.NotNull; /** * A RequestBody wrapper that tracks upload progress and displays it in the console. This class uses @@ -17,7 +17,6 @@ public class ProgressRequestBody extends RequestBody { private final RequestBody delegate; - private final File file; private final ProgressCallback progressCallback; /** Interface for progress callbacks. */ @@ -29,12 +28,10 @@ public interface ProgressCallback { * Creates a new ProgressRequestBody that wraps the given RequestBody. * * @param delegate The original RequestBody to wrap - * @param file The file being uploaded (used to get total size) * @param progressCallback Callback to receive progress updates */ - public ProgressRequestBody(RequestBody delegate, File file, ProgressCallback progressCallback) { + public ProgressRequestBody(RequestBody delegate, ProgressCallback progressCallback) { this.delegate = delegate; - this.file = file; this.progressCallback = progressCallback; } @@ -49,7 +46,7 @@ public long contentLength() throws IOException { } @Override - public void writeTo(BufferedSink sink) throws IOException { + public void writeTo(@NotNull BufferedSink sink) throws IOException { long contentLength = contentLength(); ProgressSink progressSink = new ProgressSink(sink, contentLength, progressCallback); BufferedSink bufferedSink = Okio.buffer(progressSink); @@ -75,7 +72,7 @@ public ProgressSink(Sink delegate, long totalBytes, ProgressCallback progressCal } @Override - public void write(Buffer source, long byteCount) throws IOException { + public void write(@NotNull Buffer source, long byteCount) throws IOException { super.write(source, byteCount); bytesWritten += byteCount; diff --git a/src/main/java/io/github/lambdatest/gradle/ProgressTracker.java b/src/main/java/io/github/lambdatest/gradle/ProgressTracker.java index 4c4609e..a8dc774 100644 --- a/src/main/java/io/github/lambdatest/gradle/ProgressTracker.java +++ b/src/main/java/io/github/lambdatest/gradle/ProgressTracker.java @@ -55,7 +55,7 @@ public static void updateProgress( // Move cursor to the appropriate line if (lineNumber < totalLines - 1) { // Not the last line - need to move up - System.out.print(String.format("\u001B[%dA", totalLines - lineNumber - 1)); + System.out.printf("\u001B[%dA", totalLines - lineNumber - 1); } // Clear the line and print progress @@ -70,7 +70,7 @@ public static void updateProgress( // Move cursor back to bottom if (lineNumber < totalLines - 1) { - System.out.print(String.format("\u001B[%dB", totalLines - lineNumber - 1)); + System.out.printf("\u001B[%dB", totalLines - lineNumber - 1); } System.out.flush(); @@ -119,7 +119,7 @@ public static void cleanup() { int totalLines = uploadLines.size(); if (totalLines > 0) { // Move to the first line - System.out.print(String.format("\u001B[%dA", totalLines - 1)); + System.out.printf("\u001B[%dA", totalLines - 1); // Clear all progress lines for (int i = 0; i < totalLines; i++) { System.out.print("\r\u001B[K"); // Clear current line @@ -128,7 +128,7 @@ public static void cleanup() { } } // Move back to start position - System.out.print(String.format("\u001B[%dA", totalLines - 1)); + System.out.printf("\u001B[%dA", totalLines - 1); System.out.flush(); } reset(); @@ -142,24 +142,4 @@ public static void reset() { nextLineNumber.set(0); } } - - /** - * Initializes the display with placeholder lines for all uploads. - * - * @param uploadIds Array of upload IDs to register - */ - public static void initializeDisplay(String... uploadIds) { - synchronized (consoleLock) { - reset(); - for (String uploadId : uploadIds) { - registerUpload(uploadId); - System.out.println(); // Reserve a line for each upload - } - // Move cursor back to the beginning - if (uploadIds.length > 0) { - System.out.print(String.format("\u001B[%dA", uploadIds.length)); - System.out.flush(); - } - } - } } diff --git a/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java b/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java index db8ae67..76450c9 100644 --- a/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java +++ b/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java @@ -108,7 +108,7 @@ public static String uploadAndGetId( String uploadId = progressPrefix != null ? progressPrefix : "Upload"; ProgressRequestBody.ProgressCallback callback = ProgressRequestBody.createConsoleCallback(uploadId, fileName); - body = new ProgressRequestBody(body, file, callback); + body = new ProgressRequestBody(body, callback); } Request request = new Request.Builder() From bfaccbfd208d17c3901c3205213698285ed94796 Mon Sep 17 00:00:00 2001 From: ritwickrajmakhal Date: Thu, 9 Oct 2025 19:45:42 +0530 Subject: [PATCH 4/4] Refactor ProgressRequestBody and ProgressTracker to remove fileName parameter from console callback for streamlined upload progress tracking --- .../lambdatest/gradle/ProgressRequestBody.java | 18 ++---------------- .../lambdatest/gradle/ProgressTracker.java | 7 +------ .../github/lambdatest/gradle/UploaderUtil.java | 3 +-- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java b/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java index 802db1e..501054a 100644 --- a/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java +++ b/src/main/java/io/github/lambdatest/gradle/ProgressRequestBody.java @@ -98,13 +98,11 @@ public void write(@NotNull Buffer source, long byteCount) throws IOException { * ProgressTracker for clean, fixed-line output. * * @param uploadId The unique identifier for this upload (e.g., "App", "Test Suite") - * @param fileName The name of the file being uploaded * @return A ProgressCallback that logs to console */ - public static ProgressCallback createConsoleCallback(String uploadId, String fileName) { + public static ProgressCallback createConsoleCallback(String uploadId) { return (bytesWritten, totalBytes, percentage) -> { - ProgressTracker.updateProgress( - uploadId, fileName, percentage, bytesWritten, totalBytes); + ProgressTracker.updateProgress(uploadId, percentage, bytesWritten, totalBytes); if (percentage >= 100.0f) { ProgressTracker.completeUpload(uploadId); @@ -112,18 +110,6 @@ public static ProgressCallback createConsoleCallback(String uploadId, String fil }; } - /** - * Creates a console-based progress callback (legacy version for backward compatibility). - * - * @param fileName The name of the file being uploaded - * @return A ProgressCallback that logs to console - * @deprecated Use {@link #createConsoleCallback(String, String)} instead - */ - @Deprecated - public static ProgressCallback createConsoleCallback(String fileName) { - return createConsoleCallback("Upload", fileName); - } - /** * Formats bytes into human-readable format. * diff --git a/src/main/java/io/github/lambdatest/gradle/ProgressTracker.java b/src/main/java/io/github/lambdatest/gradle/ProgressTracker.java index a8dc774..b5936da 100644 --- a/src/main/java/io/github/lambdatest/gradle/ProgressTracker.java +++ b/src/main/java/io/github/lambdatest/gradle/ProgressTracker.java @@ -36,17 +36,12 @@ private static int registerUpload(String uploadId) { * Updates the progress display for a specific upload. * * @param uploadId The unique identifier for the upload - * @param fileName The name of the file being uploaded * @param percentage The upload percentage (0-100) * @param bytesWritten Bytes uploaded so far * @param totalBytes Total bytes to upload */ public static void updateProgress( - String uploadId, - String fileName, - float percentage, - long bytesWritten, - long totalBytes) { + String uploadId, float percentage, long bytesWritten, long totalBytes) { synchronized (consoleLock) { // Register upload if not already registered (reserves a line) int lineNumber = registerUpload(uploadId); diff --git a/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java b/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java index 76450c9..167e29d 100644 --- a/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java +++ b/src/main/java/io/github/lambdatest/gradle/UploaderUtil.java @@ -104,10 +104,9 @@ public static String uploadAndGetId( // Wrap the entire multipart body with progress tracking if requested if (showProgress) { - String fileName = file.getName(); String uploadId = progressPrefix != null ? progressPrefix : "Upload"; ProgressRequestBody.ProgressCallback callback = - ProgressRequestBody.createConsoleCallback(uploadId, fileName); + ProgressRequestBody.createConsoleCallback(uploadId); body = new ProgressRequestBody(body, callback); } Request request =