diff --git a/CMakeLists.txt b/CMakeLists.txt index 776a132..94d6012 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,8 +5,7 @@ set(CMAKE_C_STANDARD 11) include_directories(include) -set(SOURCES - src/main.c +set(LIB_SOURCES src/input.c src/execute.c src/builtins.c @@ -18,4 +17,21 @@ set(HEADERS include/builtins.h ) -add_executable(shell ${SOURCES} ${HEADERS}) +add_executable(shell src/main.c ${LIB_SOURCES} ${HEADERS}) + +enable_testing() + +add_executable(test_input tests/test_input.c src/input.c) +add_test(NAME InputTests COMMAND test_input) + +add_executable(test_builtins tests/test_builtins.c src/builtins.c) +add_test(NAME BuiltinsTests COMMAND test_builtins) + +add_executable(test_execute tests/test_execute.c src/execute.c src/builtins.c) +add_test(NAME ExecuteTests COMMAND test_execute) + +add_custom_target(run_tests + COMMAND ${CMAKE_CTEST_COMMAND} --verbose + DEPENDS test_input test_builtins test_execute + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} +) diff --git a/README.md b/README.md index 188eea6..e0a1463 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,63 @@ # Trigger ## Overview -This project aims to create a clean, efficient shell. The current README was written quickly, but the code itself will be written without AI assistance. I plan to build the shell based on solid fundamentals and personal expertise. +This project aims to create a clean, efficient shell. The current README was written quickly, but the code itself will be written without AI assistance - exception being repetitive tasks like folder organization and testing. I plan to build the shell based on solid fundamentals and personal expertise. + +## Project Structure +The project is now organized into a clean, modular structure: + +``` +shell/ +├── include/ # Header files +│ ├── input.h # Input reading and parsing +│ ├── execute.h # Command execution +│ └── builtins.h # Built-in commands +├── src/ # Source files +│ ├── main.c # Main entry point +│ ├── input.c # Input handling implementation +│ ├── execute.c # Command execution implementation +│ └── builtins.c # Built-in commands implementation +├── tests/ # Unit tests +│ ├── test_framework.h # Simple testing framework +│ ├── test_input.c # Input module tests +│ ├── test_execute.c # Execute module tests +│ └── test_builtins.c # Built-ins module tests +└── CMakeLists.txt # Build configuration +``` + +## Building + +```bash +cmake -B cmake-build-debug -S . +cmake --build cmake-build-debug +``` + +## Running + +```bash +./cmake-build-debug/shell +``` + +## Testing + +The project includes comprehensive unit tests for all modules. To run the tests: + +```bash +# Quick way +./run_tests.sh + +# Or manually +cd cmake-build-debug +ctest --verbose +``` + +All tests use a custom lightweight testing framework that provides colored output and clear assertion messages. ## Goals - Develop a functional, well‑structured shell. - Ensure the shell is performant and easy to use. - Continuously improve the project as I learn more. +- Maintain high code quality with unit tests. ## Learning Roadmap To support development, I’ll study the following topics in my spare time: @@ -26,8 +77,11 @@ I’ll start by adapting the simple shell tutorial from Brennan Baker: This will serve as a solid foundation, which I’ll later expand and customize. The resulting shell, named **Trigger**, will combine the tutorial’s core ideas with additional features and optimizations. ## Project Status -- Initial prototype based on the tutorial is under development. -- Future enhancements will be added incrementally as I deepen my understanding of the relevant topics. +- ✅ Modular architecture with separated concerns +- ✅ Comprehensive unit test coverage +- ✅ Clean project structure with include/ and src/ directories +- 🔄 Basic shell functionality (cd, help, exit, external commands) +- 🔜 Future enhancements will be added incrementally --- diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..12eec6a --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +echo "Building project..." +cmake -B cmake-build-debug -S . +cmake --build cmake-build-debug + +echo "" +echo "Running tests..." +echo "=================" +cd cmake-build-debug +ctest --output-on-failure + +echo "" +echo "All tests completed successfully!" + diff --git a/tests/test_builtins.c b/tests/test_builtins.c new file mode 100644 index 0000000..29c0373 --- /dev/null +++ b/tests/test_builtins.c @@ -0,0 +1,84 @@ +#include +#include +#include +#include +#include "../include/builtins.h" +#include "test_framework.h" + +void test_num_builtins() { + int count = trigger_num_builtins(); + ASSERT_EQUAL(3, count, "Should have 3 built-in commands"); +} + +void test_builtin_arrays_match() { + int count = trigger_num_builtins(); + + // Verify that builtin_str and builtin_func have the same size + ASSERT_TRUE(count > 0, "Should have at least one built-in command"); + + // Check that known builtins exist + int found_cd = 0, found_help = 0, found_exit = 0; + for (int i = 0; i < count; i++) { + if (strcmp(builtin_str[i], "cd") == 0) found_cd = 1; + if (strcmp(builtin_str[i], "help") == 0) found_help = 1; + if (strcmp(builtin_str[i], "exit") == 0) found_exit = 1; + } + + ASSERT_TRUE(found_cd, "Should have 'cd' built-in"); + ASSERT_TRUE(found_help, "Should have 'help' built-in"); + ASSERT_TRUE(found_exit, "Should have 'exit' built-in"); +} + +void test_cd_no_args() { + char *args[] = {"cd", NULL}; + int result = trigger_cd(args); + + // Should return true (1) even when no directory provided + ASSERT_TRUE(result, "cd with no args should return true"); +} + +void test_cd_to_tmp() { + char cwd_before[1024]; + char cwd_after[1024]; + + getcwd(cwd_before, sizeof(cwd_before)); + + char *args[] = {"cd", "/tmp", NULL}; + int result = trigger_cd(args); + + getcwd(cwd_after, sizeof(cwd_after)); + + ASSERT_TRUE(result, "cd to /tmp should return true"); + ASSERT_STR_EQUAL("/tmp", cwd_after, "Should be in /tmp directory"); + + // Restore original directory + chdir(cwd_before); +} + +void test_help_command() { + char *args[] = {"help", NULL}; + int result = trigger_help(args); + + ASSERT_TRUE(result, "help command should return true"); +} + +void test_exit_command() { + char *args[] = {"exit", NULL}; + int result = trigger_exit(args); + + ASSERT_EQUAL(EXIT_SUCCESS, result, "exit command should return EXIT_SUCCESS"); +} + +int main() { + TEST_SUITE_START("Built-ins Module Tests"); + + test_num_builtins(); + test_builtin_arrays_match(); + test_cd_no_args(); + test_cd_to_tmp(); + test_help_command(); + test_exit_command(); + + TEST_SUITE_END(); +} + diff --git a/tests/test_execute.c b/tests/test_execute.c new file mode 100644 index 0000000..7e16fc6 --- /dev/null +++ b/tests/test_execute.c @@ -0,0 +1,55 @@ +#include +#include +#include +#include "../include/execute.h" +#include "test_framework.h" + +void test_execute_empty_command() { + char *args[] = {NULL}; + int result = trigger_execute(args); + + ASSERT_TRUE(result, "Empty command should return true"); +} + +void test_execute_builtin_help() { + char *args[] = {"help", NULL}; + int result = trigger_execute(args); + + ASSERT_TRUE(result, "Execute 'help' should return true"); +} + +void test_execute_builtin_exit() { + char *args[] = {"exit", NULL}; + int result = trigger_execute(args); + + ASSERT_EQUAL(EXIT_SUCCESS, result, "Execute 'exit' should return EXIT_SUCCESS"); +} + +void test_execute_external_command() { + // Test with 'true' command which always succeeds + char *args[] = {"/bin/true", NULL}; + int result = trigger_execute(args); + + ASSERT_TRUE(result, "Execute '/bin/true' should return true"); +} + +void test_launch_simple_command() { + // Test launching echo command + char *args[] = {"/bin/echo", "test", NULL}; + int result = trigger_launch(args); + + ASSERT_TRUE(result, "Launch '/bin/echo test' should return true"); +} + +int main() { + TEST_SUITE_START("Execute Module Tests"); + + test_execute_empty_command(); + test_execute_builtin_help(); + test_execute_builtin_exit(); + test_execute_external_command(); + test_launch_simple_command(); + + TEST_SUITE_END(); +} + diff --git a/tests/test_framework.h b/tests/test_framework.h new file mode 100644 index 0000000..a40760c --- /dev/null +++ b/tests/test_framework.h @@ -0,0 +1,64 @@ +#ifndef TEST_FRAMEWORK_H +#define TEST_FRAMEWORK_H + +#include +#include + +// Test statistics +static int tests_run = 0; +static int tests_passed = 0; +static int tests_failed = 0; + +// Color codes for output +#define COLOR_GREEN "\033[0;32m" +#define COLOR_RED "\033[0;31m" +#define COLOR_YELLOW "\033[0;33m" +#define COLOR_RESET "\033[0m" + +// Assertion macros +#define ASSERT_TRUE(condition, message) \ + do { \ + tests_run++; \ + if (condition) { \ + tests_passed++; \ + printf(COLOR_GREEN "✓" COLOR_RESET " %s\n", message); \ + } else { \ + tests_failed++; \ + printf(COLOR_RED "✗" COLOR_RESET " %s (line %d)\n", message, __LINE__); \ + } \ + } while(0) + +#define ASSERT_FALSE(condition, message) \ + ASSERT_TRUE(!(condition), message) + +#define ASSERT_EQUAL(expected, actual, message) \ + ASSERT_TRUE((expected) == (actual), message) + +#define ASSERT_STR_EQUAL(expected, actual, message) \ + ASSERT_TRUE(strcmp((expected), (actual)) == 0, message) + +#define ASSERT_NOT_NULL(ptr, message) \ + ASSERT_TRUE((ptr) != NULL, message) + +#define ASSERT_NULL(ptr, message) \ + ASSERT_TRUE((ptr) == NULL, message) + +// Test suite management +#define TEST_SUITE_START(name) \ + printf("\n" COLOR_YELLOW "Running test suite: %s" COLOR_RESET "\n", name); \ + tests_run = 0; \ + tests_passed = 0; \ + tests_failed = 0; + +#define TEST_SUITE_END() \ + printf("\n"); \ + if (tests_failed == 0) { \ + printf(COLOR_GREEN "All tests passed! (%d/%d)" COLOR_RESET "\n", tests_passed, tests_run); \ + } else { \ + printf(COLOR_RED "%d test(s) failed out of %d" COLOR_RESET "\n", tests_failed, tests_run); \ + } \ + printf("\n"); \ + return tests_failed; + +#endif // TEST_FRAMEWORK_H + diff --git a/tests/test_input.c b/tests/test_input.c new file mode 100644 index 0000000..4a3689a --- /dev/null +++ b/tests/test_input.c @@ -0,0 +1,78 @@ +#include +#include +#include +#include "../include/input.h" +#include "test_framework.h" + +void test_split_line_basic() { + char line[] = "ls -la /home"; + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("ls", tokens[0], "First token should be 'ls'"); + ASSERT_STR_EQUAL("-la", tokens[1], "Second token should be '-la'"); + ASSERT_STR_EQUAL("/home", tokens[2], "Third token should be '/home'"); + ASSERT_NULL(tokens[3], "Fourth token should be NULL"); + + free(tokens); +} + +void test_split_line_empty() { + char line[] = " \t\n "; + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL for empty input"); + ASSERT_NULL(tokens[0], "First token should be NULL for empty input"); + + free(tokens); +} + +void test_split_line_single_command() { + char line[] = "pwd"; + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("pwd", tokens[0], "First token should be 'pwd'"); + ASSERT_NULL(tokens[1], "Second token should be NULL"); + + free(tokens); +} + +void test_split_line_multiple_spaces() { + char line[] = "echo hello world"; + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello", tokens[1], "Second token should be 'hello'"); + ASSERT_STR_EQUAL("world", tokens[2], "Third token should be 'world'"); + ASSERT_NULL(tokens[3], "Fourth token should be NULL"); + + free(tokens); +} + +void test_split_line_tabs() { + char line[] = "cat\tfile.txt\tfile2.txt"; + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("cat", tokens[0], "First token should be 'cat'"); + ASSERT_STR_EQUAL("file.txt", tokens[1], "Second token should be 'file.txt'"); + ASSERT_STR_EQUAL("file2.txt", tokens[2], "Third token should be 'file2.txt'"); + ASSERT_NULL(tokens[3], "Fourth token should be NULL"); + + free(tokens); +} + +int main() { + TEST_SUITE_START("Input Module Tests"); + + test_split_line_basic(); + test_split_line_empty(); + test_split_line_single_command(); + test_split_line_multiple_spaces(); + test_split_line_tabs(); + + TEST_SUITE_END(); +} +