import javax.swing.*; import javax.swing.border.EmptyBorder; import javax.swing.table.DefaultTableModel; import javax.swing.text.DefaultCaret; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.*; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.text.SimpleDateFormat; import java.util.*; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; public class BakeTestRunner extends JFrame { private JPanel mainPanel; private JTable testTable; private DefaultTableModel tableModel; private JTextArea logArea; private JButton runSelectedButton; private JButton runAllButton; private JComboBox<String> languageComboBox; private JProgressBar progressBar; private JButton openLogsButton; private JButton compareSelectedButton; private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final String baseDir = System.getProperty("user.dir"); private final String logsDir = baseDir + File.separator + "logs"; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private final Color PASSED_COLOR = new Color(220, 255, 220); private final Color FAILED_COLOR = new Color(255, 220, 220); private final Color RUNNING_COLOR = new Color(220, 220, 255); enum TestStatus { NOT_RUN, RUNNING, PASSED, FAILED } public BakeTestRunner() { setTitle("Bake Test Runner"); setSize(1000, 700); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); setupUI(); loadTests(); createLogsDirectory(); } private void setupUI() { mainPanel = new JPanel(new BorderLayout(10, 10)); mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); // Top panel with controls JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5)); runSelectedButton = new JButton("Run Selected Tests"); runAllButton = new JButton("Run All Tests"); compareSelectedButton = new JButton("Compare Selected Test"); languageComboBox = new JComboBox<>(new String[]{"All", "C", "Java"}); openLogsButton = new JButton("Open Logs Directory"); controlPanel.add(runSelectedButton); controlPanel.add(runAllButton); controlPanel.add(compareSelectedButton); controlPanel.add(new JLabel("Language:")); controlPanel.add(languageComboBox); controlPanel.add(openLogsButton); // Table for test list String[] columnNames = {"#", "Language", "Test Name", "Status", "Last Run"}; tableModel = new DefaultTableModel(columnNames, 0) { @Override public boolean isCellEditable(int row, int column) { return false; } }; testTable = new JTable(tableModel); testTable.getColumnModel().getColumn(0).setPreferredWidth(30); testTable.getColumnModel().getColumn(1).setPreferredWidth(70); testTable.getColumnModel().getColumn(2).setPreferredWidth(300); testTable.getColumnModel().getColumn(3).setPreferredWidth(80); testTable.getColumnModel().getColumn(4).setPreferredWidth(150); testTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); testTable.setRowHeight(25); // Log area logArea = new JTextArea(); logArea.setEditable(false); logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); DefaultCaret caret = (DefaultCaret) logArea.getCaret(); caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); // Progress bar progressBar = new JProgressBar(0, 100); progressBar.setStringPainted(true); progressBar.setString("Ready"); // Layout JSplitPane splitPane = new JSplitPane( JSplitPane.VERTICAL_SPLIT, new JScrollPane(testTable), new JScrollPane(logArea) ); splitPane.setDividerLocation(300); mainPanel.add(controlPanel, BorderLayout.NORTH); mainPanel.add(splitPane, BorderLayout.CENTER); mainPanel.add(progressBar, BorderLayout.SOUTH); setContentPane(mainPanel); // Add action listeners runSelectedButton.addActionListener(e -> runSelectedTests()); runAllButton.addActionListener(e -> runAllTests()); compareSelectedButton.addActionListener(e -> compareSelectedTest()); openLogsButton.addActionListener(e -> openLogsDirectory()); languageComboBox.addActionListener(e -> filterTestsByLanguage()); } private void createLogsDirectory() { try { Files.createDirectories(Paths.get(logsDir)); } catch (IOException e) { logMessage("Error creating logs directory: " + e.getMessage()); } } private void openLogsDirectory() { try { Desktop.getDesktop().open(new File(logsDir)); } catch (IOException e) { logMessage("Error opening logs directory: " + e.getMessage()); JOptionPane.showMessageDialog(this, "Could not open logs directory: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } private void compareSelectedTest() { int selectedRow = testTable.getSelectedRow(); if (selectedRow == -1) { JOptionPane.showMessageDialog(this, "Please select a test to compare", "No Test Selected", JOptionPane.WARNING_MESSAGE); return; } String language = (String) tableModel.getValueAt(selectedRow, 1); String testName = (String) tableModel.getValueAt(selectedRow, 2); String logFilePath = logsDir + File.separator + language + "_" + testName + ".log"; File logFile = new File(logFilePath); if (!logFile.exists()) { JOptionPane.showMessageDialog(this, "No log file found for this test. Please run the test first.", "Log Not Found", JOptionPane.WARNING_MESSAGE); return; } showComparisonDialog(logFile, language, testName); } private void showComparisonDialog(File logFile, String language, String testName) { JDialog dialog = new JDialog(this, "Comparison: " + language + " - " + testName, true); dialog.setLayout(new BorderLayout(10, 10)); dialog.setSize(1000, 600); dialog.setLocationRelativeTo(this); try { List<String> lines = Files.readAllLines(logFile.toPath()); String content = String.join("\n", lines); // Split content to make and bake sections if possible String makeOutput = ""; String bakeOutput = ""; // Basic parsing - can be enhanced for better splitting int makeIndex = content.indexOf("=== Make Output ==="); int bakeIndex = content.indexOf("=== Bake Output ==="); int comparisonIndex = content.indexOf("=== Comparison Results ==="); if (makeIndex != -1 && bakeIndex != -1) { makeOutput = content.substring(makeIndex, bakeIndex).trim(); if (comparisonIndex != -1) { bakeOutput = content.substring(bakeIndex, comparisonIndex).trim(); } else { bakeOutput = content.substring(bakeIndex).trim(); } } JTextArea makeArea = new JTextArea(makeOutput); JTextArea bakeArea = new JTextArea(bakeOutput); JTextArea comparisonArea = new JTextArea(); if (comparisonIndex != -1) { comparisonArea.setText(content.substring(comparisonIndex).trim()); } else { comparisonArea.setText("No comparison results available."); } makeArea.setEditable(false); bakeArea.setEditable(false); comparisonArea.setEditable(false); makeArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); bakeArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); comparisonArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); JTabbedPane tabbedPane = new JTabbedPane(); tabbedPane.addTab("Make Output", new JScrollPane(makeArea)); tabbedPane.addTab("Bake Output", new JScrollPane(bakeArea)); tabbedPane.addTab("Comparison", new JScrollPane(comparisonArea)); dialog.add(tabbedPane, BorderLayout.CENTER); JButton closeButton = new JButton("Close"); closeButton.addActionListener(e -> dialog.dispose()); JPanel buttonPanel = new JPanel(); buttonPanel.add(closeButton); dialog.add(buttonPanel, BorderLayout.SOUTH); dialog.setVisible(true); } catch (IOException e) { JOptionPane.showMessageDialog(dialog, "Error reading log file: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } private void loadTests() { tableModel.setRowCount(0); // Load C tests loadTestsForLanguage("C"); // Load Java tests loadTestsForLanguage("Java"); } private void loadTestsForLanguage(String language) { File languageDir = new File(baseDir + File.separator + "tests" + File.separator + language); if (!languageDir.exists() || !languageDir.isDirectory()) { logMessage("Warning: Directory not found: " + languageDir.getPath()); return; } File[] testDirs = languageDir.listFiles(File::isDirectory); if (testDirs == null) { logMessage("Warning: No test directories found in " + languageDir.getPath()); return; } Arrays.sort(testDirs, (a, b) -> { // Extract test number for sorting Pattern pattern = Pattern.compile("test-(\\d+)"); Matcher matcherA = pattern.matcher(a.getName()); Matcher matcherB = pattern.matcher(b.getName()); if (matcherA.find() && matcherB.find()) { try { int numA = Integer.parseInt(matcherA.group(1)); int numB = Integer.parseInt(matcherB.group(1)); return Integer.compare(numA, numB); } catch (NumberFormatException e) { return a.getName().compareTo(b.getName()); } } return a.getName().compareTo(b.getName()); }); for (File testDir : testDirs) { String testName = testDir.getName(); if (testName.startsWith("test-")) { Object[] row = {tableModel.getRowCount() + 1, language, testName, "Not Run", ""}; tableModel.addRow(row); } } } private void filterTestsByLanguage() { String selectedLanguage = (String) languageComboBox.getSelectedItem(); if (selectedLanguage == null || selectedLanguage.equals("All")) { loadTests(); return; } tableModel.setRowCount(0); loadTestsForLanguage(selectedLanguage); } private void runSelectedTests() { int[] selectedRows = testTable.getSelectedRows(); if (selectedRows.length == 0) { JOptionPane.showMessageDialog(this, "Please select at least one test to run", "No Test Selected", JOptionPane.WARNING_MESSAGE); return; } List<TestInfo> testsToRun = new ArrayList<>(); for (int row : selectedRows) { String language = (String) tableModel.getValueAt(row, 1); String testName = (String) tableModel.getValueAt(row, 2); testsToRun.add(new TestInfo(row, language, testName)); } disableButtons(); runTests(testsToRun); } private void runAllTests() { List<TestInfo> testsToRun = new ArrayList<>(); for (int row = 0; row < tableModel.getRowCount(); row++) { String language = (String) tableModel.getValueAt(row, 1); String testName = (String) tableModel.getValueAt(row, 2); testsToRun.add(new TestInfo(row, language, testName)); } disableButtons(); runTests(testsToRun); } private void disableButtons() { runSelectedButton.setEnabled(false); runAllButton.setEnabled(false); compareSelectedButton.setEnabled(false); languageComboBox.setEnabled(false); } private void enableButtons() { runSelectedButton.setEnabled(true); runAllButton.setEnabled(true); compareSelectedButton.setEnabled(true); languageComboBox.setEnabled(true); } private void runTests(List<TestInfo> testsToRun) { progressBar.setValue(0); progressBar.setString("Running tests (0/" + testsToRun.size() + ")"); logArea.setText(""); executor.submit(() -> { try { int total = testsToRun.size(); int current = 0; for (int i = 0; i < testsToRun.size(); i++) { TestInfo test = testsToRun.get(i); final int currentTest = i + 1; // Update UI to show we're running this test SwingUtilities.invokeLater(() -> { tableModel.setValueAt(TestStatus.RUNNING.name(), test.row, 3); testTable.setValueAt(TestStatus.RUNNING.name(), test.row, 3); testTable.setValueAt(dateFormat.format(new Date()), test.row, 4); // Highlight the row testTable.setRowSelectionInterval(test.row, test.row); // Update the progress bar progressBar.setValue((int)((double)currentTest / total * 100)); progressBar.setString("Running tests (" + currentTest + "/" + total + ")"); }); // Run the test logMessage("\n========================================================"); logMessage("Running Test: " + test.language + " - " + test.testName); logMessage("========================================================"); boolean success = runTest(test); // Update UI with the result SwingUtilities.invokeLater(() -> { testTable.setValueAt(success ? TestStatus.PASSED.name() : TestStatus.FAILED.name(), test.row, 3); }); } // Test run complete SwingUtilities.invokeLater(() -> { progressBar.setValue(100); progressBar.setString("All tests completed"); enableButtons(); logMessage("\n========================================================"); logMessage("Test run completed at " + dateFormat.format(new Date())); logMessage("========================================================"); }); } catch (Exception e) { SwingUtilities.invokeLater(() -> { logMessage("Error running tests: " + e.getMessage()); for (StackTraceElement element : e.getStackTrace()) { logMessage(" " + element.toString()); } progressBar.setString("Error running tests"); enableButtons(); }); } }); } private boolean runTest(TestInfo test) { String testDir = baseDir + File.separator + "tests" + File.separator + test.language + File.separator + test.testName; File makeDir = new File(testDir + File.separator + "make"); File bakeDir = new File(testDir + File.separator + "bake"); if (!makeDir.exists() || !bakeDir.exists()) { logMessage("Error: Make or Bake directory not found for test " + test.testName); return false; } String logFilePath = logsDir + File.separator + test.language + "_" + test.testName + ".log"; try (PrintWriter writer = new PrintWriter(new FileWriter(logFilePath))) { // Header information writer.println("Test: " + test.language + " - " + test.testName); writer.println("Date: " + dateFormat.format(new Date())); writer.println("========================================================"); // Compare initial file state writer.println("\n=== Initial File Comparison ==="); logMessage("Comparing initial files..."); Map<String, FileInfo> makeFiles = scanDirectory(makeDir); Map<String, FileInfo> bakeFiles = scanDirectory(bakeDir); compareAndLogFiles(makeFiles, bakeFiles, writer); // Run make writer.println("\n=== Make Output ==="); logMessage("Running make..."); ProcessResult makeResult = runProcess("make", makeDir); writer.println(makeResult.output); logMessage(makeResult.output); // Run bake writer.println("\n=== Bake Output ==="); logMessage("Running bake..."); ProcessResult bakeResult = runProcess("java -cp bakefile.jar fr.monlouyan.bakefile.Main", bakeDir); writer.println(bakeResult.output); logMessage(bakeResult.output); // Compare results logMessage("Comparing results..."); writer.println("\n=== Comparison Results ==="); // Compare exit codes boolean exitCodesMatch = makeResult.exitCode == bakeResult.exitCode; writer.println("Exit codes match: " + exitCodesMatch); writer.println("Make exit code: " + makeResult.exitCode); writer.println("Bake exit code: " + bakeResult.exitCode); // Compare output patterns (ignoring the tool name differences) boolean outputPatternsMatch = compareOutputPatterns(makeResult.output, bakeResult.output); writer.println("Output patterns match: " + outputPatternsMatch); // Compare final file state writer.println("\n=== Final File State Comparison ==="); Map<String, FileInfo> makeFinalFiles = scanDirectory(makeDir); Map<String, FileInfo> bakeFinalFiles = scanDirectory(bakeDir); compareAndLogFiles(makeFinalFiles, bakeFinalFiles, writer); // Check if files were created or modified as expected boolean fileChangesMatch = compareFileChanges(makeFiles, makeFinalFiles, bakeFiles, bakeFinalFiles); writer.println("File changes match: " + fileChangesMatch); // Test summary boolean testPassed = exitCodesMatch && outputPatternsMatch && fileChangesMatch; writer.println("\n=== Test Result ==="); writer.println(testPassed ? "PASSED" : "FAILED"); logMessage(testPassed ? "Test PASSED" : "Test FAILED"); return testPassed; } catch (IOException e) { logMessage("Error running test: " + e.getMessage()); return false; } } private boolean compareFileChanges( Map<String, FileInfo> makeInitial, Map<String, FileInfo> makeFinal, Map<String, FileInfo> bakeInitial, Map<String, FileInfo> bakeFinal) { // Check if the same files were created in both directories Set<String> makeCreated = new HashSet<>(makeFinal.keySet()); makeCreated.removeAll(makeInitial.keySet()); Set<String> bakeCreated = new HashSet<>(bakeFinal.keySet()); bakeCreated.removeAll(bakeInitial.keySet()); if (!makeCreated.equals(bakeCreated)) { logMessage("Different files created:\nMake: " + makeCreated + "\nBake: " + bakeCreated); return false; } // Check if the same files were modified boolean filesMatch = true; for (String file : makeInitial.keySet()) { if (makeFinal.containsKey(file) && bakeInitial.containsKey(file) && bakeFinal.containsKey(file)) { boolean makeModified = !makeInitial.get(file).equals(makeFinal.get(file)); boolean bakeModified = !bakeInitial.get(file).equals(bakeFinal.get(file)); if (makeModified != bakeModified) { logMessage("File modification mismatch for " + file + "\nMake modified: " + makeModified + "\nBake modified: " + bakeModified); filesMatch = false; } } } return filesMatch; } private ProcessResult runProcess(String command, File directory) throws IOException { ProcessBuilder processBuilder = new ProcessBuilder("/bin/sh", "-c", command); processBuilder.directory(directory); Process process = processBuilder.start(); // Capture stdout and stderr StringBuilder output = new StringBuilder(); try (BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { String line; while ((line = stdInput.readLine()) != null) { output.append(line).append("\n"); } while ((line = stdError.readLine()) != null) { output.append("ERROR: ").append(line).append("\n"); } } try { process.waitFor(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return new ProcessResult(process.exitValue(), output.toString()); } private Map<String, FileInfo> scanDirectory(File directory) throws IOException { Map<String, FileInfo> files = new HashMap<>(); if (!directory.exists() || !directory.isDirectory()) { return files; } Files.walkFileTree(directory.toPath(), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { String relativePath = directory.toPath().relativize(file).toString(); // Skip bakefile.jar to avoid differences if (relativePath.equals("bakefile.jar")) { return FileVisitResult.CONTINUE; } FileInfo info = new FileInfo( Files.isRegularFile(file), attrs.size(), attrs.lastModifiedTime().toMillis() ); files.put(relativePath, info); return FileVisitResult.CONTINUE; } }); return files; } private void compareAndLogFiles(Map<String, FileInfo> makeFiles, Map<String, FileInfo> bakeFiles, PrintWriter writer) { Set<String> allFiles = new HashSet<>(); allFiles.addAll(makeFiles.keySet()); allFiles.addAll(bakeFiles.keySet()); List<String> sortedFiles = new ArrayList<>(allFiles); Collections.sort(sortedFiles); writer.println("File comparison:"); for (String file : sortedFiles) { FileInfo makeInfo = makeFiles.get(file); FileInfo bakeInfo = bakeFiles.get(file); writer.print(file + ": "); if (makeInfo == null) { writer.println("Only in Bake"); } else if (bakeInfo == null) { writer.println("Only in Make"); } else if (makeInfo.equals(bakeInfo)) { writer.println("Identical"); } else { writer.println("Different"); writer.println(" Make: " + makeInfo); writer.println(" Bake: " + bakeInfo); } } } private boolean compareOutputPatterns(String makeOutput, String bakeOutput) { // Normalize output by replacing tool-specific words String normalizedMake = makeOutput.replaceAll("\\bmake\\b", "TOOL") .replaceAll("\\bMake\\b", "TOOL") .replaceAll("ERROR: ", ""); String normalizedBake = bakeOutput.replaceAll("\\bbake\\b", "TOOL") .replaceAll("\\bBake\\b", "TOOL") .replaceAll("ERROR: ", "");; // Compare line by line, ignoring exact timestamps or specific paths String[] makeLines = normalizedMake.split("\n"); String[] bakeLines = normalizedBake.split("\n"); // If line counts are very different, they're probably not matching if (Math.abs(makeLines.length - bakeLines.length) > 2) { logMessage("Output line count mismatch: Make=" + makeLines.length + ", Bake=" + bakeLines.length); return false; } // Compare key patterns like error messages, file operations, etc. Pattern errorPattern = Pattern.compile(".*Error.*|.*\\*\\*\\*.*|.*failed.*", Pattern.CASE_INSENSITIVE); Pattern commandPattern = Pattern.compile("^[a-z0-9_\\-]+ .*|^\\$.*"); List<String> makeErrors = extractMatches(makeLines, errorPattern); List<String> bakeErrors = extractMatches(bakeLines, errorPattern); List<String> makeCommands = extractMatches(makeLines, commandPattern); List<String> bakeCommands = extractMatches(bakeLines, commandPattern); // If error counts are different, that's a significant difference if (makeErrors.size() != bakeErrors.size()) { logMessage("Error count mismatch: Make=" + makeErrors.size() + ", Bake=" + bakeErrors.size()); return false; } // If command counts are different, that's a significant difference if (makeCommands.size() != bakeCommands.size()) { logMessage("Command count mismatch: Make=" + makeCommands.size() + ", Bake=" + bakeCommands.size()); return false; } return true; } private List<String> extractMatches(String[] lines, Pattern pattern) { return Arrays.stream(lines) .filter(line -> pattern.matcher(line).matches()) .collect(Collectors.toList()); } private void logMessage(String message) { SwingUtilities.invokeLater(() -> { logArea.append(message + "\n"); // Scroll to bottom logArea.setCaretPosition(logArea.getDocument().getLength()); }); } private static class TestInfo { int row; String language; String testName; TestInfo(int row, String language, String testName) { this.row = row; this.language = language; this.testName = testName; } } private static class FileInfo { boolean isFile; long size; long lastModified; FileInfo(boolean isFile, long size, long lastModified) { this.isFile = isFile; this.size = size; this.lastModified = lastModified; } @Override public boolean equals(Object obj) { if (!(obj instanceof FileInfo)) { return false; } FileInfo other = (FileInfo) obj; return isFile == other.isFile && size == other.size; // We don't compare lastModified times directly } @Override public String toString() { return "isFile=" + isFile + ", size=" + size + ", lastModified=" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(lastModified)); } } private static class ProcessResult { int exitCode; String output; ProcessResult(int exitCode, String output) { this.exitCode = exitCode; this.output = output; } } public static void main(String[] args) { SwingUtilities.invokeLater(() -> { try { // Set native look and feel UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { e.printStackTrace(); } BakeTestRunner runner = new BakeTestRunner(); runner.setVisible(true); }); } }