Skip to content

Commit 79a4a56

Browse files
lneelyLevi Neelyclaude
authored
Add GitHub Actions CI workflow for unit tests (#379)
* Add unit test for psync_task_free refcount fix (#377) Adds tests/unit-tests/test_ptask_free.c to verify all code paths of the psync_task_free fix from #377: single-owner free, last-ref destroy, non-last-ref decrement, READY task signaling, and lock-before-refcnt ordering. All 5 tests pass. Also adds compiled binary to .gitignore. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add GitHub Actions CI workflow for unit tests Triggers on push/PR to automated-testing branch. Installs cmake and build-essential, builds all test targets via cmake, and runs ctest --output-on-failure. Fails workflow on any test failure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Replace cmake CI with make tests/check targets Adds tests and check targets to Makefile — no cmake required. Each test binary is built with the correct flags (pthread, -lrt, --wrap linker flags for prun/ptools_errptr). CI workflow installs only build-essential, runs make tests then make check; exits non-zero on any failure. All 8 test suites pass locally. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix CI: install libfuse3-dev so Makefile parses on Ubuntu detect_fuse.sh runs at Makefile parse time; without fuse headers the $(error) fires before any target runs. Adding libfuse3-dev unblocks make tests (test binaries themselves don't link fuse). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix makefile * Add automated testing infrastructure - Update CI workflow to run unit tests and build verification - Add Makefile targets for test compilation and execution - Implement unit tests for pdbg_path, prun, and read_response - Add test stubs for pCloud API mocking - Add test binaries for pfs_lock_ordering and signal_safety verification * Add missing dependencies to CI workflow Install libfuse-dev and libssl-dev required for build * Add test job to c-cpp.yml workflow Include unit test execution in C/C++ workflow * Add missing stubs to test_stubs.c Complete stub implementations for all required pCloud API functions * Fix stub signatures to match headers Correct function signatures for pCloud API stubs * Fix psql_* stub signatures Correct all psql function signatures to match headers * Fix stub implementations and Makefile Update stub functions and build configuration * Link real utility files instead of stubbing Update Makefile to use actual implementation files for utilities * Complete test framework with all 41 tests passing - Makefile: Add test rules with real dependencies - tests/stubs/test_stubs.c: Minimal stubs for external APIs - tests/stubs/test_stubs_cpp.c: Stubs for C++ test - pclsync/putil.c: Add null check in putil_strdup - pclsync/pdbg.c: Add recursion guard in pdbg_printf * Remove duplicate ci.yml workflow Consolidate CI configuration into c-cpp.yml * Remove compiled test binaries from git - Remove test_pfs_lock_ordering and test_signal_safety binaries - Add tests/test_* to .gitignore to prevent future commits --------- Co-authored-by: Levi Neely <lkn@darkstar.example.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 427d1c5 commit 79a4a56

11 files changed

Lines changed: 695 additions & 418 deletions

File tree

.github/workflows/c-cpp.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,15 @@ jobs:
122122
run: |
123123
nm pcloudcc | grep 'fuse_loop_mt_31'
124124
ldd pcloudcc | grep libfuse3
125+
126+
test:
127+
name: Unit Tests
128+
runs-on: ubuntu-latest
129+
steps:
130+
- uses: actions/checkout@v4
131+
- name: Install dependencies
132+
run: |
133+
sudo apt-get update
134+
sudo apt-get install -y build-essential libfuse3-dev libmbedtls-dev libsqlite3-dev libreadline-dev libudev-dev zlib1g-dev
135+
- name: Run tests
136+
run: make test

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,5 @@ tests/test_prun
3232
tests/test_ptools_errptr
3333
tests/test_ptools_params
3434
tests/test_read_response
35+
tests/test_ptask_free
36+
tests/test_*

Makefile

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,70 @@ uninstall:
145145
rm -f $(DESTDIR)/bin/pcloudcc
146146
rm -f $(DESTDIR)/lib/libpcloudcc_lib.so
147147
rm -f /etc/logrotate.d/pcloudcc
148+
149+
# ---------------------------------------------------------------------------
150+
# Unit tests — link against actual production code from pclsync/
151+
# ---------------------------------------------------------------------------
152+
UNIT_DIR := tests/unit-tests
153+
TESTS_DIR := tests
154+
155+
TEST_CFLAGS := -D_POSIX_C_SOURCE=200809L
156+
TEST_CXXFLAGS := -D_POSIX_C_SOURCE=200809L
157+
158+
TEST_BINS := \
159+
tests/test_pdbg_path \
160+
tests/test_ptools_params \
161+
tests/test_pfs_lock_ordering \
162+
tests/test_ptask_free \
163+
tests/test_prun \
164+
tests/test_ptools_errptr \
165+
tests/test_read_response \
166+
tests/test_signal_safety
167+
168+
.PHONY: test tests check clean-tests
169+
170+
test: check
171+
172+
tests: $(TEST_BINS)
173+
174+
check: tests
175+
@rc=0; \
176+
for t in $(TEST_BINS); do \
177+
echo "=== $$t ==="; \
178+
$$t || rc=$$?; \
179+
done; \
180+
exit $$rc
181+
182+
clean-tests:
183+
rm -f $(TEST_BINS)
184+
185+
tests/test_pdbg_path: $(UNIT_DIR)/test_pdbg_path.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c $(LIBDIR)/ppath.c tests/stubs/test_stubs.c
186+
$(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^
187+
188+
tests/test_ptools_params: $(UNIT_DIR)/test_ptools_params.c $(LIBDIR)/ptools.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c $(LIBDIR)/ppath.c tests/stubs/test_stubs.c
189+
$(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^
190+
191+
tests/test_pfs_lock_ordering: $(UNIT_DIR)/test_pfs_lock_ordering.c
192+
$(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $< -lpthread
193+
194+
tests/test_ptask_free: $(UNIT_DIR)/test_ptask_free.c
195+
$(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $< -lpthread
196+
197+
tests/test_prun: $(UNIT_DIR)/test_prun.c $(LIBDIR)/prun.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c $(LIBDIR)/ppath.c tests/stubs/test_stubs.c
198+
$(CC) -D_POSIX_C_SOURCE=199309L $(CFLAGS) -o $@ $^ \
199+
-Wl,--wrap=pthread_create \
200+
-Wl,--wrap=pthread_attr_destroy \
201+
-Wl,--wrap=malloc \
202+
-Wl,--wrap=free \
203+
-lpthread
204+
205+
tests/test_ptools_errptr: $(UNIT_DIR)/test_ptools_errptr.c $(LIBDIR)/ptools.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c $(LIBDIR)/ppath.c tests/stubs/test_stubs.c
206+
$(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^ \
207+
-Wl,--wrap=malloc \
208+
-Wl,--wrap=free
209+
210+
tests/test_read_response: $(UNIT_DIR)/test_read_response.cpp rpcclient.cpp tests/stubs/test_stubs_cpp.c
211+
$(CXX) $(TEST_CXXFLAGS) $(CXXFLAGS) -o $@ $^
212+
213+
tests/test_signal_safety: $(TESTS_DIR)/test_signal_safety.c
214+
$(CC) -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809L -o $@ $< -lpthread -lrt

pclsync/pdbg.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,19 @@ char *pfs_event_log_path() {
154154
}
155155

156156
int pdbg_printf(const char *file, const char *function, int unsigned line, int unsigned level, const char *fmt, ...) {
157+
/* Recursion guard */
158+
static __thread int in_pdbg_printf = 0;
159+
if (in_pdbg_printf)
160+
return 1;
161+
in_pdbg_printf = 1;
162+
157163
/* Initialize debug level from environment on first call */
158164
pdbg_init_level();
159165

160-
if (!IS_DEBUG)
166+
if (!IS_DEBUG) {
167+
in_pdbg_printf = 0;
161168
return 1;
169+
}
162170

163171
static const struct {
164172
unsigned long level;
@@ -229,6 +237,7 @@ int pdbg_printf(const char *file, const char *function, int unsigned line, int u
229237
va_end(ap);
230238
fflush(log_file);
231239
pthread_mutex_unlock(&log_mutex);
240+
in_pdbg_printf = 0;
232241
return 1;
233242
}
234243

pclsync/putil.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,12 @@ void putil_time_format(time_t tm, unsigned long ns, char *result) {
120120

121121
char *putil_strdup(const char *str) {
122122
size_t len;
123+
char *ptr;
123124
len = strlen(str) + 1;
124-
return (char *)memcpy(pmem_malloc_array(PMEM_SUBSYS_OTHER, len, sizeof(char)), str, len);
125+
ptr = (char *)pmem_malloc_array(PMEM_SUBSYS_OTHER, len, sizeof(char));
126+
if (!ptr)
127+
return NULL;
128+
return (char *)memcpy(ptr, str, len);
125129
}
126130

127131
char *putil_strnormalize_filename(const char *str) {

tests/stubs/test_stubs.c

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#define _POSIX_C_SOURCE 200809L
2+
#include <pthread.h>
3+
#include <stdio.h>
4+
#include <stdlib.h>
5+
#include <string.h>
6+
#include <time.h>
7+
#include <stdint.h>
8+
#include <sys/types.h>
9+
10+
#ifdef __cplusplus
11+
extern "C" {
12+
#endif
13+
14+
/* Include headers before implementation */
15+
#include "../pclsync/psock.h"
16+
#include "../pclsync/papi.h"
17+
#include "../pclsync/psql.h"
18+
#include "../pclsync/psettings.h"
19+
20+
/* Thread-local storage stub */
21+
__thread const char *psync_thread_name = "test";
22+
__thread uint32_t psync_error = 0;
23+
24+
/* Global stubs */
25+
const char *psync_my_auth = "test_auth";
26+
const char *apiserver = "https://api.pcloud.com";
27+
28+
/* psync_setting stubs */
29+
int psync_setting_get_bool(int setting) {
30+
(void)setting;
31+
return 0;
32+
}
33+
34+
/* papi stubs */
35+
psock_t *papi_connect(const char *hostname, int usessl) {
36+
(void)hostname;
37+
(void)usessl;
38+
return NULL;
39+
}
40+
41+
binresult *papi_send(psock_t *sock, const char *command, size_t cmdlen, const binparam *params, size_t paramcnt, int64_t datalen, int readres) {
42+
(void)sock;
43+
(void)command;
44+
(void)cmdlen;
45+
(void)params;
46+
(void)paramcnt;
47+
(void)datalen;
48+
(void)readres;
49+
return NULL;
50+
}
51+
52+
const binresult *papi_find_result(const binresult *res, const char *name, uint32_t type, const char *file, const char *function, unsigned int line) {
53+
(void)res;
54+
(void)name;
55+
(void)type;
56+
(void)file;
57+
(void)function;
58+
(void)line;
59+
return NULL;
60+
}
61+
62+
/* psock stubs */
63+
void psock_close(psock_t *sock) {
64+
(void)sock;
65+
}
66+
67+
/* psql stubs */
68+
int64_t psql_cellint(const char *sql, int64_t dflt) {
69+
(void)sql;
70+
return dflt;
71+
}
72+
73+
psync_sql_res *psql_prepare(const char *sql) {
74+
(void)sql;
75+
return NULL;
76+
}
77+
78+
void psql_bind_uint(psync_sql_res *res, int n, uint64_t val) {
79+
(void)res;
80+
(void)n;
81+
(void)val;
82+
}
83+
84+
int psql_run_free(psync_sql_res *res) {
85+
(void)res;
86+
return -1;
87+
}
88+
89+
psync_sql_res *psql_query(const char *sql) {
90+
(void)sql;
91+
return NULL;
92+
}
93+
94+
psync_variant_row psql_fetch(psync_sql_res *res) {
95+
(void)res;
96+
return NULL;
97+
}
98+
99+
void psql_free(psync_sql_res *res) {
100+
(void)res;
101+
}
102+
103+
const char *psql_expect_str(const char *name, const char *sql, uint32_t row, const psync_variant *params) {
104+
(void)name;
105+
(void)sql;
106+
(void)row;
107+
(void)params;
108+
return "";
109+
}
110+
111+
uint64_t psql_expect_num(const char *name, const char *sql, uint32_t row, const psync_variant *params) {
112+
(void)name;
113+
(void)sql;
114+
(void)row;
115+
(void)params;
116+
return 0;
117+
}
118+
119+
void psql_try_free(void) {
120+
/* no-op */
121+
}
122+
123+
int psql_reopen(const char *path) {
124+
(void)path;
125+
return 0;
126+
}
127+
128+
/* pfile stubs */
129+
int pfile_stat_mode_ok(mode_t mode) {
130+
(void)mode;
131+
return 1;
132+
}
133+
134+
int pfile_rename(const char *oldpath, const char *newpath) {
135+
(void)oldpath;
136+
(void)newpath;
137+
return 0;
138+
}
139+
140+
#ifdef __cplusplus
141+
}
142+
#endif

tests/stubs/test_stubs_cpp.c

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#define _POSIX_C_SOURCE 200809L
2+
#include <pthread.h>
3+
#include <stdio.h>
4+
#include <stdlib.h>
5+
#include <string.h>
6+
#include <stdint.h>
7+
8+
#ifdef __cplusplus
9+
extern "C" {
10+
#endif
11+
12+
/* Thread-local storage stub */
13+
__thread const char *psync_thread_name = "test";
14+
__thread uint32_t psync_error = 0;
15+
16+
/* Global stubs */
17+
const char *psync_my_auth = "test_auth";
18+
const char *apiserver = "https://api.pcloud.com";
19+
unsigned int pdbg_runtime_level = 0;
20+
21+
/* pmem stubs */
22+
void *pmem_malloc(int subsystem, size_t size) {
23+
(void)subsystem;
24+
return malloc(size);
25+
}
26+
27+
void pmem_free(int subsystem, void *ptr) {
28+
(void)subsystem;
29+
free(ptr);
30+
}
31+
32+
/* putil stub */
33+
void putil_wipe(void *mem, size_t sz) {
34+
if (!mem || sz == 0) return;
35+
volatile unsigned char *p = (volatile unsigned char *)mem;
36+
memset((void*)p, 0x00, sz);
37+
memset((void*)p, 0xFF, sz);
38+
memset((void*)p, 0x00, sz);
39+
}
40+
41+
/* prpc stub */
42+
char *prpc_sockpath(void) {
43+
const char *home = getenv("HOME");
44+
if (!home) return NULL;
45+
size_t len = strlen(home) + 20;
46+
char *path = (char *)malloc(len);
47+
if (!path) return NULL;
48+
snprintf(path, len, "%s/.pcloud/prpc.sock", home);
49+
return path;
50+
}
51+
52+
/* pdbg stub */
53+
int pdbg_printf(const char *file, const char *function, unsigned int line, unsigned int level, const char *fmt, ...) {
54+
(void)file;
55+
(void)function;
56+
(void)line;
57+
(void)level;
58+
(void)fmt;
59+
return 1;
60+
}
61+
62+
#ifdef __cplusplus
63+
}
64+
#endif

0 commit comments

Comments
 (0)