From 16467ab44a28d1fc975d8ad26cb8623bab1e2167 Mon Sep 17 00:00:00 2001 From: Connor Ford Date: Mon, 24 Aug 2026 22:47:12 -0400 Subject: [PATCH 1/6] Add completed-run review packaging --- .gitignore | 3 + .../ConformanceTestDatabase.java | 31 +- .../utilities/TestRunLogController.java | 48 ++-- tools/85b-swing-gui/build.gradle | 2 + .../pivconformance/gui/CompletedTestRun.java | 38 +++ .../gui/CopyableErrorDialog.java | 59 ++++ .../gui/GuiRunnerAppController.java | 9 + .../gui/GuiRunnerApplication.java | 3 + .../gui/GuiTestExecutionController.java | 111 +++++++- .../gui/PackageResultsAction.java | 184 ++++++++++++ .../gsa/pivconformance/gui/ReviewPackage.java | 20 ++ .../gui/ReviewPackageBuilder.java | 266 ++++++++++++++++++ .../pivconformance/gui/RunResultsSummary.java | 50 ++++ .../gui/SimpleTestExecutionPanel.java | 54 ++-- .../TestRunLogControllerLifecycleTest.java | 37 +++ .../gui/ReviewPackageBuilderTest.java | 206 ++++++++++++++ 16 files changed, 1053 insertions(+), 68 deletions(-) create mode 100644 tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CompletedTestRun.java create mode 100644 tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CopyableErrorDialog.java create mode 100644 tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java create mode 100644 tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackage.java create mode 100644 tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackageBuilder.java create mode 100644 tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/RunResultsSummary.java create mode 100644 tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/conformancelib/utilities/TestRunLogControllerLifecycleTest.java create mode 100644 tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java diff --git a/.gitignore b/.gitignore index e1ec343b..0947ebc6 100644 --- a/.gitignore +++ b/.gitignore @@ -116,4 +116,7 @@ build # Build artifacts fips201-card-conformance-tool-[0-9]*\.[0-9]*\.[0-9]*-beta-[0-9]*\.zip fips201-card-conformance-tool-[0-9]*\.[0-9]*\.[0-9]*-beta-[0-9]*/* +/fips201-card-conformance-tool-*/ +/fips201-card-conformance-tool-*.zip +/cct-review-results-*.zip libs/* diff --git a/conformancelib/src/main/java/gov/gsa/pivconformance/conformancelib/configuration/ConformanceTestDatabase.java b/conformancelib/src/main/java/gov/gsa/pivconformance/conformancelib/configuration/ConformanceTestDatabase.java index 053e1578..3f7872b3 100644 --- a/conformancelib/src/main/java/gov/gsa/pivconformance/conformancelib/configuration/ConformanceTestDatabase.java +++ b/conformancelib/src/main/java/gov/gsa/pivconformance/conformancelib/configuration/ConformanceTestDatabase.java @@ -5,12 +5,12 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; +import java.nio.file.Path; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; -import java.sql.SQLClientInfoException; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; @@ -23,6 +23,7 @@ public class ConformanceTestDatabase { private static final Logger s_logger = LoggerFactory.getLogger(ConformanceTestDatabase.class); private static final String TEST_SET = "SELECT * FROM TestCases WHERE Enabled = 1"; + private Path m_databasePath; public ConformanceTestDatabase(Connection conn) { setConnconnection(conn); @@ -32,8 +33,13 @@ public Connection getConnection() { return m_conn; } + public Path getDatabasePath() { + return m_databasePath; + } + public void setConnconnection(Connection conn) { m_conn = conn; + m_databasePath = null; } public int getTestCaseCount() { @@ -59,11 +65,14 @@ public void openDatabaseInFile(String filename) throws ConfigurationException { throw new ConfigurationException("Database file " + filename + " does not exist"); } - String dbUrl = null; - try { - Class.forName("org.sqlite.JDBC"); - dbUrl = "jdbc:sqlite:" + f.getCanonicalPath(); - } catch (IOException | ClassNotFoundException e) { + String dbUrl = null; + Path databasePath = null; + try { + Class.forName("org.sqlite.JDBC"); + File canonicalFile = f.getCanonicalFile(); + dbUrl = "jdbc:sqlite:" + canonicalFile.getPath(); + databasePath = canonicalFile.toPath().toAbsolutePath().normalize(); + } catch (IOException | ClassNotFoundException e) { s_logger.error("Unable to calculate canonical name for database file", e); throw new ConfigurationException("Unable to calculate canonical name for database file", e); } @@ -82,14 +91,10 @@ public void openDatabaseInFile(String filename) throws ConfigurationException { } catch (SQLException e) { s_logger.error("Unable to read driver metadata", e); } - } - m_conn = conn; - try { - m_conn.setClientInfo("filename", filename); - } catch (SQLClientInfoException e) { - s_logger.error("setClientInfo failed for database connection.", e); } - s_logger.info("Opened conformance test database in {}", filename); + m_conn = conn; + m_databasePath = databasePath; + s_logger.info("Opened conformance test database in {}", filename); } public List getTestCases() throws ConfigurationException { diff --git a/conformancelib/src/main/java/gov/gsa/pivconformance/conformancelib/utilities/TestRunLogController.java b/conformancelib/src/main/java/gov/gsa/pivconformance/conformancelib/utilities/TestRunLogController.java index 8f248312..54c5b61a 100644 --- a/conformancelib/src/main/java/gov/gsa/pivconformance/conformancelib/utilities/TestRunLogController.java +++ b/conformancelib/src/main/java/gov/gsa/pivconformance/conformancelib/utilities/TestRunLogController.java @@ -19,10 +19,13 @@ import java.io.File; import java.io.IOException; import java.io.PrintStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.NoSuchFileException; +import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; import java.security.ProtectionDomain; import java.util.Arrays; import java.util.Calendar; @@ -42,6 +45,7 @@ public class TestRunLogController { private static final org.slf4j.Logger s_logger = LoggerFactory.getLogger(TestRunLogController.class); private static final TestRunLogController INSTANCE = new TestRunLogController(); + private static final String CONFORMANCE_HEADER = "Date,Test Id,Description,Expected Result,Actual Result"; /* * Note that these names MUST match the user_log_config.xml appender names. @@ -369,10 +373,33 @@ public void setStartTimes() { Iterator i = m_appenders.entrySet().iterator(); while (i.hasNext()) { me = (Map.Entry>) i.next(); + String logName = me.getKey(); TimeStampedFileAppender appender = me.getValue(); + try { + appender.stop(); + String filename = m_filenames.get(logName); + resetLogFile(Paths.get(filename), "CONFORMANCELOG".equals(logName)); + appender.setFile(filename); + appender.setAppend(true); + appender.start(); + appender.setAppend(false); + } catch (IOException e) { + s_logger.error("Unable to prepare {} for a new test run", logName, e); + throw new IllegalStateException("Unable to prepare logs for a new test run", e); + } appender.setStartTime(startTime); } } + + static void resetLogFile(Path file, boolean conformanceCsv) throws IOException { + Path absolute = file.toAbsolutePath().normalize(); + Path parent = absolute.getParent(); + if (parent != null) Files.createDirectories(parent); + byte[] contents = conformanceCsv + ? (CONFORMANCE_HEADER + System.lineSeparator()).getBytes(StandardCharsets.UTF_8) + : new byte[0]; + Files.write(absolute, contents, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } @SuppressWarnings("unchecked") /** @@ -474,30 +501,9 @@ private boolean rollFile(String oldPath, String newPath) { * */ - @SuppressWarnings("unchecked") public void cleanup() { - Map.Entry me = null; - Iterator i = m_loggers.entrySet().iterator(); ArtifactWriter.prependNames(m_timeStamp); ArtifactWriter.clean(); - while (i.hasNext()) { - me = (Map.Entry) i.next(); - String loggerName = me.getKey(); - String loggerClass = me.getValue(); - - Logger logger = (Logger) LoggerFactory.getLogger(loggerClass); - TimeStampedFileAppender appender = null; - - try { - appender = (TimeStampedFileAppender) logger.getAppender(loggerName); - if (appender != null) { - File f = new File(appender.getFile()); - f.delete(); - } - } catch (Exception e) { - s_logger.warn("Can't delete {}: {}", appender.getFile(), e.getMessage()); - } - } } /** diff --git a/tools/85b-swing-gui/build.gradle b/tools/85b-swing-gui/build.gradle index de77a803..15517db6 100644 --- a/tools/85b-swing-gui/build.gradle +++ b/tools/85b-swing-gui/build.gradle @@ -57,6 +57,8 @@ dependencies { implementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' implementation 'org.junit.platform:junit-platform-launcher:1.7.0' implementation 'edu.washington.cs.types.checker:checker-framework:1.7.0' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.7.0' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.0' } extraJavaModuleInfo { diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CompletedTestRun.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CompletedTestRun.java new file mode 100644 index 00000000..a8111250 --- /dev/null +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CompletedTestRun.java @@ -0,0 +1,38 @@ +package gov.gsa.pivconformance.gui; + +import java.nio.file.Path; +import java.time.Instant; +import java.util.Objects; + +/** Immutable inputs identifying one successfully completed CCT run. */ +public final class CompletedTestRun { + private final Path m_resultsDirectory; + private final Path m_databasePath; + private final Path m_conformanceCsv; + private final String m_timeStampPrefix; + private final Instant m_startedAt; + private final Instant m_finishedAt; + private final RunResultsSummary m_summary; + + public CompletedTestRun(Path resultsDirectory, Path databasePath, Path conformanceCsv, + String timeStampPrefix, Instant startedAt, Instant finishedAt, RunResultsSummary summary) { + m_resultsDirectory = Objects.requireNonNull(resultsDirectory, "resultsDirectory").toAbsolutePath().normalize(); + m_databasePath = Objects.requireNonNull(databasePath, "databasePath").toAbsolutePath().normalize(); + m_conformanceCsv = Objects.requireNonNull(conformanceCsv, "conformanceCsv").toAbsolutePath().normalize(); + m_timeStampPrefix = Objects.requireNonNull(timeStampPrefix, "timeStampPrefix"); + m_startedAt = Objects.requireNonNull(startedAt, "startedAt"); + m_finishedAt = Objects.requireNonNull(finishedAt, "finishedAt"); + m_summary = Objects.requireNonNull(summary, "summary"); + if (m_timeStampPrefix.trim().isEmpty()) { + throw new IllegalArgumentException("A completed run must have a timestamp prefix"); + } + } + + public Path getResultsDirectory() { return m_resultsDirectory; } + public Path getDatabasePath() { return m_databasePath; } + public Path getConformanceCsv() { return m_conformanceCsv; } + public String getTimeStampPrefix() { return m_timeStampPrefix; } + public Instant getStartedAt() { return m_startedAt; } + public Instant getFinishedAt() { return m_finishedAt; } + public RunResultsSummary getSummary() { return m_summary; } +} diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CopyableErrorDialog.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CopyableErrorDialog.java new file mode 100644 index 00000000..c9460010 --- /dev/null +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CopyableErrorDialog.java @@ -0,0 +1,59 @@ +package gov.gsa.pivconformance.gui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; + +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Displays bounded, selectable diagnostics without stretching across the screen. */ +final class CopyableErrorDialog { + private static final Logger s_logger = LoggerFactory.getLogger(CopyableErrorDialog.class); + + private CopyableErrorDialog() { } + + static void show(Component parent, String title, String summary, String detailsText) { + String details = detailsText == null || detailsText.trim().isEmpty() + ? "No additional details are available. See console.log for the full application log." + : detailsText; + JTextArea textArea = createDetails(details); + JScrollPane scrollPane = new JScrollPane(textArea); + scrollPane.setPreferredSize(new Dimension(640, 180)); + JPanel content = new JPanel(new BorderLayout(0, 8)); + content.add(new JLabel(summary), BorderLayout.NORTH); + content.add(scrollPane, BorderLayout.CENTER); + content.add(new JLabel("The full diagnostic is also available in console.log."), BorderLayout.SOUTH); + Object[] options = { "Copy Details", "Close" }; + int choice = JOptionPane.showOptionDialog(parent, content, title, JOptionPane.DEFAULT_OPTION, + JOptionPane.ERROR_MESSAGE, null, options, options[1]); + if (choice == 0) copy(details, parent); + } + + static JTextArea createDetails(String details) { + JTextArea textArea = new JTextArea(details, 8, 72); + textArea.setEditable(false); + textArea.setLineWrap(true); + textArea.setWrapStyleWord(true); + textArea.setCaretPosition(0); + return textArea; + } + + private static void copy(String details, Component parent) { + try { + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(details), null); + } catch (Exception e) { + s_logger.error("Unable to copy error details", e); + JOptionPane.showMessageDialog(parent, "Unable to copy the details. See console.log instead.", + "Copy Failed", JOptionPane.ERROR_MESSAGE); + } + } +} diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerAppController.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerAppController.java index 8e6c9aaf..a9c80043 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerAppController.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerAppController.java @@ -37,6 +37,7 @@ public class GuiRunnerAppController { private GuiToggleTestTreeAction m_toggleTreeAction; private GuiDisplayAboutDialogAction m_displayAboutDialogAction; private GuiDisplayTestReportAction m_displayTestReportAction; + private PackageResultsAction m_packageResultsAction; private OpenDefaultPIVDatabaseAction m_openDefaultPIVDatabaseAction; private OpenDefaultPIVIDatabaseAction m_openDefaultPIVIDatabaseAction; private GuiTestExecutionController m_tec; @@ -55,6 +56,7 @@ public void reset() { m_toggleTreeAction = null; m_displayAboutDialogAction = null; m_displayTestReportAction = null; + m_packageResultsAction = null; m_openDefaultPIVDatabaseAction = null; m_openDefaultPIVIDatabaseAction = null; m_tec = null; @@ -138,6 +140,10 @@ public GuiDisplayTestReportAction getDisplayTestReportAction() { return m_displayTestReportAction; } + public PackageResultsAction getPackageResultsAction() { + return m_packageResultsAction; + } + public void showAboutDialog() { s_logger.error("Stubbed out showAboutDialog() is still here"); } @@ -156,6 +162,9 @@ protected void createActions() { m_toggleTreeAction = new GuiToggleTestTreeAction("Toggle test tree view", toggleIcon, "Show or hide the test tree"); ImageIcon displayReportIcon = getActionIcon("html", "Display HTML report"); m_displayTestReportAction = new GuiDisplayTestReportAction("Display Test Report", displayReportIcon, "Display test report for current log"); + ImageIcon packageIcon = getActionIcon("database_save", "Package Results"); + m_packageResultsAction = new PackageResultsAction("Package Results for Review Manager", packageIcon, + "Create a ZIP containing the latest completed run for Review Manager"); ImageIcon savingIcon = getActionIcon("folder", "Saving"); ImageIcon pivIcon = getActionIcon("PIV", "Open"); m_openDefaultPIVDatabaseAction = new OpenDefaultPIVDatabaseAction("Open Default PIV Database", pivIcon, "Open Default PIV conformance test database"); diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerApplication.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerApplication.java index 12ca7d0e..e31dc759 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerApplication.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerApplication.java @@ -95,6 +95,9 @@ public void actionPerformed(ActionEvent e) { JMenuItem mntmDisplayTestReport = new JMenuItem(c.getDisplayTestReportAction()); mnView.add(mntmDisplayTestReport); + + JMenuItem mntmPackageResults = new JMenuItem(c.getPackageResultsAction()); + mnView.add(mntmPackageResults); JMenu mnHelp = new JMenu("Help"); menuBar.add(mnHelp); diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java index 9f8dc159..bf86277b 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java @@ -4,6 +4,10 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -38,6 +42,8 @@ public class GuiTestExecutionController { private static final Logger s_logger = LoggerFactory.getLogger(GuiTestExecutionController.class); private static final GuiTestExecutionController INSTANCE = new GuiTestExecutionController(); private static final String tag30TestId = "8.2.2.1"; // TODO: Fixme + private static final DateTimeFormatter SUMMARY_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z") + .withZone(ZoneId.systemDefault()); private TestRunLogController m_trlc; private GuiTestTreePanel m_testTreePanel; @@ -106,24 +112,48 @@ public LoggerContext getLoggerContext() { public void setLoggerContext(LoggerContext ctx) { m_ctx = ctx; } + + void runAllTestsSafely(GuiTestCaseTreeNode root) { + try { + runAllTests(root); + } catch (RuntimeException e) { + s_logger.error("The test run ended unexpectedly", e); + m_running = false; + SwingUtilities.invokeLater(() -> { + GuiRunnerAppController controller = GuiRunnerAppController.getInstance(); + controller.getDisplayTestReportAction().setEnabled(true); + controller.getPackageResultsAction().setCompletedRun(null); + setDatabaseActionsEnabled(true); + m_testExecutionPanel.getRunButton().setEnabled(true); + m_testExecutionPanel.setPostRunActionsVisible(false); + CopyableErrorDialog.show(controller.getMainFrame(), "Test Run Error", + "The test run ended unexpectedly. No review package was prepared.", e.getMessage()); + }); + } + } void runAllTests(GuiTestCaseTreeNode root) { - + ConformanceTestDatabase db = GuiRunnerAppController.getInstance().getTestDatabase(); + if(db == null || db.getConnection() == null) { + s_logger.error("Unable to run tests without a valid database"); + return; + } + Path selectedDatabase = selectedDatabasePath(db); + if (selectedDatabase == null) { + s_logger.error("Unable to run tests without the selected database filename"); + return; + } + + Instant runStarted = Instant.now(); m_trlc.setStartTimes(); - + GuiDisplayTestReportAction display = GuiRunnerAppController.getInstance().getDisplayTestReportAction(); - display.setEnabled(false); + PackageResultsAction packageResults = GuiRunnerAppController.getInstance().getPackageResultsAction(); s_logger.debug("----------------------------------------"); s_logger.debug("FIPS 201 CCT " + GuiRunnerAppController.getInstance().getCctVersion()); s_logger.debug("----------------------------------------"); - ConformanceTestDatabase db = GuiRunnerAppController.getInstance().getTestDatabase(); - if(db == null || db.getConnection() == null) { - s_logger.error("Unable to run tests without a valid database"); - // XXX *** Display message don't just log it - return; - } m_running = true; GuiRunnerAppController.getInstance().reloadTree(); PCSCWrapper pcsc = PCSCWrapper.getInstance(); @@ -132,10 +162,12 @@ void runAllTests(GuiTestCaseTreeNode root) { int atomCount = 0; JProgressBar progress = m_testExecutionPanel.getTestProgressBar(); try { - SwingUtilities.invokeAndWait(() -> { + SwingUtilities.invokeAndWait(() -> { + display.setEnabled(false); + packageResults.setCompletedRun(null); + m_testExecutionPanel.setPostRunActionsVisible(false); m_testExecutionPanel.getRunButton().setEnabled(false); - // TODO: Fix this or else - m_toolBar.getComponents()[0].setEnabled(false); + setDatabaseActionsEnabled(false); progress.setMaximum(db.getTestCaseCount()); progress.setValue(0); progress.setVisible(true); @@ -265,8 +297,7 @@ void runAllTests(GuiTestCaseTreeNode root) { try { SwingUtilities.invokeAndWait(() -> { m_testExecutionPanel.getRunButton().setEnabled(true); - // TODO: Fix this or else - m_toolBar.getComponents()[0].setEnabled(true); + setDatabaseActionsEnabled(true); }); } catch (InvocationTargetException | InterruptedException e) { s_logger.error("Failed to enable run button", e); @@ -279,11 +310,61 @@ void runAllTests(GuiTestCaseTreeNode root) { m_trlc.setTimeStamps(); // Sets the timestamp for all of the logger files m_trlc.cleanup(); + Instant runFinished = Instant.now(); m_running = false; CardSettingsSingleton css = CardSettingsSingleton.getInstance(); CachingDefaultPIVApplication cpiv = (CachingDefaultPIVApplication) css.getPivHandle(); cpiv.clearCache(); - display.setEnabled(true); + try { + String timeStamp = m_trlc.getTimeStamp(); + Path resultsDirectory = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize(); + Path csv = ReviewPackageBuilder.findConformanceCsv(resultsDirectory, timeStamp); + RunResultsSummary summary = RunResultsSummary.fromCsv(csv); + CompletedTestRun completedRun = new CompletedTestRun(resultsDirectory, selectedDatabase, csv, + timeStamp, runStarted, runFinished, summary); + SwingUtilities.invokeLater(() -> { + display.setEnabled(true); + packageResults.setCompletedRun(completedRun); + m_testExecutionPanel.setPostRunActionsVisible(true); + showCompletionSummary(completedRun); + }); + } catch (Exception e) { + s_logger.error("The completed run could not be prepared for review packaging", e); + SwingUtilities.invokeLater(() -> { + display.setEnabled(true); + showCompletionError(e.getMessage()); + }); + } + } + + private Path selectedDatabasePath(ConformanceTestDatabase db) { + Path databasePath = db.getDatabasePath(); + return databasePath == null ? null : databasePath.toAbsolutePath().normalize(); + } + + private void setDatabaseActionsEnabled(boolean enabled) { + GuiRunnerAppController controller = GuiRunnerAppController.getInstance(); + controller.getOpenDatabaseAction().setEnabled(enabled); + controller.getOpenDefaultPIVDatabaseAction().setEnabled(enabled); + controller.getOpenDefaultPIVIDatabaseAction().setEnabled(enabled); + } + + private void showCompletionSummary(CompletedTestRun run) { + RunResultsSummary summary = run.getSummary(); + String message = "Conformance run completed.\n\n" + + "Database: " + run.getDatabasePath().getFileName() + "\n" + + "Results: " + summary.getPassed() + " passed, " + summary.getFailed() + " failed, " + + summary.getTotal() + " total\n" + + "Started: " + SUMMARY_TIME.format(run.getStartedAt()) + "\n" + + "Finished: " + SUMMARY_TIME.format(run.getFinishedAt()) + "\n" + + "Results folder: " + run.getResultsDirectory(); + JOptionPane.showMessageDialog(GuiRunnerAppController.getInstance().getMainFrame(), message, + "Run Complete", JOptionPane.INFORMATION_MESSAGE); + } + + private void showCompletionError(String detail) { + CopyableErrorDialog.show(GuiRunnerAppController.getInstance().getMainFrame(), "Run Finished", + "The test run finished, but its results could not be prepared for packaging.", detail); } private void registerListeners(Launcher l, List listeners) { diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java new file mode 100644 index 00000000..15455d3b --- /dev/null +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java @@ -0,0 +1,184 @@ +package gov.gsa.pivconformance.gui; + +import java.awt.Desktop; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.AbstractAction; +import javax.swing.Icon; +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Creates a local Review Manager ZIP and offers safe handoff actions. */ +public class PackageResultsAction extends AbstractAction { + private static final long serialVersionUID = 1L; + private static final Logger s_logger = LoggerFactory.getLogger(PackageResultsAction.class); + private static final String REVIEW_MANAGER_PROPERTY = "piv.reviewManager.url"; + private static final String REVIEW_MANAGER_ENVIRONMENT = "PIV_REVIEW_MANAGER_URL"; + private final ReviewPackageBuilder m_builder; + private CompletedTestRun m_completedRun; + + public PackageResultsAction(String name, Icon icon, String toolTip) { + this(name, icon, toolTip, new ReviewPackageBuilder()); + } + + PackageResultsAction(String name, Icon icon, String toolTip, ReviewPackageBuilder builder) { + super(name, icon); + putValue(SHORT_DESCRIPTION, toolTip); + m_builder = builder; + setEnabled(false); + } + + public void setCompletedRun(CompletedTestRun completedRun) { + m_completedRun = completedRun; + setEnabled(completedRun != null); + } + + public CompletedTestRun getCompletedRun() { + return m_completedRun; + } + + @Override + public void actionPerformed(ActionEvent event) { + final CompletedTestRun run = m_completedRun; + if (run == null) { + showError("No successfully completed test run is available to package."); + return; + } + setEnabled(false); + new SwingWorker() { + @Override + protected ReviewPackage doInBackground() throws Exception { + return m_builder.build(run); + } + + @Override + protected void done() { + setEnabled(m_completedRun != null); + try { + ReviewPackage reviewPackage = get(); + if (run == m_completedRun) { + showCompletion(reviewPackage); + } else { + s_logger.info("Review package created at {} after a newer test run started", + reviewPackage.getPath()); + } + } catch (Exception e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + s_logger.error("Unable to create Review Manager package", cause); + showError("The review package could not be created:\n" + cause.getMessage()); + } + } + }.execute(); + } + + private void showCompletion(ReviewPackage reviewPackage) { + URI reviewManager = configuredReviewManagerUri(); + List options = new ArrayList<>(); + options.add("Show in Folder"); + options.add("Copy Path"); + if (reviewManager != null) options.add("Open Review Manager"); + options.add("Close"); + + Path path = reviewPackage.getPath().toAbsolutePath().normalize(); + String message = "Review package created locally.\n\n" + + "File: " + path.getFileName() + "\n" + + "Path: " + path + "\n" + + "Size: " + humanSize(reviewPackage.getSize()) + " (" + reviewPackage.getSize() + " bytes)\n" + + "SHA-256: " + reviewPackage.getSha256() + "\n\n" + + "No files were uploaded. Select or drag this ZIP into Review Manager."; + int choice = JOptionPane.showOptionDialog(GuiRunnerAppController.getInstance().getMainFrame(), message, + "Package Results for Review Manager", JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE, + null, options.toArray(), options.get(options.size() - 1)); + if (choice < 0) return; + String selected = options.get(choice); + if ("Show in Folder".equals(selected)) { + showInFolder(path); + } else if ("Copy Path".equals(selected)) { + copyPath(path); + } else if ("Open Review Manager".equals(selected)) { + openReviewManager(reviewManager); + } + } + + private void showInFolder(Path path) { + try { + if (!Desktop.isDesktopSupported() || !Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { + throw new IOException("Opening folders is not supported on this system"); + } + Desktop.getDesktop().open(path.getParent().toFile()); + } catch (Exception e) { + showError("Unable to show the package folder:\n" + e.getMessage()); + } + } + + private void copyPath(Path path) { + copyText(path.toString(), "Unable to copy the package path"); + } + + private void copyText(String value, String errorSummary) { + try { + Toolkit.getDefaultToolkit().getSystemClipboard() + .setContents(new StringSelection(value), null); + } catch (Exception e) { + s_logger.error(errorSummary, e); + JOptionPane.showMessageDialog(GuiRunnerAppController.getInstance().getMainFrame(), + errorSummary + ". See console.log for details.", "Copy Failed", JOptionPane.ERROR_MESSAGE); + } + } + + private void openReviewManager(URI uri) { + try { + if (uri == null) throw new IOException("No Review Manager URL is configured"); + if (!Desktop.isDesktopSupported() || !Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + throw new IOException("Opening a browser is not supported on this system"); + } + Desktop.getDesktop().browse(uri); + } catch (Exception e) { + showError("Unable to open Review Manager:\n" + e.getMessage()); + } + } + + static URI configuredReviewManagerUri() { + String configured = System.getProperty(REVIEW_MANAGER_PROPERTY); + if (configured == null || configured.trim().isEmpty()) { + configured = System.getenv(REVIEW_MANAGER_ENVIRONMENT); + } + if (configured == null || configured.trim().isEmpty()) return null; + try { + URI uri = new URI(configured.trim()); + if (!("http".equalsIgnoreCase(uri.getScheme()) || "https".equalsIgnoreCase(uri.getScheme())) + || uri.getHost() == null) { + s_logger.warn("Ignoring invalid Review Manager URL configured in {} or {}", + REVIEW_MANAGER_PROPERTY, REVIEW_MANAGER_ENVIRONMENT); + return null; + } + return uri; + } catch (URISyntaxException e) { + s_logger.warn("Ignoring malformed Review Manager URL", e); + return null; + } + } + + private static String humanSize(long bytes) { + if (bytes < 1024) return bytes + " B"; + double kib = bytes / 1024.0; + if (kib < 1024) return String.format("%.1f KiB", kib); + return String.format("%.1f MiB", kib / 1024.0); + } + + private void showError(String message) { + CopyableErrorDialog.show(GuiRunnerAppController.getInstance().getMainFrame(), "Package Results Error", + "The review package could not be created.", message); + } +} diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackage.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackage.java new file mode 100644 index 00000000..35e52a2e --- /dev/null +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackage.java @@ -0,0 +1,20 @@ +package gov.gsa.pivconformance.gui; + +import java.nio.file.Path; + +/** Details shown to the operator after a local review package is created. */ +public final class ReviewPackage { + private final Path m_path; + private final long m_size; + private final String m_sha256; + + public ReviewPackage(Path path, long size, String sha256) { + m_path = path; + m_size = size; + m_sha256 = sha256; + } + + public Path getPath() { return m_path; } + public long getSize() { return m_size; } + public String getSha256() { return m_sha256; } +} diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackageBuilder.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackageBuilder.java new file mode 100644 index 00000000..5a4f97c3 --- /dev/null +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackageBuilder.java @@ -0,0 +1,266 @@ +package gov.gsa.pivconformance.gui; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** Selects and packages evidence for exactly one completed CCT run. */ +public class ReviewPackageBuilder { + private static final DateTimeFormatter PACKAGE_TIME = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"); + private static final long DETERMINISTIC_ZIP_TIME = 315532800000L; // 1980-01-01, valid in ZIP files + private static final String[] RUN_DIRECTORIES = { "logs", "piv-artifacts", "x509-artifacts" }; + private final Clock m_clock; + + public ReviewPackageBuilder() { + this(Clock.systemDefaultZone()); + } + + ReviewPackageBuilder(Clock clock) { + m_clock = clock; + } + + public ReviewPackage build(CompletedTestRun run) throws IOException { + if (run == null) throw new IllegalStateException("No completed test run is available to package"); + Path resultsDirectory = requireDirectory(run.getResultsDirectory(), "Results directory"); + Path database = requireRegularFile(run.getDatabasePath(), "Selected test database"); + Path conformanceCsv = requireRegularFile(run.getConformanceCsv(), "Conformance CSV"); + Path discoveredCsv = findConformanceCsv(resultsDirectory, run.getTimeStampPrefix()); + if (!Files.isSameFile(conformanceCsv, discoveredCsv)) { + throw new IOException("The completed run's conformance CSV no longer matches its recorded result"); + } + List entries = new ArrayList<>(); + String prefix = run.getTimeStampPrefix() + "-"; + + for (String directoryName : RUN_DIRECTORIES) { + Path directory = resultsDirectory.resolve(directoryName); + if ("logs".equals(directoryName) && !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("The completed run's logs directory is unavailable: " + directory); + } + if (Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { + collectRunFiles(resultsDirectory, directory, prefix, entries); + } + } + + long csvCount = entries.stream() + .filter(entry -> entry.name.startsWith("logs/") && entry.name.toLowerCase().endsWith(".csv")) + .count(); + if (csvCount != 1) { + throw new IOException("Expected exactly one conformance CSV for completed run " + + run.getTimeStampPrefix() + ", but found " + csvCount); + } + + Path trustDirectory = requireDirectory(resultsDirectory.resolve("x509-certs"), "Trust-path directory"); + int entriesBeforeTrustPath = entries.size(); + collectAllFiles(resultsDirectory, trustDirectory, entries); + if (entries.size() == entriesBeforeTrustPath) { + throw new IOException("The trust-path directory contains no files: " + trustDirectory); + } + entries.add(new SourceEntry(database, database.getFileName().toString())); + validateEntries(entries); + + Collections.sort(entries, Comparator.comparing(entry -> entry.name)); + Path target = uniqueTarget(resultsDirectory); + Path temporary = Files.createTempFile(resultsDirectory, ".cct-review-results-", ".tmp"); + long size; + String sha256; + try { + writeZip(temporary, entries); + size = Files.size(temporary); + sha256 = sha256(temporary); + moveIntoPlace(temporary, target); + } finally { + Files.deleteIfExists(temporary); + } + return new ReviewPackage(target, size, sha256); + } + + public static Path findConformanceCsv(Path resultsDirectory, String timeStampPrefix) throws IOException { + if (timeStampPrefix == null || timeStampPrefix.trim().isEmpty()) { + throw new IOException("The completed run has no timestamp prefix"); + } + Path logs = resultsDirectory.toAbsolutePath().normalize().resolve("logs"); + if (!Files.isDirectory(logs, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Results logs directory is unavailable: " + logs); + } + List matches = new ArrayList<>(); + try (Stream paths = Files.walk(logs)) { + Iterator iterator = paths.iterator(); + while (iterator.hasNext()) { + Path path = iterator.next(); + if (Files.isSymbolicLink(path)) { + if (path.getFileName().toString().startsWith(timeStampPrefix + "-")) { + throw new IOException("Run evidence may not be a symbolic link: " + path); + } + continue; + } + if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) + && path.getFileName().toString().startsWith(timeStampPrefix + "-") + && path.getFileName().toString().toLowerCase().endsWith(".csv")) { + matches.add(path.toAbsolutePath().normalize()); + } + } + } + if (matches.size() != 1) { + throw new IOException("Expected exactly one conformance CSV for the completed run, but found " + matches.size()); + } + return matches.get(0); + } + + private void collectRunFiles(Path base, Path directory, String prefix, List entries) throws IOException { + try (Stream paths = Files.walk(directory)) { + Iterator iterator = paths.iterator(); + while (iterator.hasNext()) { + Path path = iterator.next(); + if (Files.isSymbolicLink(path)) { + if (path.getFileName().toString().startsWith(prefix)) { + throw new IOException("Run evidence may not be a symbolic link: " + path); + } + continue; + } + if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) + && path.getFileName().toString().startsWith(prefix)) { + entries.add(new SourceEntry(path, entryName(base, path))); + } + } + } + } + + private void collectAllFiles(Path base, Path directory, List entries) throws IOException { + try (Stream paths = Files.walk(directory)) { + Iterator iterator = paths.iterator(); + while (iterator.hasNext()) { + Path path = iterator.next(); + if (Files.isSymbolicLink(path)) { + throw new IOException("Trust-path material may not be a symbolic link: " + path); + } + if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + entries.add(new SourceEntry(path, entryName(base, path))); + } + } + } + } + + private static Path requireDirectory(Path path, String description) throws IOException { + Path normalized = path.toAbsolutePath().normalize(); + if (Files.isSymbolicLink(normalized) || !Files.isDirectory(normalized, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException(description + " is unavailable: " + normalized); + } + return normalized; + } + + private static Path requireRegularFile(Path path, String description) throws IOException { + Path normalized = path.toAbsolutePath().normalize(); + if (Files.isSymbolicLink(normalized) || !Files.isRegularFile(normalized, LinkOption.NOFOLLOW_LINKS) + || !Files.isReadable(normalized)) { + throw new IOException(description + " is unavailable: " + normalized); + } + return normalized; + } + + private static String entryName(Path base, Path file) throws IOException { + Path normalizedBase = base.toAbsolutePath().normalize(); + Path normalizedFile = file.toAbsolutePath().normalize(); + if (!normalizedFile.startsWith(normalizedBase)) { + throw new IOException("Package input is outside the results directory: " + file); + } + return normalizedBase.relativize(normalizedFile).toString().replace('\\', '/'); + } + + private static void validateEntries(List entries) throws IOException { + Set names = new HashSet<>(); + for (SourceEntry entry : entries) { + Path normalized = Path.of(entry.name).normalize(); + if (normalized.isAbsolute() || entry.name.startsWith("../") || entry.name.contains("/../") + || !names.add(entry.name)) { + throw new IOException("Unsafe or duplicate ZIP entry: " + entry.name); + } + } + } + + private Path uniqueTarget(Path directory) { + String baseName = "cct-review-results-" + PACKAGE_TIME.format(LocalDateTime.now(m_clock)); + Path candidate = directory.resolve(baseName + ".zip"); + int suffix = 2; + while (Files.exists(candidate, LinkOption.NOFOLLOW_LINKS)) { + candidate = directory.resolve(baseName + "-" + suffix++ + ".zip"); + } + return candidate; + } + + private static void writeZip(Path output, List entries) throws IOException { + try (OutputStream fileOutput = Files.newOutputStream(output, StandardOpenOption.TRUNCATE_EXISTING); + ZipOutputStream zip = new ZipOutputStream(fileOutput)) { + byte[] buffer = new byte[16 * 1024]; + for (SourceEntry source : entries) { + ZipEntry entry = new ZipEntry(source.name); + entry.setTime(DETERMINISTIC_ZIP_TIME); + zip.putNextEntry(entry); + try (InputStream input = new BufferedInputStream(Files.newInputStream(source.path))) { + int read; + while ((read = input.read(buffer)) >= 0) { + if (read > 0) zip.write(buffer, 0, read); + } + } + zip.closeEntry(); + } + } + } + + private static void moveIntoPlace(Path source, Path target) throws IOException { + try { + Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(source, target); + } + } + + private static String sha256(Path path) throws IOException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (InputStream input = new BufferedInputStream(Files.newInputStream(path))) { + byte[] buffer = new byte[16 * 1024]; + int read; + while ((read = input.read(buffer)) >= 0) { + if (read > 0) digest.update(buffer, 0, read); + } + } + StringBuilder result = new StringBuilder(); + for (byte value : digest.digest()) result.append(String.format("%02x", value & 0xff)); + return result.toString(); + } catch (NoSuchAlgorithmException e) { + throw new IOException("SHA-256 is unavailable", e); + } + } + + private static final class SourceEntry { + private final Path path; + private final String name; + + private SourceEntry(Path path, String name) { + this.path = path; + this.name = name; + } + } +} diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/RunResultsSummary.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/RunResultsSummary.java new file mode 100644 index 00000000..94758516 --- /dev/null +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/RunResultsSummary.java @@ -0,0 +1,50 @@ +package gov.gsa.pivconformance.gui; + +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVRecord; + +/** Pass/fail counts read without changing the legacy conformance CSV format. */ +public final class RunResultsSummary { + private final int m_passed; + private final int m_failed; + private final int m_total; + + private RunResultsSummary(int passed, int failed, int total) { + m_passed = passed; + m_failed = failed; + m_total = total; + } + + public static RunResultsSummary fromCsv(Path csvPath) throws IOException { + int passed = 0; + int failed = 0; + int total = 0; + try (Reader reader = Files.newBufferedReader(csvPath, StandardCharsets.UTF_8); + CSVParser parser = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(reader)) { + if (!parser.getHeaderMap().containsKey("Actual Result")) { + throw new IOException("Conformance CSV is missing the Actual Result column: " + csvPath); + } + for (CSVRecord record : parser) { + String result = record.get("Actual Result").trim(); + if ("Pass".equalsIgnoreCase(result)) { + passed++; + } else { + failed++; + } + total++; + } + } + return new RunResultsSummary(passed, failed, total); + } + + public int getPassed() { return m_passed; } + public int getFailed() { return m_failed; } + public int getTotal() { return m_total; } +} diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/SimpleTestExecutionPanel.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/SimpleTestExecutionPanel.java index 35dd89b0..6dde303d 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/SimpleTestExecutionPanel.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/SimpleTestExecutionPanel.java @@ -9,8 +9,7 @@ import java.awt.Color; import java.awt.HeadlessException; -import java.sql.Connection; -import java.sql.SQLException; +import java.nio.file.Path; import java.util.List; import javax.swing.JLabel; @@ -55,6 +54,8 @@ public class SimpleTestExecutionPanel extends JPanel { private final JTextField m_readerStatusField; private final JProgressBar m_testProgressBar; private final JButton m_runButton; + private final JButton m_viewResultsButton; + private final JButton m_packageResultsButton; public SimpleTestExecutionPanel() { setBackground(Color.WHITE); @@ -191,14 +192,22 @@ public void actionPerformed(ActionEvent e) { dialog.setVisible(true); return; } - GuiTestExecutionController tc = GuiTestExecutionController.getInstance(); - GuiTestCaseTreeNode root = GuiRunnerAppController.getInstance().getApp().getTreePanel().getRootNode(); - new Thread(() -> { - tc.runAllTests(root); - }).start(); + GuiTestExecutionController tc = GuiTestExecutionController.getInstance(); + GuiTestCaseTreeNode root = GuiRunnerAppController.getInstance().getApp().getTreePanel().getRootNode(); + new Thread(() -> tc.runAllTestsSafely(root), "cct-test-run").start(); } }); + + m_viewResultsButton = new JButton(GuiRunnerAppController.getInstance().getDisplayTestReportAction()); + m_viewResultsButton.setText("View Results"); + m_viewResultsButton.setIcon(null); + m_viewResultsButton.setVisible(false); + + m_packageResultsButton = new JButton(GuiRunnerAppController.getInstance().getPackageResultsAction()); + m_packageResultsButton.setText("Package Results"); + m_packageResultsButton.setIcon(null); + m_packageResultsButton.setVisible(false); m_testProgressBar = new JProgressBar(); m_testProgressBar.setAlignmentY(Component.TOP_ALIGNMENT); @@ -270,7 +279,11 @@ public void actionPerformed(ActionEvent e) { .addComponent(btnRefreshReaders, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addPreferredGap(ComponentPlacement.RELATED) .addComponent(m_runButton) - .addGap(317)) + .addPreferredGap(ComponentPlacement.RELATED) + .addComponent(m_viewResultsButton) + .addPreferredGap(ComponentPlacement.RELATED) + .addComponent(m_packageResultsButton) + .addGap(106)) ); groupLayout.setVerticalGroup( groupLayout.createParallelGroup(Alignment.LEADING) @@ -310,7 +323,9 @@ public void actionPerformed(ActionEvent e) { .addPreferredGap(ComponentPlacement.UNRELATED) .addGroup(groupLayout.createParallelGroup(Alignment.BASELINE) .addComponent(btnRefreshReaders) - .addComponent(m_runButton)) + .addComponent(m_runButton) + .addComponent(m_viewResultsButton) + .addComponent(m_packageResultsButton)) .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); setLayout(groupLayout); @@ -326,16 +341,10 @@ public void actionPerformed(ActionEvent e) { public void refreshDatabaseInfo() { ConformanceTestDatabase db = GuiRunnerAppController.getInstance().getTestDatabase(); if(db != null) { - Connection c = db.getConnection(); - if(c != null) { - String filename = null; - try { - filename = c.getClientInfo("filename"); - } catch (SQLException e) { - m_databaseNameField.setText("No filename information is available."); - } - if(filename != null) { - m_databaseNameField.setText(filename); + if(db.getConnection() != null) { + Path databasePath = db.getDatabasePath(); + if(databasePath != null) { + m_databaseNameField.setText(databasePath.toString()); } else { m_databaseNameField.setText("(unavailable)"); } @@ -380,6 +389,13 @@ public JButton getRunButton() { return m_runButton; } + public void setPostRunActionsVisible(boolean visible) { + m_viewResultsButton.setVisible(visible); + m_packageResultsButton.setVisible(visible); + revalidate(); + repaint(); + } + public void refreshReaderStatus(CardSettingsSingleton css) { CardTerminal reader = css.getTerminal(); if(reader == null) { diff --git a/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/conformancelib/utilities/TestRunLogControllerLifecycleTest.java b/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/conformancelib/utilities/TestRunLogControllerLifecycleTest.java new file mode 100644 index 00000000..c681f215 --- /dev/null +++ b/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/conformancelib/utilities/TestRunLogControllerLifecycleTest.java @@ -0,0 +1,37 @@ +package gov.gsa.pivconformance.conformancelib.utilities; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class TestRunLogControllerLifecycleTest { + private static final String CSV_HEADER = "Date,Test Id,Description,Expected Result,Actual Result" + + System.lineSeparator(); + + @TempDir + Path tempDirectory; + + @Test + void resetsBaseLogsBeforeEveryRunWithoutCarryingPreviousResults() throws Exception { + Path csv = tempDirectory.resolve("logs/conformancelog/conformance_results.csv"); + Path apdu = tempDirectory.resolve("logs/apdu/apdu_transmission.log"); + + TestRunLogController.resetLogFile(csv, true); + TestRunLogController.resetLogFile(apdu, false); + Files.writeString(csv, "first-run-result" + System.lineSeparator(), StandardCharsets.UTF_8, + StandardOpenOption.APPEND); + Files.writeString(apdu, "first-run-apdu", StandardCharsets.UTF_8, StandardOpenOption.APPEND); + + TestRunLogController.resetLogFile(csv, true); + TestRunLogController.resetLogFile(apdu, false); + + assertEquals(CSV_HEADER, Files.readString(csv, StandardCharsets.UTF_8)); + assertEquals("", Files.readString(apdu, StandardCharsets.UTF_8)); + } +} diff --git a/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java b/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java new file mode 100644 index 00000000..91e04afb --- /dev/null +++ b/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java @@ -0,0 +1,206 @@ +package gov.gsa.pivconformance.gui; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import javax.swing.JTextArea; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import gov.gsa.pivconformance.conformancelib.configuration.ConformanceTestDatabase; + +class ReviewPackageBuilderTest { + private static final String PREFIX = "card-identifier_20260819_010203-20260819_020304"; + private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-08-19T12:34:56Z"), ZoneOffset.UTC); + + @TempDir + Path tempDirectory; + + @ParameterizedTest + @ValueSource(strings = { "PIV_Production_Cards.db", "PIV-I_Production_Cards.db" }) + void packagesExactlyOneCompletedRunAndSelectedDatabase(String databaseName) throws Exception { + Path database = createEvidence(databaseName, "database"); + Path csv = write("logs/conformancelog/" + PREFIX + "-conformance_results.csv", + "Date,Test Id,Description,Expected Result,Actual Result\n" + + "2026-08-19 01:02:04,1,one,Pass,Pass\n" + + "2026-08-19 01:02:05,2,two,Pass,Fail\n"); + write("logs/apdu/" + PREFIX + "-apdu_transmission.log", "masked APDU aa aa aa"); + write("logs/conformancelog/old-run-conformance_results.csv", "stale"); + write("logs/debug/debug.log", "temporary"); + write("piv-artifacts/" + PREFIX + "-chuid.bin", "current piv artifact"); + write("piv-artifacts/old-run-chuid.bin", "stale piv artifact"); + write("x509-artifacts/" + PREFIX + "-authentication.crt", "current certificate artifact"); + write("x509-certs/cacerts.jks", "trust store"); + write("x509-certs/valid/policy.xml", "policy"); + write("unused.db", "unused database"); + write("tool.jar", "executable"); + write("cct-review-results-20200101-000000.zip", "old package"); + + RunResultsSummary summary = RunResultsSummary.fromCsv(csv); + CompletedTestRun run = completedRun(database, csv, summary); + ReviewPackage result = new ReviewPackageBuilder(FIXED_CLOCK).build(run); + + assertEquals("cct-review-results-20260819-123456.zip", result.getPath().getFileName().toString()); + assertEquals(64, result.getSha256().length()); + assertTrue(result.getSize() > 0); + assertEquals(Arrays.asList( + databaseName, + "logs/apdu/" + PREFIX + "-apdu_transmission.log", + "logs/conformancelog/" + PREFIX + "-conformance_results.csv", + "piv-artifacts/" + PREFIX + "-chuid.bin", + "x509-artifacts/" + PREFIX + "-authentication.crt", + "x509-certs/cacerts.jks", + "x509-certs/valid/policy.xml"), zipEntries(result.getPath())); + assertFalse(result.getPath().getFileName().toString().contains("card-identifier")); + } + + @Test + void preservesLegacyEvidenceBytesUnchanged() throws Exception { + Path database = createEvidence("PIV_ICAM_Test_Cards.db", "database"); + Path csv = basicCsv(); + byte[] evidence = new byte[] { 0x00, 0x31, 0x32, 0x33, 0x34, (byte) 0xff, 0x0a }; + Path apdu = writeBytes("logs/apdu/" + PREFIX + "-apdu_transmission.log", evidence); + CompletedTestRun run = completedRun(database, csv, RunResultsSummary.fromCsv(csv)); + + ReviewPackage result = new ReviewPackageBuilder(FIXED_CLOCK).build(run); + assertEquals(Arrays.toString(Files.readAllBytes(apdu)), + Arrays.toString(zipEntry(result.getPath(), "logs/apdu/" + apdu.getFileName()))); + } + + @Test + void errorDetailsAreWrappedSelectableText() { + JTextArea details = CopyableErrorDialog.createDetails("A very long diagnostic path"); + assertFalse(details.isEditable()); + assertTrue(details.getLineWrap()); + assertTrue(details.getWrapStyleWord()); + assertEquals("A very long diagnostic path", details.getText()); + } + + @Test + void packageActionTracksCompletedRunAvailability() throws Exception { + PackageResultsAction action = new PackageResultsAction("Package", null, "Package results"); + assertFalse(action.isEnabled()); + + Path database = createEvidence("PIV_Production_Cards.db", "database"); + Path csv = basicCsv(); + action.setCompletedRun(completedRun(database, csv, RunResultsSummary.fromCsv(csv))); + assertTrue(action.isEnabled()); + + action.setCompletedRun(null); + assertFalse(action.isEnabled()); + } + + @Test + void retainsCanonicalSelectedDatabasePathWithoutJdbcClientInfo() throws Exception { + Path database = tempDirectory.resolve("databases").resolve("selected.db"); + Files.createDirectories(database.getParent()); + Files.createFile(database); + Path nonCanonicalPath = database.getParent().resolve("..").resolve("databases").resolve("selected.db"); + ConformanceTestDatabase selected = new ConformanceTestDatabase(null); + + try { + selected.openDatabaseInFile(nonCanonicalPath.toString()); + assertEquals(database.toRealPath(), selected.getDatabasePath()); + } finally { + if (selected.getConnection() != null) selected.getConnection().close(); + } + } + + @Test + void rejectsMissingCompletedRunAndAmbiguousCsv() throws Exception { + ReviewPackageBuilder builder = new ReviewPackageBuilder(FIXED_CLOCK); + assertThrows(IllegalStateException.class, () -> builder.build(null)); + + Path database = createEvidence("PIV_Production_Cards.db", "database"); + Path csv = basicCsv(); + write("logs/other/" + PREFIX + "-second.csv", "Date,Actual Result\nnow,Pass\n"); + CompletedTestRun run = completedRun(database, csv, RunResultsSummary.fromCsv(csv)); + IOException error = assertThrows(IOException.class, () -> builder.build(run)); + assertTrue(error.getMessage().contains("exactly one conformance CSV")); + } + + @Test + void producesDeterministicZipContent() throws Exception { + Path database = createEvidence("PIV_Production_Cards.db", "database"); + Path csv = basicCsv(); + CompletedTestRun run = completedRun(database, csv, RunResultsSummary.fromCsv(csv)); + ReviewPackageBuilder builder = new ReviewPackageBuilder(FIXED_CLOCK); + + ReviewPackage first = builder.build(run); + ReviewPackage second = builder.build(run); + assertEquals(first.getSha256(), second.getSha256()); + assertEquals("cct-review-results-20260819-123456-2.zip", second.getPath().getFileName().toString()); + } + + @Test + void parsesCompletionCounts() throws Exception { + Path csv = basicCsv(); + RunResultsSummary summary = RunResultsSummary.fromCsv(csv); + assertEquals(1, summary.getPassed()); + assertEquals(1, summary.getFailed()); + assertEquals(2, summary.getTotal()); + } + + private Path basicCsv() throws IOException { + createEvidence("x509-certs/cacerts.jks", "trust"); + return write("logs/conformancelog/" + PREFIX + "-conformance_results.csv", + "Date,Test Id,Description,Expected Result,Actual Result\n" + + "now,1,one,Pass,Pass\nnow,2,two,Pass,Fail\n"); + } + + private CompletedTestRun completedRun(Path database, Path csv, RunResultsSummary summary) { + return new CompletedTestRun(tempDirectory, database, csv, PREFIX, + Instant.parse("2026-08-19T01:02:03Z"), Instant.parse("2026-08-19T02:03:04Z"), summary); + } + + private Path createEvidence(String relative, String contents) throws IOException { + return write(relative, contents); + } + + private Path write(String relative, String contents) throws IOException { + return writeBytes(relative, contents.getBytes(StandardCharsets.UTF_8)); + } + + private Path writeBytes(String relative, byte[] contents) throws IOException { + Path path = tempDirectory.resolve(relative); + Files.createDirectories(path.getParent()); + Files.write(path, contents); + return path; + } + + private static List zipEntries(Path zipPath) throws IOException { + List names = new ArrayList<>(); + try (ZipFile zip = new ZipFile(zipPath.toFile())) { + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) names.add(entries.nextElement().getName()); + } + return names; + } + + private static byte[] zipEntry(Path zipPath, String name) throws IOException { + try (ZipFile zip = new ZipFile(zipPath.toFile())) { + ZipEntry entry = zip.getEntry(name); + assertTrue(entry != null, name); + return zip.getInputStream(entry).readAllBytes(); + } + } +} From 02be2548b321031a6fb84b0c17cb0e54d0638143 Mon Sep 17 00:00:00 2001 From: Connor Ford Date: Mon, 24 Aug 2026 22:49:33 -0400 Subject: [PATCH 2/6] Replace obsolete Grgit build plugin --- cardlib/build.gradle | 21 ++++++++------------- conformancelib/build.gradle | 2 -- tools/85b-swing-gui/build.gradle | 1 - 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/cardlib/build.gradle b/cardlib/build.gradle index 0d3f7958..88761c32 100644 --- a/cardlib/build.gradle +++ b/cardlib/build.gradle @@ -12,7 +12,6 @@ buildscript { plugins { id 'java-library' - id "org.ajoberstar.grgit" version "4.1.0" id 'com.github.johnrengelman.shadow' version '4.0.4' id 'de.jjohannes.extra-java-module-info' version '0.1' } @@ -161,13 +160,11 @@ junitPlatformTest { println 'Cloning, please wait...' File cardsDir = new File(project.getRootDir(), 'build/classes/java/test/gov/gsa/pivconformance/cardlib/test/gsa-icam-card-builder') if (!cardsDir.exists()) { - def grgit = org.ajoberstar.grgit.Grgit.clone(dir: cardsDir, uri: 'https://github.com/GSA/gsa-icam-card-builder', checkout: true, refToCheckout: 'master') - if (grgit != null) { - grgit.describe() - System.out.println 'Cloned ' + cardsDir.getName() - } else { - System.out.println 'Couldn\'t clone gsa-icam-card-builder' + project.exec { + commandLine 'git', 'clone', '--branch', 'master', '--single-branch', + 'https://github.com/GSA/gsa-icam-card-builder', cardsDir } + System.out.println 'Cloned ' + cardsDir.getName() } else { System.out.println cardsDir.getName() + ' exists, reusing' } @@ -210,12 +207,10 @@ compileJava { doFirst { version = getVersion() - def dir = "${project.rootDir}/../" - def git = org.ajoberstar.grgit.Grgit.open(dir: dir) - def commit = git.head() - - def commitId = commit.abbreviatedId - def commitDate = commit.getDate() + def repositoryDir = project.rootDir.parentFile + def commitId = ['git', 'rev-parse', '--short', 'HEAD'].execute(null, repositoryDir).text.trim() + def commitDateText = ['git', 'show', '-s', '--format=%ct', 'HEAD'].execute(null, repositoryDir).text.trim() + def commitDate = new Date(Long.parseLong(commitDateText) * 1000L) def buildDate = new Date() File resourcesDir = new File(project.getProjectDir(), 'src/main/resources/') File propertiesFile = new File(resourcesDir, 'version.properties') diff --git a/conformancelib/build.gradle b/conformancelib/build.gradle index 5bf1f8fe..36fba312 100644 --- a/conformancelib/build.gradle +++ b/conformancelib/build.gradle @@ -15,7 +15,6 @@ buildscript { plugins { id 'java-library' - id 'org.ajoberstar.grgit' version '2.1.0' id 'com.github.johnrengelman.shadow' version '4.0.4' id "de.jjohannes.extra-java-module-info" version "0.1" } @@ -302,4 +301,3 @@ task install(type: Copy) { from jar into '../libs' } - diff --git a/tools/85b-swing-gui/build.gradle b/tools/85b-swing-gui/build.gradle index 15517db6..58afe54c 100644 --- a/tools/85b-swing-gui/build.gradle +++ b/tools/85b-swing-gui/build.gradle @@ -8,7 +8,6 @@ buildscript { plugins { id 'java-library' - id 'org.ajoberstar.grgit' version '2.1.0' id 'com.github.johnrengelman.shadow' version '4.0.4' id 'de.jjohannes.extra-java-module-info' version '0.1' From fca3fb707018867d8b5bf333ffe6b9ae9c99630c Mon Sep 17 00:00:00 2001 From: Connor Ford Date: Tue, 25 Aug 2026 10:37:18 -0400 Subject: [PATCH 3/6] Automate completed-run packaging --- .../pivconformance/gui/CompletedTestRun.java | 12 +---- .../gui/GuiTestExecutionController.java | 26 +--------- .../gui/PackageResultsAction.java | 4 ++ .../pivconformance/gui/RunResultsSummary.java | 50 ------------------- .../gui/ReviewPackageBuilderTest.java | 25 +++------- 5 files changed, 14 insertions(+), 103 deletions(-) delete mode 100644 tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/RunResultsSummary.java diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CompletedTestRun.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CompletedTestRun.java index a8111250..6ad47678 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CompletedTestRun.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CompletedTestRun.java @@ -1,7 +1,6 @@ package gov.gsa.pivconformance.gui; import java.nio.file.Path; -import java.time.Instant; import java.util.Objects; /** Immutable inputs identifying one successfully completed CCT run. */ @@ -10,19 +9,13 @@ public final class CompletedTestRun { private final Path m_databasePath; private final Path m_conformanceCsv; private final String m_timeStampPrefix; - private final Instant m_startedAt; - private final Instant m_finishedAt; - private final RunResultsSummary m_summary; public CompletedTestRun(Path resultsDirectory, Path databasePath, Path conformanceCsv, - String timeStampPrefix, Instant startedAt, Instant finishedAt, RunResultsSummary summary) { + String timeStampPrefix) { m_resultsDirectory = Objects.requireNonNull(resultsDirectory, "resultsDirectory").toAbsolutePath().normalize(); m_databasePath = Objects.requireNonNull(databasePath, "databasePath").toAbsolutePath().normalize(); m_conformanceCsv = Objects.requireNonNull(conformanceCsv, "conformanceCsv").toAbsolutePath().normalize(); m_timeStampPrefix = Objects.requireNonNull(timeStampPrefix, "timeStampPrefix"); - m_startedAt = Objects.requireNonNull(startedAt, "startedAt"); - m_finishedAt = Objects.requireNonNull(finishedAt, "finishedAt"); - m_summary = Objects.requireNonNull(summary, "summary"); if (m_timeStampPrefix.trim().isEmpty()) { throw new IllegalArgumentException("A completed run must have a timestamp prefix"); } @@ -32,7 +25,4 @@ public CompletedTestRun(Path resultsDirectory, Path databasePath, Path conforman public Path getDatabasePath() { return m_databasePath; } public Path getConformanceCsv() { return m_conformanceCsv; } public String getTimeStampPrefix() { return m_timeStampPrefix; } - public Instant getStartedAt() { return m_startedAt; } - public Instant getFinishedAt() { return m_finishedAt; } - public RunResultsSummary getSummary() { return m_summary; } } diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java index bf86277b..72634b5f 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java @@ -5,9 +5,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Path; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -42,8 +39,6 @@ public class GuiTestExecutionController { private static final Logger s_logger = LoggerFactory.getLogger(GuiTestExecutionController.class); private static final GuiTestExecutionController INSTANCE = new GuiTestExecutionController(); private static final String tag30TestId = "8.2.2.1"; // TODO: Fixme - private static final DateTimeFormatter SUMMARY_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z") - .withZone(ZoneId.systemDefault()); private TestRunLogController m_trlc; private GuiTestTreePanel m_testTreePanel; @@ -144,7 +139,6 @@ void runAllTests(GuiTestCaseTreeNode root) { return; } - Instant runStarted = Instant.now(); m_trlc.setStartTimes(); GuiDisplayTestReportAction display = GuiRunnerAppController.getInstance().getDisplayTestReportAction(); @@ -310,7 +304,6 @@ void runAllTests(GuiTestCaseTreeNode root) { m_trlc.setTimeStamps(); // Sets the timestamp for all of the logger files m_trlc.cleanup(); - Instant runFinished = Instant.now(); m_running = false; CardSettingsSingleton css = CardSettingsSingleton.getInstance(); CachingDefaultPIVApplication cpiv = (CachingDefaultPIVApplication) css.getPivHandle(); @@ -319,14 +312,12 @@ void runAllTests(GuiTestCaseTreeNode root) { String timeStamp = m_trlc.getTimeStamp(); Path resultsDirectory = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize(); Path csv = ReviewPackageBuilder.findConformanceCsv(resultsDirectory, timeStamp); - RunResultsSummary summary = RunResultsSummary.fromCsv(csv); - CompletedTestRun completedRun = new CompletedTestRun(resultsDirectory, selectedDatabase, csv, - timeStamp, runStarted, runFinished, summary); + CompletedTestRun completedRun = new CompletedTestRun(resultsDirectory, selectedDatabase, csv, timeStamp); SwingUtilities.invokeLater(() -> { display.setEnabled(true); packageResults.setCompletedRun(completedRun); m_testExecutionPanel.setPostRunActionsVisible(true); - showCompletionSummary(completedRun); + packageResults.packageCompletedRun(); }); } catch (Exception e) { s_logger.error("The completed run could not be prepared for review packaging", e); @@ -349,19 +340,6 @@ private void setDatabaseActionsEnabled(boolean enabled) { controller.getOpenDefaultPIVIDatabaseAction().setEnabled(enabled); } - private void showCompletionSummary(CompletedTestRun run) { - RunResultsSummary summary = run.getSummary(); - String message = "Conformance run completed.\n\n" - + "Database: " + run.getDatabasePath().getFileName() + "\n" - + "Results: " + summary.getPassed() + " passed, " + summary.getFailed() + " failed, " - + summary.getTotal() + " total\n" - + "Started: " + SUMMARY_TIME.format(run.getStartedAt()) + "\n" - + "Finished: " + SUMMARY_TIME.format(run.getFinishedAt()) + "\n" - + "Results folder: " + run.getResultsDirectory(); - JOptionPane.showMessageDialog(GuiRunnerAppController.getInstance().getMainFrame(), message, - "Run Complete", JOptionPane.INFORMATION_MESSAGE); - } - private void showCompletionError(String detail) { CopyableErrorDialog.show(GuiRunnerAppController.getInstance().getMainFrame(), "Run Finished", "The test run finished, but its results could not be prepared for packaging.", detail); diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java index 15455d3b..42cd1839 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java @@ -50,6 +50,10 @@ public CompletedTestRun getCompletedRun() { @Override public void actionPerformed(ActionEvent event) { + packageCompletedRun(); + } + + void packageCompletedRun() { final CompletedTestRun run = m_completedRun; if (run == null) { showError("No successfully completed test run is available to package."); diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/RunResultsSummary.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/RunResultsSummary.java deleted file mode 100644 index 94758516..00000000 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/RunResultsSummary.java +++ /dev/null @@ -1,50 +0,0 @@ -package gov.gsa.pivconformance.gui; - -import java.io.IOException; -import java.io.Reader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; - -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVParser; -import org.apache.commons.csv.CSVRecord; - -/** Pass/fail counts read without changing the legacy conformance CSV format. */ -public final class RunResultsSummary { - private final int m_passed; - private final int m_failed; - private final int m_total; - - private RunResultsSummary(int passed, int failed, int total) { - m_passed = passed; - m_failed = failed; - m_total = total; - } - - public static RunResultsSummary fromCsv(Path csvPath) throws IOException { - int passed = 0; - int failed = 0; - int total = 0; - try (Reader reader = Files.newBufferedReader(csvPath, StandardCharsets.UTF_8); - CSVParser parser = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(reader)) { - if (!parser.getHeaderMap().containsKey("Actual Result")) { - throw new IOException("Conformance CSV is missing the Actual Result column: " + csvPath); - } - for (CSVRecord record : parser) { - String result = record.get("Actual Result").trim(); - if ("Pass".equalsIgnoreCase(result)) { - passed++; - } else { - failed++; - } - total++; - } - } - return new RunResultsSummary(passed, failed, total); - } - - public int getPassed() { return m_passed; } - public int getFailed() { return m_failed; } - public int getTotal() { return m_total; } -} diff --git a/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java b/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java index 91e04afb..a62b6a01 100644 --- a/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java +++ b/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java @@ -55,8 +55,7 @@ void packagesExactlyOneCompletedRunAndSelectedDatabase(String databaseName) thro write("tool.jar", "executable"); write("cct-review-results-20200101-000000.zip", "old package"); - RunResultsSummary summary = RunResultsSummary.fromCsv(csv); - CompletedTestRun run = completedRun(database, csv, summary); + CompletedTestRun run = completedRun(database, csv); ReviewPackage result = new ReviewPackageBuilder(FIXED_CLOCK).build(run); assertEquals("cct-review-results-20260819-123456.zip", result.getPath().getFileName().toString()); @@ -79,7 +78,7 @@ void preservesLegacyEvidenceBytesUnchanged() throws Exception { Path csv = basicCsv(); byte[] evidence = new byte[] { 0x00, 0x31, 0x32, 0x33, 0x34, (byte) 0xff, 0x0a }; Path apdu = writeBytes("logs/apdu/" + PREFIX + "-apdu_transmission.log", evidence); - CompletedTestRun run = completedRun(database, csv, RunResultsSummary.fromCsv(csv)); + CompletedTestRun run = completedRun(database, csv); ReviewPackage result = new ReviewPackageBuilder(FIXED_CLOCK).build(run); assertEquals(Arrays.toString(Files.readAllBytes(apdu)), @@ -102,7 +101,7 @@ void packageActionTracksCompletedRunAvailability() throws Exception { Path database = createEvidence("PIV_Production_Cards.db", "database"); Path csv = basicCsv(); - action.setCompletedRun(completedRun(database, csv, RunResultsSummary.fromCsv(csv))); + action.setCompletedRun(completedRun(database, csv)); assertTrue(action.isEnabled()); action.setCompletedRun(null); @@ -133,7 +132,7 @@ void rejectsMissingCompletedRunAndAmbiguousCsv() throws Exception { Path database = createEvidence("PIV_Production_Cards.db", "database"); Path csv = basicCsv(); write("logs/other/" + PREFIX + "-second.csv", "Date,Actual Result\nnow,Pass\n"); - CompletedTestRun run = completedRun(database, csv, RunResultsSummary.fromCsv(csv)); + CompletedTestRun run = completedRun(database, csv); IOException error = assertThrows(IOException.class, () -> builder.build(run)); assertTrue(error.getMessage().contains("exactly one conformance CSV")); } @@ -142,7 +141,7 @@ void rejectsMissingCompletedRunAndAmbiguousCsv() throws Exception { void producesDeterministicZipContent() throws Exception { Path database = createEvidence("PIV_Production_Cards.db", "database"); Path csv = basicCsv(); - CompletedTestRun run = completedRun(database, csv, RunResultsSummary.fromCsv(csv)); + CompletedTestRun run = completedRun(database, csv); ReviewPackageBuilder builder = new ReviewPackageBuilder(FIXED_CLOCK); ReviewPackage first = builder.build(run); @@ -151,15 +150,6 @@ void producesDeterministicZipContent() throws Exception { assertEquals("cct-review-results-20260819-123456-2.zip", second.getPath().getFileName().toString()); } - @Test - void parsesCompletionCounts() throws Exception { - Path csv = basicCsv(); - RunResultsSummary summary = RunResultsSummary.fromCsv(csv); - assertEquals(1, summary.getPassed()); - assertEquals(1, summary.getFailed()); - assertEquals(2, summary.getTotal()); - } - private Path basicCsv() throws IOException { createEvidence("x509-certs/cacerts.jks", "trust"); return write("logs/conformancelog/" + PREFIX + "-conformance_results.csv", @@ -167,9 +157,8 @@ private Path basicCsv() throws IOException { + "now,1,one,Pass,Pass\nnow,2,two,Pass,Fail\n"); } - private CompletedTestRun completedRun(Path database, Path csv, RunResultsSummary summary) { - return new CompletedTestRun(tempDirectory, database, csv, PREFIX, - Instant.parse("2026-08-19T01:02:03Z"), Instant.parse("2026-08-19T02:03:04Z"), summary); + private CompletedTestRun completedRun(Path database, Path csv) { + return new CompletedTestRun(tempDirectory, database, csv, PREFIX); } private Path createEvidence(String relative, String contents) throws IOException { From ec1e4f84d5587e36165dc71d527d861d2263a243 Mon Sep 17 00:00:00 2001 From: Connor Ford Date: Tue, 25 Aug 2026 10:37:54 -0400 Subject: [PATCH 4/6] Add Docker build and Linux runtime --- .dockerignore | 10 ++++++++++ .gitignore | 1 + DOCKER.md | 25 +++++++++++++++++++++++++ Dockerfile | 42 ++++++++++++++++++++++++++++++++++++++++++ cardlib/build.gradle | 17 ++++++++++++++--- docker/entrypoint.sh | 9 +++++++++ 6 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 DOCKER.md create mode 100644 Dockerfile create mode 100644 docker/entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..1ecce0b0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +.gradle +**/.gradle +**/build +libs +fips201-card-conformance-tool-* +cct-review-results-*.zip +cct-docker-data +*.log +.DS_Store diff --git a/.gitignore b/.gitignore index 0947ebc6..3162055a 100644 --- a/.gitignore +++ b/.gitignore @@ -119,4 +119,5 @@ fips201-card-conformance-tool-[0-9]*\.[0-9]*\.[0-9]*-beta-[0-9]*/* /fips201-card-conformance-tool-*/ /fips201-card-conformance-tool-*.zip /cct-review-results-*.zip +/cct-docker-data/ libs/* diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 00000000..e4fd7019 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,25 @@ +# Docker + +Build the image from the repository root: + +```sh +docker build -t piv-conformance . +``` + +The image build compiles all modules and runs the focused GUI packaging tests. Running the Swing application with a smart-card reader is supported only on a native Linux Docker host because Docker Desktop does not directly expose host USB or PC/SC devices. + +On Linux, expose the X11 display, the host PC/SC socket, and a persistent data directory: + +```sh +mkdir -p cct-docker-data +docker run --rm \ + -e DISPLAY \ + -e XAUTHORITY=/tmp/.Xauthority \ + -v "${XAUTHORITY:-$HOME/.Xauthority}:/tmp/.Xauthority:ro" \ + -v /tmp/.X11-unix:/tmp/.X11-unix:ro \ + -v /run/pcscd/pcscd.comm:/run/pcscd/pcscd.comm \ + -v "$PWD/cct-docker-data:/data" \ + piv-conformance +``` + +The container seeds `/data` with the required databases and configuration on first launch. Logs, artifacts, and generated review ZIPs remain in the mounted `cct-docker-data` directory. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..9939e121 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM eclipse-temurin:11-jdk-jammy AS build + +ARG CCT_GIT_COMMIT=container +ARG CCT_GIT_COMMIT_EPOCH=0 +ENV CCT_GIT_COMMIT=${CCT_GIT_COMMIT} \ + CCT_GIT_COMMIT_EPOCH=${CCT_GIT_COMMIT_EPOCH} + +WORKDIR /workspace +COPY . . + +RUN cd cardlib \ + && ./gradlew --no-daemon clean install \ + -x test -x junitPlatformTest -x generateHtmlTestReports +RUN cd conformancelib \ + && ./gradlew --no-daemon clean install -x test -x junitPlatformTest +RUN cd tools/85b-swing-gui \ + && ./gradlew --no-daemon clean test shadowJar + +RUN mkdir -p /opt/cct/bootstrap \ + && cp tools/85b-swing-gui/build/libs/*-shadow.jar /opt/cct/cct.jar \ + && cp cardlib/src/main/resources/user_log_config.xml /opt/cct/bootstrap/ \ + && cp conformancelib/src/main/resources/pdval.properties /opt/cct/bootstrap/ \ + && cp -R conformancelib/src/main/resources/x509-certs /opt/cct/bootstrap/ \ + && cp conformancelib/testdata/PIV*Cards.db /opt/cct/bootstrap/ \ + && cp tools/85b-swing-gui/src/main/resources/build.version /opt/cct/bootstrap/ + +FROM eclipse-temurin:11-jre-jammy AS runtime + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + fontconfig libpcsclite1 libxext6 libxi6 libxrender1 libxtst6 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /opt/cct /opt/cct +COPY docker/entrypoint.sh /usr/local/bin/cct-entrypoint +RUN chmod 0755 /usr/local/bin/cct-entrypoint \ + && mkdir -p /data \ + && chmod 0777 /data + +VOLUME ["/data"] +WORKDIR /data +ENTRYPOINT ["cct-entrypoint"] diff --git a/cardlib/build.gradle b/cardlib/build.gradle index 88761c32..a295487d 100644 --- a/cardlib/build.gradle +++ b/cardlib/build.gradle @@ -54,6 +54,16 @@ def getVersion = { -> String version = versionFile.text.replaceAll("[\\n\\r\\t ]", "") } +def gitValue = { List command, String fallback -> + try { + def process = command.execute(null, project.rootDir.parentFile) + String output = process.text.trim() + return process.exitValue() == 0 && !output.isEmpty() ? output : fallback + } catch (Exception ignored) { + return fallback + } +} + String killJavaPid() { try { RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean() @@ -207,9 +217,10 @@ compileJava { doFirst { version = getVersion() - def repositoryDir = project.rootDir.parentFile - def commitId = ['git', 'rev-parse', '--short', 'HEAD'].execute(null, repositoryDir).text.trim() - def commitDateText = ['git', 'show', '-s', '--format=%ct', 'HEAD'].execute(null, repositoryDir).text.trim() + def commitId = System.getenv('CCT_GIT_COMMIT') ?: + gitValue(['git', 'rev-parse', '--short', 'HEAD'], 'unknown') + def commitDateText = System.getenv('CCT_GIT_COMMIT_EPOCH') ?: + gitValue(['git', 'show', '-s', '--format=%ct', 'HEAD'], '0') def commitDate = new Date(Long.parseLong(commitDateText) * 1000L) def buildDate = new Date() File resourcesDir = new File(project.getProjectDir(), 'src/main/resources/') diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 00000000..7a427455 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -eu + +data_dir=${CCT_DATA_DIR:-/data} +mkdir -p "$data_dir" +cp -Rn /opt/cct/bootstrap/. "$data_dir/" +cd "$data_dir" + +exec java -jar /opt/cct/cct.jar "$@" From af2b6f8ec8bde98721c1116d383e77d1eac5ad5e Mon Sep 17 00:00:00 2001 From: Connor Ford Date: Tue, 25 Aug 2026 16:14:04 -0400 Subject: [PATCH 5/6] Narrow packaging and Docker changes --- .gitignore | 2 - cardlib/build.gradle | 15 +---- .../gui/CopyableErrorDialog.java | 59 ------------------- .../gui/GuiRunnerApplication.java | 3 - .../gui/GuiTestExecutionController.java | 41 +++---------- .../gui/PackageResultsAction.java | 4 +- .../gui/SimpleTestExecutionPanel.java | 8 ++- .../gui/ReviewPackageBuilderTest.java | 11 ---- 8 files changed, 17 insertions(+), 126 deletions(-) delete mode 100644 tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CopyableErrorDialog.java diff --git a/.gitignore b/.gitignore index 3162055a..cf31f697 100644 --- a/.gitignore +++ b/.gitignore @@ -116,8 +116,6 @@ build # Build artifacts fips201-card-conformance-tool-[0-9]*\.[0-9]*\.[0-9]*-beta-[0-9]*\.zip fips201-card-conformance-tool-[0-9]*\.[0-9]*\.[0-9]*-beta-[0-9]*/* -/fips201-card-conformance-tool-*/ -/fips201-card-conformance-tool-*.zip /cct-review-results-*.zip /cct-docker-data/ libs/* diff --git a/cardlib/build.gradle b/cardlib/build.gradle index a295487d..a4300c62 100644 --- a/cardlib/build.gradle +++ b/cardlib/build.gradle @@ -54,16 +54,6 @@ def getVersion = { -> String version = versionFile.text.replaceAll("[\\n\\r\\t ]", "") } -def gitValue = { List command, String fallback -> - try { - def process = command.execute(null, project.rootDir.parentFile) - String output = process.text.trim() - return process.exitValue() == 0 && !output.isEmpty() ? output : fallback - } catch (Exception ignored) { - return fallback - } -} - String killJavaPid() { try { RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean() @@ -217,10 +207,11 @@ compileJava { doFirst { version = getVersion() + def repositoryDir = project.rootDir.parentFile def commitId = System.getenv('CCT_GIT_COMMIT') ?: - gitValue(['git', 'rev-parse', '--short', 'HEAD'], 'unknown') + ['git', 'rev-parse', '--short', 'HEAD'].execute(null, repositoryDir).text.trim() def commitDateText = System.getenv('CCT_GIT_COMMIT_EPOCH') ?: - gitValue(['git', 'show', '-s', '--format=%ct', 'HEAD'], '0') + ['git', 'show', '-s', '--format=%ct', 'HEAD'].execute(null, repositoryDir).text.trim() def commitDate = new Date(Long.parseLong(commitDateText) * 1000L) def buildDate = new Date() File resourcesDir = new File(project.getProjectDir(), 'src/main/resources/') diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CopyableErrorDialog.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CopyableErrorDialog.java deleted file mode 100644 index c9460010..00000000 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CopyableErrorDialog.java +++ /dev/null @@ -1,59 +0,0 @@ -package gov.gsa.pivconformance.gui; - -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Toolkit; -import java.awt.datatransfer.StringSelection; - -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Displays bounded, selectable diagnostics without stretching across the screen. */ -final class CopyableErrorDialog { - private static final Logger s_logger = LoggerFactory.getLogger(CopyableErrorDialog.class); - - private CopyableErrorDialog() { } - - static void show(Component parent, String title, String summary, String detailsText) { - String details = detailsText == null || detailsText.trim().isEmpty() - ? "No additional details are available. See console.log for the full application log." - : detailsText; - JTextArea textArea = createDetails(details); - JScrollPane scrollPane = new JScrollPane(textArea); - scrollPane.setPreferredSize(new Dimension(640, 180)); - JPanel content = new JPanel(new BorderLayout(0, 8)); - content.add(new JLabel(summary), BorderLayout.NORTH); - content.add(scrollPane, BorderLayout.CENTER); - content.add(new JLabel("The full diagnostic is also available in console.log."), BorderLayout.SOUTH); - Object[] options = { "Copy Details", "Close" }; - int choice = JOptionPane.showOptionDialog(parent, content, title, JOptionPane.DEFAULT_OPTION, - JOptionPane.ERROR_MESSAGE, null, options, options[1]); - if (choice == 0) copy(details, parent); - } - - static JTextArea createDetails(String details) { - JTextArea textArea = new JTextArea(details, 8, 72); - textArea.setEditable(false); - textArea.setLineWrap(true); - textArea.setWrapStyleWord(true); - textArea.setCaretPosition(0); - return textArea; - } - - private static void copy(String details, Component parent) { - try { - Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(details), null); - } catch (Exception e) { - s_logger.error("Unable to copy error details", e); - JOptionPane.showMessageDialog(parent, "Unable to copy the details. See console.log instead.", - "Copy Failed", JOptionPane.ERROR_MESSAGE); - } - } -} diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerApplication.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerApplication.java index e31dc759..12ca7d0e 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerApplication.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerApplication.java @@ -95,9 +95,6 @@ public void actionPerformed(ActionEvent e) { JMenuItem mntmDisplayTestReport = new JMenuItem(c.getDisplayTestReportAction()); mnView.add(mntmDisplayTestReport); - - JMenuItem mntmPackageResults = new JMenuItem(c.getPackageResultsAction()); - mnView.add(mntmPackageResults); JMenu mnHelp = new JMenu("Help"); menuBar.add(mnHelp); diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java index 72634b5f..8152b264 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java @@ -108,25 +108,6 @@ public void setLoggerContext(LoggerContext ctx) { m_ctx = ctx; } - void runAllTestsSafely(GuiTestCaseTreeNode root) { - try { - runAllTests(root); - } catch (RuntimeException e) { - s_logger.error("The test run ended unexpectedly", e); - m_running = false; - SwingUtilities.invokeLater(() -> { - GuiRunnerAppController controller = GuiRunnerAppController.getInstance(); - controller.getDisplayTestReportAction().setEnabled(true); - controller.getPackageResultsAction().setCompletedRun(null); - setDatabaseActionsEnabled(true); - m_testExecutionPanel.getRunButton().setEnabled(true); - m_testExecutionPanel.setPostRunActionsVisible(false); - CopyableErrorDialog.show(controller.getMainFrame(), "Test Run Error", - "The test run ended unexpectedly. No review package was prepared.", e.getMessage()); - }); - } - } - void runAllTests(GuiTestCaseTreeNode root) { ConformanceTestDatabase db = GuiRunnerAppController.getInstance().getTestDatabase(); if(db == null || db.getConnection() == null) { @@ -161,7 +142,8 @@ void runAllTests(GuiTestCaseTreeNode root) { packageResults.setCompletedRun(null); m_testExecutionPanel.setPostRunActionsVisible(false); m_testExecutionPanel.getRunButton().setEnabled(false); - setDatabaseActionsEnabled(false); + // TODO: Fix this or else + m_toolBar.getComponents()[0].setEnabled(false); progress.setMaximum(db.getTestCaseCount()); progress.setValue(0); progress.setVisible(true); @@ -291,7 +273,8 @@ void runAllTests(GuiTestCaseTreeNode root) { try { SwingUtilities.invokeAndWait(() -> { m_testExecutionPanel.getRunButton().setEnabled(true); - setDatabaseActionsEnabled(true); + // TODO: Fix this or else + m_toolBar.getComponents()[0].setEnabled(true); }); } catch (InvocationTargetException | InterruptedException e) { s_logger.error("Failed to enable run button", e); @@ -323,7 +306,9 @@ void runAllTests(GuiTestCaseTreeNode root) { s_logger.error("The completed run could not be prepared for review packaging", e); SwingUtilities.invokeLater(() -> { display.setEnabled(true); - showCompletionError(e.getMessage()); + JOptionPane.showMessageDialog(GuiRunnerAppController.getInstance().getMainFrame(), + "The test run finished, but its results could not be packaged.\n" + e.getMessage(), + "Run Finished", JOptionPane.ERROR_MESSAGE); }); } } @@ -333,18 +318,6 @@ private Path selectedDatabasePath(ConformanceTestDatabase db) { return databasePath == null ? null : databasePath.toAbsolutePath().normalize(); } - private void setDatabaseActionsEnabled(boolean enabled) { - GuiRunnerAppController controller = GuiRunnerAppController.getInstance(); - controller.getOpenDatabaseAction().setEnabled(enabled); - controller.getOpenDefaultPIVDatabaseAction().setEnabled(enabled); - controller.getOpenDefaultPIVIDatabaseAction().setEnabled(enabled); - } - - private void showCompletionError(String detail) { - CopyableErrorDialog.show(GuiRunnerAppController.getInstance().getMainFrame(), "Run Finished", - "The test run finished, but its results could not be prepared for packaging.", detail); - } - private void registerListeners(Launcher l, List listeners) { for(TestExecutionListener listener: listeners) { l.registerTestExecutionListeners(listener); diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java index 42cd1839..51b0d58d 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java @@ -182,7 +182,7 @@ private static String humanSize(long bytes) { } private void showError(String message) { - CopyableErrorDialog.show(GuiRunnerAppController.getInstance().getMainFrame(), "Package Results Error", - "The review package could not be created.", message); + JOptionPane.showMessageDialog(GuiRunnerAppController.getInstance().getMainFrame(), message, + "Package Results Error", JOptionPane.ERROR_MESSAGE); } } diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/SimpleTestExecutionPanel.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/SimpleTestExecutionPanel.java index 6dde303d..f185c755 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/SimpleTestExecutionPanel.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/SimpleTestExecutionPanel.java @@ -192,9 +192,11 @@ public void actionPerformed(ActionEvent e) { dialog.setVisible(true); return; } - GuiTestExecutionController tc = GuiTestExecutionController.getInstance(); - GuiTestCaseTreeNode root = GuiRunnerAppController.getInstance().getApp().getTreePanel().getRootNode(); - new Thread(() -> tc.runAllTestsSafely(root), "cct-test-run").start(); + GuiTestExecutionController tc = GuiTestExecutionController.getInstance(); + GuiTestCaseTreeNode root = GuiRunnerAppController.getInstance().getApp().getTreePanel().getRootNode(); + new Thread(() -> { + tc.runAllTests(root); + }).start(); } }); diff --git a/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java b/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java index a62b6a01..02d7bd3f 100644 --- a/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java +++ b/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java @@ -19,8 +19,6 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -import javax.swing.JTextArea; - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; @@ -85,15 +83,6 @@ void preservesLegacyEvidenceBytesUnchanged() throws Exception { Arrays.toString(zipEntry(result.getPath(), "logs/apdu/" + apdu.getFileName()))); } - @Test - void errorDetailsAreWrappedSelectableText() { - JTextArea details = CopyableErrorDialog.createDetails("A very long diagnostic path"); - assertFalse(details.isEditable()); - assertTrue(details.getLineWrap()); - assertTrue(details.getWrapStyleWord()); - assertEquals("A very long diagnostic path", details.getText()); - } - @Test void packageActionTracksCompletedRunAvailability() throws Exception { PackageResultsAction action = new PackageResultsAction("Package", null, "Package results"); From e8ac1fb97a0557643a5c64393cc11a59fcdc1747 Mon Sep 17 00:00:00 2001 From: Connor Ford Date: Tue, 25 Aug 2026 16:20:05 -0400 Subject: [PATCH 6/6] Focus PR on Docker and result packaging --- .dockerignore | 3 +- .gitignore | 2 +- DOCKER.md | 2 +- Dockerfile | 7 +- docker/entrypoint.sh | 6 +- .../pivconformance/gui/CompletedTestRun.java | 6 +- .../gui/GuiRunnerAppController.java | 4 +- .../gui/GuiTestExecutionController.java | 3 +- .../gui/PackageResultsAction.java | 125 ++--------- .../gsa/pivconformance/gui/ReviewPackage.java | 20 -- .../gui/ReviewPackageBuilder.java | 212 +++--------------- .../gui/ReviewPackageBuilderTest.java | 55 ++--- 12 files changed, 80 insertions(+), 365 deletions(-) delete mode 100644 tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackage.java diff --git a/.dockerignore b/.dockerignore index 1ecce0b0..ee3ca4bc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +1,9 @@ .git -.gradle **/.gradle **/build libs fips201-card-conformance-tool-* -cct-review-results-*.zip +cct-results-*.zip cct-docker-data *.log .DS_Store diff --git a/.gitignore b/.gitignore index cf31f697..dc6e20b7 100644 --- a/.gitignore +++ b/.gitignore @@ -116,6 +116,6 @@ build # Build artifacts fips201-card-conformance-tool-[0-9]*\.[0-9]*\.[0-9]*-beta-[0-9]*\.zip fips201-card-conformance-tool-[0-9]*\.[0-9]*\.[0-9]*-beta-[0-9]*/* -/cct-review-results-*.zip +/cct-results-*.zip /cct-docker-data/ libs/* diff --git a/DOCKER.md b/DOCKER.md index e4fd7019..fd0c2537 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -22,4 +22,4 @@ docker run --rm \ piv-conformance ``` -The container seeds `/data` with the required databases and configuration on first launch. Logs, artifacts, and generated review ZIPs remain in the mounted `cct-docker-data` directory. +The container seeds `/data` with the required databases and configuration on first launch. Logs, artifacts, and generated result ZIPs remain in the mounted `cct-docker-data` directory. diff --git a/Dockerfile b/Dockerfile index 9939e121..c0c9f944 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,7 @@ FROM eclipse-temurin:11-jdk-jammy AS build -ARG CCT_GIT_COMMIT=container +ARG CCT_GIT_COMMIT=unknown ARG CCT_GIT_COMMIT_EPOCH=0 -ENV CCT_GIT_COMMIT=${CCT_GIT_COMMIT} \ - CCT_GIT_COMMIT_EPOCH=${CCT_GIT_COMMIT_EPOCH} WORKDIR /workspace COPY . . @@ -34,8 +32,7 @@ RUN apt-get update \ COPY --from=build /opt/cct /opt/cct COPY docker/entrypoint.sh /usr/local/bin/cct-entrypoint RUN chmod 0755 /usr/local/bin/cct-entrypoint \ - && mkdir -p /data \ - && chmod 0777 /data + && mkdir -p /data VOLUME ["/data"] WORKDIR /data diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 7a427455..a1ed037f 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,9 +1,5 @@ #!/bin/sh set -eu -data_dir=${CCT_DATA_DIR:-/data} -mkdir -p "$data_dir" -cp -Rn /opt/cct/bootstrap/. "$data_dir/" -cd "$data_dir" - +cp -Rn /opt/cct/bootstrap/. /data/ exec java -jar /opt/cct/cct.jar "$@" diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CompletedTestRun.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CompletedTestRun.java index 6ad47678..3c4e8b96 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CompletedTestRun.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/CompletedTestRun.java @@ -7,14 +7,11 @@ public final class CompletedTestRun { private final Path m_resultsDirectory; private final Path m_databasePath; - private final Path m_conformanceCsv; private final String m_timeStampPrefix; - public CompletedTestRun(Path resultsDirectory, Path databasePath, Path conformanceCsv, - String timeStampPrefix) { + public CompletedTestRun(Path resultsDirectory, Path databasePath, String timeStampPrefix) { m_resultsDirectory = Objects.requireNonNull(resultsDirectory, "resultsDirectory").toAbsolutePath().normalize(); m_databasePath = Objects.requireNonNull(databasePath, "databasePath").toAbsolutePath().normalize(); - m_conformanceCsv = Objects.requireNonNull(conformanceCsv, "conformanceCsv").toAbsolutePath().normalize(); m_timeStampPrefix = Objects.requireNonNull(timeStampPrefix, "timeStampPrefix"); if (m_timeStampPrefix.trim().isEmpty()) { throw new IllegalArgumentException("A completed run must have a timestamp prefix"); @@ -23,6 +20,5 @@ public CompletedTestRun(Path resultsDirectory, Path databasePath, Path conforman public Path getResultsDirectory() { return m_resultsDirectory; } public Path getDatabasePath() { return m_databasePath; } - public Path getConformanceCsv() { return m_conformanceCsv; } public String getTimeStampPrefix() { return m_timeStampPrefix; } } diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerAppController.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerAppController.java index a9c80043..1ac63614 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerAppController.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiRunnerAppController.java @@ -163,8 +163,8 @@ protected void createActions() { ImageIcon displayReportIcon = getActionIcon("html", "Display HTML report"); m_displayTestReportAction = new GuiDisplayTestReportAction("Display Test Report", displayReportIcon, "Display test report for current log"); ImageIcon packageIcon = getActionIcon("database_save", "Package Results"); - m_packageResultsAction = new PackageResultsAction("Package Results for Review Manager", packageIcon, - "Create a ZIP containing the latest completed run for Review Manager"); + m_packageResultsAction = new PackageResultsAction("Package Results", packageIcon, + "Create a ZIP containing the latest completed run"); ImageIcon savingIcon = getActionIcon("folder", "Saving"); ImageIcon pivIcon = getActionIcon("PIV", "Open"); m_openDefaultPIVDatabaseAction = new OpenDefaultPIVDatabaseAction("Open Default PIV Database", pivIcon, "Open Default PIV conformance test database"); diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java index 8152b264..d567a8f5 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/GuiTestExecutionController.java @@ -294,8 +294,7 @@ void runAllTests(GuiTestCaseTreeNode root) { try { String timeStamp = m_trlc.getTimeStamp(); Path resultsDirectory = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize(); - Path csv = ReviewPackageBuilder.findConformanceCsv(resultsDirectory, timeStamp); - CompletedTestRun completedRun = new CompletedTestRun(resultsDirectory, selectedDatabase, csv, timeStamp); + CompletedTestRun completedRun = new CompletedTestRun(resultsDirectory, selectedDatabase, timeStamp); SwingUtilities.invokeLater(() -> { display.setEnabled(true); packageResults.setCompletedRun(completedRun); diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java index 51b0d58d..3ea06edc 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/PackageResultsAction.java @@ -5,11 +5,7 @@ import java.awt.datatransfer.StringSelection; import java.awt.event.ActionEvent; import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; import javax.swing.AbstractAction; import javax.swing.Icon; @@ -19,23 +15,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** Creates a local Review Manager ZIP and offers safe handoff actions. */ +/** Creates a ZIP for the latest completed test run. */ public class PackageResultsAction extends AbstractAction { private static final long serialVersionUID = 1L; private static final Logger s_logger = LoggerFactory.getLogger(PackageResultsAction.class); - private static final String REVIEW_MANAGER_PROPERTY = "piv.reviewManager.url"; - private static final String REVIEW_MANAGER_ENVIRONMENT = "PIV_REVIEW_MANAGER_URL"; private final ReviewPackageBuilder m_builder; private CompletedTestRun m_completedRun; public PackageResultsAction(String name, Icon icon, String toolTip) { - this(name, icon, toolTip, new ReviewPackageBuilder()); - } - - PackageResultsAction(String name, Icon icon, String toolTip, ReviewPackageBuilder builder) { super(name, icon); putValue(SHORT_DESCRIPTION, toolTip); - m_builder = builder; + m_builder = new ReviewPackageBuilder(); setEnabled(false); } @@ -44,25 +34,21 @@ public void setCompletedRun(CompletedTestRun completedRun) { setEnabled(completedRun != null); } - public CompletedTestRun getCompletedRun() { - return m_completedRun; - } - @Override public void actionPerformed(ActionEvent event) { packageCompletedRun(); } void packageCompletedRun() { - final CompletedTestRun run = m_completedRun; + CompletedTestRun run = m_completedRun; if (run == null) { - showError("No successfully completed test run is available to package."); + showError("No completed test run is available to package."); return; } setEnabled(false); - new SwingWorker() { + new SwingWorker() { @Override - protected ReviewPackage doInBackground() throws Exception { + protected Path doInBackground() throws Exception { return m_builder.build(run); } @@ -70,49 +56,25 @@ protected ReviewPackage doInBackground() throws Exception { protected void done() { setEnabled(m_completedRun != null); try { - ReviewPackage reviewPackage = get(); - if (run == m_completedRun) { - showCompletion(reviewPackage); - } else { - s_logger.info("Review package created at {} after a newer test run started", - reviewPackage.getPath()); - } + Path packagePath = get(); + if (run == m_completedRun) showCompletion(packagePath); } catch (Exception e) { Throwable cause = e.getCause() == null ? e : e.getCause(); - s_logger.error("Unable to create Review Manager package", cause); - showError("The review package could not be created:\n" + cause.getMessage()); + s_logger.error("Unable to package test results", cause); + showError("The results could not be packaged:\n" + cause.getMessage()); } } }.execute(); } - private void showCompletion(ReviewPackage reviewPackage) { - URI reviewManager = configuredReviewManagerUri(); - List options = new ArrayList<>(); - options.add("Show in Folder"); - options.add("Copy Path"); - if (reviewManager != null) options.add("Open Review Manager"); - options.add("Close"); - - Path path = reviewPackage.getPath().toAbsolutePath().normalize(); - String message = "Review package created locally.\n\n" - + "File: " + path.getFileName() + "\n" - + "Path: " + path + "\n" - + "Size: " + humanSize(reviewPackage.getSize()) + " (" + reviewPackage.getSize() + " bytes)\n" - + "SHA-256: " + reviewPackage.getSha256() + "\n\n" - + "No files were uploaded. Select or drag this ZIP into Review Manager."; - int choice = JOptionPane.showOptionDialog(GuiRunnerAppController.getInstance().getMainFrame(), message, - "Package Results for Review Manager", JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE, - null, options.toArray(), options.get(options.size() - 1)); - if (choice < 0) return; - String selected = options.get(choice); - if ("Show in Folder".equals(selected)) { - showInFolder(path); - } else if ("Copy Path".equals(selected)) { - copyPath(path); - } else if ("Open Review Manager".equals(selected)) { - openReviewManager(reviewManager); - } + private void showCompletion(Path packagePath) { + Path path = packagePath.toAbsolutePath().normalize(); + Object[] options = { "Show in Folder", "Copy Path", "Close" }; + int choice = JOptionPane.showOptionDialog(GuiRunnerAppController.getInstance().getMainFrame(), + "Results packaged successfully.\n\n" + path, "Results Packaged", JOptionPane.DEFAULT_OPTION, + JOptionPane.INFORMATION_MESSAGE, null, options, options[2]); + if (choice == 0) showInFolder(path); + if (choice == 1) copyPath(path); } private void showInFolder(Path path) { @@ -127,58 +89,13 @@ private void showInFolder(Path path) { } private void copyPath(Path path) { - copyText(path.toString(), "Unable to copy the package path"); - } - - private void copyText(String value, String errorSummary) { try { Toolkit.getDefaultToolkit().getSystemClipboard() - .setContents(new StringSelection(value), null); - } catch (Exception e) { - s_logger.error(errorSummary, e); - JOptionPane.showMessageDialog(GuiRunnerAppController.getInstance().getMainFrame(), - errorSummary + ". See console.log for details.", "Copy Failed", JOptionPane.ERROR_MESSAGE); - } - } - - private void openReviewManager(URI uri) { - try { - if (uri == null) throw new IOException("No Review Manager URL is configured"); - if (!Desktop.isDesktopSupported() || !Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { - throw new IOException("Opening a browser is not supported on this system"); - } - Desktop.getDesktop().browse(uri); + .setContents(new StringSelection(path.toString()), null); } catch (Exception e) { - showError("Unable to open Review Manager:\n" + e.getMessage()); - } - } - - static URI configuredReviewManagerUri() { - String configured = System.getProperty(REVIEW_MANAGER_PROPERTY); - if (configured == null || configured.trim().isEmpty()) { - configured = System.getenv(REVIEW_MANAGER_ENVIRONMENT); + s_logger.error("Unable to copy the package path", e); + showError("Unable to copy the package path. See console.log for details."); } - if (configured == null || configured.trim().isEmpty()) return null; - try { - URI uri = new URI(configured.trim()); - if (!("http".equalsIgnoreCase(uri.getScheme()) || "https".equalsIgnoreCase(uri.getScheme())) - || uri.getHost() == null) { - s_logger.warn("Ignoring invalid Review Manager URL configured in {} or {}", - REVIEW_MANAGER_PROPERTY, REVIEW_MANAGER_ENVIRONMENT); - return null; - } - return uri; - } catch (URISyntaxException e) { - s_logger.warn("Ignoring malformed Review Manager URL", e); - return null; - } - } - - private static String humanSize(long bytes) { - if (bytes < 1024) return bytes + " B"; - double kib = bytes / 1024.0; - if (kib < 1024) return String.format("%.1f KiB", kib); - return String.format("%.1f MiB", kib / 1024.0); } private void showError(String message) { diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackage.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackage.java deleted file mode 100644 index 35e52a2e..00000000 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackage.java +++ /dev/null @@ -1,20 +0,0 @@ -package gov.gsa.pivconformance.gui; - -import java.nio.file.Path; - -/** Details shown to the operator after a local review package is created. */ -public final class ReviewPackage { - private final Path m_path; - private final long m_size; - private final String m_sha256; - - public ReviewPackage(Path path, long size, String sha256) { - m_path = path; - m_size = size; - m_sha256 = sha256; - } - - public Path getPath() { return m_path; } - public long getSize() { return m_size; } - public String getSha256() { return m_sha256; } -} diff --git a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackageBuilder.java b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackageBuilder.java index 5a4f97c3..b26d713a 100644 --- a/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackageBuilder.java +++ b/tools/85b-swing-gui/src/main/java/gov/gsa/pivconformance/gui/ReviewPackageBuilder.java @@ -1,55 +1,29 @@ package gov.gsa.pivconformance.gui; -import java.io.BufferedInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.time.Clock; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Set; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -/** Selects and packages evidence for exactly one completed CCT run. */ +/** Packages the files belonging to one completed CCT run. */ public class ReviewPackageBuilder { private static final DateTimeFormatter PACKAGE_TIME = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"); - private static final long DETERMINISTIC_ZIP_TIME = 315532800000L; // 1980-01-01, valid in ZIP files private static final String[] RUN_DIRECTORIES = { "logs", "piv-artifacts", "x509-artifacts" }; - private final Clock m_clock; - public ReviewPackageBuilder() { - this(Clock.systemDefaultZone()); - } - - ReviewPackageBuilder(Clock clock) { - m_clock = clock; - } - - public ReviewPackage build(CompletedTestRun run) throws IOException { + public Path build(CompletedTestRun run) throws IOException { if (run == null) throw new IllegalStateException("No completed test run is available to package"); Path resultsDirectory = requireDirectory(run.getResultsDirectory(), "Results directory"); - Path database = requireRegularFile(run.getDatabasePath(), "Selected test database"); - Path conformanceCsv = requireRegularFile(run.getConformanceCsv(), "Conformance CSV"); - Path discoveredCsv = findConformanceCsv(resultsDirectory, run.getTimeStampPrefix()); - if (!Files.isSameFile(conformanceCsv, discoveredCsv)) { - throw new IOException("The completed run's conformance CSV no longer matches its recorded result"); - } + Path database = requireFile(run.getDatabasePath(), "Selected test database"); List entries = new ArrayList<>(); String prefix = run.getTimeStampPrefix() + "-"; @@ -58,8 +32,8 @@ public ReviewPackage build(CompletedTestRun run) throws IOException { if ("logs".equals(directoryName) && !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { throw new IOException("The completed run's logs directory is unavailable: " + directory); } - if (Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { - collectRunFiles(resultsDirectory, directory, prefix, entries); + if (Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + collectFiles(resultsDirectory, directory, prefix, entries); } } @@ -67,96 +41,42 @@ public ReviewPackage build(CompletedTestRun run) throws IOException { .filter(entry -> entry.name.startsWith("logs/") && entry.name.toLowerCase().endsWith(".csv")) .count(); if (csvCount != 1) { - throw new IOException("Expected exactly one conformance CSV for completed run " - + run.getTimeStampPrefix() + ", but found " + csvCount); + throw new IOException("Expected exactly one conformance CSV for the completed run, but found " + csvCount); } Path trustDirectory = requireDirectory(resultsDirectory.resolve("x509-certs"), "Trust-path directory"); - int entriesBeforeTrustPath = entries.size(); - collectAllFiles(resultsDirectory, trustDirectory, entries); - if (entries.size() == entriesBeforeTrustPath) { + int entryCount = entries.size(); + collectFiles(resultsDirectory, trustDirectory, null, entries); + if (entries.size() == entryCount) { throw new IOException("The trust-path directory contains no files: " + trustDirectory); } entries.add(new SourceEntry(database, database.getFileName().toString())); - validateEntries(entries); + entries.sort(Comparator.comparing(entry -> entry.name)); - Collections.sort(entries, Comparator.comparing(entry -> entry.name)); Path target = uniqueTarget(resultsDirectory); - Path temporary = Files.createTempFile(resultsDirectory, ".cct-review-results-", ".tmp"); - long size; - String sha256; - try { - writeZip(temporary, entries); - size = Files.size(temporary); - sha256 = sha256(temporary); - moveIntoPlace(temporary, target); - } finally { - Files.deleteIfExists(temporary); - } - return new ReviewPackage(target, size, sha256); - } - - public static Path findConformanceCsv(Path resultsDirectory, String timeStampPrefix) throws IOException { - if (timeStampPrefix == null || timeStampPrefix.trim().isEmpty()) { - throw new IOException("The completed run has no timestamp prefix"); - } - Path logs = resultsDirectory.toAbsolutePath().normalize().resolve("logs"); - if (!Files.isDirectory(logs, LinkOption.NOFOLLOW_LINKS)) { - throw new IOException("Results logs directory is unavailable: " + logs); - } - List matches = new ArrayList<>(); - try (Stream paths = Files.walk(logs)) { - Iterator iterator = paths.iterator(); - while (iterator.hasNext()) { - Path path = iterator.next(); - if (Files.isSymbolicLink(path)) { - if (path.getFileName().toString().startsWith(timeStampPrefix + "-")) { - throw new IOException("Run evidence may not be a symbolic link: " + path); - } - continue; - } - if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) - && path.getFileName().toString().startsWith(timeStampPrefix + "-") - && path.getFileName().toString().toLowerCase().endsWith(".csv")) { - matches.add(path.toAbsolutePath().normalize()); - } - } - } - if (matches.size() != 1) { - throw new IOException("Expected exactly one conformance CSV for the completed run, but found " + matches.size()); - } - return matches.get(0); - } - - private void collectRunFiles(Path base, Path directory, String prefix, List entries) throws IOException { - try (Stream paths = Files.walk(directory)) { - Iterator iterator = paths.iterator(); - while (iterator.hasNext()) { - Path path = iterator.next(); - if (Files.isSymbolicLink(path)) { - if (path.getFileName().toString().startsWith(prefix)) { - throw new IOException("Run evidence may not be a symbolic link: " + path); - } - continue; - } - if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) - && path.getFileName().toString().startsWith(prefix)) { - entries.add(new SourceEntry(path, entryName(base, path))); - } + try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(target, StandardOpenOption.CREATE_NEW))) { + for (SourceEntry source : entries) { + zip.putNextEntry(new ZipEntry(source.name)); + Files.copy(source.path, zip); + zip.closeEntry(); } + } catch (IOException e) { + Files.deleteIfExists(target); + throw e; } + return target; } - private void collectAllFiles(Path base, Path directory, List entries) throws IOException { + private static void collectFiles(Path base, Path directory, String prefix, List entries) + throws IOException { try (Stream paths = Files.walk(directory)) { Iterator iterator = paths.iterator(); while (iterator.hasNext()) { - Path path = iterator.next(); - if (Files.isSymbolicLink(path)) { - throw new IOException("Trust-path material may not be a symbolic link: " + path); - } - if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { - entries.add(new SourceEntry(path, entryName(base, path))); + Path file = iterator.next(); + if (Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS) + && (prefix == null || file.getFileName().toString().startsWith(prefix))) { + String name = base.relativize(file).toString().replace('\\', '/'); + entries.add(new SourceEntry(file, name)); } } } @@ -164,96 +84,28 @@ private void collectAllFiles(Path base, Path directory, List entrie private static Path requireDirectory(Path path, String description) throws IOException { Path normalized = path.toAbsolutePath().normalize(); - if (Files.isSymbolicLink(normalized) || !Files.isDirectory(normalized, LinkOption.NOFOLLOW_LINKS)) { + if (!Files.isDirectory(normalized, LinkOption.NOFOLLOW_LINKS)) { throw new IOException(description + " is unavailable: " + normalized); } return normalized; } - private static Path requireRegularFile(Path path, String description) throws IOException { + private static Path requireFile(Path path, String description) throws IOException { Path normalized = path.toAbsolutePath().normalize(); - if (Files.isSymbolicLink(normalized) || !Files.isRegularFile(normalized, LinkOption.NOFOLLOW_LINKS) - || !Files.isReadable(normalized)) { + if (!Files.isRegularFile(normalized, LinkOption.NOFOLLOW_LINKS) || !Files.isReadable(normalized)) { throw new IOException(description + " is unavailable: " + normalized); } return normalized; } - private static String entryName(Path base, Path file) throws IOException { - Path normalizedBase = base.toAbsolutePath().normalize(); - Path normalizedFile = file.toAbsolutePath().normalize(); - if (!normalizedFile.startsWith(normalizedBase)) { - throw new IOException("Package input is outside the results directory: " + file); - } - return normalizedBase.relativize(normalizedFile).toString().replace('\\', '/'); - } - - private static void validateEntries(List entries) throws IOException { - Set names = new HashSet<>(); - for (SourceEntry entry : entries) { - Path normalized = Path.of(entry.name).normalize(); - if (normalized.isAbsolute() || entry.name.startsWith("../") || entry.name.contains("/../") - || !names.add(entry.name)) { - throw new IOException("Unsafe or duplicate ZIP entry: " + entry.name); - } - } - } - - private Path uniqueTarget(Path directory) { - String baseName = "cct-review-results-" + PACKAGE_TIME.format(LocalDateTime.now(m_clock)); + private static Path uniqueTarget(Path directory) { + String baseName = "cct-results-" + PACKAGE_TIME.format(LocalDateTime.now()); Path candidate = directory.resolve(baseName + ".zip"); int suffix = 2; - while (Files.exists(candidate, LinkOption.NOFOLLOW_LINKS)) { - candidate = directory.resolve(baseName + "-" + suffix++ + ".zip"); - } + while (Files.exists(candidate)) candidate = directory.resolve(baseName + "-" + suffix++ + ".zip"); return candidate; } - private static void writeZip(Path output, List entries) throws IOException { - try (OutputStream fileOutput = Files.newOutputStream(output, StandardOpenOption.TRUNCATE_EXISTING); - ZipOutputStream zip = new ZipOutputStream(fileOutput)) { - byte[] buffer = new byte[16 * 1024]; - for (SourceEntry source : entries) { - ZipEntry entry = new ZipEntry(source.name); - entry.setTime(DETERMINISTIC_ZIP_TIME); - zip.putNextEntry(entry); - try (InputStream input = new BufferedInputStream(Files.newInputStream(source.path))) { - int read; - while ((read = input.read(buffer)) >= 0) { - if (read > 0) zip.write(buffer, 0, read); - } - } - zip.closeEntry(); - } - } - } - - private static void moveIntoPlace(Path source, Path target) throws IOException { - try { - Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); - } catch (AtomicMoveNotSupportedException e) { - Files.move(source, target); - } - } - - private static String sha256(Path path) throws IOException { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - try (InputStream input = new BufferedInputStream(Files.newInputStream(path))) { - byte[] buffer = new byte[16 * 1024]; - int read; - while ((read = input.read(buffer)) >= 0) { - if (read > 0) digest.update(buffer, 0, read); - } - } - StringBuilder result = new StringBuilder(); - for (byte value : digest.digest()) result.append(String.format("%02x", value & 0xff)); - return result.toString(); - } catch (NoSuchAlgorithmException e) { - throw new IOException("SHA-256 is unavailable", e); - } - } - private static final class SourceEntry { private final Path path; private final String name; diff --git a/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java b/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java index 02d7bd3f..7c1ff213 100644 --- a/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java +++ b/tools/85b-swing-gui/src/test/java/gov/gsa/pivconformance/gui/ReviewPackageBuilderTest.java @@ -9,9 +9,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Clock; -import java.time.Instant; -import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; @@ -28,7 +25,6 @@ class ReviewPackageBuilderTest { private static final String PREFIX = "card-identifier_20260819_010203-20260819_020304"; - private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-08-19T12:34:56Z"), ZoneOffset.UTC); @TempDir Path tempDirectory; @@ -37,7 +33,7 @@ class ReviewPackageBuilderTest { @ValueSource(strings = { "PIV_Production_Cards.db", "PIV-I_Production_Cards.db" }) void packagesExactlyOneCompletedRunAndSelectedDatabase(String databaseName) throws Exception { Path database = createEvidence(databaseName, "database"); - Path csv = write("logs/conformancelog/" + PREFIX + "-conformance_results.csv", + write("logs/conformancelog/" + PREFIX + "-conformance_results.csv", "Date,Test Id,Description,Expected Result,Actual Result\n" + "2026-08-19 01:02:04,1,one,Pass,Pass\n" + "2026-08-19 01:02:05,2,two,Pass,Fail\n"); @@ -51,14 +47,12 @@ void packagesExactlyOneCompletedRunAndSelectedDatabase(String databaseName) thro write("x509-certs/valid/policy.xml", "policy"); write("unused.db", "unused database"); write("tool.jar", "executable"); - write("cct-review-results-20200101-000000.zip", "old package"); + write("cct-results-20200101-000000.zip", "old package"); - CompletedTestRun run = completedRun(database, csv); - ReviewPackage result = new ReviewPackageBuilder(FIXED_CLOCK).build(run); + Path result = new ReviewPackageBuilder().build(completedRun(database)); - assertEquals("cct-review-results-20260819-123456.zip", result.getPath().getFileName().toString()); - assertEquals(64, result.getSha256().length()); - assertTrue(result.getSize() > 0); + assertTrue(result.getFileName().toString().startsWith("cct-results-")); + assertTrue(result.getFileName().toString().endsWith(".zip")); assertEquals(Arrays.asList( databaseName, "logs/apdu/" + PREFIX + "-apdu_transmission.log", @@ -66,21 +60,19 @@ void packagesExactlyOneCompletedRunAndSelectedDatabase(String databaseName) thro "piv-artifacts/" + PREFIX + "-chuid.bin", "x509-artifacts/" + PREFIX + "-authentication.crt", "x509-certs/cacerts.jks", - "x509-certs/valid/policy.xml"), zipEntries(result.getPath())); - assertFalse(result.getPath().getFileName().toString().contains("card-identifier")); + "x509-certs/valid/policy.xml"), zipEntries(result)); + assertFalse(result.getFileName().toString().contains("card-identifier")); } @Test void preservesLegacyEvidenceBytesUnchanged() throws Exception { Path database = createEvidence("PIV_ICAM_Test_Cards.db", "database"); - Path csv = basicCsv(); + basicCsv(); byte[] evidence = new byte[] { 0x00, 0x31, 0x32, 0x33, 0x34, (byte) 0xff, 0x0a }; Path apdu = writeBytes("logs/apdu/" + PREFIX + "-apdu_transmission.log", evidence); - CompletedTestRun run = completedRun(database, csv); - - ReviewPackage result = new ReviewPackageBuilder(FIXED_CLOCK).build(run); + Path result = new ReviewPackageBuilder().build(completedRun(database)); assertEquals(Arrays.toString(Files.readAllBytes(apdu)), - Arrays.toString(zipEntry(result.getPath(), "logs/apdu/" + apdu.getFileName()))); + Arrays.toString(zipEntry(result, "logs/apdu/" + apdu.getFileName()))); } @Test @@ -89,8 +81,8 @@ void packageActionTracksCompletedRunAvailability() throws Exception { assertFalse(action.isEnabled()); Path database = createEvidence("PIV_Production_Cards.db", "database"); - Path csv = basicCsv(); - action.setCompletedRun(completedRun(database, csv)); + basicCsv(); + action.setCompletedRun(completedRun(database)); assertTrue(action.isEnabled()); action.setCompletedRun(null); @@ -115,30 +107,17 @@ void retainsCanonicalSelectedDatabasePathWithoutJdbcClientInfo() throws Exceptio @Test void rejectsMissingCompletedRunAndAmbiguousCsv() throws Exception { - ReviewPackageBuilder builder = new ReviewPackageBuilder(FIXED_CLOCK); + ReviewPackageBuilder builder = new ReviewPackageBuilder(); assertThrows(IllegalStateException.class, () -> builder.build(null)); Path database = createEvidence("PIV_Production_Cards.db", "database"); - Path csv = basicCsv(); + basicCsv(); write("logs/other/" + PREFIX + "-second.csv", "Date,Actual Result\nnow,Pass\n"); - CompletedTestRun run = completedRun(database, csv); + CompletedTestRun run = completedRun(database); IOException error = assertThrows(IOException.class, () -> builder.build(run)); assertTrue(error.getMessage().contains("exactly one conformance CSV")); } - @Test - void producesDeterministicZipContent() throws Exception { - Path database = createEvidence("PIV_Production_Cards.db", "database"); - Path csv = basicCsv(); - CompletedTestRun run = completedRun(database, csv); - ReviewPackageBuilder builder = new ReviewPackageBuilder(FIXED_CLOCK); - - ReviewPackage first = builder.build(run); - ReviewPackage second = builder.build(run); - assertEquals(first.getSha256(), second.getSha256()); - assertEquals("cct-review-results-20260819-123456-2.zip", second.getPath().getFileName().toString()); - } - private Path basicCsv() throws IOException { createEvidence("x509-certs/cacerts.jks", "trust"); return write("logs/conformancelog/" + PREFIX + "-conformance_results.csv", @@ -146,8 +125,8 @@ private Path basicCsv() throws IOException { + "now,1,one,Pass,Pass\nnow,2,two,Pass,Fail\n"); } - private CompletedTestRun completedRun(Path database, Path csv) { - return new CompletedTestRun(tempDirectory, database, csv, PREFIX); + private CompletedTestRun completedRun(Path database) { + return new CompletedTestRun(tempDirectory, database, PREFIX); } private Path createEvidence(String relative, String contents) throws IOException {