diff --git a/README.md b/README.md index 3090e03..97a7fdb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OnRailsLogger -Welcome to OnRailsLogger a simple logging application which can fit right into your codebase. OnRails features total of 6 different log types, and 6 different logging levels. The application is simple requiring one initialization step at your codes entrypoint and then just report your error. +Welcome to OnRailsLogger a simple logging application which can fit right into your codebase. OnRails features total of 6 different log types, each of which is individually silenceable, or choose from three builtin configurations. The application is simple requiring one initialization step at your codes entrypoint and then just log whatever you need to. **This is a simple logger, it has a synchronized block around the main logging method. This is the only thread safety included. No guarantees are given through locks/queues/semaphores.** @@ -11,14 +11,14 @@ Logs can be found in your user.home directory under OnRailsLogging and then your The logger is still in development. Simple things you may expect to exist may not at this point. They will come soon, checkout the roadmap at the bottom which details the next features to come. ## Adding the logger to a maven project -The logger uses JitPack to package and install the project. Paste the code found below into your pom.xml. **Check tags for releases, not all changes in main are in the current release VERSION: `v1.0.0`** +The logger uses JitPack to package and install the project. Paste the code found below into your pom.xml. **Check tags for releases, not all changes in main are in the current release VERSION: `v1.1.0`**. ```xml com.github.rhys-h-walker OnRailsLogging - Tag + V1.1.0 @@ -34,19 +34,16 @@ For additional instructions on how to install check out the jitpack page for the ## How to use -To use the logger you must first decide how you wish it to work. Currently there are three options: -- Console reporting -- Logging to file -- Both of the above +Before using the Logger there are two things to understand. You have the power to log to a file and console or just console. When logging to just console add true to the final argument of initializeLogger. Logging visibility can be customized, using a `HashMap`, this is the second argument in the second initializer. There are three available by default, check LoggingType for the methods. -To just log to a console ignore the initialization step. To just output to file set LoggingLevel.NONE in initialization. +Make sure to call initialize Logger before doing anything else, otherwise logs will only be shown on the console. Not calling `initializeLogger` has not been fully tested so behaviour may be random at times. ```Java -Logger.initializeLogger("applicationName"); +Logger.initializeLogger("applicationName", false); // Or -Logger.initializeLogger("applicationName", LoggingLevel.ALL); +Logger.initializeLogger("applicationName", LoggingType.defaultVisibility(), false); // Then @@ -56,6 +53,10 @@ Logger.loginfo("This is an info message"); Logger.logwarn("This is a warning message"); Logger.logdebug("This is a debug message"); Logger.logmiscellaneous("This is a miscellaneous message"); + +// Once completed + +Logger.shutdown(); // Release any system resources being used ``` The output file will look like this: @@ -75,15 +76,22 @@ When printing to console the outputs have colours like so:
- Allow for logging to be more fine grained with level - This will allow for the silencing of certain logging messages, - Basic thread safety, synchronized block in produceLog +- Error handling for most cases +- Testing for basic console output +- Error handling in file-creation/string creation +- Handle null messages with a message when printing/writing +- Inputs are now screened in methods, errors reported +- Console only reporting is always active as a fallback for when file reporting breaks +- Added shutdown method to Logger +- Added tests for basic functionality: Check README in tests directory +- Application names should no longer break file systems. Still be careful as to what you name your app ## Roadmap Updates are located in three groups, Next is what I am currently working on, soon will be after that and future has no specific date or timeframe attached to it. ### Next -- Add Junit tests for console output -- Error handling in file-creation/string creation -- Handle null messages with a message when printing/writing +- Testing via Junit: Check README in tests directory ### Soon - Configuration file support: @@ -101,9 +109,10 @@ Updates are located in three groups, Next is what I am currently working on, soo - This is an embedable component - Can be added anywhere and will just view logs for the app - Custom log formats (Allow adjustment of certain features based on user requirements) +- Reinitialization of the Logger, recovery from a crash if told to be user ## Examples -OnRailsLogger is new and there are few examples, however I have converted one of my old projects over to use it, you can find it [here](https://github.com/rhys-h-walker/rambling-jesters/tree/adding-on-rails-logging). +OnRailsLogger is new and there are few examples, however I have converted one of my old projects over to use it, you can find it [here](https://github.com/rhys-h-walker/rambling-jesters/tree/adding-on-rails-logging) it is important to note that this uses v1.0.0 and not the most up to date version. Please if you use this project contact me so I can place your project on examples. diff --git a/pom.xml b/pom.xml index 502b2cc..64f5070 100644 --- a/pom.xml +++ b/pom.xml @@ -5,10 +5,9 @@ com.github.rhys-h-walker OnRailsLogging - v1.0.0 + v1.1.0 OnRailsLogging - https://github.com/rhys-h-walker/OnRailsLogging @@ -34,7 +33,6 @@ junit-jupiter-api test - org.junit.jupiter junit-jupiter-params @@ -43,14 +41,12 @@
- + - maven-clean-plugin 3.4.0 - maven-resources-plugin 3.3.1 @@ -75,7 +71,6 @@ maven-deploy-plugin 3.1.2 - maven-site-plugin 3.12.1 diff --git a/src/main/java/com/github/rhys_h_walker/App.java b/src/main/java/com/github/rhys_h_walker/App.java deleted file mode 100644 index 879d24b..0000000 --- a/src/main/java/com/github/rhys_h_walker/App.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.github.rhys_h_walker; - -import com.github.rhys_h_walker.core_enums.LoggingType; - -public class App { - public static void main(String[] args) { - Logger.initializeLogger("testApplication"); - - Logger.logerror("This is an error message"); - - Logger.changeLogVisibility(LoggingType.PROGRESS, true); - Logger.logprogress("This is a progress message"); - Logger.loginfo("This is an info message"); - Logger.logwarn("This is a warning message"); - Logger.logdebug("This is a debug message"); - Logger.logmiscellaneous("This is a miscellaneous message"); - - } -} diff --git a/src/main/java/com/github/rhys_h_walker/FileManagement.java b/src/main/java/com/github/rhys_h_walker/FileManagement.java index 4e284e8..e8fb491 100644 --- a/src/main/java/com/github/rhys_h_walker/FileManagement.java +++ b/src/main/java/com/github/rhys_h_walker/FileManagement.java @@ -3,11 +3,13 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileReader; +import java.io.IOException; import java.io.PrintWriter; import java.time.LocalDateTime; import java.util.ArrayList; import com.github.rhys_h_walker.core_enums.LoggingType; +import com.github.rhys_h_walker.misc.Utilities; /** * A static class which implements most of the IO features required by OnRailsLogger @@ -18,24 +20,51 @@ public class FileManagement { /** * Based on a timestamp either locate or create a file for logging * @param timestamp The timestamp to use to find the log file - * @return + * @return The File object, null on any errors */ public static File locateFile(LocalDateTime ts, File applicationDirectory) { - // We have our timestamp in this format yyyy-MM-dd-HH:mm:ss - File loggingFile = new File(applicationDirectory.getAbsolutePath()+File.separator+ts.getYear()+File.separator+ts.getMonthValue()+File.separator+ts.getDayOfMonth()+File.separator+ts.getHour()+"_"+ts.getMinute()+"_"+ts.getSecond()+".log"); - - if (loggingFile.exists()){ - return loggingFile; - } else { - File parentDirectory = loggingFile.getParentFile(); - if (!parentDirectory.exists()) { - parentDirectory.mkdirs(); + + // Handle null cases for arguments + if (ts == null || applicationDirectory == null) { + System.err.println("timestamp is null or application directory is null"); + return null; + } + + try { + // We have our timestamp in this format yyyy-MM-dd-HH:mm:ss + File loggingFile = new File(Utilities.formatFilepathFromTimestamp(ts, applicationDirectory)); + + if (loggingFile.exists()){ + return loggingFile; + } else { + File parentDirectory = loggingFile.getParentFile(); + if (!parentDirectory.exists()) { + boolean success = parentDirectory.mkdirs(); + + // Handle failure of file creation gracefully + if (!success) { + System.err.println("Unsuccessfull creation of file: " + loggingFile.toString()); + return null; + } + } + return loggingFile; } - return loggingFile; + } catch (SecurityException e) { + System.err.println("No permission to create/access the directory requested\n" + e); + return null; + } catch (Exception e) { + System.err.println("General exception on creating/accessing the directory requested\n" + e); + return null; } } public static void writeToLogFile(File logFile, String timestamp, LoggingType logType, String message, PrintWriter pw) { + + if (pw == null) { + System.err.println("PrintWriter is null, returning without writing"); + return; + } + String logMessage = "[" + timestamp + "] " + logType.toString() + ": " + message; pw.println(logMessage); pw.flush(); @@ -43,31 +72,39 @@ public static void writeToLogFile(File logFile, String timestamp, LoggingType lo /** * Read in a log file and return it split by line - * @param logFile - * @return + * @param logFile The File to be read from + * @return An ArrayList of Strings, null if error */ public static ArrayList readLogFile(File logFile) { - if (!logFile.exists()) { - System.exit(1); + if (logFile == null) { + System.err.println("LogFile is null when trying to read"); + return null; } - ArrayList lines = new ArrayList<>(); - try { - BufferedReader reader = new BufferedReader(new FileReader(logFile)); - String line = reader.readLine(); - while (line != null) { - lines.add(line); - line = reader.readLine(); - } - reader.close(); + if (!logFile.exists()) { + System.err.println("File does not exist, nothing to read"); + return null; + } - return lines; - } catch(Exception e) { - // Failure detected error and then return null - System.err.println("Failed opening file " + e); + if (!logFile.canRead()) { + System.err.println("File cannot be read, incorrect permissions!"); return null; } + ArrayList logs = new ArrayList<>(); + + try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) { + String line; + while ((line = reader.readLine()) != null) { + logs.add(line); + } + } catch (IOException e) { + System.err.println("OnRailsLogger: Error reading log file: " + e.getMessage()); + // Return partial results instead of crashing + } + + return logs; + } } diff --git a/src/main/java/com/github/rhys_h_walker/Logger.java b/src/main/java/com/github/rhys_h_walker/Logger.java index 8930ad4..d394871 100644 --- a/src/main/java/com/github/rhys_h_walker/Logger.java +++ b/src/main/java/com/github/rhys_h_walker/Logger.java @@ -1,10 +1,13 @@ package com.github.rhys_h_walker; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.HashMap; import com.github.rhys_h_walker.core_enums.LoggingType; import com.github.rhys_h_walker.factories.LogFactory; import com.github.rhys_h_walker.misc.ANSI; +import com.github.rhys_h_walker.misc.Utilities; /** * The primary entrypoint for a the logger. @@ -19,6 +22,9 @@ public class Logger { private static String applicationName; private static HashMap logVisibility; + // Booleans to not spam with log errors + private static boolean logFactoryNullErrorReported = false; + // // Overrriden initializeLogger which allows for different initialization configurations // @@ -28,8 +34,8 @@ public class Logger { * @param applicationName The name of the application we are logging for * @param level A HashMap detailing the level of logging */ - public static void initializeLogger(String appName, HashMap logVisibility) { - commonConstructor(appName, logVisibility); + public static void initializeLogger(String appName, HashMap logVisibility, boolean consoleOnly) { + commonConstructor(appName, logVisibility, consoleOnly); } /** @@ -37,8 +43,8 @@ public static void initializeLogger(String appName, HashMap viewVisibilityMap() { + + return logVisibility; + + } + + /** + * Set the log visibility map + * @param newVisibility The new hashmap to set + */ + public static void changeLogVisibilityMap(HashMap newVisibility) { + + if (newVisibility == null) { + System.err.println("Map not accepted, null value"); + return; + } + + // We should have 6 values as allowed by the LoggingType enum + if (newVisibility.keySet().size() != LoggingType.values().length) { + System.err.println("Map not accepted, include all values of LoggingType"); + } + + // Bad practice but for size 6 on an infrequently called method should be ok + for (LoggingType logType : newVisibility.keySet()) { + if (newVisibility.get(logType) == null) { + System.err.println("Map not accepted, null value found for " + logType.toString()); + } + continue; + } + + logVisibility = newVisibility; + } /** @@ -67,15 +124,33 @@ public static String getApplicationName() { return applicationName; } + public static void shutdown() { + if (logFactory != null) { + logFactory.cleanup(); + logFactory = null; + } + + applicationName = null; + } + /** * Log a miscellaneous message * @param message * @return the timestamp of the log */ public static String logmiscellaneous(String message) { + + if (logVisibility == null) { + return null; + } + + if (message == null) { + message = "null"; + } + String logMessage = ""; if (logVisibility.get(LoggingType.MISCELLANEOUS)) { - logMessage = (ANSI.CYAN_BG + "Misc:" + ANSI.RESET + " " + ANSI.CYAN + message + ANSI.RESET); + logMessage = (ANSI.CYAN_BG + "MISCELLANEOUS:" + ANSI.RESET + " " + ANSI.CYAN + message + ANSI.RESET); } return produceLog(message, logMessage, LoggingType.MISCELLANEOUS); @@ -87,9 +162,18 @@ public static String logmiscellaneous(String message) { * @return the timestamp of the log */ public static String loginfo(String message) { + + if (logVisibility == null) { + return null; + } + + if (message == null) { + message = "null"; + } + String logMessage = ""; if (logVisibility.get(LoggingType.INFO)) { - logMessage = (ANSI.BLUE_BG + "Info:" + ANSI.RESET + " " + ANSI.BLUE + message + ANSI.RESET); + logMessage = (ANSI.BLUE_BG + "INFO:" + ANSI.RESET + " " + ANSI.BLUE + message + ANSI.RESET); } return produceLog(message, logMessage, LoggingType.INFO); @@ -101,9 +185,18 @@ public static String loginfo(String message) { * @return the timestamp of the log */ public static String logwarn(String message) { + + if (logVisibility == null) { + return null; + } + + if (message == null) { + message = "null"; + } + String logMessage = ""; if (logVisibility.get(LoggingType.WARN)) { - logMessage = (ANSI.MAGENTA_BG + "Warn:" + ANSI.RESET + " " + ANSI.MAGENTA + message + ANSI.RESET); + logMessage = (ANSI.MAGENTA_BG + "WARN:" + ANSI.RESET + " " + ANSI.MAGENTA + message + ANSI.RESET); } return produceLog(message, logMessage, LoggingType.WARN); @@ -115,9 +208,18 @@ public static String logwarn(String message) { * @return the timestamp of the log */ public static String logdebug(String message) { + + if (logVisibility == null) { + return null; + } + + if (message == null) { + message = "null"; + } + String logMessage = ""; if (logVisibility.get(LoggingType.DEBUG)) { - logMessage = (ANSI.YELLOW_BG + "Debug:" + ANSI.RESET + " " + ANSI.YELLOW + message + ANSI.RESET); + logMessage = (ANSI.YELLOW_BG + "DEBUG:" + ANSI.RESET + " " + ANSI.YELLOW + message + ANSI.RESET); } return produceLog(message, logMessage, LoggingType.DEBUG); @@ -129,9 +231,18 @@ public static String logdebug(String message) { * @return the timestamp of the log */ public static String logprogress(String message) { + + if (logVisibility == null) { + return null; + } + + if (message == null) { + message = "null"; + } + String logMessage = ""; if (logVisibility.get(LoggingType.PROGRESS)) { - logMessage = (ANSI.GREEN_BG + "Progress:" + ANSI.RESET + " " + ANSI.GREEN + message + ANSI.RESET); + logMessage = (ANSI.GREEN_BG + "PROGRESS:" + ANSI.RESET + " " + ANSI.GREEN + message + ANSI.RESET); } return produceLog(message, logMessage, LoggingType.PROGRESS); @@ -143,10 +254,19 @@ public static String logprogress(String message) { * @return the timestamp of the log */ public static String logerror(String message) { + + if (logVisibility == null) { + return null; + } + + if (message == null) { + message = "null"; + } + String logMessage = ""; if (logVisibility.get(LoggingType.ERROR)) { - logMessage = (ANSI.RED_BG + "Error:" + ANSI.RESET + " " + ANSI.RED + message + ANSI.RESET); + logMessage = (ANSI.RED_BG + "ERROR:" + ANSI.RESET + " " + ANSI.RED + message + ANSI.RESET); } return produceLog(message, logMessage, LoggingType.ERROR); @@ -158,7 +278,7 @@ public static String logerror(String message) { /** * Output a log message and create the log in memory - * @param message The message formatted with colours to be output + * @param message The message formatted without colours to be output * @param printableMessage The message formatted with ANSI codes * @param level The logging level for creation of the log in memory * @return the timestamp of the log, null if log factory not set, or LoggingLevel does not allow its outpu @@ -166,17 +286,36 @@ public static String logerror(String message) { private static synchronized String produceLog(String message, String printableMessage, LoggingType type) { String timestamp = null; + // Replace null message with "null" + if (message == null) { + message = "null"; + } + // So long as we are set then output if (logFactory != null) { // Output to file regardless of logging level timestamp = logFactory.createNewLog(message, type); + // If timestamp is null then logging to file has failed, give a "good enough" timestamp + if (timestamp == null) { + timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH:mm:ss")); + } + // Message will be empty if the logging level does not allow its output if (!printableMessage.equals("")){ System.out.println("[" + timestamp + "] " + printableMessage); } - } else { - System.err.println("Log factory not set, most likely no initialize call was made\n" + message); + } else if (!logFactoryNullErrorReported){ + logFactoryNullErrorReported = true; + System.err.println("Log factory not set, most likely no initialize call was made\nPlease call initialize before any other logging actions"); + } else { // Fallback option if no fileoutput is allowed + + // Message will be empty if the logging level does not allow its output + if (!printableMessage.equals("")){ + timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH:mm:ss")); + System.out.println("[" + timestamp + "] " + printableMessage); + return timestamp; + } } return timestamp; @@ -187,11 +326,54 @@ private static synchronized String produceLog(String message, String printableMe * @param appName The name of the application * @param customisedVisibility The visibility of each log */ - private static void commonConstructor(String appName, HashMap customisedVisibility) { + private static void commonConstructor(String appName, HashMap customisedVisibility, boolean consoleOnly) { + // Reset error flags + logFactoryNullErrorReported = false; - logFactory = new LogFactory(appName); + // Check the appName for issues (No app needed for console only output) + if (appName == null || appName.trim().isEmpty()) { + System.err.println("Initialization failed due to appName being null or empty. Logger will only output to console"); + return; + } + appName = Utilities.cleanFileNameString(appName); + if (appName == null) { + System.err.println("Error in sanitization of filename"); + return; + } applicationName = appName; - logVisibility = customisedVisibility; + + // Check that logging visibility is not null + if (customisedVisibility == null) { + System.err.println("customisedVisibility is null, this could be internal or based on value given \ndefaulting to default values"); + logVisibility = LoggingType.defaultVisibility(); + } else { + logVisibility = customisedVisibility; + } + + if (consoleOnly) { + // They don't want file output so silence the error + logFactoryNullErrorReported = true; + logFactory = null; + return; + } + // Attempt to make a logFactory, if this fails then output and fallback to console only + try { + logFactory = new LogFactory(appName); + + if (!logFactory.initialized()) { + System.err.println("Log factory initialization not successfull, closing it down.\nReverting to console only logging"); + logFactory = null; + } + + } catch (SecurityException e) { + System.err.println("Security restriction during initialization: " + e.getMessage()); + System.err.println("Falling back to console-only logging."); + logFactory = null; + } catch (Exception e) { + System.err.println("Unexpected error during initialization: " + e.getMessage()); + System.err.println("Falling back to console-only logging."); + logFactory = null; + } } } diff --git a/src/main/java/com/github/rhys_h_walker/factories/LogFactory.java b/src/main/java/com/github/rhys_h_walker/factories/LogFactory.java index ea2b60c..d31b220 100644 --- a/src/main/java/com/github/rhys_h_walker/factories/LogFactory.java +++ b/src/main/java/com/github/rhys_h_walker/factories/LogFactory.java @@ -24,29 +24,64 @@ public class LogFactory { private PrintWriter pw; private static final DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH:mm:ss"); + // Error reporting flags + private static boolean reportedFileSystemError = false; + private static boolean reportedPrintWriterError = false; + + // Non LogFactory error reporting fields + private boolean initialized = false; + /** * Create a LogFactory for a specific application * All logs will be located under this name like this: * * user.home/OnRailsLogging/applicationName/ regular dated directory structure * + * THIS METHOD WILL RETURN ON ANY ERROR + * * @param applicationName The name to be used */ public LogFactory(String applicationName) { + // Reset flags on creation of LogFactory + reportedFileSystemError = false; + reportedPrintWriterError = false; + // Now upon creation of this object check and create directories that are required String userHome = System.getProperty("user.home"); + + if (userHome == null) { + // User home not accessible, falling back to console Logging + System.err.println("User home not accessible, falling back to console Logging"); + return; + } + + // Save the directory that will be being used later applicationDirectory = new File(userHome, "OnRailsLogging" + File.separator + applicationName); + if (!applicationDirectory.exists()) { - applicationDirectory.mkdirs(); + boolean success = applicationDirectory.mkdirs(); + + if (!success) { + System.err.println("Creation of directory was not successfull in LogFactory constructor"); + return; + } } // Assign our current printwriter LocalDateTime now = LocalDateTime.now(); timestamp = now.format(df); + + File initialLogFile = FileManagement.locateFile(now, applicationDirectory); + if (initialLogFile == null) { + System.err.println("Error in finding the initial log file, returning in constructor"); + return; + } + try { - pw = new PrintWriter(new BufferedWriter(new FileWriter(FileManagement.locateFile(now, applicationDirectory))), true); + pw = new PrintWriter(new BufferedWriter(new FileWriter(initialLogFile)), true); } catch (IOException e) { - System.err.println(e); + System.err.println("Failed to create printwriter, returning in constructor"); + return; } // Close the PrintWriter when execution terminates @@ -54,6 +89,15 @@ public LogFactory(String applicationName) { if (pw != null) pw.close(); })); + initialized = true; + } + + /** + * Has this LogFactory been initialized + * @return true if ok, false if not ok + */ + public boolean initialized() { + return initialized; } /** @@ -69,6 +113,13 @@ public String createNewLog(String message, LoggingType logType) { // Get the current log file File logFile = FileManagement.locateFile(now, applicationDirectory); + if (logFile == null) { + if (!reportedFileSystemError) { + reportedFileSystemError = true; + System.err.println("Error in creation/access of logFile"); + } + return null; + } // printWriter will need updating if this is different if (!timestamp.equals(curTimestamp)) { @@ -77,13 +128,43 @@ public String createNewLog(String message, LoggingType logType) { pw.close(); pw = new PrintWriter(new BufferedWriter(new FileWriter(logFile)), true); } catch (IOException e) { - System.err.println(e); + + if (!reportedPrintWriterError) { + reportedPrintWriterError = true; + System.err.println("PrintWriter creation error:\n" + e); + } + + return null; } } + // Should never be required + if (pw == null || pw.checkError()) { + System.err.println("Printwriter was either null or has found an error"); + return null; + } + // Write to the log file FileManagement.writeToLogFile(logFile, timestamp, logType, message, pw); return timestamp; } + + /** + * Close any system resources correctly + * @return + */ + public boolean cleanup() { + + if (pw == null) { + return true; + } + + pw.flush(); + pw.close(); + pw = null; + + return true; + + } } diff --git a/src/main/java/com/github/rhys_h_walker/misc/Utilities.java b/src/main/java/com/github/rhys_h_walker/misc/Utilities.java index 90533f1..ab2fa1c 100644 --- a/src/main/java/com/github/rhys_h_walker/misc/Utilities.java +++ b/src/main/java/com/github/rhys_h_walker/misc/Utilities.java @@ -1,10 +1,20 @@ package com.github.rhys_h_walker.misc; +import java.io.File; +import java.time.LocalDateTime; + /** * A class of general purpose utilities */ public class Utilities { + + private static final String INVALID_FILENAME_CHARS_REGEX = "[<>:\"|?*\\\\/\\x00-\\x1F]"; + private static final String[] reservedNames = { + "CON", "PRN", "AUX", "NUL", + "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", + "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" + }; /** * Remove all leading zeros from a String @@ -29,4 +39,55 @@ else if (hasHitNonZero) { return sb.toString(); } + /** + * Create a path from a timestamp and app directory + * @param ts Timestamp to format fileString from + * @param applicationDirectory The directory to begin searching from + * @return + */ + public static String formatFilepathFromTimestamp(LocalDateTime ts, File applicationDirectory) { + return applicationDirectory.getAbsolutePath() + + File.separator + ts.getYear() + + File.separator + ts.getMonthValue() + + File.separator + ts.getDayOfMonth() + + File.separator + ts.getHour()+"_" + + ts.getMinute() + "_"+ + ts.getSecond() + ".log"; + } + + /** + * Clean a non empty String for use as a filename + * REGEX USED = INVALID_FILENAME_CHARS_REGEX: [<>:\"|?*\\\\/\\x00-\\x1F] + * Trailing dots and spaces removed + * File name size limit imposed 100 chars + * + * On error no alternative name will be given + * + * @param filename The filename to clean + * @return A cleaned version/null if errored + */ + public static String cleanFileNameString(String filename) { + + String file_t = filename.trim(); + file_t = file_t.replaceAll(INVALID_FILENAME_CHARS_REGEX, "_"); + + if (file_t.isEmpty()) { + System.err.println("file_t is empty after all illegal chars removed"); + return null; + } + + if (file_t.length() > 100) { + file_t = file_t.substring(0, 100); + } + + file_t = file_t.replaceAll("[. ]+$", ""); + + if (file_t.isEmpty() || file_t.endsWith(".")) { + System.err.println("file_t is empty after substring and dots removed chars removed"); + return null; + } + + return file_t; + } + } diff --git a/src/test/java/com/github/rhys_h_walker/Helpers.java b/src/test/java/com/github/rhys_h_walker/Helpers.java deleted file mode 100644 index 63dcdf7..0000000 --- a/src/test/java/com/github/rhys_h_walker/Helpers.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.rhys_h_walker; - -import java.util.ArrayList; - -import com.github.rhys_h_walker.core_enums.LoggingType; - -public class Helpers { - - - public static boolean isStringInList(String fullMatch, ArrayList listToSearch) { - - boolean lineFound = false; - - for (String line : listToSearch) { - - if (line.equals(fullMatch)) { - - lineFound = true; - } - } - return lineFound; - } - - public static String formatALine(String timestamp, LoggingType logType, String message) { - return "["+timestamp+"] " + logType.toString() + ": " + message; - } -} diff --git a/src/test/java/com/github/rhys_h_walker/README.md b/src/test/java/com/github/rhys_h_walker/README.md new file mode 100644 index 0000000..65f727d --- /dev/null +++ b/src/test/java/com/github/rhys_h_walker/README.md @@ -0,0 +1,64 @@ +# Testing framework: +Below is a comprehensive list of what is tested for in the framework. Check to implement for upcoming test additions. Items in "On The Backburner" may be implemented at some point, they are not guarantees however. + +## Completed +- Core: + - Logging to file + - Logging to console + - Timestamp decoder + - String in Array + - Format a String + - File location creation + - Ensure console output matches currently set visibility + - Null logging type handling +- Configuration: + - Default visibility settings + - GetApplicationName + - Custom visibility maps + - ChangeVisibility + - ViewLogVisibility + - Ensure shutdown and initialize restarts under a different name / different visibility + - Empty application names +- LoggingType: + - Test returned maps are correct +- LogFactory: + - PrintWriter recreation, for timestamps + - isInitialized status checking + +## To implement +- Core: + - File rotation behaviour - *still to implement* +- Configuration + - Nothing for now! +- LoggingType: + - Nothing for now! +- Error handling + - Permission denied scenarios + - Null configuration handling + - Null configuration handling + - System property access failures + - Security exceptions + - LogFactory initialization failures + - PrintWriter creation failures + - File locking Scenarios + - Shutdown hook execution +- LogFactory + - Constructor error handling + - ErrorFlag management +- Input validation + - Nothing for now! + +## On the Backburner +- Integration Testing + - End-to-end workflow, init->log->verify + - File+console dual output verification + - Console only mode + - Fallback behaviour chains +- Thread safety - *Not of current concern* + - Potentially will remain untested +- Cross-platform testing - *Not of current concern* + - Potentially may remain untested + - Path characters on (Windows/Unix/Mac) + - Case sensitivity (Platform dependent) + - Unicode characters + - Path length limits \ No newline at end of file diff --git a/src/test/java/com/github/rhys_h_walker/TestConsoleOutput.java b/src/test/java/com/github/rhys_h_walker/TestConsoleOutput.java new file mode 100644 index 0000000..651fbb1 --- /dev/null +++ b/src/test/java/com/github/rhys_h_walker/TestConsoleOutput.java @@ -0,0 +1,128 @@ +package com.github.rhys_h_walker; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import com.github.rhys_h_walker.core_enums.LoggingType; +import com.github.rhys_h_walker.misc.ANSI; +import com.github.rhys_h_walker.testing_utilities.ConsoleCapture; + +import java.util.function.Function; +import java.util.stream.Stream; + +class TestConsoleOutput { + + private ConsoleCapture consoleCapture; + + @BeforeEach + void setUp() { + consoleCapture = new ConsoleCapture(); + consoleCapture.startCapture(); + + // Initialize logger for console-only mode (no file output) + Logger.initializeLogger("Test", LoggingType.logVisibilityAllTrue(), true); + } + + @AfterEach + void tearDown() { + consoleCapture.stopCapture(); + Logger.shutdown(); + } + + private static Stream ansiColorTestData() { + return Stream.of( + arguments("This is a sample error message", LoggingType.ERROR, + (Function) Logger::logerror, + ANSI.RED_BG, ANSI.RED), + arguments("This is a sample warn message", LoggingType.WARN, + (Function) Logger::logwarn, + ANSI.MAGENTA_BG, ANSI.MAGENTA), + arguments("This is a sample info message", LoggingType.INFO, + (Function) Logger::loginfo, + ANSI.BLUE_BG, ANSI.BLUE), + arguments("This is a sample debug message", LoggingType.DEBUG, + (Function) Logger::logdebug, + ANSI.YELLOW_BG, ANSI.YELLOW), + arguments("This is a sample progress message", LoggingType.PROGRESS, + (Function) Logger::logprogress, + ANSI.GREEN_BG, ANSI.GREEN), + arguments("This is a sample misc message", LoggingType.MISCELLANEOUS, + (Function) Logger::logmiscellaneous, + ANSI.CYAN_BG, ANSI.CYAN) + ); + } + + @ParameterizedTest + @MethodSource("ansiColorTestData") + void testErrorMessageFormat(String message, LoggingType logType, Function logFunction, String BGCode, String CharCode) { + // When + String timestamp = logFunction.apply(message); + String output = consoleCapture.getOutput(); + + // Then + assertNotNull(timestamp, "Timestamp should not be null"); + assertTrue(output.contains("[" + timestamp + "]"), "Output should contain timestamp in brackets"); + assertTrue(output.contains(BGCode + logType.toString() +":" + ANSI.RESET), "Should contain" + BGCode + "background for LogType"); + assertTrue(output.contains(CharCode + message + ANSI.RESET), "Should contain" + CharCode + "text for message"); + } + + private static Stream testLogVisibilityDataProvider() { + return Stream.of( + arguments(LoggingType.MISCELLANEOUS, (Function) Logger::logmiscellaneous, "MISC MESSAGE"), + arguments(LoggingType.INFO, (Function) Logger::loginfo, "INFO MESSAGE"), + arguments(LoggingType.WARN, (Function) Logger::logwarn, "WARN MESSAGE"), + arguments(LoggingType.DEBUG, (Function) Logger::logdebug, "DEBUG MESSAGE"), + arguments(LoggingType.PROGRESS, (Function) Logger::logprogress, "PROGRESS MESSAGE"), + arguments(LoggingType.ERROR, (Function) Logger::logerror, "ERROR MESSAGE") + ); + } + + // Test that when true the log shows, then when false it does not show + @ParameterizedTest + @MethodSource("testLogVisibilityDataProvider") + void testLogVisibility(LoggingType logType, Function func, String message) { + + // Message should appear in console output + func.apply(message); + String output = consoleCapture.getOutput(); + assertTrue(output.contains(message)); + + Logger.changeLogVisibility(logType, false); + consoleCapture.clearCapture(); + + // Message should no longer appear in console output + func.apply(message); + output = consoleCapture.getOutput(); + assertFalse(output.contains(message)); + + } + + private static Stream testNullStringsDataProvider() { + return Stream.of( + arguments((Function) Logger::logmiscellaneous, null), + arguments((Function) Logger::loginfo), + arguments((Function) Logger::logwarn), + arguments((Function) Logger::logdebug), + arguments((Function) Logger::logprogress), + arguments((Function) Logger::logerror) + ); + } + + // Test that each logging type will output null as a String when given it + @ParameterizedTest + @MethodSource("testNullStringsDataProvider") + void testNullStrings(Function func) { + func.apply(null); + + String output = consoleCapture.getOutput(); + + assertTrue(output.contains("null")); + + consoleCapture.clearCapture(); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/rhys_h_walker/TestFileManagement.java b/src/test/java/com/github/rhys_h_walker/TestFileManagement.java new file mode 100644 index 0000000..d248607 --- /dev/null +++ b/src/test/java/com/github/rhys_h_walker/TestFileManagement.java @@ -0,0 +1,64 @@ +package com.github.rhys_h_walker; + +import java.io.File; +import java.time.LocalDateTime; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + + +import com.github.rhys_h_walker.misc.Utilities; +import com.github.rhys_h_walker.testing_utilities.Helpers; + +/** + * Test general file output: + * - File writing + - File location/creation + - Directory structure creation + - File content verification + + - Will implement file rotation + */ +public class TestFileManagement { + + private static Stream locateFileTestData() { + + File applicationDirectory = new File(System.getProperty("user.home")+File.separator+"OnRailsLogging/TestFileManagement"); + + return Stream.of( + arguments(applicationDirectory, LocalDateTime.of(2004, 3, 30, 12, 12, 12)), + arguments(applicationDirectory, LocalDateTime.of(2005, 8, 30, 10, 15, 50)), + arguments(applicationDirectory, LocalDateTime.of(1976, 01, 16, 23, 10, 1)), + arguments(applicationDirectory, LocalDateTime.of(1, 1, 1, 1, 1, 1)), + arguments(applicationDirectory, LocalDateTime.of(9999, 12, 30, 23, 59, 59)), + arguments(applicationDirectory, LocalDateTime.of(1976, 2, 21, 19, 25, 50)), + arguments(applicationDirectory, LocalDateTime.of(2007, 12, 22, 19, 14, 44)) + ); + } + + @ParameterizedTest + @MethodSource("locateFileTestData") + void testLocateFile(File applicationDirectory, LocalDateTime timestamp) { + + FileManagement.locateFile(timestamp, applicationDirectory); + + // This is our expected directory + File location = new File(Utilities.formatFilepathFromTimestamp(timestamp, applicationDirectory)); + + // Creation of the file only happens on write to we can just check path + assertTrue(location.getParentFile().exists()); + + } + + @AfterAll + static void cleanup() { + // Remove application called Test + Helpers.deleteNonEmptyTestDirectory("TestFileManagement"); + } +} diff --git a/src/test/java/com/github/rhys_h_walker/TestFileOutput.java b/src/test/java/com/github/rhys_h_walker/TestFileOutput.java index 9bfa9ec..296f0e7 100644 --- a/src/test/java/com/github/rhys_h_walker/TestFileOutput.java +++ b/src/test/java/com/github/rhys_h_walker/TestFileOutput.java @@ -8,6 +8,7 @@ import java.util.function.Function; import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -15,6 +16,7 @@ import com.github.rhys_h_walker.core_enums.LoggingType; import com.github.rhys_h_walker.misc.TimestampDecoder; +import com.github.rhys_h_walker.testing_utilities.Helpers; /** * A test class testing the output of logs to a file @@ -25,26 +27,23 @@ public class TestFileOutput { @BeforeAll public static void startup() { // Create a test-specific directory in temp space (Allows functionality on CI) - String tempDir = System.getProperty("java.io.tmpdir"); - File testDir = new File(tempDir, "OnRailsLogging-test"); + String tempDir = System.getProperty("java.io.tmpdir")+File.separator+"OnRailsLogging"+File.separator+"TestFileOutput"; + + File testDir = new File(System.getProperty("user.home"), "OnRailsLogging"+File.separator+"TestFileOutput"); + + testDir.mkdir(); if (!testDir.exists()) { - boolean created = testDir.mkdirs(); + boolean created = new File(tempDir).mkdirs(); if (!created) { throw new RuntimeException("Failed to create test directory: " + testDir.getAbsolutePath()); } + // Override user.home to point to our test directory + System.setProperty("user.home", testDir.getParent()); } - // Override user.home to point to our test directory - System.setProperty("user.home", testDir.getParent()); - - // Print debug info for CI - System.out.println("Test directory: " + testDir.getAbsolutePath()); - System.out.println("User home: " + System.getProperty("user.home")); - System.out.println("Can write: " + testDir.canWrite()); - // Startup a new application for this file and set logging to NONE - Logger.initializeLogger("TestFileOutput", LoggingType.logVisibilityAllFalse()); + Logger.initializeLogger("TestFileOutput", LoggingType.logVisibilityAllFalse(), false); } private static Stream loggingTypesDataProvider() { @@ -74,5 +73,12 @@ void testAllLogTypes(String message, LoggingType type, Function // Assert that the formatted line is present in the file assertTrue(Helpers.isStringInList(Helpers.formatALine(timestamp, type, message), fileContents)); } + + @AfterAll + static void cleanup() { + // Remove application called Test + Logger.shutdown(); + Helpers.deleteNonEmptyTestDirectory("TestFileOutput"); + } } diff --git a/src/test/java/com/github/rhys_h_walker/TestHelpers.java b/src/test/java/com/github/rhys_h_walker/TestHelpers.java index 30ad463..71bfdf7 100644 --- a/src/test/java/com/github/rhys_h_walker/TestHelpers.java +++ b/src/test/java/com/github/rhys_h_walker/TestHelpers.java @@ -13,6 +13,7 @@ import static org.junit.jupiter.params.provider.Arguments.arguments; import com.github.rhys_h_walker.core_enums.LoggingType; +import com.github.rhys_h_walker.testing_utilities.Helpers; /** * A class designed to help the Helpers class diff --git a/src/test/java/com/github/rhys_h_walker/TestLogFactory.java b/src/test/java/com/github/rhys_h_walker/TestLogFactory.java new file mode 100644 index 0000000..a4aded8 --- /dev/null +++ b/src/test/java/com/github/rhys_h_walker/TestLogFactory.java @@ -0,0 +1,56 @@ +package com.github.rhys_h_walker; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import com.github.rhys_h_walker.core_enums.LoggingType; +import com.github.rhys_h_walker.factories.LogFactory; +import com.github.rhys_h_walker.testing_utilities.Helpers; + +/** + * Generalised tests relating to the LogFactory + */ +public class TestLogFactory { + + private static String appName = "LogFactoryTests"; + + @AfterAll + static void cleanup() { + Helpers.deleteNonEmptyTestDirectory(appName); + } + + // Test that the Facotory is initialized after a name is passed + @Test + void testInstantiation() { + LogFactory logFactory = new LogFactory(appName); + + assertTrue(logFactory.initialized()); + + logFactory.cleanup(); + } + + @Test + void testTimestampChanges() { + LogFactory logFactory = new LogFactory(appName); + assertTrue(logFactory.initialized()); + + String timestamp = logFactory.createNewLog("Log1", LoggingType.ERROR); + + try { + Thread.sleep(1001); // Make sure timestamps are different + } catch (InterruptedException e) { + System.err.println("Thread sleep has been interrupted, failing test"); + assertTrue(false); + } + + String timestamp2 = logFactory.createNewLog("Log2", LoggingType.ERROR); + + assertNotEquals(timestamp, timestamp2); + + logFactory.cleanup(); + } + +} diff --git a/src/test/java/com/github/rhys_h_walker/TestLoggingType.java b/src/test/java/com/github/rhys_h_walker/TestLoggingType.java new file mode 100644 index 0000000..463e6bf --- /dev/null +++ b/src/test/java/com/github/rhys_h_walker/TestLoggingType.java @@ -0,0 +1,52 @@ +package com.github.rhys_h_walker; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.github.rhys_h_walker.core_enums.LoggingType; + +/** + * Ensure that any methods associated with LoggingType work correctly + */ + +public class TestLoggingType { + + + private static Stream testVisibilityMapCorrectnessDataProvider() { + // ArrayList represents those that should be True + return Stream.of( + arguments(LoggingType.defaultVisibility(), new ArrayList(List.of(LoggingType.ERROR))), + arguments(LoggingType.logVisibilityAllFalse(), new ArrayList()), + arguments(LoggingType.logVisibilityAllTrue(), new ArrayList(Arrays.asList(LoggingType.values()))) + ); + } + + // Check that the HashMaps being returned are correct + @ParameterizedTest + @MethodSource("testVisibilityMapCorrectnessDataProvider") + void testVisibilityMapCorrectness(HashMap mapToCheck, ArrayList shouldTrue) { + + for (LoggingType key : mapToCheck.keySet()) { + if (shouldTrue.contains(key)) { + assertTrue(mapToCheck.get(key)); + continue; + } + + assertFalse(mapToCheck.get(key)); + } + + } + + +} diff --git a/src/test/java/com/github/rhys_h_walker/TestRuntimeConfigurations.java b/src/test/java/com/github/rhys_h_walker/TestRuntimeConfigurations.java new file mode 100644 index 0000000..0ec0b63 --- /dev/null +++ b/src/test/java/com/github/rhys_h_walker/TestRuntimeConfigurations.java @@ -0,0 +1,151 @@ +package com.github.rhys_h_walker; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.HashMap; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.github.rhys_h_walker.core_enums.LoggingType; + +/** + * A test class which tests the alteration of different runtime configurations + * + * Initialization occurs per test, we are testing different initialization options + * Ensure resources are cleaned up between tests + */ + +public class TestRuntimeConfigurations { + + // Test that the default configuration is applied here + @Test + void testDefaultConfiguration() { + Logger.initializeLogger("Test", true); + + HashMap defaultExpected = LoggingType.defaultVisibility(); + HashMap set = Logger.viewVisibilityMap(); + + assertTrue(defaultExpected.equals(set)); + + Logger.shutdown(); + } + + // Test that setting an application name works correctly + @Test + void testGetApplicationName() { + String appName = "Test"; + Logger.initializeLogger(appName, true); + + assertTrue(appName.equals(Logger.getApplicationName())); + + Logger.shutdown(); + } + + // Test that shutdown acts as expected, change name and visibility of ERROR + @Test + void testShutdown() { + String startName = "Test"; + String changedName = "Test1"; + + assertNotEquals(startName, changedName); + + Logger.initializeLogger(startName, true); + + assertEquals(Logger.getApplicationName(), startName); + assertTrue(Logger.viewLogVisibility(LoggingType.ERROR)); + + Logger.shutdown(); + Logger.initializeLogger(changedName, LoggingType.logVisibilityAllFalse(), true); + + assertEquals(Logger.getApplicationName(), changedName); + assertFalse(Logger.viewLogVisibility(LoggingType.ERROR)); + + Logger.shutdown(); + } + + // + // TESTS BELOW ARE PARAMETERISED + // + + private static Stream checkMapSetDataProvider() { + return Stream.of( + arguments(LoggingType.defaultVisibility()), + arguments(LoggingType.logVisibilityAllFalse()), + arguments(LoggingType.logVisibilityAllTrue()) + ); + } + + // Test setting a custom HashMap works as expected + @ParameterizedTest + @MethodSource("checkMapSetDataProvider") + void testDifferentmaps(HashMap mapToSet) { + Logger.initializeLogger("Test", mapToSet, true); + + HashMap set = Logger.viewVisibilityMap(); + + assertEquals(set, mapToSet); + + Logger.shutdown(); + + } + + private static Stream testViewLogVisibilityDataProvider() { + return Stream.of( + arguments(LoggingType.MISCELLANEOUS, true), + arguments(LoggingType.INFO, true), + arguments(LoggingType.WARN, true), + arguments(LoggingType.DEBUG, true), + arguments(LoggingType.PROGRESS, true), + arguments(LoggingType.ERROR, false) + ); + } + + // Test that viewing a log matches the expected output and changing it matches + @ParameterizedTest + @MethodSource("testViewLogVisibilityDataProvider") + void testViewAndAlterLogVisibility(LoggingType log, Boolean logSet) { + + Logger.initializeLogger("Test", true); + + assertTrue(Logger.viewLogVisibility(log).equals(!logSet)); + + Logger.changeLogVisibility(log, logSet); + + assertTrue(Logger.viewLogVisibility(log).equals(logSet)); + + Logger.shutdown(); + } + + private static Stream invalidApplicationNameDataProvider() { + return Stream.of( + arguments(null, null), + arguments("", null), + arguments(" ", null), + arguments("\t\n", null), + arguments("app", "app_name_"), + arguments("app:name", "app_name"), + arguments("app|name", "app_name"), + arguments("app*name", "app_name"), + arguments("app?name", "app_name"), + arguments("app/name\\test", "app_name_test"), + arguments("very_long_application_name_that_exceeds_reasonable_file_system_limits_and_should_be_truncated_appropriately_for_safety", "very_long_application_name_that_exceeds_reasonable_file_system_limits_and_should_be_truncated_approp") + ); + } + + // Test that invalid names are converted correctly + @ParameterizedTest + @MethodSource("invalidApplicationNameDataProvider") + void invalidApplicationName(String name, String expected) { + Logger.initializeLogger(name, true); + assertEquals(expected, Logger.getApplicationName()); + } + +} diff --git a/src/test/java/com/github/rhys_h_walker/TestTimestampDecoder.java b/src/test/java/com/github/rhys_h_walker/TestTimestampDecoder.java index 08a1982..cc516c9 100644 --- a/src/test/java/com/github/rhys_h_walker/TestTimestampDecoder.java +++ b/src/test/java/com/github/rhys_h_walker/TestTimestampDecoder.java @@ -24,12 +24,12 @@ public class TestTimestampDecoder { public static void startup() { // Startup a new application for this file // No output to console in this test is required - Logger.initializeLogger("TestFileOutput", LoggingType.logVisibilityAllFalse()); + Logger.initializeLogger("Test", LoggingType.logVisibilityAllFalse(), true); } private static Stream testTimestampDecodedProvider() { - String knownExtension = System.getProperty("user.home")+File.separator+"OnRailsLogging"+File.separator+"TestFileOutput"+File.separator; + String knownExtension = System.getProperty("user.home")+File.separator+"OnRailsLogging"+File.separator+"Test"+File.separator; return Stream.of ( arguments("2025-07-18-14:52:26", knownExtension+"2025"+File.separator+"7"+File.separator+"18"+File.separator+"14_52_26.log"), diff --git a/src/test/java/com/github/rhys_h_walker/testing_utilities/ConsoleCapture.java b/src/test/java/com/github/rhys_h_walker/testing_utilities/ConsoleCapture.java new file mode 100644 index 0000000..0500db1 --- /dev/null +++ b/src/test/java/com/github/rhys_h_walker/testing_utilities/ConsoleCapture.java @@ -0,0 +1,38 @@ +package com.github.rhys_h_walker.testing_utilities; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +/** + * A simple console output capture class + */ + +public class ConsoleCapture { + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + public void startCapture() { + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + } + + public void stopCapture() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + public String getOutput() { + return outContent.toString(); + } + + public String getErrorOutput() { + return errContent.toString(); + } + + public void clearCapture() { + outContent.reset(); + errContent.reset(); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/rhys_h_walker/testing_utilities/Helpers.java b/src/test/java/com/github/rhys_h_walker/testing_utilities/Helpers.java new file mode 100644 index 0000000..d15ed01 --- /dev/null +++ b/src/test/java/com/github/rhys_h_walker/testing_utilities/Helpers.java @@ -0,0 +1,87 @@ +package com.github.rhys_h_walker.testing_utilities; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.github.rhys_h_walker.core_enums.LoggingType; + +public class Helpers { + + + public static boolean isStringInList(String fullMatch, ArrayList listToSearch) { + + boolean lineFound = false; + + for (String line : listToSearch) { + + if (line.equals(fullMatch)) { + + lineFound = true; + } + } + return lineFound; + } + + public static String formatALine(String timestamp, LoggingType logType, String message) { + return "["+timestamp+"] " + logType.toString() + ": " + message; + } + + public static synchronized void deleteNonEmptyTestDirectory(String baseDirectory) { + String path = System.getProperty("user.home")+File.separator+"OnRailsLogging"+File.separator+baseDirectory; + + deleteFilesVisitDirectories(path); + + try { + Files.deleteIfExists(Paths.get(path)); + } catch (IOException e) { + System.err.println("IOException when deleting directory " + path +"\n" + e.toString()); + } catch (SecurityException e) { + System.err.println("SecurityException when deleting directory " + path +"\n" + e.toString()); + } + } + + /** + * Recursively delete all files and directories + * @param path + */ + private static void deleteFilesVisitDirectories(String path) { + Set files = Stream.of(new File(path).listFiles()) + .filter(file -> !file.isDirectory()) + .map(File::getName) + .collect(Collectors.toSet()); + + for (String file : files) { + try { + Files.deleteIfExists(Paths.get(path, file)); + } catch (IOException e) { + System.err.println("IOException when deleting file " + file +"\n" + e.toString()); + } catch (SecurityException e) { + System.err.println("SecurityException when deleting file " + file +"\n" + e.toString()); + } + } + + Set directories = Stream.of(new File(path).listFiles()) + .filter(file -> file.isDirectory()) + .map(File::getName) + .collect(Collectors.toSet()); + + for (String directory : directories) { + deleteFilesVisitDirectories(path+ File.separator + directory); + try { + Files.deleteIfExists(Paths.get(path, directory)); + } catch (IOException e) { + System.err.println("IOException when deleting directory " + directory +"\n" + e.toString()); + } catch (SecurityException e) { + System.err.println("SecurityException when deleting directory " + directory +"\n" + e.toString()); + } + } + + return; + } +}