Merge remote-tracking branch 'origin/feat/rsync-stderr-mode' into dev

# Conflicts:
#	RSYNC_COMPAT.md
#	src/shared/log.c
#	src/shared/log.h
#	tests/test_client_cli.c
This commit is contained in:
2026-09-04 17:20:36 +02:00
7 changed files with 108 additions and 12 deletions
+10 -11
View File
@@ -7,10 +7,10 @@ This document maps rsync's full feature set to FastSync's current implementation
| Status | Count | Description |
|--------|-------|-------------|
| ✅ Implemented | 35 | Feature works end-to-end |
| 🔀 Alt Arg | 3 | Functionality exists but under different flag/semantics |
| ⚠️ Partial | 1 | Flag parsed/stored but behavior incomplete |
| ❌ Not Implemented | 98 | Flag not recognized or no behavior |
| **Total** | **137** | |
| 🔀 Alt Arg | 4 | Functionality exists but under different flag/semantics |
| ⚠️ Partial | 5 | Flag parsed/stored but behavior incomplete |
| ❌ Not Implemented | 97 | Flag not recognized or no behavior |
| **Total** | **141** | |
---
@@ -25,7 +25,7 @@ This document maps rsync's full feature set to FastSync's current implementation
| `-V`, `--version` | Print version | ✅ Implemented | |
| `--info=FLAGS` | Fine-grained info verbosity | ❌ Not Implemented | Removed because it had no effect |
| `--debug=FLAGS` | Fine-grained debug verbosity | ❌ Not Implemented | Removed because it had no effect |
| `--stderr=MODE` | Change stderr output mode | ❌ Not Implemented | |
| `--stderr=MODE` | Change stderr output mode | ⚠️ Partial | `errors` (default) and `all` are supported; `client` is rejected because FastSync has no rsync message channel |
| `--no-motd` | Suppress daemon MOTD | ❌ Not Implemented | |
| `--exclude=PATTERN` | Exclude files matching pattern | ✅ Implemented | Glob matching in scanner |
| `--include=PATTERN` | Include files matching pattern | ✅ Implemented | Glob matching in scanner |
@@ -248,12 +248,11 @@ Ranked by user demand, implementation complexity, and interoperability impact:
| 2 | `--ignore-times` / `-I` | Low | Medium — useful for forcing re-transfer |
| 3 | `--size-only` | Low | Medium — common migration scenario |
| 4 | `--existing` / `--ignore-existing` | Low | Medium — common sync patterns |
| 5 | `--remove-source-files` | Low | High — common for moves/backup |
| 6 | `--delete-during` | Medium | High — performance improvement |
| 7 | `--delay-updates` | Medium | High — atomic updates |
| 8 | `--chmod` | Low | Medium — permission flexibility |
| 9 | `--executability` / `-E` | Low | Low — simple flag |
| 10 | `--skip-compress` | Low | Medium — performance tuning |
| 5 | `--delete-during` | Medium | High — performance improvement |
| 6 | `--delay-updates` | Medium | High — atomic updates |
| 7 | `--chmod` | Low | Medium — permission flexibility |
| 8 | `--executability` / `-E` | Low | Low — simple flag |
| 9 | `--skip-compress` | Low | Medium — performance tuning |
---
+22
View File
@@ -86,6 +86,22 @@ static int set_nonneg_int_option(int* dest, const char* value, const char* optio
return 0;
}
static int set_stderr_mode(const char* value) {
if (strcmp(value, "errors") == 0 || strcmp(value, "e") == 0)
log_set_stderr_mode(LOG_STDERR_ERRORS);
else if (strcmp(value, "all") == 0 || strcmp(value, "a") == 0)
log_set_stderr_mode(LOG_STDERR_ALL);
else if (strcmp(value, "client") == 0 || strcmp(value, "c") == 0) {
log_message(LOG_LEVEL_ERROR,
"--stderr=client is not supported: FastSync has no client message channel");
return -1;
} else {
log_message(LOG_LEVEL_ERROR, "--stderr must be errors or all");
return -1;
}
return 0;
}
static int read_patterns_from_file(const char* filepath, char*** patterns, int* count);
/* Parse a string as an unsigned long long. Returns 0 on success, -1 on error. */
@@ -355,6 +371,12 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
}
config->log_file = lf;
log_set_file(lf);
} else if (strncmp(argv[i], "--stderr=", 9) == 0) {
if (set_stderr_mode(argv[i] + 9) != 0)
return -1;
} else if (opt_is(argv[i], "--stderr", NULL)) {
if (i + 1 >= argc || set_stderr_mode(argv[++i]) != 0)
return -1;
} else if (opt_is(argv[i], "--exclude-from", NULL) && i + 1 < argc) {
if (read_patterns_from_file(argv[++i], &config->exclude_patterns, &config->exclude_count) !=
0)
+1
View File
@@ -62,6 +62,7 @@ void print_usage(void) {
printf(" -h, --human-readable Print byte sizes in human-readable form\n");
printf(" --max-depth <n> Maximum directory depth (0=unlimited)\n");
printf(" --log-file <path> Write log messages to file\n");
printf(" --stderr=MODE Route logging to stderr: errors or all\n");
printf(" --partial Keep partial files on interrupted transfer\n");
printf(" --partial-dir <dir> Directory for partial files\n");
printf(" --fastsync-server-path <path>\n");
+10 -1
View File
@@ -9,6 +9,7 @@ static const char* log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"};
static LogLevel current_log_level = LOG_LEVEL_WARNING;
static FILE* log_fp = NULL;
static _Thread_local bool eight_bit_output;
static LogStderrMode stderr_mode = LOG_STDERR_ERRORS;
void set_log_level(LogLevel level) {
current_log_level = level;
@@ -26,6 +27,14 @@ bool log_get_8_bit_output(void) {
return eight_bit_output;
}
void log_set_stderr_mode(LogStderrMode mode) {
stderr_mode = mode;
}
LogStderrMode log_get_stderr_mode(void) {
return stderr_mode;
}
static inline void write_message(FILE* dest_io, LogLevel log_level, struct tm t, const char* format,
va_list args) {
fprintf(dest_io, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t.tm_year + 1900, t.tm_mon + 1,
@@ -46,7 +55,7 @@ void log_message(LogLevel log_level, const char* format, ...) {
return;
FILE* dest_io = stdout;
if (log_level == LOG_LEVEL_ERROR) {
if (stderr_mode == LOG_STDERR_ALL || log_level == LOG_LEVEL_ERROR) {
dest_io = stderr;
}
+3
View File
@@ -5,6 +5,7 @@
#include <stdbool.h>
typedef enum { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR } LogLevel;
typedef enum { LOG_STDERR_ERRORS, LOG_STDERR_ALL } LogStderrMode;
void log_message(LogLevel log_level, const char* message, ...);
void log_perror(const char* context);
@@ -12,5 +13,7 @@ void set_log_level(LogLevel level);
void log_set_file(FILE* fp);
void log_set_8_bit_output(bool enabled);
bool log_get_8_bit_output(void);
void log_set_stderr_mode(LogStderrMode mode);
LogStderrMode log_get_stderr_mode(void);
#endif
+36
View File
@@ -1,6 +1,7 @@
#include "test_client_cli.h"
#include "client_validation.h"
#include "config.h"
#include "log.h"
#include "test_utils.h"
#include "utils.h"
#include <stdlib.h>
@@ -415,6 +416,39 @@ static void test_parse_args_8_bit_output() {
config_delete(cfg);
}
static void test_parse_args_stderr_modes() {
static const char* const modes[] = {"errors", "all", "e", "a"};
static const LogStderrMode expected[] = {LOG_STDERR_ERRORS, LOG_STDERR_ALL, LOG_STDERR_ERRORS,
LOG_STDERR_ALL};
for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) {
Config* cfg = config_create();
char option[32];
snprintf(option, sizeof(option), "--stderr=%s", modes[i]);
char* argv[] = {"fastsync", option, "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
EXPECT_EQ_INT(log_get_stderr_mode(), expected[i]);
config_delete(cfg);
}
log_set_stderr_mode(LOG_STDERR_ERRORS);
}
static void test_parse_args_rejects_unsupported_stderr_modes() {
static const char* const modes[] = {"client", "c", "invalid"};
for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) {
Config* cfg = config_create();
char option[32];
snprintf(option, sizeof(option), "--stderr=%s", modes[i]);
char* argv[] = {"fastsync", option, "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1);
config_delete(cfg);
}
log_set_stderr_mode(LOG_STDERR_ERRORS);
}
void test_client_cli() {
test_validate_config_required_paths();
test_validate_config_incompatible_options();
@@ -442,4 +476,6 @@ void test_client_cli() {
test_parse_args_archive();
test_parse_args_fsync();
test_parse_args_8_bit_output();
test_parse_args_stderr_modes();
test_parse_args_rejects_unsupported_stderr_modes();
}
+26
View File
@@ -1,6 +1,8 @@
#include "test_log.h"
#include "log.h"
#include "test_utils.h"
#include <string.h>
#include <unistd.h>
/* Test default log level: WARNING and ERROR should print, DEBUG and INFO should not.
* We can't easily capture stderr in unit tests, so we verify the functions don't crash
@@ -90,6 +92,29 @@ static void test_log_filtering() {
EXPECT_TRUE(true);
}
static void test_log_stderr_mode_all() {
int pipe_fds[2];
EXPECT_EQ_INT(pipe(pipe_fds), 0);
int saved_stderr = dup(STDERR_FILENO);
EXPECT_TRUE(saved_stderr >= 0);
EXPECT_TRUE(dup2(pipe_fds[1], STDERR_FILENO) >= 0);
close(pipe_fds[1]);
set_log_level(LOG_LEVEL_WARNING);
log_set_stderr_mode(LOG_STDERR_ALL);
log_message(LOG_LEVEL_WARNING, "warning routed to stderr");
fflush(stderr);
EXPECT_TRUE(dup2(saved_stderr, STDERR_FILENO) >= 0);
close(saved_stderr);
char output[128] = {0};
ssize_t length = read(pipe_fds[0], output, sizeof(output) - 1);
close(pipe_fds[0]);
EXPECT_TRUE(length > 0);
EXPECT_TRUE(strstr(output, "warning routed to stderr") != NULL);
log_set_stderr_mode(LOG_STDERR_ERRORS);
}
/* Test that log_message handles various format strings */
static void test_log_message_formats() {
set_log_level(LOG_LEVEL_DEBUG);
@@ -112,5 +137,6 @@ void test_log() {
test_log_set_level_info();
test_log_set_level_error();
test_log_filtering();
test_log_stderr_mode_all();
test_log_message_formats();
}