From 9e241c7008b7925279f96531252d7166c3c9aa7b Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 16 Sep 2025 11:16:58 +0530 Subject: [PATCH 01/15] added the image comparator which is the pixel matching algorithm --- .../src/main/java/ImageComparator.java | 415 ++++++++++++++++++ 1 file changed, 415 insertions(+) create mode 100644 visual-tests/src/main/java/ImageComparator.java diff --git a/visual-tests/src/main/java/ImageComparator.java b/visual-tests/src/main/java/ImageComparator.java new file mode 100644 index 000000000..db3eeab97 --- /dev/null +++ b/visual-tests/src/main/java/ImageComparator.java @@ -0,0 +1,415 @@ +import processing.core.*; +import java.util.*; + +class ComparisonResult { + public boolean passed; + public double mismatchRatio; + public boolean isFirstRun; + public PImage diffImage; + public ComparisonDetails details; + + public ComparisonResult(boolean passed, double mismatchRatio) { + this.passed = passed; + this.mismatchRatio = mismatchRatio; + this.isFirstRun = false; + } + + public ComparisonResult(boolean passed, PImage diffImage, ComparisonDetails details) { + this.passed = passed; + this.diffImage = diffImage; + this.details = details; + this.mismatchRatio = details != null ? (double) details.significantDiffPixels / (diffImage.width * diffImage.height) : 0.0; + this.isFirstRun = false; + } + + public static ComparisonResult createFirstRun() { + ComparisonResult result = new ComparisonResult(false, 0.0); + result.isFirstRun = true; + return result; + } + + public void saveDiffImage(String filePath) { + if (diffImage != null) { + diffImage.save(filePath); + System.out.println("Diff image saved: " + filePath); + } + } +} + +class ComparisonDetails { + public int totalDiffPixels; + public int significantDiffPixels; + public List clusters; + + public ComparisonDetails(int totalDiffPixels, int significantDiffPixels, List clusters) { + this.totalDiffPixels = totalDiffPixels; + this.significantDiffPixels = significantDiffPixels; + this.clusters = clusters; + } + + public void printDetails() { + System.out.println(" Total diff pixels: " + totalDiffPixels); + System.out.println(" Significant diff pixels: " + significantDiffPixels); + System.out.println(" Clusters found: " + clusters.size()); + + long lineShiftClusters = clusters.stream().filter(c -> c.isLineShift).count(); + if (lineShiftClusters > 0) { + System.out.println(" Line shift clusters (ignored): " + lineShiftClusters); + } + + // Print cluster details + for (int i = 0; i < clusters.size(); i++) { + ClusterInfo cluster = clusters.get(i); + System.out.println(" Cluster " + (i+1) + ": size=" + cluster.size + + ", lineShift=" + cluster.isLineShift); + } + } +} + +// Individual cluster information +class ClusterInfo { + public int size; + public List pixels; + public boolean isLineShift; + + public ClusterInfo(int size, List pixels, boolean isLineShift) { + this.size = size; + this.pixels = pixels; + this.isLineShift = isLineShift; + } +} + +// Simple 2D point +class Point2D { + public int x, y; + + public Point2D(int x, int y) { + this.x = x; + this.y = y; + } +} + +// Interface for pixel matching algorithms +interface PixelMatchingAlgorithm { + ComparisonResult compare(PImage baseline, PImage actual, double threshold); +} + +// Your sophisticated pixel matching algorithm +public class ImageComparator implements PixelMatchingAlgorithm { + + // Algorithm constants + private static final int MAX_SIDE = 400; + private static final int BG_COLOR = 0xFFFFFFFF; // White background + private static final int MIN_CLUSTER_SIZE = 4; + private static final int MAX_TOTAL_DIFF_PIXELS = 40; + private static final double DEFAULT_THRESHOLD = 0.5; + private static final double ALPHA = 0.1; + + private PApplet p; // Reference to PApplet for PImage creation + + public ImageComparator(PApplet p) { + this.p = p; + } + + @Override + public ComparisonResult compare(PImage baseline, PImage actual, double threshold) { + if (baseline == null || actual == null) { + return new ComparisonResult(false, 1.0); + } + + try { + return performComparison(baseline, actual, threshold); + } catch (Exception e) { + System.err.println("Comparison failed: " + e.getMessage()); + return new ComparisonResult(false, 1.0); + } + } + + private ComparisonResult performComparison(PImage baseline, PImage actual, double threshold) { + // Calculate scaling + double scale = Math.min( + (double) MAX_SIDE / baseline.width, + (double) MAX_SIDE / baseline.height + ); + + double ratio = (double) baseline.width / baseline.height; + boolean narrow = ratio != 1.0; + if (narrow) { + scale *= 2; + } + + // Resize images + PImage scaledActual = resizeImage(actual, scale); + PImage scaledBaseline = resizeImage(baseline, scale); + + // Ensure both images have the same dimensions + int width = scaledBaseline.width; + int height = scaledBaseline.height; + + // Create canvases with background color + PImage actualCanvas = createCanvasWithBackground(scaledActual, width, height); + PImage baselineCanvas = createCanvasWithBackground(scaledBaseline, width, height); + + // Create diff output canvas + PImage diffCanvas = p.createImage(width, height, PImage.RGB); + + // Run pixelmatch equivalent + int diffCount = pixelmatch(actualCanvas, baselineCanvas, diffCanvas, width, height, DEFAULT_THRESHOLD); + + // If no differences, return early + if (diffCount == 0) { + return new ComparisonResult(true, diffCanvas, null); + } + + // Post-process to identify and filter out isolated differences + Set visited = new HashSet<>(); + List clusterSizes = new ArrayList<>(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int pos = y * width + x; + + // If this is a diff pixel and not yet visited + if (isDiffPixel(diffCanvas, x, y) && !visited.contains(pos)) { + ClusterInfo clusterInfo = findClusterSize(diffCanvas, x, y, width, height, visited); + clusterSizes.add(clusterInfo); + } + } + } + + // Determine if the differences are significant + List nonLineShiftClusters = clusterSizes.stream() + .filter(cluster -> !cluster.isLineShift && cluster.size >= MIN_CLUSTER_SIZE) + .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); + + // Calculate significant differences excluding line shifts + int significantDiffPixels = nonLineShiftClusters.stream() + .mapToInt(cluster -> cluster.size) + .sum(); + + // Determine test result + boolean passed = diffCount == 0 || + significantDiffPixels == 0 || + (significantDiffPixels <= MAX_TOTAL_DIFF_PIXELS && nonLineShiftClusters.size() <= 2); + + ComparisonDetails details = new ComparisonDetails(diffCount, significantDiffPixels, clusterSizes); + + return new ComparisonResult(passed, diffCanvas, details); + } + + private PImage resizeImage(PImage image, double scale) { + int newWidth = (int) Math.ceil(image.width * scale); + int newHeight = (int) Math.ceil(image.height * scale); + + PImage resized = p.createImage(newWidth, newHeight, PImage.RGB); + resized.copy(image, 0, 0, image.width, image.height, 0, 0, newWidth, newHeight); + + return resized; + } + + private PImage createCanvasWithBackground(PImage image, int width, int height) { + PImage canvas = p.createImage(width, height, PImage.RGB); + + // Fill with background color (white) + canvas.loadPixels(); + for (int i = 0; i < canvas.pixels.length; i++) { + canvas.pixels[i] = BG_COLOR; + } + canvas.updatePixels(); + + // Draw the image on top + canvas.copy(image, 0, 0, image.width, image.height, 0, 0, image.width, image.height); + + return canvas; + } + + private int pixelmatch(PImage actual, PImage expected, PImage diff, int width, int height, double threshold) { + int diffCount = 0; + + actual.loadPixels(); + expected.loadPixels(); + diff.loadPixels(); + + for (int i = 0; i < actual.pixels.length; i++) { + int actualColor = actual.pixels[i]; + int expectedColor = expected.pixels[i]; + + double delta = colorDelta(actualColor, expectedColor); + + if (delta > threshold) { + // Mark as different (bright red pixel) + diff.pixels[i] = 0xFFFF0000; // Red + diffCount++; + } else { + // Mark as same (dimmed version of actual image) + int dimColor = dimColor(actualColor, ALPHA); + diff.pixels[i] = dimColor; + } + } + + diff.updatePixels(); + return diffCount; + } + + private double colorDelta(int color1, int color2) { + int r1 = (color1 >> 16) & 0xFF; + int g1 = (color1 >> 8) & 0xFF; + int b1 = color1 & 0xFF; + int a1 = (color1 >> 24) & 0xFF; + + int r2 = (color2 >> 16) & 0xFF; + int g2 = (color2 >> 8) & 0xFF; + int b2 = color2 & 0xFF; + int a2 = (color2 >> 24) & 0xFF; + + int dr = r1 - r2; + int dg = g1 - g2; + int db = b1 - b2; + int da = a1 - a2; + + return Math.sqrt(dr * dr + dg * dg + db * db + da * da) / 255.0; + } + + private int dimColor(int color, double alpha) { + int r = (int) (((color >> 16) & 0xFF) * alpha); + int g = (int) (((color >> 8) & 0xFF) * alpha); + int b = (int) ((color & 0xFF) * alpha); + int a = (int) (255 * alpha); + + r = Math.max(0, Math.min(255, r)); + g = Math.max(0, Math.min(255, g)); + b = Math.max(0, Math.min(255, b)); + a = Math.max(0, Math.min(255, a)); + + return (a << 24) | (r << 16) | (g << 8) | b; + } + + private boolean isDiffPixel(PImage image, int x, int y) { + if (x < 0 || x >= image.width || y < 0 || y >= image.height) return false; + + image.loadPixels(); + int color = image.pixels[y * image.width + x]; + + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = color & 0xFF; + + return r == 255 && g == 0 && b == 0; + } + + private ClusterInfo findClusterSize(PImage diffImage, int startX, int startY, int width, int height, Set visited) { + List queue = new ArrayList<>(); + queue.add(new Point2D(startX, startY)); + + int size = 0; + List clusterPixels = new ArrayList<>(); + + while (!queue.isEmpty()) { + Point2D point = queue.remove(0); + int pos = point.y * width + point.x; + + // Skip if already visited + if (visited.contains(pos)) continue; + + // Skip if not a diff pixel + if (!isDiffPixel(diffImage, point.x, point.y)) continue; + + // Mark as visited + visited.add(pos); + size++; + clusterPixels.add(point); + + // Add neighbors to queue + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) continue; + + int nx = point.x + dx; + int ny = point.y + dy; + + // Skip if out of bounds + if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; + + // Skip if already visited + int npos = ny * width + nx; + if (!visited.contains(npos)) { + queue.add(new Point2D(nx, ny)); + } + } + } + } + + // Determine if this is a line shift + boolean isLineShift = detectLineShift(clusterPixels, diffImage, width, height); + + return new ClusterInfo(size, clusterPixels, isLineShift); + } + + private boolean detectLineShift(List clusterPixels, PImage diffImage, int width, int height) { + if (clusterPixels.isEmpty()) return false; + + int linelikePixels = 0; + + for (Point2D pixel : clusterPixels) { + int neighbors = 0; + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) continue; // Skip self + + int nx = pixel.x + dx; + int ny = pixel.y + dy; + + // Skip if out of bounds + if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; + + // Check if neighbor is a diff pixel + if (isDiffPixel(diffImage, nx, ny)) { + neighbors++; + } + } + } + + // Line-like pixels typically have 1-2 neighbors + if (neighbors <= 2) { + linelikePixels++; + } + } + + // If most pixels (>80%) in the cluster have ≤2 neighbors, it's likely a line shift + return (double) linelikePixels / clusterPixels.size() > 0.8; + } + + // Configuration methods + public ImageComparator setMaxSide(int maxSide) { + // For future configurability + return this; + } + + public ImageComparator setMinClusterSize(int minClusterSize) { + // For future configurability + return this; + } + + public ImageComparator setMaxTotalDiffPixels(int maxTotalDiffPixels) { + // For future configurability + return this; + } +} + +// Utility class for algorithm configuration +class ComparatorConfig { + public int maxSide = 400; + public int minClusterSize = 4; + public int maxTotalDiffPixels = 40; + public double threshold = 0.5; + public double alpha = 0.1; + public int backgroundColor = 0xFFFFFFFF; + + public ComparatorConfig() {} + + public ComparatorConfig(int maxSide, int minClusterSize, int maxTotalDiffPixels) { + this.maxSide = maxSide; + this.minClusterSize = minClusterSize; + this.maxTotalDiffPixels = maxTotalDiffPixels; + } +} \ No newline at end of file From a5ded6ae6896d9d9a78952fee98493d35d395798 Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 16 Sep 2025 11:17:55 +0530 Subject: [PATCH 02/15] added build.gradle file --- visual-tests/build.gradle.kts | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 visual-tests/build.gradle.kts diff --git a/visual-tests/build.gradle.kts b/visual-tests/build.gradle.kts new file mode 100644 index 000000000..1aa74d91b --- /dev/null +++ b/visual-tests/build.gradle.kts @@ -0,0 +1,67 @@ +plugins { + java + application +} + +repositories { + mavenCentral() + maven { url = uri("https://jogamp.org/deployment/maven") } +} + +//dependencies { +// // Reference to Processing core +// implementation(project(":core")) +// testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") +//} +dependencies { + implementation(project(":core")) + testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") +} + +application { + mainClass.set("ProcessingVisualTestExamples") +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +// Visual testing tasks +tasks.register("runVisualTests") { + description = "Run all visual tests" + classpath = sourceSets.main.get().runtimeClasspath + mainClass.set("ProcessingVisualTestExamples") +} + +tasks.register("runSimpleTest") { + description = "Verify visual testing setup" + classpath = sourceSets.main.get().runtimeClasspath + mainClass.set("SimpleTest") +} + +tasks.register("updateBaselines") { + description = "Update visual test baselines" + classpath = sourceSets.main.get().runtimeClasspath + mainClass.set("ProcessingCIHelper") + args("--update") +} + +tasks.register("runCITests") { + description = "Run visual tests in CI" + classpath = sourceSets.main.get().runtimeClasspath + mainClass.set("ProcessingCIHelper") + args("--ci") + systemProperty("java.awt.headless", "true") +} + +tasks.register("cleanVisualTestFiles") { + delete(fileTree(".") { + include("__screenshots__/**") + include("diff_*.png") + }) +} + +tasks.named("clean") { + dependsOn("cleanVisualTestFiles") +} \ No newline at end of file From 8e7f7d0fa274b1ec5510bcc377b083da684e2946 Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 16 Sep 2025 11:18:35 +0530 Subject: [PATCH 03/15] added the test runner --- .../src/main/java/VisualTestRunner.java | 634 ++++++++++++++++++ 1 file changed, 634 insertions(+) create mode 100644 visual-tests/src/main/java/VisualTestRunner.java diff --git a/visual-tests/src/main/java/VisualTestRunner.java b/visual-tests/src/main/java/VisualTestRunner.java new file mode 100644 index 000000000..20385ac80 --- /dev/null +++ b/visual-tests/src/main/java/VisualTestRunner.java @@ -0,0 +1,634 @@ +// Processing Visual Test Runner - Modular test execution infrastructure +// Uses ImageComparator for sophisticated pixel matching + +import processing.core.*; +import java.io.*; +import java.nio.file.*; +import java.util.*; + +// Core visual tester class +public class VisualTestRunner { + + private String screenshotDir; + private PixelMatchingAlgorithm pixelMatcher; + private String platform; + + public VisualTestRunner(PixelMatchingAlgorithm pixelMatcher) { + this.pixelMatcher = pixelMatcher; + this.screenshotDir = "__screenshots__"; + this.platform = detectPlatform(); + createDirectoryIfNotExists(screenshotDir); + } + + public VisualTestRunner(PixelMatchingAlgorithm pixelMatcher, String screenshotDir) { + this.pixelMatcher = pixelMatcher; + this.screenshotDir = screenshotDir; + this.platform = detectPlatform(); + createDirectoryIfNotExists(screenshotDir); + } + + // Main test execution method + public TestResult runVisualTest(String testName, ProcessingSketch sketch) { + return runVisualTest(testName, sketch, new TestConfig()); + } + + public TestResult runVisualTest(String testName, ProcessingSketch sketch, TestConfig config) { + try { + System.out.println("Running visual test: " + testName); + + // Capture screenshot from sketch + PImage actualImage = captureSketch(sketch, config); + + // Compare with baseline + ComparisonResult comparison = compareWithBaseline(testName, actualImage, config); + + return new TestResult(testName, comparison); + + } catch (Exception e) { + return TestResult.createError(testName, e.getMessage()); + } + } + + // Capture PImage from Processing sketch + private PImage captureSketch(ProcessingSketch sketch, TestConfig config) { + SketchRunner runner = new SketchRunner(sketch, config); + runner.run(); + return runner.getImage(); + } + + // Compare actual image with baseline + private ComparisonResult compareWithBaseline(String testName, PImage actualImage, TestConfig config) { + String baselinePath = getBaselinePath(testName); + + PImage baselineImage = loadBaseline(baselinePath); + + if (baselineImage == null) { + // First run - save as baseline + saveBaseline(testName, actualImage); + return ComparisonResult.createFirstRun(); + } + + // Use your sophisticated pixel matching algorithm + ComparisonResult result = pixelMatcher.compare(baselineImage, actualImage, config.threshold); + + // Save diff images if test failed + if (!result.passed && result.diffImage != null) { + saveDiffImage(testName, result.diffImage); + } + + return result; + } + + // Save diff image for debugging + private void saveDiffImage(String testName, PImage diffImage) { + String sanitizedName = testName.replaceAll("[^a-zA-Z0-9-_]", "-"); + String diffPath = "diff_" + sanitizedName + "-" + platform + ".png"; + diffImage.save(diffPath); + System.out.println("Diff image saved: " + diffPath); + } + + // Utility methods + private String detectPlatform() { + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("mac")) return "darwin"; + if (os.contains("win")) return "win32"; + return "linux"; + } + + private void createDirectoryIfNotExists(String dir) { + try { + Files.createDirectories(Paths.get(dir)); + } catch (IOException e) { + System.err.println("Failed to create directory: " + dir); + } + } + + private String getBaselinePath(String testName) { + String sanitizedName = testName.replaceAll("[^a-zA-Z0-9-_]", "-"); + return screenshotDir + "/" + sanitizedName + "-" + platform + ".png"; + } + + private PImage loadBaseline(String path) { + File file = new File(path); + if (!file.exists()) return null; + + // Create a temporary PApplet to load the image + PApplet tempApplet = new PApplet(); + return tempApplet.loadImage(path); + } + + private void saveBaseline(String testName, PImage image) { + String path = getBaselinePath(testName); + image.save(path); + System.out.println("Baseline created: " + path); + } +} + +// Test runner that executes Processing sketches +class SketchRunner extends PApplet { + + private ProcessingSketch userSketch; + private TestConfig config; + private PImage capturedImage; + private boolean rendered = false; + + public SketchRunner(ProcessingSketch userSketch, TestConfig config) { + this.userSketch = userSketch; + this.config = config; + } + + public void settings() { + size(config.width, config.height); + } + + public void setup() { + // Disable animations for consistent testing + noLoop(); + + // Set background if specified + if (config.backgroundColor != null) { + background(config.backgroundColor[0], config.backgroundColor[1], config.backgroundColor[2]); + } + + // Call user setup + userSketch.setup(this); + } + + public void draw() { + if (!rendered) { + // Call user draw function + userSketch.draw(this); + + // Capture the frame + capturedImage = get(); // get() returns a PImage of the entire canvas + rendered = true; + } + } + + public void run() { + String[] args = {"SketchRunner"}; + PApplet.runSketch(args, this); + + // Wait for rendering to complete + while (!rendered) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + + // Additional wait time for any processing + try { + Thread.sleep(config.renderWaitTime); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // Clean up + exit(); + } + + public PImage getImage() { + return capturedImage; + } +} + +// Interface for user sketches +interface ProcessingSketch { + void setup(PApplet p); + void draw(PApplet p); +} + +// Test configuration class +class TestConfig { + public int width = 800; + public int height = 600; + public int[] backgroundColor = {255, 255, 255}; // RGB + public long renderWaitTime = 2000; // milliseconds + public double threshold = 0.1; + + public TestConfig() {} + + public TestConfig(int width, int height) { + this.width = width; + this.height = height; + } + + public TestConfig(int width, int height, int[] backgroundColor) { + this.width = width; + this.height = height; + this.backgroundColor = backgroundColor; + } + + public TestConfig setThreshold(double threshold) { + this.threshold = threshold; + return this; + } + + public TestConfig setRenderWaitTime(long waitTime) { + this.renderWaitTime = waitTime; + return this; + } +} + +// Enhanced test result with detailed information +class TestResult { + public String testName; + public boolean passed; + public double mismatchRatio; + public String error; + public boolean isFirstRun; + public ComparisonDetails details; + + public TestResult(String testName, ComparisonResult comparison) { + this.testName = testName; + this.passed = comparison.passed; + this.mismatchRatio = comparison.mismatchRatio; + this.isFirstRun = comparison.isFirstRun; + this.details = comparison.details; + } + + public static TestResult createError(String testName, String error) { + TestResult result = new TestResult(); + result.testName = testName; + result.passed = false; + result.error = error; + return result; + } + + private TestResult() {} // For error constructor + + public void printResult() { + System.out.print(testName + ": "); + if (error != null) { + System.out.println("ERROR - " + error); + } else if (isFirstRun) { + System.out.println("BASELINE CREATED"); + } else if (passed) { + System.out.println("PASSED"); + } else { + System.out.println("FAILED (mismatch: " + String.format("%.4f", mismatchRatio * 100) + "%)"); + if (details != null) { + details.printDetails(); + } + } + } +} + +// Test suite for organizing multiple tests +class ProcessingTestSuite { + private VisualTestRunner tester; + private List tests; + + public ProcessingTestSuite(VisualTestRunner tester) { + this.tester = tester; + this.tests = new ArrayList<>(); + } + + public void addTest(String name, ProcessingSketch sketch) { + addTest(name, sketch, new TestConfig()); + } + + public void addTest(String name, ProcessingSketch sketch, TestConfig config) { + tests.add(new VisualTest(name, sketch, config)); + } + + public List runAll() { + System.out.println("Running " + tests.size() + " visual tests..."); + List results = new ArrayList<>(); + + for (VisualTest test : tests) { + TestResult result = tester.runVisualTest(test.name, test.sketch, test.config); + result.printResult(); + results.add(result); + } + + return results; + } + + public TestResult runTest(String testName) { + VisualTest test = tests.stream() + .filter(t -> t.name.equals(testName)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Test not found: " + testName)); + + return tester.runVisualTest(test.name, test.sketch, test.config); + } + + public List getTestNames() { + return tests.stream().map(t -> t.name).collect(ArrayList::new, ArrayList::add, ArrayList::addAll); + } + + // Helper class for internal test storage + private static class VisualTest { + String name; + ProcessingSketch sketch; + TestConfig config; + + VisualTest(String name, ProcessingSketch sketch, TestConfig config) { + this.name = name; + this.sketch = sketch; + this.config = config; + } + } +} + +// Baseline manager for updating reference images +class BaselineManager { + private VisualTestRunner tester; + + public BaselineManager(VisualTestRunner tester) { + this.tester = tester; + } + + public void updateBaseline(String testName, ProcessingSketch sketch) { + updateBaseline(testName, sketch, new TestConfig()); + } + + public void updateBaseline(String testName, ProcessingSketch sketch, TestConfig config) { + System.out.println("Updating baseline for: " + testName); + + // Capture new image + SketchRunner runner = new SketchRunner(sketch, config); + runner.run(); + PImage newImage = runner.getImage(); + + // Save as baseline + String baselinePath = "__screenshots__/" + + testName.replaceAll("[^a-zA-Z0-9-_]", "-") + + "-" + detectPlatform() + ".png"; + newImage.save(baselinePath); + + System.out.println("Baseline updated: " + baselinePath); + } + + public void updateAllBaselines(ProcessingTestSuite suite) { + System.out.println("Updating all baselines..."); + List testNames = suite.getTestNames(); + + for (String testName : testNames) { + // Re-run the test to get the sketch and config + TestResult result = suite.runTest(testName); + // Note: In a real implementation, you'd need to store the sketch reference + // This is a simplified version + } + } + + private String detectPlatform() { + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("mac")) return "darwin"; + if (os.contains("win")) return "win32"; + return "linux"; + } +} + +// Test execution utilities +class TestExecutor { + + public static void runSingleTest(String testName, ProcessingSketch sketch) { + runSingleTest(testName, sketch, new TestConfig()); + } + + public static void runSingleTest(String testName, ProcessingSketch sketch, TestConfig config) { + // Initialize comparator + PApplet tempApplet = new PApplet(); + ImageComparator comparator = new ImageComparator(tempApplet); + + // Run test + VisualTestRunner tester = new VisualTestRunner(comparator); + TestResult result = tester.runVisualTest(testName, sketch, config); + result.printResult(); + } + + public static void updateSingleBaseline(String testName, ProcessingSketch sketch) { + updateSingleBaseline(testName, sketch, new TestConfig()); + } + + public static void updateSingleBaseline(String testName, ProcessingSketch sketch, TestConfig config) { + // Initialize comparator + PApplet tempApplet = new PApplet(); + ImageComparator comparator = new ImageComparator(tempApplet); + + // Update baseline + VisualTestRunner tester = new VisualTestRunner(comparator); + BaselineManager manager = new BaselineManager(tester); + manager.updateBaseline(testName, sketch, config); + } +} + +// Example usage and test implementations +class ProcessingVisualTestExamples { + + public static void main(String[] args) { + // Initialize with your sophisticated pixel matching algorithm + PApplet tempApplet = new PApplet(); + + ImageComparator comparator = new ImageComparator(tempApplet); + VisualTestRunner tester = new VisualTestRunner(comparator); + ProcessingTestSuite suite = new ProcessingTestSuite(tester); + + // Add example tests + suite.addTest("red-circle", new RedCircleSketch()); + suite.addTest("blue-square", new BlueSquareSketch()); + suite.addTest("gradient-background", new GradientBackgroundSketch(), + new TestConfig(600, 400)); + suite.addTest("complex-pattern", new ComplexPatternSketch(), + new TestConfig(800, 600).setThreshold(0.15)); + + // Run all tests + List results = suite.runAll(); + + // Print detailed summary + printTestSummary(results); + + // Handle command line arguments + if (args.length > 0) { + handleCommandLineArgs(args, suite); + } + } + + private static void printTestSummary(List results) { + long passed = results.stream().filter(r -> r.passed).count(); + long failed = results.size() - passed; + long baselines = results.stream().filter(r -> r.isFirstRun).count(); + long errors = results.stream().filter(r -> r.error != null).count(); + + System.out.println("\n=== Test Summary ==="); + System.out.println("Total: " + results.size()); + System.out.println("Passed: " + passed); + System.out.println("Failed: " + failed); + System.out.println("Baselines Created: " + baselines); + System.out.println("Errors: " + errors); + + // Print detailed failure information + results.stream() + .filter(r -> !r.passed && !r.isFirstRun && r.error == null) + .forEach(r -> { + System.out.println("\n--- Failed Test: " + r.testName + " ---"); + if (r.details != null) { + r.details.printDetails(); + } + }); + } + + private static void handleCommandLineArgs(String[] args, ProcessingTestSuite suite) { + if (args[0].equals("--update")) { + // Update specific baselines or all + BaselineManager manager = new BaselineManager(null); // Will need tester reference + if (args.length > 1) { + for (int i = 1; i < args.length; i++) { + System.out.println("Updating baseline: " + args[i]); + // Update specific test baseline + } + } else { + System.out.println("Updating all baselines..."); + manager.updateAllBaselines(suite); + } + } else if (args[0].equals("--run")) { + // Run specific test + if (args.length > 1) { + String testName = args[1]; + TestResult result = suite.runTest(testName); + result.printResult(); + } + } + } + + // Example sketch: Red circle + static class RedCircleSketch implements ProcessingSketch { + public void setup(PApplet p) { + p.noStroke(); + } + + public void draw(PApplet p) { + p.background(255); + p.fill(255, 0, 0); + p.ellipse(p.width/2, p.height/2, 100, 100); + } + } + + // Example sketch: Blue square + static class BlueSquareSketch implements ProcessingSketch { + public void setup(PApplet p) { + p.stroke(0); + p.strokeWeight(2); + } + + public void draw(PApplet p) { + p.background(255); + p.fill(0, 0, 255); + p.rect(p.width/2 - 50, p.height/2 - 50, 100, 100); + } + } + + // Example sketch: Gradient background + static class GradientBackgroundSketch implements ProcessingSketch { + public void setup(PApplet p) { + p.noStroke(); + } + + public void draw(PApplet p) { + for (int y = 0; y < p.height; y++) { + float inter = PApplet.map(y, 0, p.height, 0, 1); + int c = p.lerpColor(p.color(255, 0, 0), p.color(0, 0, 255), inter); + p.stroke(c); + p.line(0, y, p.width, y); + } + } + } + + // Example sketch: Complex pattern (more likely to have minor differences) + static class ComplexPatternSketch implements ProcessingSketch { + public void setup(PApplet p) { + p.noStroke(); + } + + public void draw(PApplet p) { + p.background(20, 20, 40); + + for (int x = 0; x < p.width; x += 15) { + for (int y = 0; y < p.height; y += 15) { + float noise = (float) (Math.sin(x * 0.02) * Math.cos(y * 0.02)); + int brightness = (int) ((noise + 1) * 127.5); + + p.fill(brightness, brightness * 0.7f, brightness * 1.2f); + p.rect(x, y, 12, 12); + + // Add some text that might cause line shifts + if (x % 60 == 0 && y % 60 == 0) { + p.fill(255); + p.textSize(8); + p.text(brightness, x + 2, y + 10); + } + } + } + } + } +} + +// CI Integration helper +class ProcessingCIHelper { + + public static void main(String[] args) { + if (args.length > 0 && args[0].equals("--ci")) { + runCITests(); + } else { + ProcessingVisualTestExamples.main(args); + } + } + + public static void runCITests() { + System.out.println("Running visual tests in CI mode..."); + + // Initialize comparator + PApplet tempApplet = new PApplet(); + + ImageComparator comparator = new ImageComparator(tempApplet); + VisualTestRunner tester = new VisualTestRunner(comparator); + ProcessingTestSuite suite = new ProcessingTestSuite(tester); + + // Add your actual test cases here + suite.addTest("ci-test-1", new ProcessingVisualTestExamples.RedCircleSketch()); + suite.addTest("ci-test-2", new ProcessingVisualTestExamples.BlueSquareSketch()); + + // Run tests + List results = suite.runAll(); + + // Check for failures + boolean hasFailures = results.stream().anyMatch(r -> !r.passed && !r.isFirstRun); + boolean hasErrors = results.stream().anyMatch(r -> r.error != null); + + if (hasFailures || hasErrors) { + System.err.println("Visual tests failed!"); + System.exit(1); + } else { + System.out.println("All visual tests passed!"); + System.exit(0); + } + } + +// public static void updateCIBaselines(String[] testNames) { +// System.out.println("Updating baselines in CI mode..."); +// +// // Initialize components +// PApplet tempApplet = new PApplet(); +// +// ImageComparator comparator = new ImageComparator(tempApplet); +// VisualTestRunner = new VisualTestRunner(comparator); +// BaselineManager manager = new BaselineManager(tester); +// +// if (testNames.length == 0) { +// System.out.println("No specific tests specified, updating all..."); +// // Update all baselines - you'd need to implement this based on your test discovery +// } else { +// for (String testName : testNames) { +// System.out.println("Updating baseline for: " + testName); +// // Update specific baseline - you'd need the corresponding sketch +// } +// } +// +// System.out.println("Baseline update completed!"); +// } +} \ No newline at end of file From 2f18f215c988269a4d0a0deb011bc3734079833a Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 16 Sep 2025 11:19:17 +0530 Subject: [PATCH 04/15] added the simple test --- visual-tests/src/main/java/SimpleTest.java | 145 +++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 visual-tests/src/main/java/SimpleTest.java diff --git a/visual-tests/src/main/java/SimpleTest.java b/visual-tests/src/main/java/SimpleTest.java new file mode 100644 index 000000000..5c59885d4 --- /dev/null +++ b/visual-tests/src/main/java/SimpleTest.java @@ -0,0 +1,145 @@ +// SimpleTest.java - Fixed version for quick verification +import processing.core.*; +import java.util.*; + +public class SimpleTest { + + public static void main(String[] args) { + System.out.println("=== Processing Visual Testing - Quick Test ===\n"); + + try { + // Step 1: Initialize Processing environment + System.out.println("1. Initializing Processing environment..."); + PApplet tempApplet = new PApplet(); + // Fixed: Removed tempApplet.init() - not needed in modern Processing + System.out.println("✓ Processing initialized"); + + // Step 2: Create comparator + System.out.println("2. Creating image comparator..."); + ImageComparator comparator = new ImageComparator(tempApplet); + VisualTestRunner tester = new VisualTestRunner(comparator); + System.out.println("✓ Comparator created"); + + // Step 3: Run basic tests + System.out.println("3. Running basic tests...\n"); + + // Test 1: Simple red circle + System.out.println("--- Test 1: Red Circle ---"); + ProcessingSketch redCircle = new SimpleRedCircle(); + TestResult result1 = tester.runVisualTest("red-circle", redCircle); + result1.printResult(); + + // Test 2: Blue square + System.out.println("\n--- Test 2: Blue Square ---"); + ProcessingSketch blueSquare = new SimpleBlueSquare(); + TestResult result2 = tester.runVisualTest("blue-square", blueSquare); + result2.printResult(); + + // Step 4: Test comparison with identical image (should pass on second run) + if (!result1.isFirstRun) { + System.out.println("\n--- Test 3: Identical Image (Should Pass) ---"); + TestResult result3 = tester.runVisualTest("red-circle", redCircle); + result3.printResult(); + + if (result3.passed) { + System.out.println("✓ Identical image comparison works!"); + } else { + System.out.println("✗ Identical image comparison failed!"); + } + } + + // Step 5: Test comparison with different image (should fail) + System.out.println("\n--- Test 4: Different Image (Should Fail) ---"); + ProcessingSketch greenCircle = new SimpleGreenCircle(); + TestResult result4 = tester.runVisualTest("red-circle", greenCircle); + result4.printResult(); + + if (!result4.passed && !result4.isFirstRun) { + System.out.println("✓ Different image detection works!"); + if (result4.details != null) { + System.out.println("Algorithm detected " + result4.details.totalDiffPixels + " different pixels"); + } + } + + // Step 6: Test suite functionality + System.out.println("\n--- Test 5: Test Suite ---"); + ProcessingTestSuite suite = new ProcessingTestSuite(tester); + suite.addTest("suite-red", new SimpleRedCircle()); + suite.addTest("suite-blue", new SimpleBlueSquare()); + suite.addTest("suite-gradient", new SimpleGradient()); + + List suiteResults = suite.runAll(); + + long passed = suiteResults.stream().filter(r -> r.passed).count(); + long total = suiteResults.size(); + System.out.println("Suite results: " + passed + "/" + total + " passed"); + + // Final summary + System.out.println("\n=== Test Summary ==="); + System.out.println("✓ Processing environment works"); + System.out.println("✓ Image comparator works"); + System.out.println("✓ Baseline creation works"); + System.out.println("✓ Visual test execution works"); + System.out.println("✓ Test suite functionality works"); + System.out.println("\nCheck the '__screenshots__' folder for baseline images!"); + System.out.println("Check for 'diff_*.png' files if any tests failed!"); + + } catch (Exception e) { + System.err.println("Test failed with error: " + e.getMessage()); + e.printStackTrace(); + } + } + + // Simple test sketches + static class SimpleRedCircle implements ProcessingSketch { + public void setup(PApplet p) { + p.noStroke(); + } + + public void draw(PApplet p) { + p.background(255, 255, 255); + p.fill(255, 0, 0); + p.ellipse(p.width/2, p.height/2, 100, 100); + } + } + + static class SimpleBlueSquare implements ProcessingSketch { + public void setup(PApplet p) { + p.stroke(0); + p.strokeWeight(2); + } + + public void draw(PApplet p) { + p.background(255); + p.fill(0, 0, 255); + p.rect(p.width/2 - 50, p.height/2 - 50, 100, 100); + } + } + + static class SimpleGreenCircle implements ProcessingSketch { + public void setup(PApplet p) { + p.noStroke(); + } + + public void draw(PApplet p) { + p.background(255, 255, 255); + p.fill(0, 255, 0); // Green instead of red + p.ellipse(p.width/2, p.height/2, 100, 100); + } + } + + static class SimpleGradient implements ProcessingSketch { + public void setup(PApplet p) { + p.noStroke(); + } + + public void draw(PApplet p) { + for (int y = 0; y < p.height; y++) { + float inter = PApplet.map(y, 0, p.height, 0, 1); + int c = p.lerpColor(p.color(255, 0, 0), p.color(0, 0, 255), inter); + p.stroke(c); + p.line(0, y, p.width, y); + } + } + } +} \ No newline at end of file From 4e7183fe51d8038f9a276f6db5513fb8332f1827 Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 7 Oct 2025 02:04:38 +0530 Subject: [PATCH 05/15] fixing the build issues --- build.gradle.kts | 5 + .../test/visual}/ImageComparator.java | 2 + .../test/visual}/VisualTestRunner.java | 313 +++++------------- 3 files changed, 81 insertions(+), 239 deletions(-) rename visual-tests/src/main/java/{ => processing/test/visual}/ImageComparator.java (99%) rename visual-tests/src/main/java/{ => processing/test/visual}/VisualTestRunner.java (59%) diff --git a/build.gradle.kts b/build.gradle.kts index 0675c2db3..18ef17eb3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,4 +8,9 @@ plugins { // Set the build directory to not /build to prevent accidental deletion through the clean action // Can be deleted after the migration to Gradle is complete +tasks.register("visualTests") { + description = "Run visual regression tests" + dependsOn(":visual-testing:runVisualTests") +} + layout.buildDirectory = file(".build") \ No newline at end of file diff --git a/visual-tests/src/main/java/ImageComparator.java b/visual-tests/src/main/java/processing/test/visual/ImageComparator.java similarity index 99% rename from visual-tests/src/main/java/ImageComparator.java rename to visual-tests/src/main/java/processing/test/visual/ImageComparator.java index db3eeab97..6420c48cd 100644 --- a/visual-tests/src/main/java/ImageComparator.java +++ b/visual-tests/src/main/java/processing/test/visual/ImageComparator.java @@ -1,3 +1,5 @@ +package + import processing.core.*; import java.util.*; diff --git a/visual-tests/src/main/java/VisualTestRunner.java b/visual-tests/src/main/java/processing/test/visual/VisualTestRunner.java similarity index 59% rename from visual-tests/src/main/java/VisualTestRunner.java rename to visual-tests/src/main/java/processing/test/visual/VisualTestRunner.java index 20385ac80..b75df0a59 100644 --- a/visual-tests/src/main/java/VisualTestRunner.java +++ b/visual-tests/src/main/java/processing/test/visual/VisualTestRunner.java @@ -1,11 +1,11 @@ -// Processing Visual Test Runner - Modular test execution infrastructure -// Uses ImageComparator for sophisticated pixel matching - import processing.core.*; import java.io.*; import java.nio.file.*; import java.util.*; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; + // Core visual tester class public class VisualTestRunner { @@ -108,29 +108,72 @@ private String getBaselinePath(String testName) { return screenshotDir + "/" + sanitizedName + "-" + platform + ".png"; } + // Replace loadBaseline method: private PImage loadBaseline(String path) { File file = new File(path); - if (!file.exists()) return null; + if (!file.exists()) { + System.out.println("loadBaseline: File doesn't exist: " + file.getAbsolutePath()); + return null; + } - // Create a temporary PApplet to load the image - PApplet tempApplet = new PApplet(); - return tempApplet.loadImage(path); + try { + System.out.println("loadBaseline: Loading from " + file.getAbsolutePath()); + + // Use Java ImageIO instead of PApplet + BufferedImage img = ImageIO.read(file); + + if (img == null) { + System.out.println("loadBaseline: ImageIO returned null"); + return null; + } + + // Convert BufferedImage to PImage + PImage pImg = new PImage(img.getWidth(), img.getHeight(), PImage.RGB); + img.getRGB(0, 0, pImg.width, pImg.height, pImg.pixels, 0, pImg.width); + pImg.updatePixels(); + + System.out.println("loadBaseline: ✓ Loaded " + pImg.width + "x" + pImg.height); + return pImg; + + } catch (Exception e) { + System.err.println("loadBaseline: Error loading image: " + e.getMessage()); + e.printStackTrace(); + return null; + } } + // Replace saveBaseline method: private void saveBaseline(String testName, PImage image) { String path = getBaselinePath(testName); - image.save(path); - System.out.println("Baseline created: " + path); + + if (image == null) { + System.out.println("saveBaseline: ✗ Image is null!"); + return; + } + + try { + // Convert PImage to BufferedImage + BufferedImage bImg = new BufferedImage(image.width, image.height, BufferedImage.TYPE_INT_RGB); + image.loadPixels(); + bImg.setRGB(0, 0, image.width, image.height, image.pixels, 0, image.width); + + // Use Java ImageIO to save + File outputFile = new File(path); + outputFile.getParentFile().mkdirs(); // Ensure directory exists + + ImageIO.write(bImg, "PNG", outputFile); + + } catch (Exception e) { + e.printStackTrace(); + } } } - -// Test runner that executes Processing sketches class SketchRunner extends PApplet { private ProcessingSketch userSketch; private TestConfig config; private PImage capturedImage; - private boolean rendered = false; + private volatile boolean rendered = false; public SketchRunner(ProcessingSketch userSketch, TestConfig config) { this.userSketch = userSketch; @@ -142,7 +185,6 @@ public void settings() { } public void setup() { - // Disable animations for consistent testing noLoop(); // Set background if specified @@ -156,11 +198,8 @@ public void setup() { public void draw() { if (!rendered) { - // Call user draw function userSketch.draw(this); - - // Capture the frame - capturedImage = get(); // get() returns a PImage of the entire canvas + capturedImage = get(); rendered = true; } } @@ -169,25 +208,36 @@ public void run() { String[] args = {"SketchRunner"}; PApplet.runSketch(args, this); - // Wait for rendering to complete - while (!rendered) { + // Simple polling with timeout + int maxWait = 100; // 10 seconds max + int waited = 0; + + while (!rendered && waited < maxWait) { try { - Thread.sleep(10); + Thread.sleep(100); + waited++; + } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } - // Additional wait time for any processing + // Additional wait time try { Thread.sleep(config.renderWaitTime); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } - // Clean up - exit(); + if (surface != null) { + surface.setVisible(false); + } + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } public PImage getImage() { @@ -206,7 +256,7 @@ class TestConfig { public int width = 800; public int height = 600; public int[] backgroundColor = {255, 255, 255}; // RGB - public long renderWaitTime = 2000; // milliseconds + public long renderWaitTime = 100; // milliseconds public double threshold = 0.1; public TestConfig() {} @@ -416,219 +466,4 @@ public static void updateSingleBaseline(String testName, ProcessingSketch sketch BaselineManager manager = new BaselineManager(tester); manager.updateBaseline(testName, sketch, config); } -} - -// Example usage and test implementations -class ProcessingVisualTestExamples { - - public static void main(String[] args) { - // Initialize with your sophisticated pixel matching algorithm - PApplet tempApplet = new PApplet(); - - ImageComparator comparator = new ImageComparator(tempApplet); - VisualTestRunner tester = new VisualTestRunner(comparator); - ProcessingTestSuite suite = new ProcessingTestSuite(tester); - - // Add example tests - suite.addTest("red-circle", new RedCircleSketch()); - suite.addTest("blue-square", new BlueSquareSketch()); - suite.addTest("gradient-background", new GradientBackgroundSketch(), - new TestConfig(600, 400)); - suite.addTest("complex-pattern", new ComplexPatternSketch(), - new TestConfig(800, 600).setThreshold(0.15)); - - // Run all tests - List results = suite.runAll(); - - // Print detailed summary - printTestSummary(results); - - // Handle command line arguments - if (args.length > 0) { - handleCommandLineArgs(args, suite); - } - } - - private static void printTestSummary(List results) { - long passed = results.stream().filter(r -> r.passed).count(); - long failed = results.size() - passed; - long baselines = results.stream().filter(r -> r.isFirstRun).count(); - long errors = results.stream().filter(r -> r.error != null).count(); - - System.out.println("\n=== Test Summary ==="); - System.out.println("Total: " + results.size()); - System.out.println("Passed: " + passed); - System.out.println("Failed: " + failed); - System.out.println("Baselines Created: " + baselines); - System.out.println("Errors: " + errors); - - // Print detailed failure information - results.stream() - .filter(r -> !r.passed && !r.isFirstRun && r.error == null) - .forEach(r -> { - System.out.println("\n--- Failed Test: " + r.testName + " ---"); - if (r.details != null) { - r.details.printDetails(); - } - }); - } - - private static void handleCommandLineArgs(String[] args, ProcessingTestSuite suite) { - if (args[0].equals("--update")) { - // Update specific baselines or all - BaselineManager manager = new BaselineManager(null); // Will need tester reference - if (args.length > 1) { - for (int i = 1; i < args.length; i++) { - System.out.println("Updating baseline: " + args[i]); - // Update specific test baseline - } - } else { - System.out.println("Updating all baselines..."); - manager.updateAllBaselines(suite); - } - } else if (args[0].equals("--run")) { - // Run specific test - if (args.length > 1) { - String testName = args[1]; - TestResult result = suite.runTest(testName); - result.printResult(); - } - } - } - - // Example sketch: Red circle - static class RedCircleSketch implements ProcessingSketch { - public void setup(PApplet p) { - p.noStroke(); - } - - public void draw(PApplet p) { - p.background(255); - p.fill(255, 0, 0); - p.ellipse(p.width/2, p.height/2, 100, 100); - } - } - - // Example sketch: Blue square - static class BlueSquareSketch implements ProcessingSketch { - public void setup(PApplet p) { - p.stroke(0); - p.strokeWeight(2); - } - - public void draw(PApplet p) { - p.background(255); - p.fill(0, 0, 255); - p.rect(p.width/2 - 50, p.height/2 - 50, 100, 100); - } - } - - // Example sketch: Gradient background - static class GradientBackgroundSketch implements ProcessingSketch { - public void setup(PApplet p) { - p.noStroke(); - } - - public void draw(PApplet p) { - for (int y = 0; y < p.height; y++) { - float inter = PApplet.map(y, 0, p.height, 0, 1); - int c = p.lerpColor(p.color(255, 0, 0), p.color(0, 0, 255), inter); - p.stroke(c); - p.line(0, y, p.width, y); - } - } - } - - // Example sketch: Complex pattern (more likely to have minor differences) - static class ComplexPatternSketch implements ProcessingSketch { - public void setup(PApplet p) { - p.noStroke(); - } - - public void draw(PApplet p) { - p.background(20, 20, 40); - - for (int x = 0; x < p.width; x += 15) { - for (int y = 0; y < p.height; y += 15) { - float noise = (float) (Math.sin(x * 0.02) * Math.cos(y * 0.02)); - int brightness = (int) ((noise + 1) * 127.5); - - p.fill(brightness, brightness * 0.7f, brightness * 1.2f); - p.rect(x, y, 12, 12); - - // Add some text that might cause line shifts - if (x % 60 == 0 && y % 60 == 0) { - p.fill(255); - p.textSize(8); - p.text(brightness, x + 2, y + 10); - } - } - } - } - } -} - -// CI Integration helper -class ProcessingCIHelper { - - public static void main(String[] args) { - if (args.length > 0 && args[0].equals("--ci")) { - runCITests(); - } else { - ProcessingVisualTestExamples.main(args); - } - } - - public static void runCITests() { - System.out.println("Running visual tests in CI mode..."); - - // Initialize comparator - PApplet tempApplet = new PApplet(); - - ImageComparator comparator = new ImageComparator(tempApplet); - VisualTestRunner tester = new VisualTestRunner(comparator); - ProcessingTestSuite suite = new ProcessingTestSuite(tester); - - // Add your actual test cases here - suite.addTest("ci-test-1", new ProcessingVisualTestExamples.RedCircleSketch()); - suite.addTest("ci-test-2", new ProcessingVisualTestExamples.BlueSquareSketch()); - - // Run tests - List results = suite.runAll(); - - // Check for failures - boolean hasFailures = results.stream().anyMatch(r -> !r.passed && !r.isFirstRun); - boolean hasErrors = results.stream().anyMatch(r -> r.error != null); - - if (hasFailures || hasErrors) { - System.err.println("Visual tests failed!"); - System.exit(1); - } else { - System.out.println("All visual tests passed!"); - System.exit(0); - } - } - -// public static void updateCIBaselines(String[] testNames) { -// System.out.println("Updating baselines in CI mode..."); -// -// // Initialize components -// PApplet tempApplet = new PApplet(); -// -// ImageComparator comparator = new ImageComparator(tempApplet); -// VisualTestRunner = new VisualTestRunner(comparator); -// BaselineManager manager = new BaselineManager(tester); -// -// if (testNames.length == 0) { -// System.out.println("No specific tests specified, updating all..."); -// // Update all baselines - you'd need to implement this based on your test discovery -// } else { -// for (String testName : testNames) { -// System.out.println("Updating baseline for: " + testName); -// // Update specific baseline - you'd need the corresponding sketch -// } -// } -// -// System.out.println("Baseline update completed!"); -// } } \ No newline at end of file From 593413819e641cc057f7a5e5b7be27e25eb638dd Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 7 Oct 2025 02:06:13 +0530 Subject: [PATCH 06/15] added junit as dependency --- visual-tests/build.gradle.kts | 187 ++++++++++++++++++++++++++++++---- 1 file changed, 170 insertions(+), 17 deletions(-) diff --git a/visual-tests/build.gradle.kts b/visual-tests/build.gradle.kts index 1aa74d91b..648be87a7 100644 --- a/visual-tests/build.gradle.kts +++ b/visual-tests/build.gradle.kts @@ -15,9 +15,17 @@ repositories { //} dependencies { implementation(project(":core")) - testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") + + // JUnit BOM to manage versions + testImplementation(platform("org.junit:junit-bom:5.9.3")) + testImplementation("org.junit.jupiter:junit-jupiter") + //testRuntimeOnly("org.junit.platform:test-platform-launcher:1.9.3") + + // Optional: AssertJ for better assertions + testImplementation("org.assertj:assertj-core:3.24.2") } + application { mainClass.set("ProcessingVisualTestExamples") } @@ -27,39 +35,184 @@ java { targetCompatibility = JavaVersion.VERSION_17 } -// Visual testing tasks -tasks.register("runVisualTests") { - description = "Run all visual tests" - classpath = sourceSets.main.get().runtimeClasspath - mainClass.set("ProcessingVisualTestExamples") +//// Visual testing tasks +//tasks.register("runVisualTests") { +// description = "Run all visual tests" +// classpath = sourceSets.main.get().runtimeClasspath +// mainClass.set("ProcessingVisualTestExamples") +//} +// +//tasks.register("runSimpleTest") { +// description = "Verify visual testing setup" +// classpath = sourceSets.main.get().runtimeClasspath +// mainClass.set("SimpleTest") +//} +// +//tasks.register("updateBaselines") { +// description = "Update visual test baselines" +// classpath = sourceSets.main.get().runtimeClasspath +// mainClass.set("ProcessingCIHelper") +// args("--update") +//} +// +//tasks.register("runCITests") { +// description = "Run visual tests in CI" +// classpath = sourceSets.main.get().runtimeClasspath +// mainClass.set("ProcessingCIHelper") +// args("--ci") +// systemProperty("java.awt.headless", "true") +//} +// +//tasks.register("cleanVisualTestFiles") { +// delete(fileTree(".") { +// include("__screenshots__/**") +// include("diff_*.png") +// }) +//} +// +//tasks.named("clean") { +// dependsOn("cleanVisualTestFiles") +//} + +tasks.test { + useJUnitPlatform() + + testLogging { + events("passed", "skipped", "failed") + showStandardStreams = true + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } + + // Disable parallel execution to avoid Processing window conflicts + maxParallelForks = 1 + + // Add system properties + systemProperty("java.awt.headless", "false") +} + +// Task to update baselines using JUnit +tasks.register("updateBaselines") { + description = "Update visual test baselines" + group = "verification" + + useJUnitPlatform { + includeTags("baseline") + } + + systemProperty("update.baselines", "true") + maxParallelForks = 1 + + testLogging { + events("passed", "skipped", "failed") + showStandardStreams = true + } +} + +// Task to run only visual tests (excluding slow tests) +tasks.register("visualTest") { + description = "Run visual tests (excluding slow tests)" + group = "verification" + + useJUnitPlatform { + excludeTags("slow") + } + + maxParallelForks = 1 } +// Task to run tests for specific feature +tasks.register("testShapes") { + description = "Run shape-related visual tests" + group = "verification" + + useJUnitPlatform { + includeTags("shapes") + } + + maxParallelForks = 1 +} + +// Legacy task - keep for backward compatibility during migration tasks.register("runSimpleTest") { - description = "Verify visual testing setup" + description = "[DEPRECATED] Use 'test' instead - Verify visual testing setup" + group = "verification" classpath = sourceSets.main.get().runtimeClasspath mainClass.set("SimpleTest") + + doFirst { + println("⚠️ WARNING: This task is deprecated. Please use './gradlew test' instead") + } } -tasks.register("updateBaselines") { - description = "Update visual test baselines" +// Legacy task - keep for backward compatibility +tasks.register("runVisualTests") { + description = "[DEPRECATED] Use 'test' instead - Run all visual tests" + group = "verification" classpath = sourceSets.main.get().runtimeClasspath - mainClass.set("ProcessingCIHelper") - args("--update") + mainClass.set("ProcessingVisualTestExamples") + + doFirst { + println("⚠️ WARNING: This task is deprecated. Please use './gradlew test' instead") + } } -tasks.register("runCITests") { - description = "Run visual tests in CI" - classpath = sourceSets.main.get().runtimeClasspath - mainClass.set("ProcessingCIHelper") - args("--ci") - systemProperty("java.awt.headless", "true") +// CI-specific test task +tasks.register("ciTest") { + description = "Run visual tests in CI mode" + group = "verification" + + useJUnitPlatform() + + outputs.upToDateWhen { false } + + maxParallelForks = 1 + + testLogging { + events("passed", "skipped", "failed") + showStandardStreams = true + //exceptionFormat = org.gradle.api.tasks.testing.TestExceptionFormat.FULL + } + + // Generate XML reports for CI + reports { + junitXml.required.set(true) + html.required.set(true) + } + + // Fail fast in CI + failFast = true } +// Clean task for visual test artifacts tasks.register("cleanVisualTestFiles") { + description = "Clean visual test artifacts" + group = "build" + + delete(fileTree(".") { + include("diff_*.png") + }) + + // Don't delete baselines by default - be explicit + doLast { + println("✓ Cleaned diff images") + println(" Baselines preserved in __screenshots__/") + println(" To clean baselines: rm -rf __screenshots__/") + } +} + +// Separate task to clean everything including baselines (dangerous!) +tasks.register("cleanAllVisualTestFiles") { + description = "Clean ALL visual test files INCLUDING BASELINES (use with caution!)" + group = "build" + delete(fileTree(".") { include("__screenshots__/**") include("diff_*.png") }) + + doFirst { + println("⚠️ WARNING: This will delete all baseline images!") + } } tasks.named("clean") { From c4915dd5750918f48048009f8e24ce2aaceedd59 Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 7 Oct 2025 02:07:41 +0530 Subject: [PATCH 07/15] removing custom class implementation --- visual-tests/src/main/java/SimpleTest.java | 250 +++++++++------------ 1 file changed, 105 insertions(+), 145 deletions(-) diff --git a/visual-tests/src/main/java/SimpleTest.java b/visual-tests/src/main/java/SimpleTest.java index 5c59885d4..ae31df988 100644 --- a/visual-tests/src/main/java/SimpleTest.java +++ b/visual-tests/src/main/java/SimpleTest.java @@ -1,145 +1,105 @@ -// SimpleTest.java - Fixed version for quick verification -import processing.core.*; -import java.util.*; - -public class SimpleTest { - - public static void main(String[] args) { - System.out.println("=== Processing Visual Testing - Quick Test ===\n"); - - try { - // Step 1: Initialize Processing environment - System.out.println("1. Initializing Processing environment..."); - PApplet tempApplet = new PApplet(); - // Fixed: Removed tempApplet.init() - not needed in modern Processing - System.out.println("✓ Processing initialized"); - - // Step 2: Create comparator - System.out.println("2. Creating image comparator..."); - ImageComparator comparator = new ImageComparator(tempApplet); - VisualTestRunner tester = new VisualTestRunner(comparator); - System.out.println("✓ Comparator created"); - - // Step 3: Run basic tests - System.out.println("3. Running basic tests...\n"); - - // Test 1: Simple red circle - System.out.println("--- Test 1: Red Circle ---"); - ProcessingSketch redCircle = new SimpleRedCircle(); - TestResult result1 = tester.runVisualTest("red-circle", redCircle); - result1.printResult(); - - // Test 2: Blue square - System.out.println("\n--- Test 2: Blue Square ---"); - ProcessingSketch blueSquare = new SimpleBlueSquare(); - TestResult result2 = tester.runVisualTest("blue-square", blueSquare); - result2.printResult(); - - // Step 4: Test comparison with identical image (should pass on second run) - if (!result1.isFirstRun) { - System.out.println("\n--- Test 3: Identical Image (Should Pass) ---"); - TestResult result3 = tester.runVisualTest("red-circle", redCircle); - result3.printResult(); - - if (result3.passed) { - System.out.println("✓ Identical image comparison works!"); - } else { - System.out.println("✗ Identical image comparison failed!"); - } - } - - // Step 5: Test comparison with different image (should fail) - System.out.println("\n--- Test 4: Different Image (Should Fail) ---"); - ProcessingSketch greenCircle = new SimpleGreenCircle(); - TestResult result4 = tester.runVisualTest("red-circle", greenCircle); - result4.printResult(); - - if (!result4.passed && !result4.isFirstRun) { - System.out.println("✓ Different image detection works!"); - if (result4.details != null) { - System.out.println("Algorithm detected " + result4.details.totalDiffPixels + " different pixels"); - } - } - - // Step 6: Test suite functionality - System.out.println("\n--- Test 5: Test Suite ---"); - ProcessingTestSuite suite = new ProcessingTestSuite(tester); - suite.addTest("suite-red", new SimpleRedCircle()); - suite.addTest("suite-blue", new SimpleBlueSquare()); - suite.addTest("suite-gradient", new SimpleGradient()); - - List suiteResults = suite.runAll(); - - long passed = suiteResults.stream().filter(r -> r.passed).count(); - long total = suiteResults.size(); - System.out.println("Suite results: " + passed + "/" + total + " passed"); - - // Final summary - System.out.println("\n=== Test Summary ==="); - System.out.println("✓ Processing environment works"); - System.out.println("✓ Image comparator works"); - System.out.println("✓ Baseline creation works"); - System.out.println("✓ Visual test execution works"); - System.out.println("✓ Test suite functionality works"); - System.out.println("\nCheck the '__screenshots__' folder for baseline images!"); - System.out.println("Check for 'diff_*.png' files if any tests failed!"); - - } catch (Exception e) { - System.err.println("Test failed with error: " + e.getMessage()); - e.printStackTrace(); - } - } - - // Simple test sketches - static class SimpleRedCircle implements ProcessingSketch { - public void setup(PApplet p) { - p.noStroke(); - } - - public void draw(PApplet p) { - p.background(255, 255, 255); - p.fill(255, 0, 0); - p.ellipse(p.width/2, p.height/2, 100, 100); - } - } - - static class SimpleBlueSquare implements ProcessingSketch { - public void setup(PApplet p) { - p.stroke(0); - p.strokeWeight(2); - } - - public void draw(PApplet p) { - p.background(255); - p.fill(0, 0, 255); - p.rect(p.width/2 - 50, p.height/2 - 50, 100, 100); - } - } - - static class SimpleGreenCircle implements ProcessingSketch { - public void setup(PApplet p) { - p.noStroke(); - } - - public void draw(PApplet p) { - p.background(255, 255, 255); - p.fill(0, 255, 0); // Green instead of red - p.ellipse(p.width/2, p.height/2, 100, 100); - } - } - - static class SimpleGradient implements ProcessingSketch { - public void setup(PApplet p) { - p.noStroke(); - } - - public void draw(PApplet p) { - for (int y = 0; y < p.height; y++) { - float inter = PApplet.map(y, 0, p.height, 0, 1); - int c = p.lerpColor(p.color(255, 0, 0), p.color(0, 0, 255), inter); - p.stroke(c); - p.line(0, y, p.width, y); - } - } - } -} \ No newline at end of file +//// SimpleTest.java - Fixed version for quick verification +//import processing.core.*; +//import java.util.*; +// +//public class SimpleTest { +// +// public static void main(String[] args) { +// System.out.println("=== Processing Visual Testing - Quick Test ===\n"); +// +// try { +// PApplet tempApplet = new PApplet(); +// +// ImageComparator comparator = new ImageComparator(tempApplet); +// VisualTestRunner tester = new VisualTestRunner(comparator); +// +// ProcessingSketch redCircle = new SimpleRedCircle(); +// TestResult result1 = tester.runVisualTest("red-circle", redCircle); +// result1.printResult(); +// +// ProcessingSketch blueSquare = new SimpleBlueSquare(); +// TestResult result2 = tester.runVisualTest("blue-square", blueSquare); +// result2.printResult(); +// +// // Step 4: Test comparison with identical image (should pass on second run) +// if (!result1.isFirstRun) { +// TestResult result3 = tester.runVisualTest("red-circle", redCircle); +// result3.printResult(); +// +// if (result3.passed) { +// System.out.println("✓ Identical image comparison works!"); +// } else { +// System.out.println("✗ Identical image comparison failed!"); +// } +// } +// ProcessingTestSuite suite = new ProcessingTestSuite(tester); +// suite.addTest("suite-red", new SimpleRedCircle()); +// suite.addTest("suite-blue", new SimpleBlueSquare()); +// suite.addTest("suite-gradient", new SimpleGradient()); +// +// List suiteResults = suite.runAll(); +// +// long passed = suiteResults.stream().filter(r -> r.passed).count(); +// long total = suiteResults.size(); +// System.out.println("Suite results: " + passed + "/" + total + " passed"); +// System.exit(0); +// +// } catch (Exception e) { +// System.err.println("Test failed with error: " + e.getMessage()); +// e.printStackTrace(); +// } +// } +// +// // Simple test sketches +// static class SimpleRedCircle implements ProcessingSketch { +// public void setup(PApplet p) { +// p.noStroke(); +// } +// +// public void draw(PApplet p) { +// p.background(255, 255, 255); +// p.fill(255, 0, 0); +// p.ellipse(p.width/2, p.height/2, 100, 100); +// } +// } +// +// static class SimpleBlueSquare implements ProcessingSketch { +// public void setup(PApplet p) { +// p.stroke(0); +// p.strokeWeight(2); +// } +// +// public void draw(PApplet p) { +// p.background(255); +// p.fill(0, 0, 255); +// p.rect(p.width/2 - 50, p.height/2 - 50, 100, 100); +// } +// } +// +// static class SimpleGreenCircle implements ProcessingSketch { +// public void setup(PApplet p) { +// p.noStroke(); +// } +// +// public void draw(PApplet p) { +// p.background(255, 255, 255); +// p.fill(0, 255, 0); // Green instead of red +// p.ellipse(p.width/2, p.height/2, 100, 100); +// } +// } +// +// static class SimpleGradient implements ProcessingSketch { +// public void setup(PApplet p) { +// p.noStroke(); +// } +// +// public void draw(PApplet p) { +// for (int y = 0; y < p.height; y++) { +// float inter = PApplet.map(y, 0, p.height, 0, 1); +// int c = p.lerpColor(p.color(255, 0, 0), p.color(0, 0, 255), inter); +// p.stroke(c); +// p.line(0, y, p.width, y); +// } +// } +// } +//} \ No newline at end of file From d549a1813ac0ecdf78a072f157ecf3cfadbd72ee Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 7 Oct 2025 02:08:20 +0530 Subject: [PATCH 08/15] inclding visual-tests in settings --- settings.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 8f8cb74c7..b0be4a376 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,5 +11,7 @@ include( "java:libraries:pdf", "java:libraries:serial", "java:libraries:svg", + ":visual-tests" ) -include("app:utils") \ No newline at end of file +include("app:utils") +include(":visual-tests") \ No newline at end of file From cac895a14a0038dbd41ae94a48b8f9064ee4d555 Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 7 Oct 2025 03:32:41 +0530 Subject: [PATCH 09/15] fixed the overlapping cmd --- build.gradle.kts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 18ef17eb3..525641e50 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,9 +8,5 @@ plugins { // Set the build directory to not /build to prevent accidental deletion through the clean action // Can be deleted after the migration to Gradle is complete -tasks.register("visualTests") { - description = "Run visual regression tests" - dependsOn(":visual-testing:runVisualTests") -} layout.buildDirectory = file(".build") \ No newline at end of file From 803516feed383bfc07f8ac449f2a27f783ab5c27 Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 7 Oct 2025 03:34:18 +0530 Subject: [PATCH 10/15] cleaning --- visual-tests/build.gradle.kts | 143 ++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 68 deletions(-) diff --git a/visual-tests/build.gradle.kts b/visual-tests/build.gradle.kts index 648be87a7..f5931f0a2 100644 --- a/visual-tests/build.gradle.kts +++ b/visual-tests/build.gradle.kts @@ -8,20 +8,14 @@ repositories { maven { url = uri("https://jogamp.org/deployment/maven") } } -//dependencies { -// // Reference to Processing core -// implementation(project(":core")) -// testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") -//} dependencies { implementation(project(":core")) // JUnit BOM to manage versions testImplementation(platform("org.junit:junit-bom:5.9.3")) testImplementation("org.junit.jupiter:junit-jupiter") - //testRuntimeOnly("org.junit.platform:test-platform-launcher:1.9.3") + testImplementation("org.junit.platform:junit-platform-suite:1.9.3") - // Optional: AssertJ for better assertions testImplementation("org.assertj:assertj-core:3.24.2") } @@ -35,45 +29,6 @@ java { targetCompatibility = JavaVersion.VERSION_17 } -//// Visual testing tasks -//tasks.register("runVisualTests") { -// description = "Run all visual tests" -// classpath = sourceSets.main.get().runtimeClasspath -// mainClass.set("ProcessingVisualTestExamples") -//} -// -//tasks.register("runSimpleTest") { -// description = "Verify visual testing setup" -// classpath = sourceSets.main.get().runtimeClasspath -// mainClass.set("SimpleTest") -//} -// -//tasks.register("updateBaselines") { -// description = "Update visual test baselines" -// classpath = sourceSets.main.get().runtimeClasspath -// mainClass.set("ProcessingCIHelper") -// args("--update") -//} -// -//tasks.register("runCITests") { -// description = "Run visual tests in CI" -// classpath = sourceSets.main.get().runtimeClasspath -// mainClass.set("ProcessingCIHelper") -// args("--ci") -// systemProperty("java.awt.headless", "true") -//} -// -//tasks.register("cleanVisualTestFiles") { -// delete(fileTree(".") { -// include("__screenshots__/**") -// include("diff_*.png") -// }) -//} -// -//tasks.named("clean") { -// dependsOn("cleanVisualTestFiles") -//} - tasks.test { useJUnitPlatform() @@ -108,52 +63,104 @@ tasks.register("updateBaselines") { } } -// Task to run only visual tests (excluding slow tests) -tasks.register("visualTest") { - description = "Run visual tests (excluding slow tests)" +tasks.register("testShapes") { + description = "Run shape-related visual tests" group = "verification" useJUnitPlatform { - excludeTags("slow") + includeTags("shapes") } maxParallelForks = 1 } -// Task to run tests for specific feature -tasks.register("testShapes") { - description = "Run shape-related visual tests" +tasks.register("testBasicShapes") { + description = "Run basic shapes visual tests" group = "verification" useJUnitPlatform { - includeTags("shapes") + includeTags("basic") } + outputs.upToDateWhen { false } maxParallelForks = 1 + + // Add test logging to see what's happening + testLogging { + events("passed", "skipped", "failed", "started") + showStandardStreams = true + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } } -// Legacy task - keep for backward compatibility during migration -tasks.register("runSimpleTest") { - description = "[DEPRECATED] Use 'test' instead - Verify visual testing setup" +// Task to run ONLY visual tests (no other test types) +tasks.register("visualTests") { + description = "Run all visual tests" group = "verification" - classpath = sourceSets.main.get().runtimeClasspath - mainClass.set("SimpleTest") - doFirst { - println("⚠️ WARNING: This task is deprecated. Please use './gradlew test' instead") + useJUnitPlatform { + // Include all tests in the visual test package + includeEngines("junit-jupiter") + } + + filter { + includeTestsMatching("visual.*") + } + + outputs.upToDateWhen { false } + maxParallelForks = 1 + + testLogging { + events("passed", "skipped", "failed", "started") + showStandardStreams = true + displayGranularity = 2 } } -// Legacy task - keep for backward compatibility -tasks.register("runVisualTests") { - description = "[DEPRECATED] Use 'test' instead - Run all visual tests" +tasks.register("testRendering") { + description = "Run rendering visual tests" group = "verification" - classpath = sourceSets.main.get().runtimeClasspath - mainClass.set("ProcessingVisualTestExamples") - doFirst { - println("⚠️ WARNING: This task is deprecated. Please use './gradlew test' instead") + useJUnitPlatform { + includeTags("rendering") } + + outputs.upToDateWhen { false } + maxParallelForks = 1 + + testLogging { + events("passed", "skipped", "failed", "started") + showStandardStreams = true + } +} + +tasks.register("runSuite") { + description = "Run specific test suite (use -PsuiteClass=SuiteName)" + group = "verification" + + useJUnitPlatform { + val suiteClass = project.findProperty("suiteClass") as String? + ?: "visual.suites.AllVisualTests" + includeTags(suiteClass) + } + + outputs.upToDateWhen { false } + maxParallelForks = 1 +} + +// Update baselines for specific suite +tasks.register("updateBaselinesForSuite") { + description = "Update baselines for specific suite (use -Psuite=tag)" + group = "verification" + + useJUnitPlatform { + val suite = project.findProperty("suite") as String? ?: "baseline" + includeTags(suite, "baseline") + } + + systemProperty("update.baselines", "true") + outputs.upToDateWhen { false } + maxParallelForks = 1 } // CI-specific test task From ace6f7bec12a18ac3739e5d965d04f1cfa2e44b3 Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 7 Oct 2025 03:35:37 +0530 Subject: [PATCH 11/15] adding packages --- .../test/visual/ImageComparator.java | 2 +- .../test/visual/VisualTestRunner.java | 192 ++---------------- 2 files changed, 23 insertions(+), 171 deletions(-) diff --git a/visual-tests/src/main/java/processing/test/visual/ImageComparator.java b/visual-tests/src/main/java/processing/test/visual/ImageComparator.java index 6420c48cd..66bb34b50 100644 --- a/visual-tests/src/main/java/processing/test/visual/ImageComparator.java +++ b/visual-tests/src/main/java/processing/test/visual/ImageComparator.java @@ -1,4 +1,4 @@ -package +package processing.test.visual; import processing.core.*; import java.util.*; diff --git a/visual-tests/src/main/java/processing/test/visual/VisualTestRunner.java b/visual-tests/src/main/java/processing/test/visual/VisualTestRunner.java index b75df0a59..0f0ffe100 100644 --- a/visual-tests/src/main/java/processing/test/visual/VisualTestRunner.java +++ b/visual-tests/src/main/java/processing/test/visual/VisualTestRunner.java @@ -1,3 +1,5 @@ +package processing.test.visual; + import processing.core.*; import java.io.*; import java.nio.file.*; @@ -82,7 +84,16 @@ private ComparisonResult compareWithBaseline(String testName, PImage actualImage // Save diff image for debugging private void saveDiffImage(String testName, PImage diffImage) { String sanitizedName = testName.replaceAll("[^a-zA-Z0-9-_]", "-"); - String diffPath = "diff_" + sanitizedName + "-" + platform + ".png"; + String diffPath; + if (sanitizedName.contains("/")) { + diffPath = "diff_" + sanitizedName.replace("/", "_") + "-" + platform + ".png"; + } else { + diffPath = "diff_" + sanitizedName + "-" + platform + ".png"; + } + + File diffFile = new File(diffPath); + diffFile.getParentFile().mkdirs(); + diffImage.save(diffPath); System.out.println("Diff image saved: " + diffPath); } @@ -104,8 +115,9 @@ private void createDirectoryIfNotExists(String dir) { } private String getBaselinePath(String testName) { - String sanitizedName = testName.replaceAll("[^a-zA-Z0-9-_]", "-"); - return screenshotDir + "/" + sanitizedName + "-" + platform + ".png"; + String sanitizedName = testName.replaceAll("[^a-zA-Z0-9-_/]", "-"); + + return screenshotDir + "/" + sanitizedName + "-" + platform + ".png"; } // Replace loadBaseline method: @@ -157,13 +169,17 @@ private void saveBaseline(String testName, PImage image) { image.loadPixels(); bImg.setRGB(0, 0, image.width, image.height, image.pixels, 0, image.width); - // Use Java ImageIO to save + // Create File object and ensure parent directories exist File outputFile = new File(path); - outputFile.getParentFile().mkdirs(); // Ensure directory exists + outputFile.getParentFile().mkdirs(); // This creates nested directories + // Use Java ImageIO to save ImageIO.write(bImg, "PNG", outputFile); + System.out.println("Baseline saved: " + path); + } catch (Exception e) { + System.err.println("Failed to save baseline: " + path); e.printStackTrace(); } } @@ -201,6 +217,7 @@ public void draw() { userSketch.draw(this); capturedImage = get(); rendered = true; + noLoop(); } } @@ -245,88 +262,6 @@ public PImage getImage() { } } -// Interface for user sketches -interface ProcessingSketch { - void setup(PApplet p); - void draw(PApplet p); -} - -// Test configuration class -class TestConfig { - public int width = 800; - public int height = 600; - public int[] backgroundColor = {255, 255, 255}; // RGB - public long renderWaitTime = 100; // milliseconds - public double threshold = 0.1; - - public TestConfig() {} - - public TestConfig(int width, int height) { - this.width = width; - this.height = height; - } - - public TestConfig(int width, int height, int[] backgroundColor) { - this.width = width; - this.height = height; - this.backgroundColor = backgroundColor; - } - - public TestConfig setThreshold(double threshold) { - this.threshold = threshold; - return this; - } - - public TestConfig setRenderWaitTime(long waitTime) { - this.renderWaitTime = waitTime; - return this; - } -} - -// Enhanced test result with detailed information -class TestResult { - public String testName; - public boolean passed; - public double mismatchRatio; - public String error; - public boolean isFirstRun; - public ComparisonDetails details; - - public TestResult(String testName, ComparisonResult comparison) { - this.testName = testName; - this.passed = comparison.passed; - this.mismatchRatio = comparison.mismatchRatio; - this.isFirstRun = comparison.isFirstRun; - this.details = comparison.details; - } - - public static TestResult createError(String testName, String error) { - TestResult result = new TestResult(); - result.testName = testName; - result.passed = false; - result.error = error; - return result; - } - - private TestResult() {} // For error constructor - - public void printResult() { - System.out.print(testName + ": "); - if (error != null) { - System.out.println("ERROR - " + error); - } else if (isFirstRun) { - System.out.println("BASELINE CREATED"); - } else if (passed) { - System.out.println("PASSED"); - } else { - System.out.println("FAILED (mismatch: " + String.format("%.4f", mismatchRatio * 100) + "%)"); - if (details != null) { - details.printDetails(); - } - } - } -} - // Test suite for organizing multiple tests class ProcessingTestSuite { private VisualTestRunner tester; @@ -384,86 +319,3 @@ private static class VisualTest { } } } - -// Baseline manager for updating reference images -class BaselineManager { - private VisualTestRunner tester; - - public BaselineManager(VisualTestRunner tester) { - this.tester = tester; - } - - public void updateBaseline(String testName, ProcessingSketch sketch) { - updateBaseline(testName, sketch, new TestConfig()); - } - - public void updateBaseline(String testName, ProcessingSketch sketch, TestConfig config) { - System.out.println("Updating baseline for: " + testName); - - // Capture new image - SketchRunner runner = new SketchRunner(sketch, config); - runner.run(); - PImage newImage = runner.getImage(); - - // Save as baseline - String baselinePath = "__screenshots__/" + - testName.replaceAll("[^a-zA-Z0-9-_]", "-") + - "-" + detectPlatform() + ".png"; - newImage.save(baselinePath); - - System.out.println("Baseline updated: " + baselinePath); - } - - public void updateAllBaselines(ProcessingTestSuite suite) { - System.out.println("Updating all baselines..."); - List testNames = suite.getTestNames(); - - for (String testName : testNames) { - // Re-run the test to get the sketch and config - TestResult result = suite.runTest(testName); - // Note: In a real implementation, you'd need to store the sketch reference - // This is a simplified version - } - } - - private String detectPlatform() { - String os = System.getProperty("os.name").toLowerCase(); - if (os.contains("mac")) return "darwin"; - if (os.contains("win")) return "win32"; - return "linux"; - } -} - -// Test execution utilities -class TestExecutor { - - public static void runSingleTest(String testName, ProcessingSketch sketch) { - runSingleTest(testName, sketch, new TestConfig()); - } - - public static void runSingleTest(String testName, ProcessingSketch sketch, TestConfig config) { - // Initialize comparator - PApplet tempApplet = new PApplet(); - ImageComparator comparator = new ImageComparator(tempApplet); - - // Run test - VisualTestRunner tester = new VisualTestRunner(comparator); - TestResult result = tester.runVisualTest(testName, sketch, config); - result.printResult(); - } - - public static void updateSingleBaseline(String testName, ProcessingSketch sketch) { - updateSingleBaseline(testName, sketch, new TestConfig()); - } - - public static void updateSingleBaseline(String testName, ProcessingSketch sketch, TestConfig config) { - // Initialize comparator - PApplet tempApplet = new PApplet(); - ImageComparator comparator = new ImageComparator(tempApplet); - - // Update baseline - VisualTestRunner tester = new VisualTestRunner(comparator); - BaselineManager manager = new BaselineManager(tester); - manager.updateBaseline(testName, sketch, config); - } -} \ No newline at end of file From 5e7f798634e9c0e86655dd702e40d9479194b506 Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 7 Oct 2025 03:36:15 +0530 Subject: [PATCH 12/15] added updated screenshot structure --- .../basic-shapes/blue-square-linux.png | Bin 0 -> 3380 bytes .../basic-shapes/custom-size-rect-linux.png | Bin 0 -> 2756 bytes .../basic-shapes/green-circle-linux.png | Bin 0 -> 4449 bytes .../basic-shapes/red-circle-linux.png | Bin 0 -> 4228 bytes .../rendering/linear-gradient-linux.png | Bin 0 -> 2943 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 visual-tests/__screenshots__/basic-shapes/blue-square-linux.png create mode 100644 visual-tests/__screenshots__/basic-shapes/custom-size-rect-linux.png create mode 100644 visual-tests/__screenshots__/basic-shapes/green-circle-linux.png create mode 100644 visual-tests/__screenshots__/basic-shapes/red-circle-linux.png create mode 100644 visual-tests/__screenshots__/rendering/linear-gradient-linux.png diff --git a/visual-tests/__screenshots__/basic-shapes/blue-square-linux.png b/visual-tests/__screenshots__/basic-shapes/blue-square-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..e2f49aa29383b0f68da47afcd5d075dd94262884 GIT binary patch literal 3380 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i0*Z)=h^jL%@cj04aSW-5dwXqTuS6IR z>%rGP?|NSOT%TEEu%JbLgHMI=*$1vXl5dauGcW|n|7Kt~@LzcY149xMH-m!OC}T7b zMpMFQE}&K}xKa1#hha=jUEMo%rh2nR)|~jG9}XV<{{H@V;YRz}2bi|SH-5byZh7PV zx3`a-6BwZ)q5Sr5%cUDx_ppd<;9PUsErC%VsFmsUCc%gU-XK-Ga#+MZ2t^!F+n_h9 zY&1YdQ^>&Qg}+>D^8a6H{`-3$r_KY_4GnL@6F$as>LdWO?6;pkf6nb*-~Sd;1~3Q+ vM=&rLHnNT~Mgw6qC5+|*YUP5B`xzevnB6ryCbcywWzv$%l1`qNUh*td zd0EmHFsa1KS;Nz6uHqDxx#xzyvb++G`_EY)B!9Y|pTfbYaWa1YL`9YbD)oQe+5{RV z{rM^_>BQhQDmfYmqbXrDJB*eIqXpw=?J!zJQn#JZ%ePR)bMEkLUg2yZ)(3_QdT|N( RO8{G^44$rjF6*2UngB3MedhoG literal 0 HcmV?d00001 diff --git a/visual-tests/__screenshots__/basic-shapes/green-circle-linux.png b/visual-tests/__screenshots__/basic-shapes/green-circle-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..d4434cc2bc691ecc5dd3cc94da012d16a9bca840 GIT binary patch literal 4449 zcmeI0YgCg*8pkIuWCLcaD21$|gnFvk)yQS3Vt^2}pdu8cS`rTmiKwZfP)H-;l905r zY%3RSt3)B-5m^K@#0z48T$Zk&f@Ez7q;e^1j>aSqL=wmb6L$2R_TcxPvtRi7f8Lqr znVJ9m=AHRwcifKm7OhwW0C+EYXXK{}E56lG zaa{~_kg&sJC*ztc`uo+_JCfX!1ucHj>r)T^yE{-;SlHZhX7!a@&AS7I{L9|aM{kW3 zt}f1->m&tFi5_2=3cuDt3D&pFG(>F9>b-#gKcm)$0br~|i2yfG8UPFg3;bf~C~(T1 z2>5{lH1Jp}0ieUx8*C9Se6%nH3ocmL2){QAkG^S4ij6}ZQw?Yr`rc-qwKYI zbQcq>Bnr^^zGbFKQ7h|)e{cQm8AJIvZemAqJbJSSg5A8k$*9S&4%W7QM19PQrCh}i zELo*ONvd7Fzt~bWFy+J6?G;@N{#;Rqe^O?4DP|&CJc89`#~U96Si2vDlIm1jAoogymXpX|DL{{vW2~dcMIujqj4*Zj%Gsb0_iIRL^4o@wrxUu=E@KyzNQZC#j~S*x=@EI4p=Mgp74{a*2S@q`E7+`3 zVL*Xh%e75LfVMRfelgDaH0was;4?`qUfvWqvWQM~_4asqyDIrAmesscY*=rJ>TE-e zRVtBbV*)vJ5A%64saTCOn2JMOwI9I39!ywg4WZ=nhyM}D<5G^b6O?goMEZ=crY6H! zAx&63YpH!5<}Ja&f!EeqE%#p^`l3pHajpzKc2S9()E$6hCoYV8ME!N3vDvLGGU6%~b!vt%5nu9{RT0aS33}Kba1nA=v(PUBr;rS@7#jwV*u5%p3 z!ro=oCcSi1KP%TucM`0Hb#_kW@@~fU*59a7H@+w=4nu4Z_jfXD(tAWVdezwF*{&~x zps603avf|gc}Hhxe881~zosvud%jVJTf=aFGc*@hAlgVesyeIQ|4|MjcZ2R9VAE2X z^1SbWPP(CI*6^%&7O`!iqmo(=wCE*)6LsamX6&d7EYeV9XDr4}@p4HzIf(F}DQ_xF z6Vi1(&kbHBNiLinbW}I=I_WR-1@VCOU7=SaeMi8K^i@a+J>jyenb`or(kwe1L0zXV z)ZsRo7sDz}n>8}h+IitLN9g>pSkwGYSYiKVrRR~Vv)~gP+76UoZJMR8Nk`!Nx!}s*7Ko+kLE?*eQI0!0hCbK z*Mu3bu7WI#w>yz8XgNyDZ2!dNZJ_<}i*I!$+XYdu(P&Lp;!6BE7veIS62VWmKeb7i z^s`FDd#EvQevTJq_S91M;P`-JL#y~OEUOQhANI;9Y#!%$66xzL*=pcu`>vMOK-a0`P9c(LMf}TIq(^ro%(;E1~CXKeVy4d$H=YAO@~a1I9}4Ewx^dJMS*#Z-Ar4Kw81Q$FM)u`GY%s&uHPw8& ztPN=2VBtA~9G={zUqot9myfS6O9HkJA!@XbT1iFs6GXp3pbIjso1K$>wkCx8=r=Va zxxagg&GY}R<|FPu<Z-Tv8j>G7GDTBVL!v|o3wyDScK!%CErSjrT%<(HO8bXA^gD&PIQF5#W{Sn#FKq6 zu2Qv;D~$LQe>{BsmQu*);qP-_#W=DceWa{2YRrpN3!`%ib;X@H$`5bShZsBSLpRo` zc=43m_+iYSRIU;VJf|)tYZ`wsw#2ZH_Fi%bdAliMd}x&modR1;arT!@^odThGsTx> z@rvUL4&`9}I;8+og*hGXU@6QgO(wl@sB%|hd_kUKr(&;S)b>aI*YH>gxL=Vw#dPE} znTH#a28>V6wZ2MqXrEcQ+&TGWJI*oa{L$Iy{K|RL`P6yd**Vz^BJc~>goT^Z!ew#c fzWo1uGH@-^*2-+jN&eLuhZ{@w3; zzdr{0`)X^h(F6c!@7%HN?*ObY2LK^!AP~{l)3*jdch}Bso>g3tn z=D@6oBcFSEZb(`CuOKfD8m7^Iiro14{v&svGr~qJ{`#&WOU!!BGglvVA1ax6VPYGc zp8sIo#GjHagoNv&m>2cI@+g~p8*RY2W;2rjK;lLO56t~27?6dt1q@Oq7U)`W0ccY* z09y-D1Zb=#0}!?Bv`oQr7c2|me?1GF0u;&tl`<}_nwR?c;+9yZS$#HPpu|SrB$YRGB&N} zGB*9r8$7_HDkqdBdZE0pd8s@s8mdc1|2?g!<{f)tV5nMmp<9|Ol}UGF{Li6?|NUnN z!a`$vICAZ8HCc|;^&EBJq>tbWTS`h4_$Yt)v%>#x~G4=YZv`Val6P{T#j79*_y{C z-x$2rSMi`R!j0-i+Ik*ElG10Wf>I?m*&>)3K_KW_av5jsBm2vJ=?&qRMD0DRz$a=3 zXSvM-uLj5RN7}0M0@X224w=~EUU6_Ujm7*K-{i8xRDCE$%^-1Of~0P&N9EV}h&3!M z1O8hn7;ElLQT)@FD@HzH>x|4rW30`&jN$%4-j<9_sfn~N=7@MFyG*Rf{hk`xt3mg# z?E0h(njhm2oGm4ZpB^zD5Mz`<^DAXy{roAI{N_ugR%y3HQ5dUrTm~*KzK#{5(rmoV zYM%~uL9^5hNIw$H?79WUe${#5>gq;CC}MTJR@l*z!`9IuZO+8z#6_12@_yZKYDU?D z;Ee==(zvLygn58(hoNZALsF`l$)<)THHHHuP|MXdE}m19YaAZo6@f+fzzC-*0#w4n zKG8m4;zPms_6;1JnxY?mFfRRE>IHOQ1C+Dbug^IQv+x)v$gYlj=D2D~&_X(Nf@VS6 z0LpQ;LxDAs%41PCn+zC_A!pa_+-cds4(>rn7f@vTz_VBFZRs_3>J&AYu43|Fq`u-f z%^7?Ov#C`lOjmIwtos-vPYUMK6VpQLlQfnMVgyUJO5CHm$c?A@0B@LN+hmetwYZw2 zK{*Z2Y)Gz{z>NimsFAi&Fy0EKs*u|%^1+zEN?1%hldq5Zf%OyO1Z(9j7Ek~4gKh5J zlY%U5j-BQZ$i!+Lo4jI=TN@sshhiX4lcjU7mpnK-+q}g1+?#v1nAwJ$f=PEqW-N^` z`=)$}qb2l17u-(sElrYV&*;wI#bWlHs&I<=G}8hGdLF$7o)rt`&pJ(5W18%KL~p?< zJ?LTKRCy9(!71B~>FhCXykIzbl)kffNO~FQK-r!BZer<`RH`L54R_74&bPX@3siG& zJymA?7d9&b=mF&VCd-9bveqBmKO0v{lfZU({t^2~#l9$hNWluy_lxCYy)T~$^_%Mi zJWF&FadqJ+_MNp5A?n)ioP1$p>D)n6vyHB;_!Rko}Dsv$jq!D zo%W}T6WR+KsjRCjMNTa>FG!{Jx@<((L8x(5u&6o2ZwHj5O3rdi6J~e*7H)b&2T!v2 zpk?0=S{`ah`EH;(jD9}+g6Of70Xks%t9$#Yj`6Q!lW@V=Rpl6y0yqm_$Ocko(#-y5 zm_{ph(L-?>wnMk;>g)s(eX-{|zRl3>_me1kei98yA}}}vukm(rRzti*EGMp{ z+=B(%uTb>g`>Hvmdg9_})t%ge;_KDE43{y(>BmA&VnR~4~9w;TRl06TsBw{g5E G-~R=h2r~2l literal 0 HcmV?d00001 diff --git a/visual-tests/__screenshots__/rendering/linear-gradient-linux.png b/visual-tests/__screenshots__/rendering/linear-gradient-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..87bb74dfc26bbc8a7eb9abf7333500b87bec0a24 GIT binary patch literal 2943 zcmX|DYgCh07X5gGlvinWKoF%B%OFru&?pEhh)gx8ASq!13ItjrVu`3pggAwTIyj<& z*aQe_tC;ed6$})H;Hp?uB%mW920|Q%m*rW3AXFZ6K6Bt_Zoa$sKKtx@?!B;|8fdo6 zb{RnsW-dBTTy6O>8Z%TG?M zPaaPr-Z)1qrlSXph-bORI^QH&zNXErAtpcHNzk9tJb%4DV+H%%nGf8@V{xB*;;ps` zYuHz;cv#H%2v^$@uLe(qkgFX(7564Ap*@fEhfIgeL0E$uo!oN zeGVOYV*?89K39kK(6PSF#Iq1}rRbIHhb6RaHl_sU<26KipH$F4a?yFX9umCr86 z)gr;M6X`~46=&gu}H<~M@3nprFDs+ptRHD4XS;8y`#;zFBr&I-rl z&DgFzgkLcG$9Rq>&+x3!-`J1Imd2;A<-y+`KgY8vJOf-Uk56;v>A|K%*;LHDIpK4` z@@$8pRl{W-32-}}w|zk$KdtRQ?Y5wcKScd!ycPg@JVvJ3JFlnRH@;IgcVZdAvGt!0 zZCYe#=x2NMfIfCbf0|>Do?oz|{tbzD#DtH~gt)QJO7|IED4KD2nh7QeD-2f2{L#8# z`S&?-F~pm1EnheC_0fSR^fx#6E(2dK*#3Fg33Pubt$um>>fA--uxwSaGjXHDo1U${ z?yRGwSS9*xR2E7+d$IZf*TWMV*(jOr<{N^YyCe@I`HR;7N*{0C3)YiHjQhE!2Hr6` zD@@_Jrg7s5EnjVAey!_9UBBQA<7Am7_MgjCMtE35D!7TY1sgXaMYm*jAKw7QSI}QK zwUFXl>2I@I47C&~f#sV#sz7{(ab=qw)?TA%Rxqd#rW*NQ74dFJ5y6x*JvJkM?&$nXY)MQ`MrJnd zj{5?H4>4cfwjt%!(Fd-s{|MzJm2w2rI+Mi7d2N&Jyj^$CjZlf0#b+mZn}eKH?F$?nybc~M-Rf}1u63WWSV%!FIH zto89YP`HU?!EMWsVufOP2_pcJvlE|&RDDcLZaV)*UfH7DBT1<#VSt>nBPs1*7-o=i zznZe46f#JutoB(`ioO&^blDesYWX@>Kj;xVfeEa{_pw#WQLa+bxFLKccpNV2d2X}V zu=tS^p&cZ}?vXQb9g7swEQc$aC9uY-?Ev;7@p_|J2f;*=p_@ue0CSdftx+0d!+JaC z{(BU}_x;q_bK|HknjSO4KJ10a8O9cm!|y zE+8Xg@VvcvCD#A<)ZQtjJ=Cw0+I~{0r`5w)*vjb|HX#G6a^j+%VcMgkMN-V#RC#Po z^LCIiCF-eeX+RlV#oLHDUm)hnO$ritNbEE8P?kcFi8%hk&RFZEII!qmkU5SmrIf1P zVg$OCK{H>l61Kd>(5gBJ~RNe!TjM+bW!fW*4S=4kV_5+ zaY#`u^%5Y9V~af32V$y*_a{^q#lmSj8a+^UEf%Y&E@dfNzWXqLJX5^Uk?K_SAO*%b z*%(H_R-U8~G&6?k4$BMmk>XI~FMp%Lm=!Z3@1;=j#AF!LnnmD9_1$b*%eUxAfnu#o z?+G+QHu#@DmLYUtAM;@f#TqGc>y9c54S=Gj?hL;W?i}r`cPnC%azknBudz_BSDDN8 z7B(WUY(Ml>?-}rVNFmQ2&_xQ(py0O@xM1^AhdKjTaJiBUYg&t#*sw5BSAVkvMY||QSp8c;w2R!H)yvU}_Bwxj>LOfc z;+Sqq#4()a;WtMGo|{REhEd_#%}BAH_O7L{0PV;x=O#3}g7AFN`>fuuC9ozH%ml{o zb-XMU-kjo4pD)u!0a+a9vn6sAug>sWs{~%B7LlVYf^+{3;@5}e>@DIhjSNwWJ2(%Ae}j8V=Jl{X|s8;~#|wA6zp zoi6sqi{2waIyvTT3SVC0%tW)zQP~uyub7RG6-G5jUGY zx78@>acFz6r1RJ7VfHcZaC+Lm=SnWZfitw4-%yFQ zw(L)yTayeYnctwEPgA2W{0j9< Date: Tue, 7 Oct 2025 03:38:06 +0530 Subject: [PATCH 13/15] refactoring --- .../test/visual/BaselineManager.java | 54 +++++++++++++++++++ .../test/visual/ProcessingSketch.java | 9 ++++ .../processing/test/visual/TestConfig.java | 33 ++++++++++++ .../processing/test/visual/TestResult.java | 45 ++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 visual-tests/src/main/java/processing/test/visual/BaselineManager.java create mode 100644 visual-tests/src/main/java/processing/test/visual/ProcessingSketch.java create mode 100644 visual-tests/src/main/java/processing/test/visual/TestConfig.java create mode 100644 visual-tests/src/main/java/processing/test/visual/TestResult.java diff --git a/visual-tests/src/main/java/processing/test/visual/BaselineManager.java b/visual-tests/src/main/java/processing/test/visual/BaselineManager.java new file mode 100644 index 000000000..a9d1186dc --- /dev/null +++ b/visual-tests/src/main/java/processing/test/visual/BaselineManager.java @@ -0,0 +1,54 @@ +package processing.test.visual; + +import processing.core.PImage; + +import java.util.List; + +// Baseline manager for updating reference images +public class BaselineManager { + private VisualTestRunner tester; + + public BaselineManager(VisualTestRunner tester) { + this.tester = tester; + } + + public void updateBaseline(String testName, ProcessingSketch sketch) { + updateBaseline(testName, sketch, new TestConfig()); + } + + public void updateBaseline(String testName, ProcessingSketch sketch, TestConfig config) { + System.out.println("Updating baseline for: " + testName); + + // Capture new image + SketchRunner runner = new SketchRunner(sketch, config); + runner.run(); + PImage newImage = runner.getImage(); + + // Save as baseline + String baselinePath = "__screenshots__/" + + testName.replaceAll("[^a-zA-Z0-9-_]", "-") + + "-" + detectPlatform() + ".png"; + newImage.save(baselinePath); + + System.out.println("Baseline updated: " + baselinePath); + } + + public void updateAllBaselines(ProcessingTestSuite suite) { + System.out.println("Updating all baselines..."); + List testNames = suite.getTestNames(); + + for (String testName : testNames) { + // Re-run the test to get the sketch and config + TestResult result = suite.runTest(testName); + // Note: In a real implementation, you'd need to store the sketch reference + // This is a simplified version + } + } + + private String detectPlatform() { + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("mac")) return "darwin"; + if (os.contains("win")) return "win32"; + return "linux"; + } +} diff --git a/visual-tests/src/main/java/processing/test/visual/ProcessingSketch.java b/visual-tests/src/main/java/processing/test/visual/ProcessingSketch.java new file mode 100644 index 000000000..c879ffbb1 --- /dev/null +++ b/visual-tests/src/main/java/processing/test/visual/ProcessingSketch.java @@ -0,0 +1,9 @@ +package processing.test.visual; + +import processing.core.PApplet; + +// Interface for user sketches +public interface ProcessingSketch { + void setup(PApplet p); + void draw(PApplet p); +} diff --git a/visual-tests/src/main/java/processing/test/visual/TestConfig.java b/visual-tests/src/main/java/processing/test/visual/TestConfig.java new file mode 100644 index 000000000..ff69b2e75 --- /dev/null +++ b/visual-tests/src/main/java/processing/test/visual/TestConfig.java @@ -0,0 +1,33 @@ +package processing.test.visual; + +// Test configuration class +public class TestConfig { + public int width = 800; + public int height = 600; + public int[] backgroundColor = {255, 255, 255}; // RGB + public long renderWaitTime = 100; // milliseconds + public double threshold = 0.1; + + public TestConfig() {} + + public TestConfig(int width, int height) { + this.width = width; + this.height = height; + } + + public TestConfig(int width, int height, int[] backgroundColor) { + this.width = width; + this.height = height; + this.backgroundColor = backgroundColor; + } + + public TestConfig setThreshold(double threshold) { + this.threshold = threshold; + return this; + } + + public TestConfig setRenderWaitTime(long waitTime) { + this.renderWaitTime = waitTime; + return this; + } +} diff --git a/visual-tests/src/main/java/processing/test/visual/TestResult.java b/visual-tests/src/main/java/processing/test/visual/TestResult.java new file mode 100644 index 000000000..139e57561 --- /dev/null +++ b/visual-tests/src/main/java/processing/test/visual/TestResult.java @@ -0,0 +1,45 @@ +package processing.test.visual; + +// Enhanced test result with detailed information +public class TestResult { + public String testName; + public boolean passed; + public double mismatchRatio; + public String error; + public boolean isFirstRun; + public ComparisonDetails details; + + public TestResult(String testName, ComparisonResult comparison) { + this.testName = testName; + this.passed = comparison.passed; + this.mismatchRatio = comparison.mismatchRatio; + this.isFirstRun = comparison.isFirstRun; + this.details = comparison.details; + } + + public static TestResult createError(String testName, String error) { + TestResult result = new TestResult(); + result.testName = testName; + result.passed = false; + result.error = error; + return result; + } + + private TestResult() {} // For error constructor + + public void printResult() { + System.out.print(testName + ": "); + if (error != null) { + System.out.println("ERROR - " + error); + } else if (isFirstRun) { + System.out.println("BASELINE CREATED"); + } else if (passed) { + System.out.println("PASSED"); + } else { + System.out.println("FAILED (mismatch: " + String.format("%.4f", mismatchRatio * 100) + "%)"); + if (details != null) { + details.printDetails(); + } + } + } +} From 4fcd95b535141d85c246366706c29c55c805d0e9 Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 7 Oct 2025 03:38:45 +0530 Subject: [PATCH 14/15] added tests in suits --- .../src/test/java/visual/base/VisualTest.java | 61 ++++++++++++ .../java/visual/rendering/GradientTest.java | 35 +++++++ .../java/visual/shapes/BasicShapeTest.java | 92 +++++++++++++++++++ .../java/visual/suites/BasicShapesSuite.java | 11 +++ .../java/visual/suites/RenderingSuite.java | 11 +++ 5 files changed, 210 insertions(+) create mode 100644 visual-tests/src/test/java/visual/base/VisualTest.java create mode 100644 visual-tests/src/test/java/visual/rendering/GradientTest.java create mode 100644 visual-tests/src/test/java/visual/shapes/BasicShapeTest.java create mode 100644 visual-tests/src/test/java/visual/suites/BasicShapesSuite.java create mode 100644 visual-tests/src/test/java/visual/suites/RenderingSuite.java diff --git a/visual-tests/src/test/java/visual/base/VisualTest.java b/visual-tests/src/test/java/visual/base/VisualTest.java new file mode 100644 index 000000000..e574a5b6f --- /dev/null +++ b/visual-tests/src/test/java/visual/base/VisualTest.java @@ -0,0 +1,61 @@ +package visual.base; + +import org.junit.jupiter.api.*; +import processing.core.*; +import static org.junit.jupiter.api.Assertions.*; +import processing.test.visual.*; +import java.nio.file.*; +import java.io.File; + +/** + * Base class for Processing visual tests using JUnit 5 + */ +public abstract class VisualTest { + + protected static VisualTestRunner testRunner; + protected static ImageComparator comparator; + + @BeforeAll + public static void setupTestRunner() { + PApplet tempApplet = new PApplet(); + comparator = new ImageComparator(tempApplet); + testRunner = new VisualTestRunner(comparator); + + System.out.println("Visual test runner initialized"); + } + + /** + * Helper method to run a visual test + */ + protected void assertVisualMatch(String testName, ProcessingSketch sketch) { + assertVisualMatch(testName, sketch, new TestConfig()); + } + + protected void assertVisualMatch(String testName, ProcessingSketch sketch, TestConfig config) { + TestResult result = testRunner.runVisualTest(testName, sketch, config); + + // Print result for debugging + result.printResult(); + + // Handle different result types + if (result.isFirstRun) { + // First run - baseline created, mark as skipped + Assumptions.assumeTrue(false, "Baseline created for " + testName + ". Run tests again to verify."); + } else if (result.error != null) { + fail("Test error: " + result.error); + } else { + // Assert that the test passed + Assertions.assertTrue(result.passed, + String.format("Visual test '%s' failed with mismatch ratio: %.4f%%", + testName, result.mismatchRatio * 100)); + } + } + + /** + * Update baseline for a specific test (useful for maintenance) + */ + protected void updateBaseline(String testName, ProcessingSketch sketch, TestConfig config) { + BaselineManager manager = new BaselineManager(testRunner); + manager.updateBaseline(testName, sketch, config); + } +} \ No newline at end of file diff --git a/visual-tests/src/test/java/visual/rendering/GradientTest.java b/visual-tests/src/test/java/visual/rendering/GradientTest.java new file mode 100644 index 000000000..479d0872b --- /dev/null +++ b/visual-tests/src/test/java/visual/rendering/GradientTest.java @@ -0,0 +1,35 @@ +package visual.rendering; + +import org.junit.jupiter.api.*; +import processing.core.*; +import visual.base.VisualTest; +import processing.test.visual.ProcessingSketch; +import processing.test.visual.TestConfig; + +@Tag("rendering") +@Tag("gradients") +public class GradientTest extends VisualTest { + + @Test + @DisplayName("Linear gradient renders correctly") + public void testLinearGradient() { + TestConfig config = new TestConfig(600, 400); + + assertVisualMatch("rendering/linear-gradient", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.noStroke(); + } + + @Override + public void draw(PApplet p) { + for (int y = 0; y < p.height; y++) { + float inter = PApplet.map(y, 0, p.height, 0, 1); + int c = p.lerpColor(p.color(255, 0, 0), p.color(0, 0, 255), inter); + p.stroke(c); + p.line(0, y, p.width, y); + } + } + }, config); + } +} \ No newline at end of file diff --git a/visual-tests/src/test/java/visual/shapes/BasicShapeTest.java b/visual-tests/src/test/java/visual/shapes/BasicShapeTest.java new file mode 100644 index 000000000..6756b2557 --- /dev/null +++ b/visual-tests/src/test/java/visual/shapes/BasicShapeTest.java @@ -0,0 +1,92 @@ +package visual.shapes; + + +import org.junit.jupiter.api.*; +import processing.core.*; +import visual.base.*; +import processing.test.visual.*; + +@Tag("basic") +@Tag("shapes") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class BasicShapeTest extends VisualTest { + + @Test + @Order(1) + @DisplayName("Red circle renders correctly") + public void testRedCircle() { + assertVisualMatch("basic-shapes/red-circle", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.noStroke(); + } + + @Override + public void draw(PApplet p) { + p.background(255, 255, 255); + p.fill(255, 0, 0); + p.ellipse(p.width/2, p.height/2, 100, 100); + } + }); + } + + @Test + @Order(2) + @DisplayName("Blue square renders correctly") + public void testBlueSquare() { + assertVisualMatch("basic-shapes/blue-square", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.stroke(0); + p.strokeWeight(2); + } + + @Override + public void draw(PApplet p) { + p.background(255); + p.fill(0, 0, 255); + p.rect(p.width/2 - 50, p.height/2 - 50, 100, 100); + } + }); + } + + @Test + @Order(3) + @DisplayName("Green circle renders correctly") + public void testGreenCircle() { + assertVisualMatch("basic-shapes/green-circle", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.noStroke(); + } + + @Override + public void draw(PApplet p) { + p.background(255, 255, 255); + p.fill(0, 255, 0); + p.ellipse(p.width/2, p.height/2, 100, 100); + } + }); + } + + @Test + @Order(4) + @DisplayName("Custom size canvas") + public void testCustomSize() { + TestConfig config = new TestConfig(600, 400); + + assertVisualMatch("basic-shapes/custom-size-rect", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.noStroke(); + } + + @Override + public void draw(PApplet p) { + p.background(240, 240, 240); + p.fill(128, 0, 128); + p.rect(50, 50, p.width - 100, p.height - 100); + } + }, config); + } +} \ No newline at end of file diff --git a/visual-tests/src/test/java/visual/suites/BasicShapesSuite.java b/visual-tests/src/test/java/visual/suites/BasicShapesSuite.java new file mode 100644 index 000000000..7145e0415 --- /dev/null +++ b/visual-tests/src/test/java/visual/suites/BasicShapesSuite.java @@ -0,0 +1,11 @@ +package visual.suites; + +import org.junit.platform.suite.api.*; + +@Suite +@SuiteDisplayName("Basic Shapes Visual Tests") +@SelectPackages("visual.shapes") +@IncludeTags("basic") +public class BasicShapesSuite { + // Empty class - just holds annotations +} \ No newline at end of file diff --git a/visual-tests/src/test/java/visual/suites/RenderingSuite.java b/visual-tests/src/test/java/visual/suites/RenderingSuite.java new file mode 100644 index 000000000..6637cd06d --- /dev/null +++ b/visual-tests/src/test/java/visual/suites/RenderingSuite.java @@ -0,0 +1,11 @@ +package visual.suites; + +import org.junit.platform.suite.api.*; + +@Suite +@SuiteDisplayName("Rendering Tests") +@SelectPackages("processing.test.visual.rendering") +@IncludeTags("rendering") +public class RenderingSuite { + // Empty class - just holds annotations +} From f778c9903df41e81e7e33fcaf0f9461a0b825967 Mon Sep 17 00:00:00 2001 From: Vaivaswat Date: Tue, 7 Oct 2025 11:31:51 +0530 Subject: [PATCH 15/15] removed simple test --- visual-tests/src/main/java/SimpleTest.java | 105 --------------------- 1 file changed, 105 deletions(-) delete mode 100644 visual-tests/src/main/java/SimpleTest.java diff --git a/visual-tests/src/main/java/SimpleTest.java b/visual-tests/src/main/java/SimpleTest.java deleted file mode 100644 index ae31df988..000000000 --- a/visual-tests/src/main/java/SimpleTest.java +++ /dev/null @@ -1,105 +0,0 @@ -//// SimpleTest.java - Fixed version for quick verification -//import processing.core.*; -//import java.util.*; -// -//public class SimpleTest { -// -// public static void main(String[] args) { -// System.out.println("=== Processing Visual Testing - Quick Test ===\n"); -// -// try { -// PApplet tempApplet = new PApplet(); -// -// ImageComparator comparator = new ImageComparator(tempApplet); -// VisualTestRunner tester = new VisualTestRunner(comparator); -// -// ProcessingSketch redCircle = new SimpleRedCircle(); -// TestResult result1 = tester.runVisualTest("red-circle", redCircle); -// result1.printResult(); -// -// ProcessingSketch blueSquare = new SimpleBlueSquare(); -// TestResult result2 = tester.runVisualTest("blue-square", blueSquare); -// result2.printResult(); -// -// // Step 4: Test comparison with identical image (should pass on second run) -// if (!result1.isFirstRun) { -// TestResult result3 = tester.runVisualTest("red-circle", redCircle); -// result3.printResult(); -// -// if (result3.passed) { -// System.out.println("✓ Identical image comparison works!"); -// } else { -// System.out.println("✗ Identical image comparison failed!"); -// } -// } -// ProcessingTestSuite suite = new ProcessingTestSuite(tester); -// suite.addTest("suite-red", new SimpleRedCircle()); -// suite.addTest("suite-blue", new SimpleBlueSquare()); -// suite.addTest("suite-gradient", new SimpleGradient()); -// -// List suiteResults = suite.runAll(); -// -// long passed = suiteResults.stream().filter(r -> r.passed).count(); -// long total = suiteResults.size(); -// System.out.println("Suite results: " + passed + "/" + total + " passed"); -// System.exit(0); -// -// } catch (Exception e) { -// System.err.println("Test failed with error: " + e.getMessage()); -// e.printStackTrace(); -// } -// } -// -// // Simple test sketches -// static class SimpleRedCircle implements ProcessingSketch { -// public void setup(PApplet p) { -// p.noStroke(); -// } -// -// public void draw(PApplet p) { -// p.background(255, 255, 255); -// p.fill(255, 0, 0); -// p.ellipse(p.width/2, p.height/2, 100, 100); -// } -// } -// -// static class SimpleBlueSquare implements ProcessingSketch { -// public void setup(PApplet p) { -// p.stroke(0); -// p.strokeWeight(2); -// } -// -// public void draw(PApplet p) { -// p.background(255); -// p.fill(0, 0, 255); -// p.rect(p.width/2 - 50, p.height/2 - 50, 100, 100); -// } -// } -// -// static class SimpleGreenCircle implements ProcessingSketch { -// public void setup(PApplet p) { -// p.noStroke(); -// } -// -// public void draw(PApplet p) { -// p.background(255, 255, 255); -// p.fill(0, 255, 0); // Green instead of red -// p.ellipse(p.width/2, p.height/2, 100, 100); -// } -// } -// -// static class SimpleGradient implements ProcessingSketch { -// public void setup(PApplet p) { -// p.noStroke(); -// } -// -// public void draw(PApplet p) { -// for (int y = 0; y < p.height; y++) { -// float inter = PApplet.map(y, 0, p.height, 0, 1); -// int c = p.lerpColor(p.color(255, 0, 0), p.color(0, 0, 255), inter); -// p.stroke(c); -// p.line(0, y, p.width, y); -// } -// } -// } -//} \ No newline at end of file