diff --git a/src/cli.c b/src/cli.c index e98b6ca8..6c4ff903 100644 --- a/src/cli.c +++ b/src/cli.c @@ -75,7 +75,8 @@ static void print_usage_and_exit(FILE *stream, const struct cpulimitcfg *cfg, fprintf( stream, " COMMAND [ARG]... run the command and limit CPU usage (implies -z)\n"); - exit(exit_code); + fflush(stream); + _exit(exit_code); } /** @@ -206,10 +207,11 @@ static void validate_target_options(const struct cpulimitcfg *cfg) { * @param cfg Pointer to configuration structure to be filled with parsed values * * This function processes all command-line options, validates the input, - * and exits the program (via exit()) if any errors are encountered or if + * and exits the program (via _exit()) if any errors are encountered or if * help is requested. Upon successful return, cfg contains valid configuration. * - * @note This function calls exit() and does not return on error or help request + * @note This function calls _exit() and does not return on error or help + * request */ void parse_arguments(int argc, char *const *argv, struct cpulimitcfg *cfg) { int opt, n_cpu; diff --git a/src/cli.h b/src/cli.h index 13b071c2..9cdb7587 100644 --- a/src/cli.h +++ b/src/cli.h @@ -102,10 +102,11 @@ struct cpulimitcfg { * @param cfg Pointer to configuration structure to be filled with parsed values * * This function processes all command-line options, validates the input, - * and exits the program (via exit()) if any errors are encountered or if + * and exits the program (via _exit()) if any errors are encountered or if * help is requested. Upon successful return, cfg contains valid configuration. * - * @note This function calls exit() and does not return on error or help request + * @note This function calls _exit() and does not return on error or help + * request */ void parse_arguments(int argc, char *const *argv, struct cpulimitcfg *cfg); diff --git a/src/limit_process.c b/src/limit_process.c index 45cd1699..1d564399 100644 --- a/src/limit_process.c +++ b/src/limit_process.c @@ -42,9 +42,7 @@ #include /* Very small value to prevent division by zero in calculations */ -#ifndef EPSILON #define EPSILON 1e-12 -#endif /* * Base control time slot in microseconds. diff --git a/src/process_group.c b/src/process_group.c index f40c5980..839258b5 100644 --- a/src/process_group.c +++ b/src/process_group.c @@ -188,6 +188,12 @@ int init_process_group(struct process_group *pgroup, pid_t target_pid, /* Record baseline timestamp for CPU usage calculation */ if (get_current_time(&pgroup->last_update) != 0) { + clear_list(pgroup->proclist); + free(pgroup->proclist); + pgroup->proclist = NULL; + process_table_destroy(pgroup->proctable); + free(pgroup->proctable); + pgroup->proctable = NULL; exit(EXIT_FAILURE); } /* Perform initial scan to populate process list */ @@ -205,6 +211,7 @@ int init_process_group(struct process_group *pgroup, pid_t target_pid, * 2. Destroys and frees the process hashtable * 3. Sets both pointers to NULL for safety * + * @note Safe to call with NULL pgroup (does nothing) * @note Safe to call even if pgroup is partially initialized (NULLs are * handled) * @note Does not send any signals to processes; they continue running @@ -212,6 +219,9 @@ int init_process_group(struct process_group *pgroup, pid_t target_pid, * re-initialization */ int close_process_group(struct process_group *pgroup) { + if (pgroup == NULL) { + return 0; + } if (pgroup->proclist != NULL) { clear_list(pgroup->proclist); free(pgroup->proclist); @@ -243,7 +253,8 @@ static struct process *process_dup(const struct process *proc) { fprintf(stderr, "Memory allocation failed for duplicated process\n"); exit(EXIT_FAILURE); } - return (struct process *)memcpy(p, proc, sizeof(struct process)); + memcpy(p, proc, sizeof(struct process)); + return p; } /** @@ -287,6 +298,7 @@ static struct process *process_dup(const struct process *proc) { * - Handles backward time jumps (system clock adjustment) * - New processes have cpu_usage=-1 until first valid measurement * + * @note Does nothing if pgroup is NULL * @note Should be called periodically (e.g., every 100ms) during CPU limiting * @note Calls exit(EXIT_FAILURE) on critical errors (iterator init, time * retrieval) @@ -298,6 +310,10 @@ void update_process_group(struct process_group *pgroup) { struct timespec now; double dt; + if (pgroup == NULL) { + return; + } + /* Get current timestamp for delta calculation */ if (get_current_time(&now) != 0) { exit(EXIT_FAILURE); @@ -417,7 +433,8 @@ void update_process_group(struct process_group *pgroup) { * @brief Calculate aggregate CPU usage across all processes in the group * @param pgroup Pointer to the process_group structure to query * @return Sum of CPU usage values for all processes with known usage, or - * -1.0 if no processes have valid CPU measurements yet + * -1.0 if no processes have valid CPU measurements yet, or + * -1.0 if pgroup is NULL * * CPU usage is expressed as a fraction of total system CPU capacity: * - 0.0 = idle @@ -430,11 +447,15 @@ void update_process_group(struct process_group *pgroup) { * 3. Returns -1 if all processes have unknown usage (first update cycle) * * @note Returns -1 rather than 0 to distinguish "no usage" from "unknown" + * @note Returns -1 if pgroup is NULL * @note Thread-safe if pgroup is not being modified concurrently */ double get_process_group_cpu_usage(const struct process_group *pgroup) { const struct list_node *node; double cpu_usage = -1; + if (pgroup == NULL) { + return -1; + } for (node = first_node(pgroup->proclist); node != NULL; node = node->next) { const struct process *p = (struct process *)node->data; /* Skip processes without valid CPU measurements yet */ diff --git a/src/process_group.h b/src/process_group.h index d5a6cc33..2a304bc6 100644 --- a/src/process_group.h +++ b/src/process_group.h @@ -140,6 +140,7 @@ int init_process_group(struct process_group *pgroup, pid_t target_pid, * 2. Destroys and frees the process hashtable * 3. Sets both pointers to NULL for safety * + * @note Safe to call with NULL pgroup (does nothing) * @note Safe to call even if pgroup is partially initialized (NULLs are * handled) * @note Does not send any signals to processes; they continue running @@ -166,6 +167,7 @@ int close_process_group(struct process_group *pgroup); * - Handles backward time jumps (system clock adjustment) * - New processes have cpu_usage=-1 until first valid measurement * + * @note Does nothing if pgroup is NULL * @note Should be called periodically (e.g., every 100ms) during CPU limiting * @note Calls exit(EXIT_FAILURE) on critical errors (iterator init, time * retrieval) @@ -176,7 +178,8 @@ void update_process_group(struct process_group *pgroup); * @brief Calculate aggregate CPU usage across all processes in the group * @param pgroup Pointer to the process_group structure to query * @return Sum of CPU usage values for all processes with known usage, or - * -1.0 if no processes have valid CPU measurements yet + * -1.0 if no processes have valid CPU measurements yet, or + * -1.0 if pgroup is NULL * * CPU usage is expressed as a fraction of total system CPU capacity: * - 0.0 = idle @@ -189,6 +192,7 @@ void update_process_group(struct process_group *pgroup); * 3. Returns -1 if all processes have unknown usage (first update cycle) * * @note Returns -1 rather than 0 to distinguish "no usage" from "unknown" + * @note Returns -1 if pgroup is NULL * @note Thread-safe if pgroup is not being modified concurrently */ double get_process_group_cpu_usage(const struct process_group *pgroup); diff --git a/src/process_iterator_apple.c b/src/process_iterator_apple.c index 3ed97bd3..9a94b36f 100644 --- a/src/process_iterator_apple.c +++ b/src/process_iterator_apple.c @@ -318,6 +318,9 @@ static int read_process_info(pid_t pid, struct process *p, int read_cmd) { * and processes not matching the PID filter criteria. */ int get_next_process(struct process_iterator *it, struct process *p) { + if (it == NULL || p == NULL) { + return -1; + } if (it->i >= it->count) { return -1; } @@ -363,6 +366,9 @@ int get_next_process(struct process_iterator *it, struct process *p) { * After this call, the iterator must not be used until re-initialized. */ int close_process_iterator(struct process_iterator *it) { + if (it == NULL) { + return -1; + } if (it->pidlist != NULL) { free(it->pidlist); it->pidlist = NULL; diff --git a/src/process_iterator_freebsd.c b/src/process_iterator_freebsd.c index 677fc865..892c3f16 100644 --- a/src/process_iterator_freebsd.c +++ b/src/process_iterator_freebsd.c @@ -187,15 +187,26 @@ static int get_single_process(kvm_t *kd, pid_t pid, struct process *process, * @brief Internal helper to get parent process ID without opening kvm * @param kd Kernel virtual memory descriptor (must be already open) * @param pid Process ID to query - * @return Parent process ID on success, -1 on error or if process not found + * @return Parent process ID on success, -1 on error, if process not found, + * if the process is a system process (e.g., PID 0 swapper), or if + * the process is a zombie * * Uses an existing kvm descriptor to query PPID. This avoids the overhead * of repeatedly opening and closing kvm when checking multiple processes. + * System processes (P_SYSTEM flag) and zombie processes are treated as not + * found and return -1. */ static pid_t _getppid_of(kvm_t *kd, pid_t pid) { int count; struct kinfo_proc *kproc = kvm_getprocs(kd, KERN_PROC_PID, pid, &count); - return (count == 0 || kproc == NULL) ? (pid_t)(-1) : kproc->ki_ppid; + if (count == 0 || kproc == NULL) { + return (pid_t)(-1); + } + /* Skip system processes (e.g., PID 0 swapper) and zombie processes */ + if ((kproc->ki_flag & P_SYSTEM) || (kproc->ki_stat == SZOMB)) { + return (pid_t)(-1); + } + return kproc->ki_ppid; } /** @@ -292,7 +303,7 @@ int is_child_of(pid_t child_pid, pid_t parent_pid) { if (kd == NULL) { fprintf(stderr, "kvm_openfiles: %s\n", errbuf); free(errbuf); - exit(EXIT_FAILURE); + return 0; } free(errbuf); ret = _is_child_of(kd, child_pid, parent_pid); @@ -376,15 +387,25 @@ int get_next_process(struct process_iterator *it, struct process *p) { * After this call, the iterator must not be used until re-initialized. */ int close_process_iterator(struct process_iterator *it) { + int ret = 0; + if (it == NULL) { + return -1; + } if (it->procs != NULL) { free(it->procs); it->procs = NULL; } - if (kvm_close(it->kd) != 0) { - perror("kvm_close"); - return -1; + if (it->kd != NULL) { + if (kvm_close(it->kd) != 0) { + perror("kvm_close"); + ret = -1; + } + it->kd = NULL; } - return 0; + it->i = 0; + it->count = 0; + it->filter = NULL; + return ret; } #endif diff --git a/src/process_iterator_linux.c b/src/process_iterator_linux.c index d7481673..8a551a00 100644 --- a/src/process_iterator_linux.c +++ b/src/process_iterator_linux.c @@ -150,15 +150,25 @@ static int read_process_info(pid_t pid, struct process *p, int read_cmd) { if (!read_cmd) { return 0; } - /* Read command path from /proc/[pid]/cmdline */ - snprintf(exefile, sizeof(exefile), "/proc/%ld/cmdline", (long)p->pid); - if ((buffer = read_line_from_file(exefile)) == NULL) { - return -1; + /* Read command path directly from /proc/[pid]/cmdline */ + { + FILE *fp; + size_t n; + snprintf(exefile, sizeof(exefile), "/proc/%ld/cmdline", (long)p->pid); + fp = fopen(exefile, "r"); + if (fp == NULL) { + return -1; + } + /* + * Read raw bytes directly into p->command, bounded by buffer size. + * cmdline separates arguments with null bytes; the first null byte + * terminates the program path naturally when used as a C string. + */ + n = fread(p->command, 1, sizeof(p->command) - 1, fp); + fclose(fp); + p->command[n] = '\0'; + return (n > 0) ? 0 : -1; } - strncpy(p->command, buffer, sizeof(p->command) - 1); - p->command[sizeof(p->command) - 1] = '\0'; - free(buffer); - return 0; } /** @@ -329,6 +339,9 @@ int is_child_of(pid_t child_pid, pid_t parent_pid) { int get_next_process(struct process_iterator *it, struct process *p) { const struct dirent *dit = NULL; + if (it == NULL || p == NULL) { + return -1; + } if (it->end_of_processes) { return -1; } diff --git a/tests/cpulimit_test.c b/tests/cpulimit_test.c index 6224e7de..b4e0f80a 100644 --- a/tests/cpulimit_test.c +++ b/tests/cpulimit_test.c @@ -652,6 +652,9 @@ static void test_process_table_init_destroy(void) { /* Test destroy with NULL (should not crash) */ process_table_destroy(NULL); + + /* Test init with NULL (should not crash) */ + process_table_init(NULL, 16); } /** @@ -716,6 +719,11 @@ static void test_process_table_add_find(void) { found = process_table_find(NULL, 100); assert(found == NULL); + /* Test add with NULL process (should not crash or modify table) */ + process_table_add(&pt, NULL); + found = process_table_find(&pt, 100); + assert(found == p1); + process_table_destroy(&pt); } @@ -1639,10 +1647,16 @@ static void test_process_group_init_invalid_pid(void) { /** * @brief Test find_process_by_pid function - * @note Verifies that the current process can be found by its PID + * @note Verifies that the current process can be found by its PID, + * and that invalid PIDs return 0 */ static void test_process_group_find_by_pid(void) { assert(find_process_by_pid(getpid()) == getpid()); + + /* Invalid PIDs must return 0 */ + assert(find_process_by_pid((pid_t)0) == 0); + assert(find_process_by_pid((pid_t)-1) == 0); + assert(find_process_by_pid((pid_t)INT_MAX) == 0); } /** @@ -1694,6 +1708,9 @@ static void test_process_group_find_by_name(void) { assert(find_process_by_name(wrong_name) == 0); free(wrong_name); + + /* Test Case 4: NULL name should return 0 (not found) */ + assert(find_process_by_name(NULL) == 0); } /** @@ -1726,6 +1743,11 @@ static void test_process_iterator_getppid_of(void) { /* Verify current process's parent PID */ assert(getppid_of(getpid()) == getppid()); + + /* Test with invalid/non-existent PIDs: must return (pid_t)-1 */ + assert(getppid_of((pid_t)-1) == (pid_t)-1); + assert(getppid_of((pid_t)0) == (pid_t)-1); + assert(getppid_of((pid_t)INT_MAX) == (pid_t)-1); } /** @@ -2285,6 +2307,673 @@ static void test_limiter_run_pid_or_exe_mode(void) { assert(WEXITSTATUS(status) == EXIT_FAILURE); } +/*************************************************************************** + * ADDITIONAL LIST MODULE TESTS + ***************************************************************************/ + +/** + * @brief Test adding a NULL data element to the list + * @note Verifies that add_elem accepts NULL as valid data pointer + */ +static void test_list_add_null_elem(void) { + struct list l; + const struct list_node *node; + + init_list(&l); + + /* Adding NULL data is valid: the list stores only the pointer */ + node = add_elem(&l, NULL); + assert(node != NULL); + assert(node->data == NULL); + assert(get_list_count(&l) == 1); + assert(is_empty_list(&l) == 0); + + clear_list(&l); +} + +/** + * @brief Test locate_node and locate_elem when a node has NULL data + * @note Covers the cur->data == NULL guard in locate_node + */ +static void test_list_locate_null_data(void) { + struct list l; + struct list_node *found_node; + struct process *p; + pid_t search_pid; + + p = (struct process *)malloc(sizeof(struct process)); + assert(p != NULL); + p->pid = (pid_t)100; + + init_list(&l); + + /* Insert a node with NULL data first, then a valid process node */ + add_elem(&l, NULL); + add_elem(&l, p); + + /* locate_node must skip the NULL-data node and find p */ + search_pid = (pid_t)100; + found_node = locate_node(&l, &search_pid, offsetof(struct process, pid), + sizeof(pid_t)); + assert(found_node != NULL); + assert(((struct process *)found_node->data)->pid == (pid_t)100); + + /* locate_elem must return p for the same query */ + assert(locate_elem(&l, &search_pid, offsetof(struct process, pid), + sizeof(pid_t)) == p); + + /* Searching for a non-existent PID must return NULL */ + search_pid = (pid_t)999; + assert(locate_node(&l, &search_pid, offsetof(struct process, pid), + sizeof(pid_t)) == NULL); + + clear_list(&l); + free(p); +} + +/*************************************************************************** + * ADDITIONAL UTIL MODULE TESTS + ***************************************************************************/ + +/** + * @brief Test timediff_in_ms when the earlier timestamp is after the later one + * @note Verifies that a negative difference is returned correctly + */ +static void test_util_timediff_negative(void) { + struct timespec earlier, later; + double diff_ms; + + /* later < earlier => negative result */ + earlier.tv_sec = 200; + earlier.tv_nsec = 0; + later.tv_sec = 100; + later.tv_nsec = 0; + diff_ms = timediff_in_ms(&later, &earlier); + assert(diff_ms < 0.0); + assert(diff_ms >= -100001.0 && diff_ms <= -99999.0); + + /* Sub-second negative difference */ + earlier.tv_sec = 100; + earlier.tv_nsec = 500000000L; + later.tv_sec = 100; + later.tv_nsec = 0; + diff_ms = timediff_in_ms(&later, &earlier); + assert(diff_ms < 0.0); + assert(diff_ms >= -501.0 && diff_ms <= -499.0); +} + +#if defined(__linux__) +/** + * @brief Test read_line_from_file with various inputs + * @note Covers NULL path, non-existent file, empty file, and valid file paths + */ +static void test_util_read_line_from_file(void) { + char *line; + + /* NULL file name must return NULL */ + line = read_line_from_file(NULL); + assert(line == NULL); + + /* Non-existent file must return NULL */ + line = read_line_from_file("/nonexistent/path/to/file_cpulimit_test.txt"); + assert(line == NULL); + + /* /dev/null is empty: getline returns < 0, function must return NULL */ + line = read_line_from_file("/dev/null"); + assert(line == NULL); + + /* /proc/version always exists and has content on Linux */ + line = read_line_from_file("/proc/version"); + assert(line != NULL); + assert(strlen(line) > 0); + free(line); +} +#endif /* __linux__ */ + +/*************************************************************************** + * ADDITIONAL PROCESS_TABLE MODULE TESTS + ***************************************************************************/ + +/** + * @brief Test that adding a duplicate PID leaves the original entry unchanged + * @note Covers the duplicate-check branch in process_table_add + */ +static void test_process_table_duplicate_pid(void) { + struct process_table pt; + struct process *p1, *p2; + const struct process *found; + + process_table_init(&pt, 16); + + p1 = (struct process *)malloc(sizeof(struct process)); + p2 = (struct process *)malloc(sizeof(struct process)); + assert(p1 != NULL && p2 != NULL); + + p1->pid = (pid_t)100; + p1->ppid = (pid_t)1; + p2->pid = (pid_t)100; /* Same PID as p1 */ + p2->ppid = (pid_t)2; + + /* Add first process */ + process_table_add(&pt, p1); + + /* Add second with same PID: must be silently ignored */ + process_table_add(&pt, p2); + + /* Only p1 must be in the table */ + found = process_table_find(&pt, (pid_t)100); + assert(found == p1); + + process_table_destroy(&pt); + + /* p2 was never owned by the table so it must be freed manually */ + free(p2); +} + +/** + * @brief Test process_table_del when the target PID is not in its bucket + * @note Covers the "process not found in non-empty bucket" branch in + * process_table_del + */ +static void test_process_table_del_not_found_in_bucket(void) { + struct process_table pt; + struct process *p; + int ret; + + /* + * hashsize=16: pid 100 hashes to bucket 4 (100 % 16 = 4), + * pid 116 also hashes to bucket 4 (116 % 16 = 4). + * Add pid 100, then try to delete pid 116 (same bucket, not present). + */ + process_table_init(&pt, 16); + + p = (struct process *)malloc(sizeof(struct process)); + assert(p != NULL); + p->pid = (pid_t)100; + p->ppid = (pid_t)1; + + process_table_add(&pt, p); + + /* Bucket 4 exists but pid 116 is not in it */ + ret = process_table_del(&pt, (pid_t)116); + assert(ret == 1); + + /* p (pid 100) must still be in the table */ + assert(process_table_find(&pt, (pid_t)100) == p); + + process_table_destroy(&pt); +} + +/** + * @brief Test process_table_remove_stale with a NULL active_list + * @note Verifies that a NULL active_list causes all entries to be removed + */ +static void test_process_table_remove_stale_null_active(void) { + struct process_table pt; + struct process *p; + + process_table_init(&pt, 16); + + p = (struct process *)malloc(sizeof(struct process)); + assert(p != NULL); + p->pid = (pid_t)100; + p->ppid = (pid_t)1; + + process_table_add(&pt, p); + assert(process_table_find(&pt, (pid_t)100) != NULL); + + /* NULL active_list: locate_elem returns NULL for every PID -> remove all */ + process_table_remove_stale(&pt, NULL); + + /* The process must have been removed (and freed by destroy_node) */ + assert(process_table_find(&pt, (pid_t)100) == NULL); + + process_table_destroy(&pt); +} + +/*************************************************************************** + * ADDITIONAL SIGNAL_HANDLER MODULE TESTS + ***************************************************************************/ + +/** + * @brief Test that SIGHUP sets the quit flag but not the TTY flag + * @note Covers the default case in sig_handler for non-TTY signals + */ +static void test_signal_handler_sighup(void) { + pid_t pid; + int status; + + pid = fork(); + assert(pid >= 0); + if (pid == 0) { + /* Child: install handlers and raise SIGHUP */ + configure_signal_handler(); + if (raise(SIGHUP) != 0) { + _exit(1); + } + if (!is_quit_flag_set()) { + _exit(2); /* quit flag must be set */ + } + if (is_terminated_by_tty()) { + _exit(3); /* SIGHUP must NOT set TTY flag */ + } + _exit(0); + } + + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) == 0); +} + +/** + * @brief Test that SIGQUIT sets both the quit flag and the TTY flag + * @note Covers the SIGQUIT case in sig_handler + */ +static void test_signal_handler_sigquit(void) { + pid_t pid; + int status; + + pid = fork(); + assert(pid >= 0); + if (pid == 0) { + /* Child: install handlers and raise SIGQUIT */ + configure_signal_handler(); + if (raise(SIGQUIT) != 0) { + _exit(1); + } + if (!is_quit_flag_set()) { + _exit(2); /* quit flag must be set */ + } + if (!is_terminated_by_tty()) { + _exit(3); /* SIGQUIT MUST set TTY flag */ + } + _exit(0); + } + + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) == 0); +} + +/*************************************************************************** + * ADDITIONAL PROCESS_ITERATOR MODULE TESTS + ***************************************************************************/ + +/** + * @brief Test closing a process iterator immediately after initialization + * @note Verifies that close_process_iterator succeeds without any iteration + */ +static void test_process_iterator_close_immediately(void) { + struct process_iterator it; + struct process_filter filter; + + /* Close without iterating: all-processes filter */ + filter.pid = (pid_t)0; + filter.include_children = 0; + filter.read_cmd = 0; + assert(init_process_iterator(&it, &filter) == 0); + assert(close_process_iterator(&it) == 0); + + /* Close without iterating: single-process filter */ + filter.pid = getpid(); + filter.include_children = 0; + filter.read_cmd = 0; + assert(init_process_iterator(&it, &filter) == 0); + assert(close_process_iterator(&it) == 0); +} + +/*************************************************************************** + * ADDITIONAL CLI MODULE TESTS + ***************************************************************************/ + +/** + * @brief Test parse_arguments with unknown short and long options + * @note Covers the case '?' branch for both optopt!=0 and optopt==0 + */ +static void test_cli_unknown_options(void) { + pid_t pid; + int status; + + /* Unknown short option -x → exit(EXIT_FAILURE) */ + { + char prog[] = "cpulimit"; + char opt_x[] = "-x"; + char *argv[3]; + struct cpulimitcfg cfg; + + argv[0] = prog; + argv[1] = opt_x; + argv[2] = NULL; + pid = fork(); + assert(pid >= 0); + if (pid == 0) { + (void)close(STDERR_FILENO); + parse_arguments(2, (char *const *)argv, &cfg); + _exit(EXIT_SUCCESS); + } + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) == EXIT_FAILURE); + } + + /* Unknown long option --unknown → exit(EXIT_FAILURE) */ + { + char prog[] = "cpulimit"; + char opt_unknown[] = "--unknown"; + char *argv[3]; + struct cpulimitcfg cfg; + + argv[0] = prog; + argv[1] = opt_unknown; + argv[2] = NULL; + pid = fork(); + assert(pid >= 0); + if (pid == 0) { + (void)close(STDERR_FILENO); + parse_arguments(2, (char *const *)argv, &cfg); + _exit(EXIT_SUCCESS); + } + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) == EXIT_FAILURE); + } +} + +/** + * @brief Test parse_arguments with a long option that is missing its argument + * @note Covers the optopt==0 branch in case ':' (long option missing value) + */ +static void test_cli_missing_long_opt_arg(void) { + pid_t pid; + int status; + + /* --limit without =VALUE at end of argv → exit(EXIT_FAILURE) */ + { + char prog[] = "cpulimit"; + char opt_limit[] = "--limit"; + char *argv[3]; + struct cpulimitcfg cfg; + + argv[0] = prog; + argv[1] = opt_limit; + argv[2] = NULL; + pid = fork(); + assert(pid >= 0); + if (pid == 0) { + (void)close(STDERR_FILENO); + parse_arguments(2, (char *const *)argv, &cfg); + _exit(EXIT_SUCCESS); + } + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) == EXIT_FAILURE); + } +} + +/** + * @brief Test parse_arguments with CPU limit at boundary values + * @note Covers the limit<=0 and limit>100*ncpu branches in parse_limit_option + */ +static void test_cli_limit_boundaries(void) { + pid_t pid; + int status; + struct cpulimitcfg cfg; + + /* Limit of 0 → fails */ + { + char prog[] = "cpulimit"; + char opt_l[] = "-l"; + char val_0[] = "0"; + char opt_e[] = "-e"; + char val_exe[] = "test"; + char *argv[6]; + + argv[0] = prog; + argv[1] = opt_l; + argv[2] = val_0; + argv[3] = opt_e; + argv[4] = val_exe; + argv[5] = NULL; + pid = fork(); + assert(pid >= 0); + if (pid == 0) { + (void)close(STDERR_FILENO); + parse_arguments(5, (char *const *)argv, &cfg); + _exit(EXIT_SUCCESS); + } + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) == EXIT_FAILURE); + } + + /* Limit greater than 100*ncpu → fails */ + { + char prog[] = "cpulimit"; + char opt_l[] = "-l"; + char val_big[] = "999999"; + char opt_e[] = "-e"; + char val_exe[] = "test"; + char *argv[6]; + + argv[0] = prog; + argv[1] = opt_l; + argv[2] = val_big; + argv[3] = opt_e; + argv[4] = val_exe; + argv[5] = NULL; + pid = fork(); + assert(pid >= 0); + if (pid == 0) { + (void)close(STDERR_FILENO); + parse_arguments(5, (char *const *)argv, &cfg); + _exit(EXIT_SUCCESS); + } + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) == EXIT_FAILURE); + } + + /* Limit exactly 100 (100% of one CPU) → succeeds */ + { + char prog[] = "cpulimit"; + char opt_l[] = "-l"; + char val_100[] = "100"; + char opt_e[] = "-e"; + char val_exe[] = "test"; + char *argv[6]; + + argv[0] = prog; + argv[1] = opt_l; + argv[2] = val_100; + argv[3] = opt_e; + argv[4] = val_exe; + argv[5] = NULL; + parse_arguments(5, (char *const *)argv, &cfg); + assert(cfg.limit > 0.99 && cfg.limit < 1.01); + } +} + +/** + * @brief Test parse_arguments with PID values at boundary conditions + * @note Covers the pid<=1 branch in parse_pid_option for PID=0 and PID=1 + */ +static void test_cli_pid_boundaries(void) { + pid_t pid; + int status; + + /* PID of 0 → fails (must be > 1) */ + { + char prog[] = "cpulimit"; + char opt_l[] = "-l"; + char val_50[] = "50"; + char opt_p[] = "-p"; + char val_0[] = "0"; + char *argv[6]; + struct cpulimitcfg cfg; + + argv[0] = prog; + argv[1] = opt_l; + argv[2] = val_50; + argv[3] = opt_p; + argv[4] = val_0; + argv[5] = NULL; + pid = fork(); + assert(pid >= 0); + if (pid == 0) { + (void)close(STDERR_FILENO); + parse_arguments(5, (char *const *)argv, &cfg); + _exit(EXIT_SUCCESS); + } + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) == EXIT_FAILURE); + } + + /* PID of 1 → fails (must be > 1) */ + { + char prog[] = "cpulimit"; + char opt_l[] = "-l"; + char val_50[] = "50"; + char opt_p[] = "-p"; + char val_1[] = "1"; + char *argv[6]; + struct cpulimitcfg cfg; + + argv[0] = prog; + argv[1] = opt_l; + argv[2] = val_50; + argv[3] = opt_p; + argv[4] = val_1; + argv[5] = NULL; + pid = fork(); + assert(pid >= 0); + if (pid == 0) { + (void)close(STDERR_FILENO); + parse_arguments(5, (char *const *)argv, &cfg); + _exit(EXIT_SUCCESS); + } + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) == EXIT_FAILURE); + } +} + +/** + * @brief Test parse_arguments with long-option equivalents of short flags + * @note Covers --pid= and --include-children long options + */ +static void test_cli_long_options_extended(void) { + struct cpulimitcfg cfg; + + /* Test --pid=PID and --limit=N together */ + { + char prog[] = "cpulimit"; + char opt_limit[] = "--limit=50"; + char opt_pid[] = "--pid=1234"; + char *argv[4]; + + argv[0] = prog; + argv[1] = opt_limit; + argv[2] = opt_pid; + argv[3] = NULL; + parse_arguments(3, (char *const *)argv, &cfg); + assert(cfg.target_pid == (pid_t)1234); + assert(cfg.limit > 0.49 && cfg.limit < 0.51); + assert(cfg.lazy_mode == 1); /* --pid implies lazy */ + } + + /* Test --include-children long option */ + { + char prog[] = "cpulimit"; + char opt_limit[] = "--limit=50"; + char opt_include[] = "--include-children"; + char opt_e[] = "-e"; + char val_exe[] = "test"; + char *argv[6]; + + argv[0] = prog; + argv[1] = opt_limit; + argv[2] = opt_include; + argv[3] = opt_e; + argv[4] = val_exe; + argv[5] = NULL; + parse_arguments(5, (char *const *)argv, &cfg); + assert(cfg.include_children == 1); + } +} + +/*************************************************************************** + * ADDITIONAL LIMITER MODULE TESTS + ***************************************************************************/ + +/** + * @brief Test run_command_mode with a command that exits with non-zero status + * @note Verifies that the exit code of the command is propagated correctly + */ +static void test_limiter_command_nonzero_exit(void) { + pid_t pid; + int status; + struct cpulimitcfg cfg; + char cmd[] = "false"; + char *args[2]; + + args[0] = cmd; + args[1] = NULL; + memset(&cfg, 0, sizeof(struct cpulimitcfg)); + cfg.program_name = "test"; + cfg.command_mode = 1; + cfg.command_args = (char *const *)args; + cfg.limit = 0.5; + cfg.lazy_mode = 1; + + /* 'false' always exits with status 1 */ + pid = fork(); + assert(pid >= 0); + if (pid == 0) { + run_command_mode(&cfg); + _exit(EXIT_SUCCESS); /* Should not reach here */ + } + + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) != EXIT_SUCCESS); +} + +/** + * @brief Test run_command_mode with a command that does not exist + * @note Verifies that execvp failure results in EXIT_FAILURE being propagated + */ +static void test_limiter_missing_command(void) { + pid_t pid; + int status; + struct cpulimitcfg cfg; + char cmd[] = "nonexistent_cmd_cpulimit_xyz_12345"; + char *args[2]; + + args[0] = cmd; + args[1] = NULL; + memset(&cfg, 0, sizeof(struct cpulimitcfg)); + cfg.program_name = "test"; + cfg.command_mode = 1; + cfg.command_args = (char *const *)args; + cfg.limit = 0.5; + cfg.lazy_mode = 1; + + pid = fork(); + assert(pid >= 0); + if (pid == 0) { + /* Suppress execvp error message */ + (void)close(STDERR_FILENO); + run_command_mode(&cfg); + _exit(EXIT_SUCCESS); /* Should not reach here */ + } + + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status)); + /* execvp fails → child exits with EXIT_FAILURE */ + assert(WEXITSTATUS(status) == EXIT_FAILURE); +} + /** @def RUN_TEST(test_func) * @brief Macro to run a test function and print its status * @param test_func Name of the test function to run @@ -2296,6 +2985,25 @@ static void test_limiter_run_pid_or_exe_mode(void) { printf("%s() passed.\n", #test_func); \ } while (0) +/** + * @brief Test NULL safety of process_group functions + * @note Verifies that close_process_group, update_process_group, and + * get_process_group_cpu_usage safely handle NULL pgroup pointers + */ +static void test_process_group_null_safety(void) { + double cpu_usage; + + /* close_process_group(NULL) must not crash and must return 0 */ + assert(close_process_group(NULL) == 0); + + /* update_process_group(NULL) must not crash */ + update_process_group(NULL); + + /* get_process_group_cpu_usage(NULL) must return -1 */ + cpu_usage = get_process_group_cpu_usage(NULL); + assert(cpu_usage >= -1.00001 && cpu_usage <= -0.99999); +} + /** * @brief Main test function * @param argc Argument count @@ -2321,6 +3029,8 @@ int main(int argc, char *argv[]) { RUN_TEST(test_list_locate); RUN_TEST(test_list_clear_and_destroy); RUN_TEST(test_list_edge_cases); + RUN_TEST(test_list_add_null_elem); + RUN_TEST(test_list_locate_null_data); /* Util module tests */ printf("\n=== UTIL MODULE TESTS ===\n"); @@ -2334,6 +3044,10 @@ int main(int argc, char *argv[]) { RUN_TEST(test_util_time_edge_cases); RUN_TEST(test_util_file_basename_edge_cases); RUN_TEST(test_util_long2pid_t_edge_cases); + RUN_TEST(test_util_timediff_negative); +#if defined(__linux__) + RUN_TEST(test_util_read_line_from_file); +#endif /* Process table module tests */ printf("\n=== PROCESS_TABLE MODULE TESTS ===\n"); @@ -2343,10 +3057,15 @@ int main(int argc, char *argv[]) { RUN_TEST(test_process_table_remove_stale); RUN_TEST(test_process_table_collisions); RUN_TEST(test_process_table_empty_buckets); + RUN_TEST(test_process_table_duplicate_pid); + RUN_TEST(test_process_table_del_not_found_in_bucket); + RUN_TEST(test_process_table_remove_stale_null_active); /* Signal handler module tests */ printf("\n=== SIGNAL_HANDLER MODULE TESTS ===\n"); RUN_TEST(test_signal_handler_flags); + RUN_TEST(test_signal_handler_sighup); + RUN_TEST(test_signal_handler_sigquit); /* Process iterator module tests */ printf("\n=== PROCESS_ITERATOR MODULE TESTS ===\n"); @@ -2357,6 +3076,7 @@ int main(int argc, char *argv[]) { RUN_TEST(test_process_iterator_getppid_of); RUN_TEST(test_process_iterator_is_child_of); RUN_TEST(test_process_iterator_filter_edge_cases); + RUN_TEST(test_process_iterator_close_immediately); /* Process group module tests */ printf("\n=== PROCESS_GROUP MODULE TESTS ===\n"); @@ -2367,6 +3087,7 @@ int main(int argc, char *argv[]) { RUN_TEST(test_process_group_find_by_name); RUN_TEST(test_process_group_cpu_usage); RUN_TEST(test_process_group_rapid_updates); + RUN_TEST(test_process_group_null_safety); /* Limit process module tests */ printf("\n=== LIMIT_PROCESS MODULE TESTS ===\n"); @@ -2380,11 +3101,18 @@ int main(int argc, char *argv[]) { RUN_TEST(test_cli_parse_arguments_command_mode); RUN_TEST(test_cli_parse_arguments_long_opts); RUN_TEST(test_cli_parse_arguments_exit_cases); + RUN_TEST(test_cli_unknown_options); + RUN_TEST(test_cli_missing_long_opt_arg); + RUN_TEST(test_cli_limit_boundaries); + RUN_TEST(test_cli_pid_boundaries); + RUN_TEST(test_cli_long_options_extended); /* Limiter module tests */ printf("\n=== LIMITER MODULE TESTS ===\n"); RUN_TEST(test_limiter_run_command_mode); RUN_TEST(test_limiter_run_pid_or_exe_mode); + RUN_TEST(test_limiter_command_nonzero_exit); + RUN_TEST(test_limiter_missing_command); printf("\n=== ALL TESTS PASSED ===\n");