diff --git a/Makefile b/Makefile index f6254fc..b468cea 100644 --- a/Makefile +++ b/Makefile @@ -42,4 +42,9 @@ jar: deploy-tests: @echo "Deploying JAR to 'bake' directories..." @find $(TESTDIR) -type d -name 'bake' -exec cp $(JARNAME) {} \; + @echo "Done." + +run_test: + @echo "Running tests..." + @java -cp build fr.monlouyan.bakefile.tests.BakeTestRunner @echo "Done." \ No newline at end of file diff --git a/src/fr/monlouyan/bakefile/tests/BakeTestRunner.java b/src/fr/monlouyan/bakefile/tests/BakeTestRunner.java new file mode 100644 index 0000000..e3a49c9 --- /dev/null +++ b/src/fr/monlouyan/bakefile/tests/BakeTestRunner.java @@ -0,0 +1,748 @@ +package fr.monlouyan.bakefile.tests; + +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"); + String normalizedBake = bakeOutput.replaceAll("\\bbake\\b", "TOOL") + .replaceAll("\\bBake\\b", "TOOL"); + + // 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); + }); + } +} \ No newline at end of file