SAE32_2024/tests/BakeTestRunner.java

748 lines
30 KiB
Java

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);
});
}
}