/* * Constellation - An open source and standard compliant SDI * http://www.constellation-sdi.org * * Copyright 2014 Geomatys. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.constellation.sql; import org.apache.sis.util.logging.Logging; import org.geotoolkit.internal.sql.DefaultDataSource; import org.geotoolkit.io.X364; import org.geotoolkit.util.FileUtilities; import java.io.File; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.constellation.test.utils.TestDatabaseHandler; // Geotoolkit dependencies /** * <p>Allow to store results of a {@code Cite tests} session into a database. * A comparison between the current and the previous session can be performed, * and a summary is then displayed.</p> * <p>If some tests are now failing but passed in the previous session, then the * compilation will fail and the user will be informed about the tests that * are now failing. The complete error logs can be retrieved in the directory * specified.</p> * * @version $Id$ * @author Cédric Briançon (Geomatys) * * @since 0.4 */ public class ResultsDatabase { private static final Logger LOGGER = Logging.getLogger("org.constellation.sql"); /** * The pattern for the ouput of a date. */ public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); /** * Insertion requests. */ private static final String INSERT_RESULT = "INSERT INTO \"Results\" VALUES (?,?,?,?,?);"; private static final String INSERT_SERVICE = "INSERT INTO \"Services\" VALUES (?,?);"; private static final String INSERT_SUITE = "INSERT INTO \"Suites\" VALUES (?,?,?,?);"; private static final String INSERT_DESCRIPTION = "INSERT INTO \"TestsDescriptions\" VALUES (?,?);"; /** * Count requests. */ private static final String COUNT_SERVICE = "SELECT COUNT(name) FROM \"Services\" "+ "WHERE name=? AND version=?;"; private static final String COUNT_SUITE = "SELECT COUNT(date) FROM \"Suites\" "+ "WHERE date=?;"; /** * Select requests. */ private static final String SELECT_TESTS_FAILED = "SELECT date,id,directory,groupnode FROM \"Results\" WHERE date=? AND passed=FALSE;"; private static final String SELECT_TESTS_PASSED = "SELECT date,id,directory,groupnode FROM \"Results\" WHERE date=? AND passed=TRUE;"; private static final String SELECT_TESTS_DESC = "SELECT assertion FROM \"TestsDescriptions\" WHERE id=?;"; private static final String SELECT_PREVIOUS_SUITE = "SELECT date FROM \"Suites\" WHERE lastsuccess='TRUE' AND service=? AND version=?;"; private static final String SELECT_TEST_RESULT_FROM_ID = "SELECT date,r.id,directory,passed,assertion,groupnode FROM \"Results\" r, \"TestsDescriptions\" t " + "WHERE date=? and r.id=? and r.id=t.id;"; /** * Delete requests. */ private static final String DELETE_SUITE = "DELETE FROM \"Suites\" WHERE date=?;"; /** * Update requests. */ private static final String UPDATE_SUITE_SUCCESS = "UPDATE \"Suites\" set lastsuccess='TRUE' WHERE date=?;"; private static final String UPDATE_SUITE_REPLACED = "UPDATE \"Suites\" set lastsuccess='FALSE' WHERE service=? and version =?;"; /** * The connection to the database of Cite Tests results. */ protected final Connection connection; /** * List of existing services, already checked from the database as present. */ private final List<Service> existingServices = new ArrayList<>(); /** * List of existing suites, already checked from the database as present. */ private final List<Suite> existingSuites = new ArrayList<>(); /** * Stores the ids for a session, and their number of occurrences. */ private final Map<String,Integer> ids = new HashMap<>(); /** * Initialize the connection to the database. * * @throws SQLException if an exception occurs while trying to connect to the database. */ public ResultsDatabase() throws SQLException { final String DB_NAME = TestDatabaseHandler.testProperties.getProperty("results_db_name"); final String PROTOCOL = TestDatabaseHandler.testProperties.getProperty("results_db_protocol"); final String HOST = TestDatabaseHandler.testProperties.getProperty("results_db_host"); final String USER = TestDatabaseHandler.testProperties.getProperty("results_db_user"); final String PASS = TestDatabaseHandler.testProperties.getProperty("results_db_pass"); final DefaultDataSource ds = new DefaultDataSource(PROTOCOL +"://"+ HOST +"/"+ DB_NAME); connection = ds.getConnection(USER, PASS); } /** * Compare the results between the current session and the previous one. * * @param date The execution date of the tests. * @param service The service name. * @param version The service version. * @return {@code True} if there is no test that fails for this session and succeed * for the previous one. {@code False} if there is one or more new problems. * @throws SQLException */ public boolean compareResults(final Date date, final String service, final String version) throws SQLException { // Contains all tests that have failed for the current session. final List<Result> currentTestsFailed = getTestsFailed(date); final List<Result> currentTestsPassed = getTestsPassed(date); final int nbCurrentTest = currentTestsFailed.size() + currentTestsPassed.size(); final Date previousSuite = getPreviousSuiteDate(service, version); // Contains all tests that have failed for the previous session. final List<Result> previousTestsFailed; // Contains all tests that have Passed for the previous session. final List<Result> previousTestsPassed; // Will contain the tests that have passed for the last session, but not for the current one. final List<Result> problematicTests = new ArrayList<>(); // Will contain the groupNode tests that have passed for the last session, but not for the current one (not problematic). final List<Result> problematicGroupNodeTests = new ArrayList<>(); // Will contain the tests that have passed for the current session, but not for the last one. final List<Result> newlyPassedTests = new ArrayList<>(); // Will contain the tests that have been activated for the current session. final List<Result> newlyActivatedTests = new ArrayList<>(); // Will contain the tests that have been deactivated for the current session. final List<Result> disapearTests = new ArrayList<>(); final boolean missingTest; if (previousSuite != null) { previousTestsFailed = getTestsFailed(previousSuite); previousTestsPassed = getTestsPassed(previousSuite); for (Result currentTest : currentTestsFailed) { // add to the problematic list if a test fail and was passed in the last session if (!isTestPresentInList(currentTest, previousTestsFailed)) { if (isTestPresentInList(currentTest, previousTestsPassed)) { if (currentTest.isGroupNode()) { problematicGroupNodeTests.add(currentTest); } else { problematicTests.add(currentTest); } } else { newlyActivatedTests.add(currentTest); } } } for (Result currentTest : currentTestsPassed) { // add to the newlyPassed list if a test pass and was failed in the last session if (!isTestPresentInList(currentTest, previousTestsPassed)) { if (isTestPresentInList(currentTest, previousTestsFailed)) { newlyPassedTests.add(currentTest); } else { newlyActivatedTests.add(currentTest); } } } int nbPreviousTest = previousTestsFailed.size() + previousTestsPassed.size(); missingTest = nbCurrentTest < nbPreviousTest; //if some test have been deactivated we list them if (missingTest) { previousTestsPassed.addAll(previousTestsFailed); for (Result previousTest: previousTestsPassed) { if (!isTestPresentInList(previousTest, currentTestsFailed) && !isTestPresentInList(previousTest, currentTestsPassed)) { disapearTests.add(previousTest); } } } } else { missingTest = false; } displayComparisonResults(problematicTests, problematicGroupNodeTests, newlyPassedTests, newlyActivatedTests, disapearTests, date, previousSuite, service, version); return problematicTests.isEmpty() && !missingTest; } /** * Insert a result into the matching table. * * @param result A result to insert. * @param service The service name. * @param version The service version. * @return * @throws SQLException if an error occurs in the insert request. */ public int insertResult(final Result result, final String service, final String version) throws SQLException { return insertResult(service, version, result.getId(), result.getDirectory(), result.isPassed(), result.isGroupNode(), result.getDate()); } /** * Insert a result into the matching table. * * @param service The service name. * @param version The service version. * @param id The identifier of the test. * @param directory The directory where the logs are stored. * @param passed Defined whether the test has passed or not. * @param date The date when the test has been executed. * @return * @throws SQLException if an error occurs in the insert request. */ public int insertResult(final String service, final String version, final String id, final String directory, final boolean passed, final boolean groupNode, final Date date) throws SQLException { ensureConnectionOpened(); // try to add the service. addService(service, version); // add the test suite. addSuite(date, service, version); // Verify that the name is not of the test id is not already defined. // If already defined, we use the id name concatenated with the index number, // and we increment the count of this id into the hashmap. final String finalId; final Integer numberId = ids.get(id); if (numberId == null) { ids.put(id, 0); finalId = id; } else { ids.put(id, numberId + 1); finalId = id + (numberId + 1); } final PreparedStatement ps = connection.prepareStatement(INSERT_RESULT); ps.setTimestamp(1, new Timestamp(date.getTime())); ps.setString(2, finalId); ps.setBoolean(3, passed); ps.setString(4, directory); ps.setBoolean(5, groupNode); final int result = ps.executeUpdate(); ps.close(); return result; } /** * Delete the specified suite of tests. * * @param date The date of the suite to delete. * @throws SQLException if an error occurs in the delete request. */ @Deprecated public void deleteSuite(final Date date) throws SQLException { ensureConnectionOpened(); final PreparedStatement ps = connection.prepareStatement(DELETE_SUITE); ps.setTimestamp(1, new Timestamp(date.getTime())); ps.execute(); ps.close(); } /** * Set the flag lastsuccess of the previous suite to false, and set the flag of the last suite * identified by the specified date to true. * * @param date The date of the suite to update. * @param service The service name. * @param version The service version. * @throws SQLException if an error occurs in the update request. */ public void setSuiteLastSuccess(final Date date, final String service, final String version) throws SQLException { ensureConnectionOpened(); PreparedStatement ps = connection.prepareStatement(UPDATE_SUITE_REPLACED); ps.setString(1, service); ps.setString(2, version); ps.execute(); ps = connection.prepareStatement(UPDATE_SUITE_SUCCESS); ps.setTimestamp(1, new Timestamp(date.getTime())); ps.execute(); ps.close(); } /** * Close the connection to the database * * @throws SQLException if an error occurs when closing the connection. */ public void close() throws SQLException { if (connection != null && !connection.isClosed()) { connection.close(); } } /** * Add a service if it is not already present into the database. * * @param service The service name. * @param version The service version. * @throws SQLException */ private void addService(final String service, final String version) throws SQLException { if (!serviceExists(service, version)) { insertService(service, version); } } /** * Add a suite if it is not already present into the database. * * @param date The date of the suite. * @param service The service name. * @param version The service version. * @throws SQLException */ private void addSuite(final Date date, final String service, final String version) throws SQLException { if (!suiteExists(date, service, version)) { insertSuite(date, service, version); } } /** * Display the results for the current session, compared to the last one. * * @param problematicTests List of problematic tests, that no more passed. * @param problematicGroupTests List of problematic tests, that no more passed. * @param newlyPassedTests List of tests that are corrected in the current session. * @param newlyActivatedTests List of tests that appears in the current session. * @param deActivatedTests List of tests that disapears in the current session. * @param date The date of the suite of tests. * @param previous The date of the previous suite of tests. Note that if it is * {@code null}, we can't compare the current session with * another one. * @param service The service name. * @param version The service version. */ private void displayComparisonResults(final List<Result> problematicTests, final List<Result> problematicGroupTests, final List<Result> newlyPassedTests, final List<Result> newlyActivatedTests, final List<Result> deActivatedTests, final Date date, final Date previous, final String service, final String version) { final char endOfLine = '\n'; final char tab = '\t'; final StringBuilder sb = new StringBuilder(); final boolean x364 = X364.isSupported(); if (previous == null) { if (x364) {sb.append(X364.FOREGROUND_BLUE.sequence());} if (x364) {sb.append(X364.BOLD.sequence());} sb.append("This is the first session of tests launched for "); sb.append(service).append(" ").append(version) .append(". We can't compare the results with a previous one."); if (x364) {sb.append(X364.RESET.sequence());} LOGGER.info(sb.toString()); return; } if (x364) {sb.append(X364.FOREGROUND_MAGENTA.sequence());} if (x364) {sb.append(X364.BOLD.sequence());} sb.append("Results for the session "); sb.append(service).append(" ").append(version).append(", executed at ").append(DATE_FORMAT.format(date)); sb.append(" compared to the one at ").append(previous.toString()).append(endOfLine); if (x364) {sb.append(X364.RESET.sequence());} if (x364) {sb.append(X364.FOREGROUND_GREEN.sequence());} if (newlyPassedTests.isEmpty()) { sb.append(tab).append("No tests have been corrected in the current session.").append(endOfLine); } else { sb.append(tab).append("Tests which have been corrected in the current session:").append(endOfLine); for (Result res : newlyPassedTests) { sb.append(tab).append(tab).append(res.toString()).append(endOfLine); } } if (x364) {sb.append(X364.FOREGROUND_DEFAULT.sequence());} if (problematicTests.isEmpty()) { if (x364) {sb.append(X364.FOREGROUND_GREEN.sequence());} sb.append(tab).append("No new tests have failed in the current session.").append(endOfLine); if (x364) {sb.append(X364.FOREGROUND_DEFAULT.sequence());} } else { if (x364) {sb.append(X364.FOREGROUND_RED.sequence());} if (x364) {sb.append(X364.BOLD.sequence());} sb.append(tab).append("/!\\ Some tests are now failing ! You should fix them to restore the build /!\\") .append(endOfLine); for (Result res : problematicTests) { sb.append(tab).append(tab).append("Id: ").append(res.getId()).append(endOfLine); sb.append(tab).append(tab).append(tab).append("==> Directory: ").append(res.getDirectory()) .append(endOfLine); } } if (!problematicGroupTests.isEmpty()) { if (x364) {sb.append(X364.FOREGROUND_RED.sequence());} sb.append(tab).append("Some Group tests are now failing because of new activated test failling (not an error)") .append(endOfLine); for (Result res : problematicGroupTests) { sb.append(tab).append(tab).append("Id: ").append(res.getId()).append(endOfLine); sb.append(tab).append(tab).append(tab).append("==> Directory: ").append(res.getDirectory()) .append(endOfLine); } if (x364) {sb.append(X364.FOREGROUND_DEFAULT.sequence());} } if (!newlyActivatedTests.isEmpty()) { if (x364) {sb.append(X364.FOREGROUND_GREEN.sequence());} sb.append(tab).append("New tests have been activated in the current session.").append(endOfLine); for (Result res : newlyActivatedTests) { sb.append(tab).append(tab).append("Id: ").append(res.getId()); sb.append(" Result:").append(res.isPassed()).append(endOfLine); } if (x364) {sb.append(X364.FOREGROUND_DEFAULT.sequence());} } if (!deActivatedTests.isEmpty()) { if (x364) {sb.append(X364.FOREGROUND_RED.sequence());} if (x364) {sb.append(X364.BOLD.sequence());} sb.append(tab).append("/!\\ Some tests have been deactivated in the current session! ! You should reactivate them to restore the build /!\\") .append(endOfLine); for (Result res : deActivatedTests) { sb.append(tab).append(tab).append("Id: ").append(res.getId()).append(endOfLine); sb.append(tab).append(tab).append(tab).append("==> Directory: ").append(res.getDirectory()); sb.append(" Previous result:").append(res.isPassed()).append(endOfLine); } if (x364) {sb.append(X364.FOREGROUND_DEFAULT.sequence());} } if (x364) {sb.append(X364.RESET.sequence());} LOGGER.info(sb.toString()); } /** * Throws an {@link SQLException} if the connection to the database is closed. * This method should be invoked before trying to perform an action on the * database. */ private void ensureConnectionOpened() throws SQLException { if (connection == null || connection.isClosed()) { throw new SQLException("The connection to the database is already closed. " + "Unable to perform the wished action."); } } /** * Returns the date of the previous session of tests, or {@code null} if there is no other * test suite in the database. * * @param service The service name. * @param version The service version. * @throws SQLException */ private Date getPreviousSuiteDate(final String service, final String version) throws SQLException { ensureConnectionOpened(); final PreparedStatement ps = connection.prepareStatement(SELECT_PREVIOUS_SUITE); ps.setString(1, service); ps.setString(2, version); final ResultSet rs = ps.executeQuery(); final Date dateResult = (rs.next()) ? rs.getTimestamp(1) : null; rs.close(); return dateResult; } /** * Esxtract a description of the test from a file. * * @param id The identifier of the test * @param directory The directory where to find the log file. * @return An assertion. */ public String extractDescription(final String id, final String directory) { final File f = new File("target/logs/" + directory + "/log.xml"); String assertion = null; if (f.exists()) { try { final String xml = FileUtilities.getStringFromFile(f); if (xml.indexOf("<assertion>") != -1) { assertion = xml.substring(xml.indexOf("<assertion>") + 11, xml.indexOf("</assertion>")); final PreparedStatement ps = connection.prepareStatement(INSERT_DESCRIPTION); ps.setString(1, id); ps.setString(2, assertion); ps.executeUpdate(); } } catch (SQLException ex) { LOGGER.log(Level.SEVERE, "Error while inserting new assertion for test:" + id, ex); } catch (IOException ex) { LOGGER.log(Level.WARNING, "Error while reading log file for test:" + id, ex); } } return assertion; } /** * Returns a list of {@link Result} that have failed for the session at the date specified. * This list can be empty, but never {@code null}. * * @param date The date of the session. * @throws SQLException */ protected List<Result> getTestsFailed(final Date date) throws SQLException { ensureConnectionOpened(); final PreparedStatement psCurrent = connection.prepareStatement(SELECT_TESTS_FAILED); psCurrent.setTimestamp(1, new Timestamp(date.getTime())); final List<Result> results = new ArrayList<>(); final ResultSet rs = psCurrent.executeQuery(); while (rs.next()) { final String id = rs.getString(2); final String directory = rs.getString(3); final boolean isGroupNode = rs.getBoolean(4); final PreparedStatement psDesc = connection.prepareStatement(SELECT_TESTS_DESC); psDesc.setString(1, id); final ResultSet rs2 = psDesc.executeQuery(); final String assertion; if (rs2.next()) { assertion = rs2.getString(1); } else { assertion = extractDescription(id, directory); } results.add(new Result(rs.getTimestamp(1), id, directory, false, isGroupNode, assertion)); rs2.close(); } rs.close(); return results; } /** * Returns a list of {@link Result} that have passed for the session at the date specified. * This list can be empty, but never {@code null}. * * @param date The date of the session. * @throws SQLException */ private List<Result> getTestsPassed(final Date date) throws SQLException { ensureConnectionOpened(); final PreparedStatement psCurrent = connection.prepareStatement(SELECT_TESTS_PASSED); psCurrent.setTimestamp(1, new Timestamp(date.getTime())); final List<Result> results = new ArrayList<>(); final ResultSet rs = psCurrent.executeQuery(); while (rs.next()) { final String id = rs.getString(2); final String directory = rs.getString(3); final boolean isGroupNode = rs.getBoolean(4); final PreparedStatement psDesc = connection.prepareStatement(SELECT_TESTS_DESC); psDesc.setString(1, id); final ResultSet rs2 = psDesc.executeQuery(); final String assertion; if (rs2.next()) { assertion = rs2.getString(1); } else { assertion = extractDescription(id, directory); } results.add(new Result(rs.getTimestamp(1), id, directory, true, isGroupNode, assertion)); rs2.close(); } rs.close(); return results; } /** * Returns the test for the given id and date. * * @param date The date of the session. * @param id The test id. * @return The {@linkplain Result results} for this test, or {@code null} if the test is * not found in the database. * @throws SQLException */ protected Result getTest(final Date date, final String id) throws SQLException { ensureConnectionOpened(); final PreparedStatement psCurrent = connection.prepareStatement(SELECT_TEST_RESULT_FROM_ID); psCurrent.setTimestamp(1, new Timestamp(date.getTime())); psCurrent.setString(2, id); Result result = null; final ResultSet rs = psCurrent.executeQuery(); if (rs.next()) { result = new Result(rs.getDate(1), rs.getString(2), rs.getString(3), rs.getBoolean(4), rs.getBoolean(5), rs.getString(6)); } rs.close(); return result; } /** * Inserts a new service in the database. * * @param service The service name. * @param version The service version. * * @throws SQLException if an error occurs while inserting the new service. */ private void insertService(final String service, final String version) throws SQLException { ensureConnectionOpened(); final PreparedStatement ps = connection.prepareStatement(INSERT_SERVICE); ps.setString(1, service); ps.setString(2, version); ps.execute(); ps.close(); existingServices.add(new Service(service, version)); } /** * Inserts a new suite in the database. * * @param date The date of the suite. * @param service The service name. * @param version The service version. * @throws SQLException */ private void insertSuite(final Date date, final String service, final String version) throws SQLException { ensureConnectionOpened(); final PreparedStatement ps = connection.prepareStatement(INSERT_SUITE); ps.setTimestamp(1, new Timestamp(date.getTime())); ps.setString(2, service); ps.setString(3, version); ps.setBoolean(4, false); ps.execute(); ps.close(); existingSuites.add(new Suite(date, service, version)); } /** * Returns {@code true} if the test is present in the list specified. {@code False} * otherwise. * * @param test The test to verify the existence in the list. * @param tests The list of tests into which we search the test. */ private boolean isTestPresentInList(final Result test, final List<Result> tests) { for (Result currentTest : tests) { if (currentTest.getId().equals(test.getId())) { return true; } } return false; } /** * Verify if the service already exists in {@link #existingServices} and if not in the * database. * * @param service The service name. * @param version The service version. * @return {@code True} if the service exists, false otherwise. * * @throws SQLException if an error occurs while trying to get the services contained in the database. */ private boolean serviceExists(final String service, final String version) throws SQLException { // Verify that the service is not already present in the list. if (!existingServices.isEmpty() && existingServices.contains(new Service(service, version))) { return true; } ensureConnectionOpened(); final PreparedStatement ps = connection.prepareStatement(COUNT_SERVICE); ps.setString(1, service); ps.setString(2, version); final ResultSet rs = ps.executeQuery(); int result = 0; if (rs.next()) { result = rs.getInt(1); } rs.close(); ps.close(); if (result == 0) { return false; } return true; } /** * Verify if the suite already exists in {@link #existingSuites} and if not in the * database. * * @param date The date of the suite. * @param service The service name. * @param version The service version. * @return {@code True} if the service exists, false otherwise. * @throws SQLException if an error occurs while trying to get the suites contained in the database. */ private boolean suiteExists(final Date date, final String service, final String version) throws SQLException { // Verify that the service is not already present in the list. if (!existingSuites.isEmpty() && existingSuites.contains(new Suite(date, service, version))) { return true; } ensureConnectionOpened(); final PreparedStatement ps = connection.prepareStatement(COUNT_SUITE); ps.setTimestamp(1, new Timestamp(date.getTime())); final ResultSet rs = ps.executeQuery(); int result = 0; if (rs.next()) { result = rs.getInt(1); } rs.close(); ps.close(); if (result == 0) { return false; } return true; } }