From a1f111b6f3f21e3290e9fbbd8203d542bf655bd5 Mon Sep 17 00:00:00 2001 From: "Ewanseiha Odion (Student)" <ewanseiha2.odion@live.uwe.ac.uk@csctcloud.prxhn32zsyjupl12zde3wlfkch.cwx.internal.cloudapp.net> Date: Tue, 2 Apr 2024 09:13:08 +0100 Subject: [PATCH] added files --- .gitgnore | 1 + .gitmodules | 6 + .vscode/configurationCache.log | 1 + .vscode/dryrun.log | 7 + README.md | 109 +++++---- build/app | 0 build/app2 | 0 build/log.o | 0 build/main.o | 0 build/main_part2.o | 0 build/simpletest.o | 0 build/test | 0 iot-starter-0 | 1 + iot-starter-0-1 | 1 + log.in | 2 + makeFile | 75 +++++++ screenshots/task 1.png | Bin 0 -> 12093 bytes screenshots/task2.png | Bin 0 -> 3388 bytes screenshots/task3.png | Bin 0 -> 13006 bytes src/log.cpp | 86 ++++++++ src/log.hpp | 15 ++ src/main.cpp | 34 +++ src/main_part2.cpp | 34 +++ src/simpletest.cpp | 391 +++++++++++++++++++++++++++++++++ src/simpletest.h | 235 ++++++++++++++++++++ src/unit_test.cpp | 34 +++ 26 files changed, 973 insertions(+), 59 deletions(-) create mode 100644 .gitgnore create mode 100644 .gitmodules create mode 100644 .vscode/configurationCache.log create mode 100644 .vscode/dryrun.log create mode 100644 build/app create mode 100644 build/app2 create mode 100644 build/log.o create mode 100644 build/main.o create mode 100644 build/main_part2.o create mode 100644 build/simpletest.o create mode 100644 build/test create mode 160000 iot-starter-0 create mode 160000 iot-starter-0-1 create mode 100644 log.in create mode 100644 makeFile create mode 100644 screenshots/task 1.png create mode 100644 screenshots/task2.png create mode 100644 screenshots/task3.png create mode 100644 src/log.cpp create mode 100644 src/log.hpp create mode 100644 src/main.cpp create mode 100644 src/main_part2.cpp create mode 100644 src/simpletest.cpp create mode 100644 src/simpletest.h create mode 100644 src/unit_test.cpp diff --git a/.gitgnore b/.gitgnore new file mode 100644 index 0000000..c795b05 --- /dev/null +++ b/.gitgnore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..fdfd20e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "simpletest_test"] + path = simpletest_test + url = https://github.com/kudaba/simpletest_test.git +[submodule "simpletest"] + path = simpletest + url = https://github.com/kudaba/simpletest.git diff --git a/.vscode/configurationCache.log b/.vscode/configurationCache.log new file mode 100644 index 0000000..697d0b3 --- /dev/null +++ b/.vscode/configurationCache.log @@ -0,0 +1 @@ +{"buildTargets":["all","build","build/","build/app","build/app2","build/log.o","build/main.o","build/main_part2.o","build/simpletest.o","build/test","clean","log_level","log_message","run"],"launchTargets":["Makefile | /home/ewanseiha2.odion/iot_starter-1/build>app()"],"customConfigurationProvider":{"workspaceBrowse":{"browsePath":["/home/ewanseiha2.odion/iot_starter-1/include","/home/ewanseiha2.odion/iot_starter-1/src"],"compilerArgs":["-lpthread","-o","build/app","./src/main.cpp","-lpthread","-o","build/app","./src/main.cpp","-lpthread","-o","build/app","./src/main.cpp","-lpthread","-o","build/app","./src/main.cpp","-lpthread","-o","build/app","./src/main.cpp","-lpthread","-o","build/app","./src/main.cpp","build/main_part2.o","-lpthread","-o","build/app","./src/main.cpp","build/main_part2.o","build/main.o","-lpthread","-o","build/app2","./src/main_part2.cpp","build/main_part2.o","build/main.o","-lpthread","-o","build/app2","./src/main_part2.cpp","build/log.o","-lpthread","-o","build/app2","./src/unit_test.cpp","build/log.o","build/simpletest.o"]},"fileIndex":[["/home/ewanseiha2.odion/iot_starter-1/src/main.cpp",{"uri":{"$mid":1,"fsPath":"/home/ewanseiha2.odion/iot_starter-1/src/main.cpp","path":"/home/ewanseiha2.odion/iot_starter-1/src/main.cpp","scheme":"file"},"configuration":{"defines":[],"standard":"c++17","includePath":["/home/ewanseiha2.odion/iot_starter-1/include"],"forcedInclude":[],"intelliSenseMode":"clang-x64","compilerPath":"/usr/bin/clang++","compilerArgs":["-lpthread","-o","build/app","./src/main.cpp","build/log.o"],"windowsSdkVersion":""},"compileCommand":{"command":"clang++ -std=c++17 -I./include -lpthread -o build/app ./src/main.cpp build/log.o","directory":"/home/ewanseiha2.odion/iot_starter-1","file":"/home/ewanseiha2.odion/iot_starter-1/src/main.cpp"}}],["/home/ewanseiha2.odion/iot_starter-1/src/main_part2.cpp",{"uri":{"$mid":1,"fsPath":"/home/ewanseiha2.odion/iot_starter-1/src/main_part2.cpp","path":"/home/ewanseiha2.odion/iot_starter-1/src/main_part2.cpp","scheme":"file"},"configuration":{"defines":[],"standard":"c++17","includePath":["/home/ewanseiha2.odion/iot_starter-1/include"],"forcedInclude":[],"intelliSenseMode":"clang-x64","compilerPath":"/usr/bin/clang++","compilerArgs":["-lpthread","-o","build/app2","./src/main_part2.cpp","build/log.o"],"windowsSdkVersion":""},"compileCommand":{"command":"clang++ -std=c++17 -I./include -lpthread -o build/app2 ./src/main_part2.cpp build/log.o","directory":"/home/ewanseiha2.odion/iot_starter-1","file":"/home/ewanseiha2.odion/iot_starter-1/src/main_part2.cpp"}}],["/home/ewanseiha2.odion/iot_starter-1/src/log.cpp",{"uri":{"$mid":1,"fsPath":"/home/ewanseiha2.odion/iot_starter-1/src/log.cpp","path":"/home/ewanseiha2.odion/iot_starter-1/src/log.cpp","scheme":"file"},"configuration":{"defines":[],"standard":"c++17","includePath":["/home/ewanseiha2.odion/iot_starter-1/include"],"forcedInclude":[],"intelliSenseMode":"clang-x64","compilerPath":"/usr/bin/clang++","compilerArgs":["-c","-o","build/log.o"],"windowsSdkVersion":""},"compileCommand":{"command":"clang++ -c -std=c++17 -I./include ./src/log.cpp -o build/log.o","directory":"/home/ewanseiha2.odion/iot_starter-1","file":"/home/ewanseiha2.odion/iot_starter-1/src/log.cpp"}}],["/home/ewanseiha2.odion/iot_starter-1/src/unit_test.cpp",{"uri":{"$mid":1,"fsPath":"/home/ewanseiha2.odion/iot_starter-1/src/unit_test.cpp","path":"/home/ewanseiha2.odion/iot_starter-1/src/unit_test.cpp","scheme":"file"},"configuration":{"defines":[],"standard":"c++17","includePath":["/home/ewanseiha2.odion/iot_starter-1/include"],"forcedInclude":[],"intelliSenseMode":"clang-x64","compilerPath":"/usr/bin/clang++","compilerArgs":["-lpthread","-o","build/app2","./src/unit_test.cpp","build/log.o","build/simpletest.o"],"windowsSdkVersion":""},"compileCommand":{"command":"clang++ -std=c++17 -I./include -lpthread -o build/app2 ./src/unit_test.cpp build/log.o build/simpletest.o","directory":"/home/ewanseiha2.odion/iot_starter-1","file":"/home/ewanseiha2.odion/iot_starter-1/src/unit_test.cpp"}}]]}} \ No newline at end of file diff --git a/.vscode/dryrun.log b/.vscode/dryrun.log new file mode 100644 index 0000000..5cd357d --- /dev/null +++ b/.vscode/dryrun.log @@ -0,0 +1,7 @@ +make --dry-run --keep-going --print-directory +make: Entering directory '/home/ewanseiha2.odion/iot_starter-1' + +make: Nothing to be done for 'all'. + +make: Leaving directory '/home/ewanseiha2.odion/iot_starter-1' + diff --git a/README.md b/README.md index 54b5031..5bbc6c5 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,84 @@ -# iot-starter-0 +The first task I worked with function, it was divided into three part +In the first part I implemented a function "std::string line(std::string message);" that returns a log line'd message +"line("[ERROR]: Invalid operation") +// => "Invalid operation"" +I added this line function in the main_part source file and then included some tests for the line fuction + std::string logMessage = "[ERROR]: Invalid Operation"; + std::string logMessage_2 = "[INFO]: This is a log message"; +After i saved changes ,built and ran the program. +As i encountered minor issues i fixed it. At the end It printed "Invalid operation" in the terminal. +In the second part of the task I implemented the function "std::string level(std::string message);" that returns a log line's log level whuch should return +"level("[ERROR]: Invalid operation") +// => "ERROR"" +Once again I added the line function in the main_part source file and include some tests for the level function then saved the changes. -## Getting started +Build and run the program, then fixed any issues.Once the program builds successfully, I ran it to verify that the level function behaves as expected. It should print "ERROR" to the console in uppercase. -To make it easy for you to get started with GitLab, here's a list of recommended next steps. -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! -## Add your files +For the third part, I implement was std::string reformat(std::string ) that reformat the log line, puttting the message first before and the log level after it in parentheses. +firstly, i added the 'reformat' function to the main_part source file and included some tests for the reformat function.Saved the changes followed up by building and run the program. If there were any issues, fixed them. -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: +Once the program builds successfully,I ran it to verify that the reformat function behaves as expected. It should print success to the terminal. -``` -cd existing_repo -git remote add origin https://gitlab.uwe.ac.uk/eo2-odion/iot-starter-0.git -git branch -M main -git push -uf origin main -``` +Below is a screenshot of the expected output + -## Integrate with your tools -- [ ] [Set up project integrations](https://gitlab.uwe.ac.uk/eo2-odion/iot-starter-0/-/settings/integrations) +For the second task I was expected to develop a simple class for process log files, intergrating the operations from task 1. For this i implemented new files namely 'log.hpp' and 'log.cpp' -## Collaborate with your team +Firstly, I began by creating two new files, log.hpp and log.cpp, where the header file contains the interface for the logging API and the source file contains its implementation. Ensuring the header file starts with #pragma once to prevent multiple inclusions. Modifying the makefile to include these new source files by adding them to the CPP_SOURCES variable. -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) + In the log.hpp file,I defined the Log class with methods for creating a log, navigating to the next message, retrieving the current line and level, and reformatting the log message. Unlike before, these methods don't take any arguments. Implementing each method in log.cpp using the Log::method_name(arguments) syntax. -## Test and Deploy + The constructor Log() initializes the log object, while the destructor ~Log() closes the log file if it was opened successfully with create_log(). Implementing these appropriately in the log.cpp. -Use the built-in continuous integration in GitLab. + I made use of file handling libraries such as <iostream> and <fstream> for C++ streams or <stdio.h> for C file handling to open and manipulate log files. The create_log() method opens the specified log file, returning true if successful and false otherwise. -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) +I implemented each method defined in the Log class, ensuring that they perform their intended functions, such as moving to the next message, retrieving the line and level, and reformatting the log message. -*** +I created a new main_part2.cpp file to test the implementation of the Log class.I used this file to instantiate a Log object, call its methods, and verify that they work as expected. -# Editing this README +I added a new target in the makefile to build the app2 executable for testing the Log class implementation. Ensured that this rule compiles the main_part2.cpp file along with other necessary source files. -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. +I utilized a provided example log file (log.in) to test the implementation of the Log class and ensure that it handles log files correctly. -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +Below is a screenshot of the expected output for this task + -## Name -Choose a self-explaining name for your project. -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +For the third task I was expected to develop unit tests to validate the Log class implementation. For this i was expected to use the framework simpletest and add simpletest as a submodule to my git repo. +Firstly, I added Simpletest as a submodule to your git repository using the command: -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. +csharp +Copy code +git submodule add repo-url +Replacing repo-url with the URL of the Simpletest repository. -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +I created a new file, simpletest_log.cpp, where I write unit tests for the Log class. Use the provided example as a template to structure the tests. -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. +I included the log.hpp header file and the simpletest.h header file in the test_log.cpp file. -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. +I define test groups as an array of character pointers. Each group represents a set of related tests. -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. +Wrote test functions using the DEFINE_TEST_G macro. Each test function should have a unique name and belong to a test group. -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. + Inside each test function, I instantiate the Log class, perform operations, and use TEST_MESSAGE macro to assert conditions and report test results. -## Contributing -State if you are open to contributions and what your requirements are for accepting them. +I wrote the main function to execute all test groups. Iterate through each group, execute its tests, and aggregate the test results. + +I modified the makefile to include a new rule for building the tests. This rule should compile the test_log.cpp file along with the log.cpp and any other necessary source files. + + Once everything was set up, I built and run the tests using the appropriate commands specified in the makefile. Ensure that all tests pass successfully. + + +Below is a screenshot of the expected output for this task + -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. -## License -For open source projects, say how it is licensed. -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/build/app b/build/app new file mode 100644 index 0000000..e69de29 diff --git a/build/app2 b/build/app2 new file mode 100644 index 0000000..e69de29 diff --git a/build/log.o b/build/log.o new file mode 100644 index 0000000..e69de29 diff --git a/build/main.o b/build/main.o new file mode 100644 index 0000000..e69de29 diff --git a/build/main_part2.o b/build/main_part2.o new file mode 100644 index 0000000..e69de29 diff --git a/build/simpletest.o b/build/simpletest.o new file mode 100644 index 0000000..e69de29 diff --git a/build/test b/build/test new file mode 100644 index 0000000..e69de29 diff --git a/iot-starter-0 b/iot-starter-0 new file mode 160000 index 0000000..f3dfeba --- /dev/null +++ b/iot-starter-0 @@ -0,0 +1 @@ +Subproject commit f3dfebab3c8e817ed2648a051e07f629739117b3 diff --git a/iot-starter-0-1 b/iot-starter-0-1 new file mode 160000 index 0000000..f3dfeba --- /dev/null +++ b/iot-starter-0-1 @@ -0,0 +1 @@ +Subproject commit f3dfebab3c8e817ed2648a051e07f629739117b3 diff --git a/log.in b/log.in new file mode 100644 index 0000000..86571c0 --- /dev/null +++ b/log.in @@ -0,0 +1,2 @@ +[ERROR]: Invalid operation +[INFO]: Operation completed \ No newline at end of file diff --git a/makeFile b/makeFile new file mode 100644 index 0000000..7dc5177 --- /dev/null +++ b/makeFile @@ -0,0 +1,75 @@ + + +CC = clang++ +LD = clang++ +CPPFLAGS = -std=c++17 -I./include +LDFLAGS = -lpthread +ROOTDIR = ./ +CP = cp +ECHO = echo +BUILD_DIR = build +CPP_SOURCES = ./src/log.cpp ./simpletest/simpletest.cpp +CPP_HEADERS = ./src/log.hpp ./simpletest/simpletest.h +C_SOURCES = +APP = app +APP2 = app2 +TEST = test + + + +OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(CPP_SOURCES:.cpp=.o))) + +vpath %.cpp $(sort $(dir $(CPP_SOURCES))) +vpath %.cpp src + +# OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o))) +# vpath %.c $(sort $(dir $(C_SOURCES))) + +$(BUILD_DIR)/%.o: %.cpp $(CPP_HEADERS) Makefile | $(BUILD_DIR) + $(ECHO) compiling $< + $(CC) -c $(CPPFLAGS) $< -o $@ + +$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR) + $(ECHO) compiling $< + clang -c $(CFLAGS) $< -o $@ + +all: $(BUILD_DIR) $(BUILD_DIR)/$(APP) $(BUILD_DIR)/$(APP2) $(BUILD_DIR)/$(TEST) + +run: $(BUILD_DIR)/$(APP) $(BUILD_DIR)/$(APP2) $(BUILD_DIR)/$(TEST) + $(BUILD_DIR)/$(APP) + $(BUILD_DIR)/$(APP2) + $(BUILD_DIR)/$(TEST) + +$(BUILD_DIR)/$(APP): main.cpp $(OBJECTS) Makefile + $(ECHO) linking $< + $(CC) $(CPPFLAGS) $(LDFLAGS) -o $@ ./src/main.cpp $(OBJECTS) + $(ECHO) success + +$(BUILD_DIR)/$(APP2): main_part2.cpp $(OBJECTS) Makefile + $(ECHO) linking $< + $(CC) $(CPPFLAGS) $(LDFLAGS) -o $@ ./src/main_part2.cpp $(OBJECTS) + $(ECHO) success + +$(BUILD_DIR)/$(TEST): unit_test.cpp $(OBJECTS) Makefile + $(ECHO) linking $< + $(CC) $(CPPFLAGS) $(LDFLAGS) -o $@ ./src/unit_test.cpp $(OBJECTS) + $(ECHO) success + + +$(BUILD_DIR): + mkdir $@ + +####################################### +# Clean up +####################################### +clean: + -rm -fR $(BUILD_DIR)/$(APP) $(BUILD_DIR)/*.o + +####################################### +# Dependencies +####################################### +-include $(shell mkdir .dep 2>/dev/null) $(wildcard .dep/*) + +.PHONY: clean all + + diff --git a/screenshots/task 1.png b/screenshots/task 1.png new file mode 100644 index 0000000000000000000000000000000000000000..1f6bc9bb7bee729c722fa2efcc414f71e7a66cee GIT binary patch literal 12093 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 S6=z^zVDNPHb6Mw<&;$T+ICx3` literal 0 HcmV?d00001 diff --git a/screenshots/task2.png b/screenshots/task2.png new file mode 100644 index 0000000000000000000000000000000000000000..2b86dd5f0b4b04c74e14c9f592afcf81e11bb82a GIT binary patch literal 3388 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%0Af{Uhgt+Nvah zb4+(rW^KLJ)BNV|->pY>U+T5|dgJEG;|ZG#Dhnl^A3j;~H10^kBtx;|`uC4aS>LLc zmw#4vx!`pE)Op(88h(AjTb_SjuD)^pwf69JbEoOZh8yjZ%&b~d!&~!n$FpBIWnA+^ z+4cqP`qzD}Pd-X`-qcBR!nF){eyZ}D@=ZVL(vJNmn@<<lFO+%^8+giKdeg&6??3G0 ze5v>5)AyFuPpc0xB(JxZJ*%&9w{`CQRk5esv@coq&3(anfI$+o{8D=KpRwM3A45q) R$Qw{Y-P6_2Wt~$(699IiM6>_^ literal 0 HcmV?d00001 diff --git a/screenshots/task3.png b/screenshots/task3.png new file mode 100644 index 0000000000000000000000000000000000000000..7c338a1eaca723ebdd787ee49679c538861e17ca GIT binary patch literal 13006 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%0Df^zc26ejTbu$j?83W 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 BOLYJM literal 0 HcmV?d00001 diff --git a/src/log.cpp b/src/log.cpp new file mode 100644 index 0000000..9a268a9 --- /dev/null +++ b/src/log.cpp @@ -0,0 +1,86 @@ +#include <iostream> +#include <sstream> +#include <string> +#include <fstream> +#include "log.hpp" + +Log::Log(){ + +} +Log::~Log(){ + +} + +std::string Log::line(std::string message) { + size_t pos = message.find(':'); + if (pos != std::string::npos) { + return message.substr(pos + 2); + } else { + return message; + } +} + + +std::string Log::level(std::string message) { + std::string level; + bool foundStart = false; + + for (char& c : message) { + if (c == '[') { + foundStart = true; + continue; + } + + if (c == ']' && foundStart) { + break; + } + + if (foundStart) { + level += c; + } + } + + return level; +} +std::string Log::reformat(std::string message) { + // Find the position of ':' + size_t colonPos = message.find(':'); + + if (colonPos != std::string::npos) { + // Extract the info between '[' and ']' + size_t bracketPos = message.find('['); + if (bracketPos != std::string::npos) { + std::string info = message.substr(bracketPos + 1, colonPos - bracketPos - 1); + + // Remove trailing square brackets if present + size_t bracketEndPos = info.find(']'); + if (bracketEndPos != std::string::npos) { + info.erase(bracketEndPos, 1); + } + + // Extract the message after ':' + std::string msg = message.substr(colonPos + 2); // Skipping ': ' characters + + // Construct the reformatted message + return msg + " (" + info + ")"; + } + } + + // Return original message if format is not as expected + return message; +} + +bool Log::create_log(std::string filename) { + std::ifstream file(filename); + return file.good(); + } + +bool Log::next(std::ifstream& file) { + std::string line; + while (std::getline(file, line)) { + if (!line.empty()) { + return true; + } + } + return false; + } diff --git a/src/log.hpp b/src/log.hpp new file mode 100644 index 0000000..fce7344 --- /dev/null +++ b/src/log.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include <string> + +class Log { +private: +public: + Log(); + ~Log(); + bool create_log(std::string filename); + bool next(std::ifstream& file); + std::string line(std::string message); + std::string level(std::string message); + std::string reformat(std::string message); +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..b007956 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,34 @@ +#include <iostream> +#include <sstream> +#include <string> +#include <fstream> +#include "log.hpp" + + + +int main() { + + Log logger; + + bool create = logger.create_log("log.in"); + std::cout << create << std::endl; + + std::ifstream file("log.in"); + + bool nxt = logger.next(file); + std::cout << nxt << std::endl; + + std::string logMessage = "[ERROR]: Invalid Operation"; + std::string logMessage_2 = "[INFO]: This is a log message"; + + std::string logLine = logger.line(logMessage); + std::cout << logLine << std::endl; + + std::string logLine_2 = logger.level(logMessage); + std::cout << logLine_2 << std::endl; + + std::string logLine_3 = logger.reformat(logMessage); + std::cout << logLine_3 << std::endl; + + return 0; +} diff --git a/src/main_part2.cpp b/src/main_part2.cpp new file mode 100644 index 0000000..b007956 --- /dev/null +++ b/src/main_part2.cpp @@ -0,0 +1,34 @@ +#include <iostream> +#include <sstream> +#include <string> +#include <fstream> +#include "log.hpp" + + + +int main() { + + Log logger; + + bool create = logger.create_log("log.in"); + std::cout << create << std::endl; + + std::ifstream file("log.in"); + + bool nxt = logger.next(file); + std::cout << nxt << std::endl; + + std::string logMessage = "[ERROR]: Invalid Operation"; + std::string logMessage_2 = "[INFO]: This is a log message"; + + std::string logLine = logger.line(logMessage); + std::cout << logLine << std::endl; + + std::string logLine_2 = logger.level(logMessage); + std::cout << logLine_2 << std::endl; + + std::string logLine_3 = logger.reformat(logMessage); + std::cout << logLine_3 << std::endl; + + return 0; +} diff --git a/src/simpletest.cpp b/src/simpletest.cpp new file mode 100644 index 0000000..85abbb4 --- /dev/null +++ b/src/simpletest.cpp @@ -0,0 +1,391 @@ +#include "simpletest.h" +#include <stdio.h> +#include <string.h> +#include <cstdint> +#include <stdarg.h> +#include <time.h> + +//--------------------------------------------------------------------------------- +// statics +//--------------------------------------------------------------------------------- +void DefaultPrint(char const* string) { printf("%s", string); } + +TestFixture* TestFixture::ourFirstTest; +TestFixture* TestFixture::ourLastTest; +thread_local TestFixture* TestFixture::ourCurrentTest; +void (*TestFixture::Print)(char const* string) = DefaultPrint; + +//--------------------------------------------------------------------------------- +// Standard type printers +//--------------------------------------------------------------------------------- +TempString::TempString(const TempString& other) +{ + if (other.myTextPointer == other.myTextBuffer) + { + strcpy(myTextBuffer, other.myTextBuffer); + myTextPointer = myTextBuffer; + } + else + { + myTextPointer = other.myTextPointer; + } +} +TempString TypeToString(int value) +{ + TempString tempString; + if (TestFixture::GetCurrentTest()->GetPrintMethod() == TestFixture::PrintHexadecimal) + snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%08X", value); + else + snprintf(tempString.myTextBuffer, STRING_LENGTH, "%d", value); + return tempString; +} +TempString TypeToString(unsigned int value) +{ + TempString tempString; + if (TestFixture::GetCurrentTest()->GetPrintMethod() == TestFixture::PrintHexadecimal) + snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%08X", value); + else + snprintf(tempString.myTextBuffer, STRING_LENGTH, "%u", value); + return tempString; +} +TempString TypeToString(long value) +{ + TempString tempString; + if (TestFixture::GetCurrentTest()->GetPrintMethod() == TestFixture::PrintHexadecimal) + snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%016lX", value); + else + snprintf(tempString.myTextBuffer, STRING_LENGTH, "%ld", value); + return tempString; +} +TempString TypeToString(unsigned long value) +{ + TempString tempString; + if (TestFixture::GetCurrentTest()->GetPrintMethod() == TestFixture::PrintHexadecimal) + snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%016lX", value); + else + snprintf(tempString.myTextBuffer, STRING_LENGTH, "%lu", value); + return tempString; +} +TempString TypeToString(long long value) +{ + TempString tempString; + if (TestFixture::GetCurrentTest()->GetPrintMethod() == TestFixture::PrintHexadecimal) + snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%016llX", value); + else + snprintf(tempString.myTextBuffer, STRING_LENGTH, "%lld", value); + return tempString; +} +TempString TypeToString(unsigned long long value) +{ + TempString tempString; + if (TestFixture::GetCurrentTest()->GetPrintMethod() == TestFixture::PrintHexadecimal) + snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%016llX", value); + else + snprintf(tempString.myTextBuffer, STRING_LENGTH, "%llu", value); + return tempString; +} +TempString TypeToString(float value) +{ + TempString tempString; + snprintf(tempString.myTextBuffer, STRING_LENGTH, "%.16g", value); + return tempString; +} +TempString TypeToString(double value) +{ + TempString tempString; + snprintf(tempString.myTextBuffer, STRING_LENGTH, "%.16g", value); + return tempString; +} +TempString TypeToString(bool value) +{ + return TempString(value ? "true" : "false"); +} +TempString TypeToString(char const* value) +{ + return TempString(value ? value : "(nullptr)"); +} +TempString TypeToString(void const* value) +{ + if (value == nullptr) + return TempString("(nullptr)"); + TempString tempString; + snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%p", value); + return tempString; +} +TempString TypeToString(void const* value, char const* extra) +{ + if (value == nullptr) + return TempString("(nullptr)"); + TempString tempString; + snprintf(tempString.myTextBuffer, STRING_LENGTH, "(0x%p) %s", value, extra); + return tempString; +} + +//--------------------------------------------------------------------------------- +// Fixture implementation +//--------------------------------------------------------------------------------- +TestFixture::TestFixture() + : myNextTest(nullptr) + , myNextError(nullptr) + , myNumTestsChecked(0) + , myNumErrors(0) + , myPrintMethod(PrintDefault) +{ + // global link list registration, add in order of discovery + if (ourFirstTest == nullptr) + { + ourFirstTest = this; + ourLastTest = this; + } + else + { + ourLastTest->myNextTest = this; + ourLastTest = this; + } +} +//--------------------------------------------------------------------------------- +bool TestFixture::ExecuteTest() +{ + myNumTestsChecked = myNumErrors = 0; + myNextError = (TestError*)myMessageSpace; + + TestFixture* lastCurrent = ourCurrentTest; + ourCurrentTest = this; + Setup(); + RunTest(); + TearDown(); + ourCurrentTest = lastCurrent; + + return myNumErrors == 0; +} +//--------------------------------------------------------------------------------- +// Utility to print a part of a string to show where the error is and put elipse +// where the string is truncated +//--------------------------------------------------------------------------------- +static void locCopyStringWithElipse(char dest[STRING_EQ_PRINT_LENGTH], char const* string, size_t offset = 0) +{ + char const* start = string + offset - STRING_EQ_PRINT_LENGTH / 2; + if (start < string) + start = string; + + int i = 0; + for (; i < STRING_EQ_PRINT_LENGTH - 1 && start[i]; ++i) + { + if (i < 3 && start > string) + dest[i] = '.'; + else if (start[i] == '\r' || start[i] == '\n' || start[i] == '\t') + dest[i] = '\\'; // simply replace this with '\', we're just aiming for a general idea not an exact representation + else + dest[i] = start[i]; + } + + dest[i] = 0; + + if (i == STRING_EQ_PRINT_LENGTH - 1 && start[i]) + { + dest[i - 1] = '.'; + dest[i - 2] = '.'; + dest[i - 3] = '.'; + } +} +//--------------------------------------------------------------------------------- +// Instead of just check for error and printing the string, try go be smart about +// how the information is written out: +// ... quick brown fox jumps over ... +// ^ +// ... quick brown fox jamps over ... +//--------------------------------------------------------------------------------- +bool TestFixture::TestStrings(char const* left, char const* right, char const* prefix, char const* condition) +{ + AddTest(); + if (left == right) + { + return true; + } + + char leftLine[STRING_EQ_PRINT_LENGTH]; + char rightLine[STRING_EQ_PRINT_LENGTH]; + char locationLine[STRING_EQ_PRINT_LENGTH]; + + if (left == nullptr || right == nullptr) + { + locationLine[0] = '^'; + locationLine[1] = 0; + if (left == nullptr) + { + strcpy(leftLine, "nullptr"); + locCopyStringWithElipse(rightLine, right); + } + else + { + locCopyStringWithElipse(leftLine, left); + strcpy(rightLine, "nullptr"); + } + } + else + { + char const* testLeft = left; + char const* testRight = right; + + int offset = 0; + for (; *testLeft && *testRight; ++offset, ++testLeft, ++testRight) + { + if (*testLeft != *testRight) + break; + } + + // reached the end of both strings, so they're the same + if (!*testLeft && !*testRight) + return true; + + locCopyStringWithElipse(leftLine, left, offset); + locCopyStringWithElipse(rightLine, right, offset); + + if (offset > STRING_EQ_PRINT_LENGTH / 2) + offset = STRING_EQ_PRINT_LENGTH / 2; + + memset(locationLine, ' ', offset); + locationLine[offset] = '^'; + locationLine[offset + 1] = 0; + } + + AddError(); + LogMessage(prefix, condition, leftLine, locationLine, rightLine); + return false; +} +//--------------------------------------------------------------------------------- +// Write error into current error object and advance pointer if there's still enough space +//--------------------------------------------------------------------------------- +void TestFixture::LogMessage(char const* string, ...) +{ + uintptr_t spaceLeft = (myMessageSpace + MESSAGE_SPACE) - (char*)myNextError; + + if (spaceLeft == 0) + { + return; + } + + spaceLeft -= sizeof(TestError); + + va_list args; + + va_start(args, string); + + int printedChars = vsnprintf(myNextError->message, spaceLeft, string, args); + + va_end(args); + + // if there isn't a reasonable amount of space left then just advance to end and stop printing errors + if (printedChars < (int)(spaceLeft - sizeof(TestError) - 64)) + { + uintptr_t nextOffset = (uintptr_t(myNextError->message) + printedChars + sizeof(TestError) * 2 - 1); + nextOffset -= nextOffset % alignof(TestError); + TestError* next = (TestError*)(nextOffset); + myNextError->next = next; + } + else + { + myNextError->next = (TestError*)(myMessageSpace + MESSAGE_SPACE); + } + + myNextError = myNextError->next; +} +TestFixture const* TestFixture::LinkTest(TestFixture* test) +{ + test->myNextTest = TestFixture::ourFirstTest; + TestFixture::ourFirstTest = test; + return test; +} + +//--------------------------------------------------------------------------------- +// Standard / Example runners +//--------------------------------------------------------------------------------- +static bool locExecuteTest(TestFixture* test, TestFixture::OutputMode output) +{ + clock_t start = 0; + if (output == TestFixture::Verbose) + { + TestFixture::Printf("Running [%s/%s]", test->TestGroup(), test->TestName()); + start = clock(); + } + + if (test->ExecuteTest()) + { + if (output == TestFixture::Verbose) + { + clock_t end = clock(); + TestFixture::Printf(": Passed %d out of %d tests in %g seconds\n", test->NumTests(), test->NumTests(), float(end - start) / (float)CLOCKS_PER_SEC); + } + return true; + } + + if (output != TestFixture::Silent) + { + if (output != TestFixture::Verbose) + TestFixture::Printf("[%s/%s]", test->TestGroup(), test->TestName()); + + TestFixture::Printf(": Failed %d out of %d tests\n", test->NumErrors(), test->NumTests()); + + for (TestError const* err = test->GetFirstError(), *e = test->GetLastError(); err != e; err = err->next) + { + TestFixture::Printf("%s\n", err->message); + } + } + + return false; +} +void TestFixture::Printf(char const* string, ...) +{ + char tempSpace[4096]; + va_list args; + + va_start(args, string); + + vsnprintf(tempSpace, sizeof(tempSpace), string, args); + + va_end(args); + + TestFixture::Print(tempSpace); +} +bool TestFixture::ExecuteAllTests(char const* groupFilter, char const* nameFilter, OutputMode output) +{ + if (output != Silent) + { + if (groupFilter == nullptr && nameFilter == nullptr) + Printf("Running all tests.\n"); + else if (groupFilter != nullptr && nameFilter == nullptr) + Printf("Running all tests in groups [%s].\n", groupFilter); + else if (groupFilter == nullptr && nameFilter != nullptr) + Printf("Running all tests named [%s].\n", nameFilter); + else + Printf("Running all tests named [%s/%s].\n", groupFilter, nameFilter); + } + + int count = 0; + int passes = 0; + int fails = 0; + bool passed = true; + for (auto i = TestFixture::GetFirstTest(); i; i = i->GetNextTest()) + { + bool matchGroup = groupFilter == nullptr || strcmp(groupFilter, i->TestGroup()) == 0; + bool matchName = nameFilter == nullptr || strcmp(nameFilter, i->TestName()) == 0; + if (matchGroup && matchName) + { + ++count; + passed &= locExecuteTest(i, output); + passes += i->NumTests(); + fails += i->NumErrors(); + } + } + + if (output != Silent) + { + if (count == 0) + Printf("Failed to find any tests.\n"); + else if (passed) + Printf("%d Tests finished. All %d assertions are passing.\n", count, passes); + else + Printf("%d Tests finished, %d of %d assertions failed. Some tests are reporting errors.\n", count, fails, passes); + } + return passed; +} diff --git a/src/simpletest.h b/src/simpletest.h new file mode 100644 index 0000000..4659561 --- /dev/null +++ b/src/simpletest.h @@ -0,0 +1,235 @@ +#pragma once + +//--------------------------------------------------------------------------------- +// Config +//--------------------------------------------------------------------------------- +#if !defined(MESSAGE_SPACE) +#define MESSAGE_SPACE 10 * 1024 // default 10k of message space is reserved per test +#endif +#if !defined(STRING_LENGTH) +#define STRING_LENGTH 64 // size of temp strings for converting types +#endif +#if !defined(STRING_EQ_PRINT_LENGTH) +#define STRING_EQ_PRINT_LENGTH 80 // max line length to show when comparing two strings +#endif +#if !defined(BASE_FIXTURE) +#define BASE_FIXTURE TestFixture // use TestFixture as the test base class by default +#endif +#if !defined(ERROR_ACTION) +#define ERROR_ACTION // Defined any code to run on error. You can use this to debug break or do anything really +#endif + +//--------------------------------------------------------------------------------- +// Link list of errors build into MESSAGE_SPACE +//--------------------------------------------------------------------------------- +struct TestError +{ + TestError* next; + char message[1]; +}; + +//--------------------------------------------------------------------------------- +// simple converter of basic types to text +// TODO: Use a global template function for conversion so users can override this +//--------------------------------------------------------------------------------- +struct TempString +{ + TempString() : myTextPointer(myTextBuffer) { myTextBuffer[0] = 0; } + TempString(const TempString& other); + TempString(char const* string) : myTextPointer(string) {} + + char const* operator*() const { return myTextPointer; } + + char const* myTextPointer; + char myTextBuffer[STRING_LENGTH]; +}; + +TempString TypeToString(int value); +TempString TypeToString(unsigned int value); +TempString TypeToString(long value); +TempString TypeToString(unsigned long value); +TempString TypeToString(long long value); +TempString TypeToString(unsigned long long value); +TempString TypeToString(float value); +TempString TypeToString(double value); +TempString TypeToString(bool value); +TempString TypeToString(char const* value); +TempString TypeToString(void const* value); +TempString TypeToString(void const* value, char const* extra); + +inline TempString TypeToString(char value) { return TypeToString((int)value); } +inline TempString TypeToString(unsigned char value) { return TypeToString((unsigned int)value); } +inline TempString TypeToString(short value) { return TypeToString((int)value); } +inline TempString TypeToString(unsigned short value) { return TypeToString((unsigned int)value); } +inline TempString TypeToString(char* value) { return TypeToString((char const*)value); } +inline TempString TypeToString(void* value) { return TypeToString((void const*)value); } + +// if nothing specified then print some memory +template<typename T> +TempString TypeToString(T const&) { return TempString(); } + +template<typename T> +TempString TypeToString(T const* pointer) +{ + return pointer == nullptr ? + TypeToString((void const*)pointer) : + TypeToString((void const*)pointer, *TypeToString(*pointer)); +} + +template<typename T> +TempString TypeToString(T* pointer) { return TypeToString((T const*)pointer); } + +inline TempString TypeToStringFallback(TempString string, char const* fallback) { return (*string)[0] ? string : TempString(fallback); } + +//--------------------------------------------------------------------------------- +// Test fixture is the core of SimpleTest. It provides fixture behavior, access +// to registered tests and stores the results of a test run +// Everything is local here so tests can be multithreaded without any extra work +//--------------------------------------------------------------------------------- +class TestFixture +{ +public: + TestFixture(); + virtual ~TestFixture() {}; + + virtual bool ExecuteTest(); + + virtual char const* TestName() const = 0; + virtual char const* TestGroup() const = 0; + + // Reporting used during testing process + void AddTest() { ++myNumTestsChecked; } + void AddError() { ++myNumErrors; } + void LogMessage(char const* string, ...); + + // Custom test for strings to print out where the comparison failed + bool TestStrings(char const* left, char const* right, char const* prefix, char const* condition); + + // Stats from execution + int NumTests() const { return myNumTestsChecked; } + int NumErrors() const { return myNumErrors; } + + // Access to any errrors generated + TestError const* GetFirstError() const { return (TestError*)myMessageSpace; } + TestError const* GetLastError() const { return myNextError; } + + // Access to registered tests + static TestFixture* GetFirstTest() { return ourFirstTest; } + static TestFixture* GetCurrentTest() { return ourCurrentTest; } + TestFixture* GetNextTest() const { return myNextTest; } + + enum OutputMode + { + Silent, + Normal, + Verbose + }; + + enum PrintMethod + { + PrintDefault, + PrintHexadecimal, + }; + + PrintMethod GetPrintMethod() const { return myPrintMethod; } + void SetPrintMethod(PrintMethod aPrintMethod) { myPrintMethod = aPrintMethod; } + + // Default execution implementation + static void (*Print)(char const* string); + static void Printf(char const* string, ...); + + static bool ExecuteAllTests(char const* groupFilter = nullptr, char const* nameFilter = nullptr, OutputMode output = Normal); + static bool ExecuteAllTests(OutputMode output) { return ExecuteAllTests(nullptr, nullptr, output); } + + static bool ExecuteTestGroup(char const* groupFilter, OutputMode output = Normal) { return ExecuteAllTests(groupFilter, nullptr, output); } + +protected: + virtual void RunTest() = 0; + virtual void Setup() {} + virtual void TearDown() {} + + // Test registration + static TestFixture const* LinkTest(TestFixture* test); + static TestFixture* ourFirstTest; + static TestFixture* ourLastTest; + + TestFixture* myNextTest; + TestError* myNextError; + + int myNumTestsChecked; + int myNumErrors; + + PrintMethod myPrintMethod; + + char myMessageSpace[MESSAGE_SPACE]; + + // allow access to current test outside of main code block + static thread_local TestFixture* ourCurrentTest; +}; + +//--------------------------------------------------------------------------------- +// Test definition macros +//--------------------------------------------------------------------------------- +#define DEFINE_TEST_FULL(name, group, fixture) \ +struct TOK(group, name) final : public fixture { \ + char const* TestName() const override { return #name; } \ + char const* TestGroup() const override { return #group; } \ + void RunTest() override; \ +} TOK(TOK(group, name), Instance); \ +void TOK(group, name)::RunTest() + +#define DEFINE_TEST(name) DEFINE_TEST_FULL(name, Global, BASE_FIXTURE) +#define DEFINE_TEST_G(name, group) DEFINE_TEST_FULL(name, group, BASE_FIXTURE) +#define DEFINE_TEST_F(name, fixture) DEFINE_TEST_FULL(name, Global, fixture) +#define DEFINE_TEST_GF(name, group, fixture) DEFINE_TEST_FULL(name, group, fixture) + +//--------------------------------------------------------------------------------- +// Utils +//--------------------------------------------------------------------------------- +template <typename T> +T TestDifference(T const& a, T const& b) { return a > b ? a - b : b - a; } + +// why are these still needed? +#define STR2(x) #x +#define STR(x) STR2(x) + +#define TOK2(a, b) a ## b +#define TOK(a, b) TOK2(a, b) + +//--------------------------------------------------------------------------------- +// Error reporting and setup, don't call directly +//--------------------------------------------------------------------------------- +#define TEST_TYPE_TO_STRING(var, arg) *TypeToStringFallback(TypeToString(var), STR(arg)) +#define TEST_ERROR_PREFIX_ __FILE__ "(" STR(__LINE__) "): Condition [%s] Failed. " +#define TEST_ERROR_(message, ...) do { TestFixture* __fx = TestFixture::GetCurrentTest(); __fx->AddError(); __fx->LogMessage(TEST_ERROR_PREFIX_ message, ##__VA_ARGS__); ERROR_ACTION; } while(0) +#define TEST_BEGIN_(a) do { auto const& test_value_ = a +#define TEST_CHECK_(cond, condtext, message, ...) do { TestFixture::GetCurrentTest()->AddTest(); if (!(cond)) TEST_ERROR_(message, condtext, ##__VA_ARGS__); } while(0) +#define TEST_END_ } while(0) + +//--------------------------------------------------------------------------------- +// Tests +// +// Note: Value caching is only enabled on left hand side. This splits the difference +// between preventing side effects (i.e. x++ double incrementing) and allowing the +// compiler to infer values (i.e. TEST_EQ(unsigned(1), 1) will try to cache 1 as an int then omit a compile warning). +// This means that the right hand side will get evaluated multiple times, so please avoid +// expressions like: TEST_EQ(a++, b++) as they won't work. Tests should always be written +// as following: +// TEST_EQ(expression, constant) +//--------------------------------------------------------------------------------- +#define TEST_OPERATOR(a, b, op1, op2) TEST_BEGIN_(a); TEST_CHECK_((test_value_) op1 (b), STR(a) " " STR(op1) " " STR(b), "'%s' " STR(op2) " '%s'", TEST_TYPE_TO_STRING(test_value_, a), TEST_TYPE_TO_STRING(b, b)); TEST_END_ + +#define TEST(cond) TEST_EQ(cond, true) +#define TEST_FAIL(cond) TEST_EQ(cond, false) + +#define TEST_EQ(a, b) TEST_OPERATOR(a, b, ==, !=) +#define TEST_NEQ(a, b) TEST_OPERATOR(a, b, !=, ==) +#define TEST_GREATER(a, b) TEST_OPERATOR(a, b, >, <=) +#define TEST_GREATER_EQUAL(a, b) TEST_OPERATOR(a, b, >=, <) +#define TEST_LESS(a, b) TEST_OPERATOR(a, b, <, >=) +#define TEST_LESS_EQUAL(a, b) TEST_OPERATOR(a, b, <=, >) + +#define TEST_STR_EQ(a, b) do { if(!TestFixture::GetCurrentTest()->TestStrings(a, b, TEST_ERROR_PREFIX_ "\n%s\n%s\n%s", STR(a) " == " STR(b))) { ERROR_ACTION; } } while(0) +#define TEST_CLOSE(a, b, eps) TEST_BEGIN_(TestDifference(a, b)); TEST_CHECK_(test_value_ <= eps, STR(a) " Close to " STR(b), "Difference of %s is greater than expected amount of " STR(eps) " when comparing %s and %s", TEST_TYPE_TO_STRING(test_value_, TestDifference(a, b)), TEST_TYPE_TO_STRING(a, a), TEST_TYPE_TO_STRING(b, b)); TEST_END_ +#define TEST_DIFFERS(a, b, eps) TEST_BEGIN_(TestDifference(a, b)); TEST_CHECK_(test_value_ >= eps, STR(a) " Differs from " STR(b), "Difference of %s is less than expected amount of " STR(eps) " when comparing %s and %s", TEST_TYPE_TO_STRING(test_value_, TestDifference(a, b)), TEST_TYPE_TO_STRING(a, a), TEST_TYPE_TO_STRING(b, b)); TEST_END_ +#define TEST_MESSAGE(cond, message, ...) TEST_BEGIN_(cond); TEST_CHECK_(test_value_, STR(cond), message, ##__VA_ARGS__); TEST_END_ diff --git a/src/unit_test.cpp b/src/unit_test.cpp new file mode 100644 index 0000000..4d21a93 --- /dev/null +++ b/src/unit_test.cpp @@ -0,0 +1,34 @@ +#include "log.hpp" +#include "simpletest.h" +#include <iostream> +#include <fstream> + +DEFINE_TEST_G(Log, LogTests) { + Log log; + bool success = log.create_log("./log.in"); + TEST_MESSAGE(success == true, "Failed to open log!!!!"); + std::cout << success << std::endl; + + // Open the file before passing it to the next function + std::ifstream file("./log.in"); + success = log.next(file); // Pass the file object to the next function + std::cout << success << std::endl; + TEST_MESSAGE(success == true, "Failed to read log!!!!"); + + std::string line = log.line("[ERROR]: Invalid operation"); + std::cout << line << std::endl; + TEST_MESSAGE(line.compare(" Invalid operation") == 0, "Expecting Invalid operation!!!!"); + + std::string level = log.level("[ERROR]: Invalid operation"); + std::cout << level << std::endl; + TEST_MESSAGE(level.compare("ERROR") == 0, "Expecting ERROR level!!!!"); + + std::string reformatted = log.reformat("[INFO]: This is a log message"); + std::cout << reformatted << std::endl; + TEST_MESSAGE(reformatted.compare("This is a log message (INFO)") == 0, "Expecting reformatted message!!!!"); +} + +int main() { + TestFixture::ExecuteTestGroup("LogTests", TestFixture::Verbose); + return 0; +} \ No newline at end of file -- GitLab