Update.
This commit is contained in:
commit
bde988cd6a
131
.gitea/workflows/build.yaml
Normal file
131
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,131 @@
|
||||
name: Build and Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master, develop]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
|
||||
jobs:
|
||||
build-framework:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential uuid-dev valgrind
|
||||
|
||||
- name: Build CelerityAPI Example
|
||||
run: make all
|
||||
|
||||
- name: Build DocDB
|
||||
run: make -f Makefile.docdb all
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binaries
|
||||
path: |
|
||||
example
|
||||
loadtest
|
||||
docserver
|
||||
docclient
|
||||
|
||||
test-framework:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-framework
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential uuid-dev
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
make all
|
||||
make -f Makefile.docdb all
|
||||
|
||||
- name: Test CelerityAPI
|
||||
run: |
|
||||
./example --port 9000 &
|
||||
sleep 2
|
||||
./loadtest -c 10 -d 3 http://127.0.0.1:9000/health
|
||||
pkill -f "./example --port 9000" || true
|
||||
|
||||
- name: Test DocDB
|
||||
run: |
|
||||
./docserver --port 9080 &
|
||||
sleep 2
|
||||
./docclient --port 9080 --quick
|
||||
pkill -f "./docserver --port 9080" || true
|
||||
|
||||
test-docdb-full:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-framework
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential uuid-dev
|
||||
|
||||
- name: Build DocDB
|
||||
run: make -f Makefile.docdb all
|
||||
|
||||
- name: Run full test suite
|
||||
run: |
|
||||
./docserver --port 9080 &
|
||||
sleep 2
|
||||
./docclient --port 9080
|
||||
pkill -f "./docserver --port 9080" || true
|
||||
|
||||
- name: Test persistence
|
||||
run: |
|
||||
rm -rf ./docdb_data
|
||||
./docserver --port 9080 &
|
||||
sleep 1
|
||||
curl -X POST http://127.0.0.1:9080/collections -H "Content-Type: application/json" -d '{"name":"ci_test"}'
|
||||
curl -X POST http://127.0.0.1:9080/collections/ci_test/documents -H "Content-Type: application/json" -d '{"test":"data"}'
|
||||
pkill -f "./docserver --port 9080" || true
|
||||
sleep 1
|
||||
./docserver --port 9080 &
|
||||
sleep 1
|
||||
curl -s http://127.0.0.1:9080/collections/ci_test | grep -q "ci_test"
|
||||
pkill -f "./docserver --port 9080" || true
|
||||
|
||||
memory-check:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-framework
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential uuid-dev valgrind
|
||||
|
||||
- name: Build with debug symbols
|
||||
run: |
|
||||
make debug
|
||||
make -f Makefile.docdb debug
|
||||
|
||||
- name: Memory check CelerityAPI
|
||||
run: |
|
||||
timeout 10 valgrind --leak-check=full --error-exitcode=1 ./example --port 9000 &
|
||||
sleep 3
|
||||
curl http://127.0.0.1:9000/health || true
|
||||
curl http://127.0.0.1:9000/users || true
|
||||
pkill -f "./example" || true
|
||||
|
||||
- name: Memory check DocDB
|
||||
run: |
|
||||
timeout 10 valgrind --leak-check=full --error-exitcode=1 ./docserver --port 9080 &
|
||||
sleep 3
|
||||
curl -X POST http://127.0.0.1:9080/collections -H "Content-Type: application/json" -d '{"name":"valgrind_test"}' || true
|
||||
curl http://127.0.0.1:9080/collections || true
|
||||
pkill -f "./docserver" || true
|
||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
example
|
||||
loadtest
|
||||
docserver
|
||||
docclient
|
||||
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
*.dSYM/
|
||||
|
||||
docdb_data/
|
||||
|
||||
*.log
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.vscode/
|
||||
.idea/
|
||||
*.sublime-*
|
||||
|
||||
core
|
||||
vgcore.*
|
||||
*.core
|
||||
|
||||
compile_commands.json
|
||||
.cache/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
38
Makefile
Normal file
38
Makefile
Normal file
@ -0,0 +1,38 @@
|
||||
CC = gcc
|
||||
CFLAGS = -std=c11 -Wall -Wextra -O2
|
||||
LDFLAGS = -lpthread
|
||||
|
||||
TARGET = example
|
||||
LOADTEST = loadtest
|
||||
SOURCES = celerityapi.c example.c
|
||||
HEADERS = celerityapi.h
|
||||
|
||||
.PHONY: all clean run debug test
|
||||
|
||||
all: $(TARGET) $(LOADTEST)
|
||||
|
||||
$(TARGET): $(SOURCES) $(HEADERS)
|
||||
$(CC) $(CFLAGS) -o $(TARGET) $(SOURCES) $(LDFLAGS)
|
||||
|
||||
$(LOADTEST): loadtest.c
|
||||
$(CC) $(CFLAGS) -o $(LOADTEST) loadtest.c $(LDFLAGS) -lm
|
||||
|
||||
debug: CFLAGS += -g -DDEBUG
|
||||
debug: clean all
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET) $(LOADTEST)
|
||||
|
||||
run: $(TARGET)
|
||||
./$(TARGET)
|
||||
|
||||
test: $(TARGET) $(LOADTEST)
|
||||
@echo "Starting server..."
|
||||
@./$(TARGET) --port 9000 &
|
||||
@sleep 2
|
||||
@echo "Running load test..."
|
||||
@./$(LOADTEST) -c 50 -d 5 http://127.0.0.1:9000/health
|
||||
@pkill -f "./$(TARGET) --port 9000" || true
|
||||
|
||||
valgrind: debug
|
||||
valgrind --leak-check=full --show-leak-kinds=all ./$(TARGET)
|
||||
88
Makefile.docdb
Normal file
88
Makefile.docdb
Normal file
@ -0,0 +1,88 @@
|
||||
CC = gcc
|
||||
CFLAGS = -std=c11 -Wall -Wextra -O2
|
||||
LDFLAGS = -lpthread -luuid
|
||||
|
||||
DOCSERVER = docserver
|
||||
DOCCLIENT = docclient
|
||||
SERVER_SOURCES = celerityapi.c docserver.c
|
||||
CLIENT_SOURCES = docclient.c
|
||||
HEADERS = celerityapi.h
|
||||
|
||||
.PHONY: all clean server client run test test-quick test-stress valgrind help
|
||||
|
||||
all: $(DOCSERVER) $(DOCCLIENT)
|
||||
|
||||
$(DOCSERVER): $(SERVER_SOURCES) $(HEADERS)
|
||||
$(CC) $(CFLAGS) -o $(DOCSERVER) $(SERVER_SOURCES) $(LDFLAGS)
|
||||
|
||||
$(DOCCLIENT): $(CLIENT_SOURCES)
|
||||
$(CC) $(CFLAGS) -o $(DOCCLIENT) $(CLIENT_SOURCES)
|
||||
|
||||
server: $(DOCSERVER)
|
||||
|
||||
client: $(DOCCLIENT)
|
||||
|
||||
debug: CFLAGS += -g -DDEBUG
|
||||
debug: clean all
|
||||
|
||||
clean:
|
||||
rm -f $(DOCSERVER) $(DOCCLIENT)
|
||||
rm -rf ./docdb_data
|
||||
|
||||
run: $(DOCSERVER)
|
||||
./$(DOCSERVER)
|
||||
|
||||
test: $(DOCSERVER) $(DOCCLIENT)
|
||||
@echo "Starting DocDB server..."
|
||||
@rm -rf ./docdb_data
|
||||
@./$(DOCSERVER) --port 9080 &
|
||||
@sleep 1
|
||||
@echo "Running test client..."
|
||||
@./$(DOCCLIENT) --port 9080 || true
|
||||
@pkill -f "./$(DOCSERVER) --port 9080" || true
|
||||
|
||||
test-quick: $(DOCSERVER) $(DOCCLIENT)
|
||||
@echo "Starting DocDB server..."
|
||||
@rm -rf ./docdb_data
|
||||
@./$(DOCSERVER) --port 9080 &
|
||||
@sleep 1
|
||||
@echo "Running quick tests..."
|
||||
@./$(DOCCLIENT) --port 9080 --quick || true
|
||||
@pkill -f "./$(DOCSERVER) --port 9080" || true
|
||||
|
||||
test-stress: $(DOCSERVER) $(DOCCLIENT)
|
||||
@echo "Starting DocDB server..."
|
||||
@rm -rf ./docdb_data
|
||||
@./$(DOCSERVER) --port 9080 &
|
||||
@sleep 1
|
||||
@echo "Running stress tests..."
|
||||
@./$(DOCCLIENT) --port 9080 --stress-only || true
|
||||
@pkill -f "./$(DOCSERVER) --port 9080" || true
|
||||
|
||||
test-persistence: $(DOCSERVER) $(DOCCLIENT)
|
||||
@./$(DOCCLIENT) --port 9080 --test-persistence
|
||||
|
||||
valgrind: debug
|
||||
valgrind --leak-check=full --show-leak-kinds=all ./$(DOCSERVER)
|
||||
|
||||
help:
|
||||
@echo "DocDB Build System"
|
||||
@echo ""
|
||||
@echo "Targets:"
|
||||
@echo " all Build server and client"
|
||||
@echo " server Build server only"
|
||||
@echo " client Build client only"
|
||||
@echo " debug Build with debug symbols"
|
||||
@echo " clean Remove binaries and data"
|
||||
@echo " run Build and run server"
|
||||
@echo " test Run full test suite"
|
||||
@echo " test-quick Run basic tests only"
|
||||
@echo " test-stress Run stress tests only"
|
||||
@echo " valgrind Run with memory leak detection"
|
||||
@echo " help Show this help"
|
||||
@echo ""
|
||||
@echo "Server options:"
|
||||
@echo " ./docserver --host 0.0.0.0 --port 8080 --data ./data"
|
||||
@echo ""
|
||||
@echo "Client options:"
|
||||
@echo " ./docclient --host 127.0.0.1 --port 8080 --verbose"
|
||||
256
README.md
Normal file
256
README.md
Normal file
@ -0,0 +1,256 @@
|
||||
# CelerityAPI
|
||||
|
||||
A high-performance HTTP web framework written in pure C, inspired by FastAPI. Zero external dependencies beyond standard C library and POSIX APIs.
|
||||
|
||||
## Features
|
||||
|
||||
- Non-blocking I/O with epoll
|
||||
- Trie-based URL routing with path parameters
|
||||
- JSON parsing and serialization
|
||||
- Middleware pipeline (logging, CORS, timing, auth)
|
||||
- Dependency injection system
|
||||
- Request validation with schemas
|
||||
- WebSocket support
|
||||
- OpenAPI schema generation
|
||||
- Thread-safe design
|
||||
|
||||
## Requirements
|
||||
|
||||
- GCC with C11 support
|
||||
- POSIX-compliant system (Linux, macOS, BSD)
|
||||
- uuid-dev (for DocDB)
|
||||
- pthread
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
make all
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```c
|
||||
#include "celerityapi.h"
|
||||
|
||||
static http_response_t* handle_hello(http_request_t *req, dependency_context_t *deps) {
|
||||
(void)req; (void)deps;
|
||||
http_response_t *resp = http_response_create(200);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "message", json_string("Hello, World"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
app_context_t *app = app_create("MyAPI", "1.0.0");
|
||||
ROUTE_GET(app, "/hello", handle_hello);
|
||||
app_run(app, "127.0.0.1", 8000);
|
||||
app_destroy(app);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Application
|
||||
|
||||
```c
|
||||
app_context_t* app_create(const char *title, const char *version);
|
||||
void app_add_route(app_context_t *app, http_method_t method, const char *path,
|
||||
route_handler_fn handler, field_schema_t *schema);
|
||||
void app_add_middleware(app_context_t *app, middleware_fn middleware, void *ctx);
|
||||
void app_add_dependency(app_context_t *app, const char *name, dep_factory_fn factory,
|
||||
dep_cleanup_fn cleanup, dependency_scope_t scope);
|
||||
void app_run(app_context_t *app, const char *host, int port);
|
||||
void app_destroy(app_context_t *app);
|
||||
```
|
||||
|
||||
### Route Macros
|
||||
|
||||
```c
|
||||
ROUTE_GET(app, "/path", handler);
|
||||
ROUTE_POST(app, "/path", handler, schema);
|
||||
ROUTE_PUT(app, "/path", handler, schema);
|
||||
ROUTE_DELETE(app, "/path", handler);
|
||||
ROUTE_PATCH(app, "/path", handler, schema);
|
||||
```
|
||||
|
||||
### JSON
|
||||
|
||||
```c
|
||||
json_value_t* json_parse(const char *json_str);
|
||||
char* json_serialize(json_value_t *value);
|
||||
json_value_t* json_object(void);
|
||||
json_value_t* json_array(void);
|
||||
json_value_t* json_string(const char *value);
|
||||
json_value_t* json_number(double value);
|
||||
json_value_t* json_bool(bool value);
|
||||
json_value_t* json_null(void);
|
||||
void json_object_set(json_value_t *obj, const char *key, json_value_t *value);
|
||||
json_value_t* json_object_get(json_value_t *obj, const char *key);
|
||||
void json_array_append(json_value_t *arr, json_value_t *value);
|
||||
void json_value_free(json_value_t *value);
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```c
|
||||
http_response_t* http_response_create(int status_code);
|
||||
void http_response_set_header(http_response_t *resp, const char *key, const char *value);
|
||||
void http_response_set_body(http_response_t *resp, const char *body, size_t length);
|
||||
void http_response_set_json(http_response_t *resp, json_value_t *json);
|
||||
void http_response_free(http_response_t *resp);
|
||||
```
|
||||
|
||||
### Built-in Middleware
|
||||
|
||||
```c
|
||||
http_response_t* logging_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx);
|
||||
http_response_t* cors_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx);
|
||||
http_response_t* timing_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx);
|
||||
http_response_t* auth_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx);
|
||||
```
|
||||
|
||||
## Path Parameters
|
||||
|
||||
```c
|
||||
ROUTE_GET(app, "/users/{id}", handle_get_user);
|
||||
|
||||
static http_response_t* handle_get_user(http_request_t *req, dependency_context_t *deps) {
|
||||
char *user_id = dict_get(req->path_params, "id");
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Request Validation
|
||||
|
||||
```c
|
||||
field_schema_t *schema = schema_create("user", TYPE_OBJECT);
|
||||
|
||||
field_schema_t *name = schema_create("name", TYPE_STRING);
|
||||
schema_set_required(name, true);
|
||||
schema_set_min(name, 3);
|
||||
schema_set_max(name, 100);
|
||||
schema_add_field(schema, name);
|
||||
|
||||
field_schema_t *age = schema_create("age", TYPE_INT);
|
||||
schema_set_min(age, 0);
|
||||
schema_set_max(age, 150);
|
||||
schema_add_field(schema, age);
|
||||
|
||||
ROUTE_POST(app, "/users", handle_create_user, schema);
|
||||
```
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
```c
|
||||
static void* database_factory(dependency_context_t *ctx) {
|
||||
return create_db_connection();
|
||||
}
|
||||
|
||||
static void database_cleanup(void *instance) {
|
||||
close_db_connection(instance);
|
||||
}
|
||||
|
||||
app_add_dependency(app, "database", database_factory, database_cleanup, DEP_SINGLETON);
|
||||
|
||||
static http_response_t* handler(http_request_t *req, dependency_context_t *deps) {
|
||||
db_t *db = dep_resolve(deps, "database");
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# DocDB
|
||||
|
||||
A document database built on CelerityAPI with file-based persistence.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
make -f Makefile.docdb all
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
./docserver --host 127.0.0.1 --port 8080 --data ./data
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
./docclient --host 127.0.0.1 --port 8080
|
||||
./docclient --quick # Basic tests only
|
||||
./docclient --stress-only # Performance tests
|
||||
```
|
||||
|
||||
## REST API
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | /health | Health check |
|
||||
| GET | /stats | Database statistics |
|
||||
| GET | /collections | List collections |
|
||||
| POST | /collections | Create collection |
|
||||
| GET | /collections/{name} | Get collection info |
|
||||
| DELETE | /collections/{name} | Delete collection |
|
||||
| GET | /collections/{name}/documents | List documents |
|
||||
| POST | /collections/{name}/documents | Create document |
|
||||
| GET | /collections/{name}/documents/{id} | Get document |
|
||||
| PUT | /collections/{name}/documents/{id} | Update document |
|
||||
| DELETE | /collections/{name}/documents/{id} | Delete document |
|
||||
| POST | /collections/{name}/query | Query documents |
|
||||
| POST | /collections/{name}/bulk | Bulk insert |
|
||||
| DELETE | /collections/{name}/bulk | Bulk delete |
|
||||
|
||||
## Examples
|
||||
|
||||
Create collection:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/collections \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"users"}'
|
||||
```
|
||||
|
||||
Create document:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/collections/users/documents \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"Alice","email":"alice@example.com"}'
|
||||
```
|
||||
|
||||
Query documents:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/collections/users/query \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"filter":{"name":"Alice"},"limit":10}'
|
||||
```
|
||||
|
||||
Bulk insert:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/collections/users/bulk \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"documents":[{"name":"Bob"},{"name":"Carol"}]}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
celerityapi.h Framework header
|
||||
celerityapi.c Framework implementation
|
||||
example.c Framework usage example
|
||||
loadtest.c HTTP load testing tool
|
||||
docserver.c Document database server
|
||||
docclient.c DocDB test client
|
||||
Makefile Framework build
|
||||
Makefile.docdb DocDB build
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
2679
celerityapi.c
Normal file
2679
celerityapi.c
Normal file
File diff suppressed because it is too large
Load Diff
481
celerityapi.h
Normal file
481
celerityapi.h
Normal file
@ -0,0 +1,481 @@
|
||||
#ifndef CELERITYAPI_H
|
||||
#define CELERITYAPI_H
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <time.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/epoll.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#define CELERITY_VERSION "1.0.0"
|
||||
#define MAX_EVENTS 1024
|
||||
#define BUFFER_SIZE 65536
|
||||
#define MAX_HEADERS 100
|
||||
#define MAX_PATH_SEGMENTS 32
|
||||
#define MAX_CHILDREN 64
|
||||
#define DICT_INITIAL_SIZE 64
|
||||
#define MAX_CONNECTIONS 10000
|
||||
#define WS_FRAME_MAX_SIZE 65535
|
||||
#define SHA1_BLOCK_SIZE 64
|
||||
#define SHA1_DIGEST_SIZE 20
|
||||
|
||||
typedef struct dict_entry dict_entry_t;
|
||||
typedef struct dict dict_t;
|
||||
typedef struct array array_t;
|
||||
typedef struct string_builder string_builder_t;
|
||||
typedef struct json_value json_value_t;
|
||||
typedef struct http_request http_request_t;
|
||||
typedef struct http_response http_response_t;
|
||||
typedef struct route_node route_node_t;
|
||||
typedef struct router router_t;
|
||||
typedef struct dependency dependency_t;
|
||||
typedef struct dependency_context dependency_context_t;
|
||||
typedef struct field_schema field_schema_t;
|
||||
typedef struct validation_error validation_error_t;
|
||||
typedef struct validation_result validation_result_t;
|
||||
typedef struct middleware_node middleware_node_t;
|
||||
typedef struct middleware_pipeline middleware_pipeline_t;
|
||||
typedef struct connection connection_t;
|
||||
typedef struct socket_server socket_server_t;
|
||||
typedef struct websocket websocket_t;
|
||||
typedef struct openapi_generator openapi_generator_t;
|
||||
typedef struct app_context app_context_t;
|
||||
|
||||
typedef http_response_t* (*route_handler_fn)(http_request_t *req, dependency_context_t *deps);
|
||||
typedef http_response_t* (*next_fn)(void *ctx);
|
||||
typedef http_response_t* (*middleware_fn)(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx);
|
||||
typedef void* (*dep_factory_fn)(dependency_context_t *ctx);
|
||||
typedef void (*dep_cleanup_fn)(void *instance);
|
||||
typedef void (*ws_message_handler_fn)(websocket_t *ws, const char *message, size_t length);
|
||||
|
||||
typedef enum {
|
||||
HTTP_GET,
|
||||
HTTP_POST,
|
||||
HTTP_PUT,
|
||||
HTTP_DELETE,
|
||||
HTTP_PATCH,
|
||||
HTTP_HEAD,
|
||||
HTTP_OPTIONS,
|
||||
HTTP_UNKNOWN
|
||||
} http_method_t;
|
||||
|
||||
typedef enum {
|
||||
JSON_NULL,
|
||||
JSON_BOOL,
|
||||
JSON_NUMBER,
|
||||
JSON_STRING,
|
||||
JSON_ARRAY,
|
||||
JSON_OBJECT
|
||||
} json_type_t;
|
||||
|
||||
typedef enum {
|
||||
TYPE_STRING,
|
||||
TYPE_INT,
|
||||
TYPE_FLOAT,
|
||||
TYPE_BOOL,
|
||||
TYPE_ARRAY,
|
||||
TYPE_OBJECT,
|
||||
TYPE_NULL
|
||||
} field_type_t;
|
||||
|
||||
typedef enum {
|
||||
DEP_FACTORY,
|
||||
DEP_SINGLETON,
|
||||
DEP_REQUEST_SCOPED
|
||||
} dependency_scope_t;
|
||||
|
||||
typedef enum {
|
||||
WS_OPCODE_CONTINUATION = 0x0,
|
||||
WS_OPCODE_TEXT = 0x1,
|
||||
WS_OPCODE_BINARY = 0x2,
|
||||
WS_OPCODE_CLOSE = 0x8,
|
||||
WS_OPCODE_PING = 0x9,
|
||||
WS_OPCODE_PONG = 0xA
|
||||
} ws_opcode_t;
|
||||
|
||||
struct dict_entry {
|
||||
char *key;
|
||||
void *value;
|
||||
dict_entry_t *next;
|
||||
};
|
||||
|
||||
struct dict {
|
||||
dict_entry_t **buckets;
|
||||
size_t size;
|
||||
size_t count;
|
||||
pthread_mutex_t lock;
|
||||
};
|
||||
|
||||
struct array {
|
||||
void **items;
|
||||
size_t count;
|
||||
size_t capacity;
|
||||
};
|
||||
|
||||
struct string_builder {
|
||||
char *data;
|
||||
size_t length;
|
||||
size_t capacity;
|
||||
};
|
||||
|
||||
struct json_value {
|
||||
json_type_t type;
|
||||
union {
|
||||
bool bool_value;
|
||||
double number_value;
|
||||
char *string_value;
|
||||
struct {
|
||||
json_value_t **items;
|
||||
size_t count;
|
||||
} array_value;
|
||||
struct {
|
||||
char **keys;
|
||||
json_value_t **values;
|
||||
size_t count;
|
||||
} object_value;
|
||||
};
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
char *key;
|
||||
char *value;
|
||||
} header_t;
|
||||
|
||||
struct http_request {
|
||||
http_method_t method;
|
||||
char *path;
|
||||
char *query_string;
|
||||
char *http_version;
|
||||
header_t *headers;
|
||||
size_t header_count;
|
||||
char *body;
|
||||
size_t body_length;
|
||||
dict_t *query_params;
|
||||
dict_t *path_params;
|
||||
json_value_t *json_body;
|
||||
connection_t *conn;
|
||||
};
|
||||
|
||||
struct http_response {
|
||||
int status_code;
|
||||
char *status_message;
|
||||
header_t *headers;
|
||||
size_t header_count;
|
||||
size_t header_capacity;
|
||||
char *body;
|
||||
size_t body_length;
|
||||
};
|
||||
|
||||
struct route_node {
|
||||
char *segment;
|
||||
bool is_parameter;
|
||||
char *param_name;
|
||||
route_node_t **children;
|
||||
size_t child_count;
|
||||
route_handler_fn handlers[8];
|
||||
field_schema_t *request_schema;
|
||||
char *operation_id;
|
||||
char *summary;
|
||||
char *description;
|
||||
char **tags;
|
||||
size_t tag_count;
|
||||
};
|
||||
|
||||
struct router {
|
||||
route_node_t *root;
|
||||
array_t *routes;
|
||||
};
|
||||
|
||||
struct dependency {
|
||||
char *name;
|
||||
dep_factory_fn factory;
|
||||
dep_cleanup_fn cleanup;
|
||||
dependency_scope_t scope;
|
||||
char **dependencies;
|
||||
size_t dep_count;
|
||||
};
|
||||
|
||||
struct dependency_context {
|
||||
dict_t *instances;
|
||||
dict_t *registry;
|
||||
http_request_t *request;
|
||||
array_t *cleanup_list;
|
||||
bool is_resolving;
|
||||
};
|
||||
|
||||
struct field_schema {
|
||||
char *name;
|
||||
field_type_t type;
|
||||
bool required;
|
||||
void *default_value;
|
||||
double min_value;
|
||||
double max_value;
|
||||
bool has_min;
|
||||
bool has_max;
|
||||
char *regex_pattern;
|
||||
char **enum_values;
|
||||
size_t enum_count;
|
||||
field_schema_t **nested_schema;
|
||||
size_t nested_count;
|
||||
};
|
||||
|
||||
struct validation_error {
|
||||
char *field_path;
|
||||
char *error_message;
|
||||
};
|
||||
|
||||
struct validation_result {
|
||||
bool valid;
|
||||
validation_error_t **errors;
|
||||
size_t error_count;
|
||||
json_value_t *validated_data;
|
||||
};
|
||||
|
||||
struct middleware_node {
|
||||
middleware_fn handler;
|
||||
void *context;
|
||||
middleware_node_t *next;
|
||||
};
|
||||
|
||||
struct middleware_pipeline {
|
||||
middleware_node_t *head;
|
||||
middleware_node_t *tail;
|
||||
size_t count;
|
||||
};
|
||||
|
||||
struct connection {
|
||||
int fd;
|
||||
char *request_buffer;
|
||||
size_t buffer_size;
|
||||
size_t buffer_used;
|
||||
time_t last_activity;
|
||||
bool is_websocket;
|
||||
websocket_t *ws;
|
||||
};
|
||||
|
||||
struct socket_server {
|
||||
int fd;
|
||||
struct sockaddr_in addr;
|
||||
int epoll_fd;
|
||||
struct epoll_event *events;
|
||||
int max_events;
|
||||
bool running;
|
||||
connection_t **connections;
|
||||
size_t max_connections;
|
||||
};
|
||||
|
||||
struct websocket {
|
||||
int client_fd;
|
||||
bool is_upgraded;
|
||||
char *receive_buffer;
|
||||
size_t buffer_size;
|
||||
size_t buffer_used;
|
||||
ws_message_handler_fn on_message;
|
||||
void *user_data;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
char *title;
|
||||
char *version;
|
||||
char *description;
|
||||
char *base_url;
|
||||
} openapi_info_t;
|
||||
|
||||
struct openapi_generator {
|
||||
openapi_info_t info;
|
||||
router_t *router;
|
||||
dict_t *schemas;
|
||||
dict_t *security_schemes;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
char *path;
|
||||
http_method_t method;
|
||||
route_handler_fn handler;
|
||||
field_schema_t *request_schema;
|
||||
char *operation_id;
|
||||
char *summary;
|
||||
char *description;
|
||||
char **tags;
|
||||
size_t tag_count;
|
||||
} route_info_t;
|
||||
|
||||
struct app_context {
|
||||
socket_server_t *server;
|
||||
router_t *router;
|
||||
middleware_pipeline_t *middleware;
|
||||
dict_t *dep_registry;
|
||||
openapi_generator_t *openapi;
|
||||
dict_t *config;
|
||||
ws_message_handler_fn ws_handler;
|
||||
void *ws_user_data;
|
||||
volatile bool running;
|
||||
};
|
||||
|
||||
dict_t* dict_create(size_t initial_size);
|
||||
void dict_set(dict_t *dict, const char *key, void *value);
|
||||
void* dict_get(dict_t *dict, const char *key);
|
||||
bool dict_has(dict_t *dict, const char *key);
|
||||
void dict_remove(dict_t *dict, const char *key);
|
||||
void dict_free(dict_t *dict);
|
||||
void dict_free_with_values(dict_t *dict, void (*free_fn)(void*));
|
||||
char** dict_keys(dict_t *dict, size_t *count);
|
||||
|
||||
array_t* array_create(size_t initial_capacity);
|
||||
void array_append(array_t *arr, void *item);
|
||||
void* array_get(array_t *arr, size_t index);
|
||||
void array_free(array_t *arr);
|
||||
void array_free_with_items(array_t *arr, void (*free_fn)(void*));
|
||||
|
||||
string_builder_t* sb_create(void);
|
||||
void sb_append(string_builder_t *sb, const char *str);
|
||||
void sb_append_char(string_builder_t *sb, char c);
|
||||
void sb_append_int(string_builder_t *sb, int value);
|
||||
void sb_append_double(string_builder_t *sb, double value);
|
||||
char* sb_to_string(string_builder_t *sb);
|
||||
void sb_free(string_builder_t *sb);
|
||||
|
||||
char* url_decode(const char *str);
|
||||
char* url_encode(const char *str);
|
||||
char* base64_encode(const unsigned char *data, size_t len);
|
||||
unsigned char* base64_decode(const char *str, size_t *out_len);
|
||||
char* str_trim(const char *str);
|
||||
char** str_split(const char *str, char delimiter, size_t *count);
|
||||
void str_split_free(char **parts, size_t count);
|
||||
char* str_duplicate(const char *str);
|
||||
bool str_starts_with(const char *str, const char *prefix);
|
||||
bool str_ends_with(const char *str, const char *suffix);
|
||||
|
||||
json_value_t* json_parse(const char *json_str);
|
||||
char* json_serialize(json_value_t *value);
|
||||
json_value_t* json_object_get(json_value_t *obj, const char *key);
|
||||
void json_object_set(json_value_t *obj, const char *key, json_value_t *value);
|
||||
json_value_t* json_array_get(json_value_t *arr, size_t index);
|
||||
void json_array_append(json_value_t *arr, json_value_t *value);
|
||||
json_value_t* json_null(void);
|
||||
json_value_t* json_bool(bool value);
|
||||
json_value_t* json_number(double value);
|
||||
json_value_t* json_string(const char *value);
|
||||
json_value_t* json_array(void);
|
||||
json_value_t* json_object(void);
|
||||
void json_value_free(json_value_t *value);
|
||||
json_value_t* json_deep_copy(json_value_t *value);
|
||||
|
||||
http_request_t* http_parse_request(const char *raw_request, size_t length);
|
||||
char* http_serialize_response(http_response_t *response, size_t *out_length);
|
||||
dict_t* http_parse_query_string(const char *query);
|
||||
http_method_t http_method_from_string(const char *method);
|
||||
const char* http_method_to_string(http_method_t method);
|
||||
const char* http_status_message(int status_code);
|
||||
void http_request_free(http_request_t *req);
|
||||
http_response_t* http_response_create(int status_code);
|
||||
void http_response_set_header(http_response_t *resp, const char *key, const char *value);
|
||||
void http_response_set_body(http_response_t *resp, const char *body, size_t length);
|
||||
void http_response_set_json(http_response_t *resp, json_value_t *json);
|
||||
void http_response_free(http_response_t *resp);
|
||||
|
||||
router_t* router_create(void);
|
||||
void router_add_route(router_t *router, http_method_t method, const char *path,
|
||||
route_handler_fn handler, field_schema_t *schema);
|
||||
route_node_t* router_match(router_t *router, http_method_t method, const char *path,
|
||||
dict_t **path_params);
|
||||
void router_free(router_t *router);
|
||||
|
||||
field_schema_t* schema_create(const char *name, field_type_t type);
|
||||
void schema_set_required(field_schema_t *schema, bool required);
|
||||
void schema_set_min(field_schema_t *schema, double min);
|
||||
void schema_set_max(field_schema_t *schema, double max);
|
||||
void schema_set_pattern(field_schema_t *schema, const char *pattern);
|
||||
void schema_set_enum(field_schema_t *schema, const char **values, size_t count);
|
||||
void schema_add_field(field_schema_t *parent, field_schema_t *child);
|
||||
validation_result_t* validate_json(json_value_t *data, field_schema_t *schema);
|
||||
char* validation_errors_to_json(validation_result_t *result);
|
||||
void validation_result_free(validation_result_t *result);
|
||||
void schema_free(field_schema_t *schema);
|
||||
|
||||
middleware_pipeline_t* middleware_pipeline_create(void);
|
||||
void middleware_add(middleware_pipeline_t *pipeline, middleware_fn fn, void *ctx);
|
||||
http_response_t* middleware_execute(middleware_pipeline_t *pipeline, http_request_t *req,
|
||||
route_handler_fn final_handler, dependency_context_t *deps);
|
||||
void middleware_pipeline_free(middleware_pipeline_t *pipeline);
|
||||
|
||||
http_response_t* cors_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx);
|
||||
http_response_t* logging_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx);
|
||||
http_response_t* timing_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx);
|
||||
http_response_t* auth_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx);
|
||||
|
||||
dependency_context_t* dep_context_create(http_request_t *req, dict_t *registry);
|
||||
void dep_register(dict_t *registry, const char *name, dep_factory_fn factory,
|
||||
dep_cleanup_fn cleanup, dependency_scope_t scope);
|
||||
void* dep_resolve(dependency_context_t *ctx, const char *name);
|
||||
void dep_context_cleanup(dependency_context_t *ctx);
|
||||
void dep_context_free(dependency_context_t *ctx);
|
||||
|
||||
socket_server_t* socket_server_create(const char *host, int port);
|
||||
connection_t* socket_server_accept(socket_server_t *server);
|
||||
ssize_t socket_read(connection_t *conn);
|
||||
ssize_t socket_write(connection_t *conn, const char *buf, size_t len);
|
||||
void socket_server_run(socket_server_t *server, app_context_t *app);
|
||||
void socket_server_stop(socket_server_t *server);
|
||||
void socket_server_free(socket_server_t *server);
|
||||
void connection_free(connection_t *conn);
|
||||
|
||||
bool websocket_handshake(connection_t *conn, http_request_t *req);
|
||||
websocket_t* websocket_create(int client_fd);
|
||||
int websocket_send_text(websocket_t *ws, const char *message, size_t length);
|
||||
int websocket_send_binary(websocket_t *ws, const unsigned char *data, size_t length);
|
||||
int websocket_receive(websocket_t *ws, char **message, size_t *length, ws_opcode_t *opcode);
|
||||
void websocket_send_ping(websocket_t *ws);
|
||||
void websocket_send_pong(websocket_t *ws, const char *data, size_t length);
|
||||
void websocket_close(websocket_t *ws, uint16_t code, const char *reason);
|
||||
void websocket_free(websocket_t *ws);
|
||||
|
||||
openapi_generator_t* openapi_create(const char *title, const char *version, const char *description);
|
||||
char* openapi_generate_schema(openapi_generator_t *gen);
|
||||
void openapi_add_schema(openapi_generator_t *gen, const char *name, field_schema_t *schema);
|
||||
void openapi_free(openapi_generator_t *gen);
|
||||
|
||||
app_context_t* app_create(const char *title, const char *version);
|
||||
void app_add_route(app_context_t *app, http_method_t method, const char *path,
|
||||
route_handler_fn handler, field_schema_t *schema);
|
||||
void app_add_middleware(app_context_t *app, middleware_fn middleware, void *ctx);
|
||||
void app_add_dependency(app_context_t *app, const char *name, dep_factory_fn factory,
|
||||
dep_cleanup_fn cleanup, dependency_scope_t scope);
|
||||
void app_set_websocket_handler(app_context_t *app, ws_message_handler_fn handler, void *user_data);
|
||||
void app_run(app_context_t *app, const char *host, int port);
|
||||
void app_stop(app_context_t *app);
|
||||
void app_destroy(app_context_t *app);
|
||||
|
||||
void celerity_log(const char *level, const char *format, ...);
|
||||
|
||||
#define LOG_INFO(fmt, ...) celerity_log("INFO", fmt, ##__VA_ARGS__)
|
||||
#define LOG_ERROR(fmt, ...) celerity_log("ERROR", fmt, ##__VA_ARGS__)
|
||||
#define LOG_DEBUG(fmt, ...) celerity_log("DEBUG", fmt, ##__VA_ARGS__)
|
||||
#define LOG_WARN(fmt, ...) celerity_log("WARN", fmt, ##__VA_ARGS__)
|
||||
|
||||
#define ROUTE_GET(app, path, handler) \
|
||||
app_add_route(app, HTTP_GET, path, handler, NULL)
|
||||
#define ROUTE_POST(app, path, handler, schema) \
|
||||
app_add_route(app, HTTP_POST, path, handler, schema)
|
||||
#define ROUTE_PUT(app, path, handler, schema) \
|
||||
app_add_route(app, HTTP_PUT, path, handler, schema)
|
||||
#define ROUTE_DELETE(app, path, handler) \
|
||||
app_add_route(app, HTTP_DELETE, path, handler, NULL)
|
||||
#define ROUTE_PATCH(app, path, handler, schema) \
|
||||
app_add_route(app, HTTP_PATCH, path, handler, schema)
|
||||
|
||||
#endif
|
||||
922
docclient.c
Normal file
922
docclient.c
Normal file
@ -0,0 +1,922 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
|
||||
#define BUFFER_SIZE 65536
|
||||
#define MAX_RESPONSE_SIZE 1048576
|
||||
|
||||
typedef struct {
|
||||
char host[256];
|
||||
int port;
|
||||
} server_config_t;
|
||||
|
||||
typedef struct {
|
||||
int status_code;
|
||||
char *body;
|
||||
size_t body_length;
|
||||
} http_response_t;
|
||||
|
||||
typedef struct {
|
||||
int total;
|
||||
int passed;
|
||||
int failed;
|
||||
} test_stats_t;
|
||||
|
||||
static server_config_t g_server = {"127.0.0.1", 8080};
|
||||
static test_stats_t g_stats = {0, 0, 0};
|
||||
static bool g_verbose = false;
|
||||
|
||||
static double get_time_ms(void) {
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0;
|
||||
}
|
||||
|
||||
static int connect_to_server(void) {
|
||||
struct addrinfo hints, *result;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
char port_str[16];
|
||||
snprintf(port_str, sizeof(port_str), "%d", g_server.port);
|
||||
if (getaddrinfo(g_server.host, port_str, &hints, &result) != 0) {
|
||||
return -1;
|
||||
}
|
||||
int sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
|
||||
if (sock < 0) {
|
||||
freeaddrinfo(result);
|
||||
return -1;
|
||||
}
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 5;
|
||||
tv.tv_usec = 0;
|
||||
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 500000;
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
int opt = 1;
|
||||
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
|
||||
if (connect(sock, result->ai_addr, result->ai_addrlen) < 0) {
|
||||
close(sock);
|
||||
freeaddrinfo(result);
|
||||
return -1;
|
||||
}
|
||||
freeaddrinfo(result);
|
||||
return sock;
|
||||
}
|
||||
|
||||
static http_response_t* http_request(const char *method, const char *path,
|
||||
const char *body, size_t body_len) {
|
||||
int sock = connect_to_server();
|
||||
if (sock < 0) return NULL;
|
||||
char request[BUFFER_SIZE];
|
||||
int req_len;
|
||||
if (body && body_len > 0) {
|
||||
req_len = snprintf(request, sizeof(request),
|
||||
"%s %s HTTP/1.1\r\n"
|
||||
"Host: %s:%d\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
"Content-Length: %zu\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n",
|
||||
method, path, g_server.host, g_server.port, body_len);
|
||||
if (req_len + (int)body_len < (int)sizeof(request)) {
|
||||
memcpy(request + req_len, body, body_len);
|
||||
req_len += body_len;
|
||||
}
|
||||
} else {
|
||||
req_len = snprintf(request, sizeof(request),
|
||||
"%s %s HTTP/1.1\r\n"
|
||||
"Host: %s:%d\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n",
|
||||
method, path, g_server.host, g_server.port);
|
||||
}
|
||||
if (send(sock, request, req_len, 0) != req_len) {
|
||||
close(sock);
|
||||
return NULL;
|
||||
}
|
||||
char *response_buf = malloc(MAX_RESPONSE_SIZE);
|
||||
if (!response_buf) {
|
||||
close(sock);
|
||||
return NULL;
|
||||
}
|
||||
size_t total_read = 0;
|
||||
ssize_t n;
|
||||
size_t content_length = 0;
|
||||
char *header_end = NULL;
|
||||
bool headers_parsed = false;
|
||||
while ((n = recv(sock, response_buf + total_read,
|
||||
MAX_RESPONSE_SIZE - total_read - 1, 0)) > 0) {
|
||||
total_read += n;
|
||||
response_buf[total_read] = '\0';
|
||||
if (!headers_parsed) {
|
||||
header_end = strstr(response_buf, "\r\n\r\n");
|
||||
if (header_end) {
|
||||
headers_parsed = true;
|
||||
char *cl = strcasestr(response_buf, "content-length:");
|
||||
if (cl && cl < header_end) {
|
||||
content_length = (size_t)atoi(cl + 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (headers_parsed && header_end) {
|
||||
size_t body_received = total_read - (header_end + 4 - response_buf);
|
||||
if (content_length > 0 && body_received >= content_length) break;
|
||||
if (content_length == 0 && body_received > 0) break;
|
||||
}
|
||||
if (total_read >= MAX_RESPONSE_SIZE - 1) break;
|
||||
}
|
||||
close(sock);
|
||||
response_buf[total_read] = '\0';
|
||||
http_response_t *resp = calloc(1, sizeof(http_response_t));
|
||||
if (!resp) {
|
||||
free(response_buf);
|
||||
return NULL;
|
||||
}
|
||||
char *status_line = strstr(response_buf, "HTTP/");
|
||||
if (status_line) {
|
||||
char *space = strchr(status_line, ' ');
|
||||
if (space) resp->status_code = atoi(space + 1);
|
||||
}
|
||||
char *body_start = strstr(response_buf, "\r\n\r\n");
|
||||
if (body_start) {
|
||||
body_start += 4;
|
||||
resp->body_length = total_read - (body_start - response_buf);
|
||||
resp->body = malloc(resp->body_length + 1);
|
||||
if (resp->body) {
|
||||
memcpy(resp->body, body_start, resp->body_length);
|
||||
resp->body[resp->body_length] = '\0';
|
||||
}
|
||||
}
|
||||
free(response_buf);
|
||||
return resp;
|
||||
}
|
||||
|
||||
static void http_response_free(http_response_t *resp) {
|
||||
if (!resp) return;
|
||||
free(resp->body);
|
||||
free(resp);
|
||||
}
|
||||
|
||||
static char* json_get_string(const char *json, const char *key) {
|
||||
char pattern[256];
|
||||
snprintf(pattern, sizeof(pattern), "\"%s\":", key);
|
||||
char *pos = strstr(json, pattern);
|
||||
if (!pos) return NULL;
|
||||
pos += strlen(pattern);
|
||||
while (*pos == ' ' || *pos == '\t') pos++;
|
||||
if (*pos != '"') return NULL;
|
||||
pos++;
|
||||
char *end = strchr(pos, '"');
|
||||
if (!end) return NULL;
|
||||
size_t len = end - pos;
|
||||
char *result = malloc(len + 1);
|
||||
if (!result) return NULL;
|
||||
memcpy(result, pos, len);
|
||||
result[len] = '\0';
|
||||
return result;
|
||||
}
|
||||
|
||||
static double json_get_number(const char *json, const char *key) {
|
||||
char pattern[256];
|
||||
snprintf(pattern, sizeof(pattern), "\"%s\":", key);
|
||||
char *pos = strstr(json, pattern);
|
||||
if (!pos) return 0;
|
||||
pos += strlen(pattern);
|
||||
while (*pos == ' ' || *pos == '\t') pos++;
|
||||
return atof(pos);
|
||||
}
|
||||
|
||||
static bool json_get_bool(const char *json, const char *key) {
|
||||
char pattern[256];
|
||||
snprintf(pattern, sizeof(pattern), "\"%s\":", key);
|
||||
char *pos = strstr(json, pattern);
|
||||
if (!pos) return false;
|
||||
pos += strlen(pattern);
|
||||
while (*pos == ' ' || *pos == '\t') pos++;
|
||||
return strncmp(pos, "true", 4) == 0;
|
||||
}
|
||||
|
||||
static void print_separator(void) {
|
||||
printf("════════════════════════════════════════════════════════════════\n");
|
||||
}
|
||||
|
||||
static void print_header(const char *title) {
|
||||
printf("\n");
|
||||
print_separator();
|
||||
printf(" %s\n", title);
|
||||
print_separator();
|
||||
}
|
||||
|
||||
static void print_test_result(const char *name, bool passed, double duration_ms,
|
||||
const char *details) {
|
||||
g_stats.total++;
|
||||
if (passed) {
|
||||
g_stats.passed++;
|
||||
printf(" [PASS] %-40s (%.1fms)\n", name, duration_ms);
|
||||
} else {
|
||||
g_stats.failed++;
|
||||
printf(" [FAIL] %-40s (%.1fms)\n", name, duration_ms);
|
||||
if (details) printf(" └─ %s\n", details);
|
||||
}
|
||||
}
|
||||
|
||||
static bool test_health_check(void) {
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("GET", "/health", NULL, 0);
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Health Check", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 200;
|
||||
if (passed && resp->body) {
|
||||
char *status = json_get_string(resp->body, "status");
|
||||
passed = status && strcmp(status, "healthy") == 0;
|
||||
free(status);
|
||||
}
|
||||
print_test_result("Health Check", passed, duration,
|
||||
passed ? NULL : "Unexpected response");
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_stats_endpoint(void) {
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("GET", "/stats", NULL, 0);
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Stats Endpoint", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 200;
|
||||
print_test_result("Stats Endpoint", passed, duration,
|
||||
passed ? NULL : "Unexpected status code");
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_create_collection(const char *name) {
|
||||
char body[256];
|
||||
snprintf(body, sizeof(body), "{\"name\":\"%s\"}", name);
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("POST", "/collections", body, strlen(body));
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
char msg[256];
|
||||
snprintf(msg, sizeof(msg), "Create Collection '%s'", name);
|
||||
print_test_result(msg, false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 201;
|
||||
char test_name[256];
|
||||
snprintf(test_name, sizeof(test_name), "Create Collection '%s'", name);
|
||||
print_test_result(test_name, passed, duration,
|
||||
passed ? NULL : "Failed to create collection");
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_create_collection_duplicate(const char *name) {
|
||||
char body[256];
|
||||
snprintf(body, sizeof(body), "{\"name\":\"%s\"}", name);
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("POST", "/collections", body, strlen(body));
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Create Duplicate Collection", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 409;
|
||||
print_test_result("Create Duplicate Collection (expect 409)", passed, duration,
|
||||
passed ? NULL : "Should return 409 Conflict");
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_get_collection(const char *name) {
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "/collections/%s", name);
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("GET", path, NULL, 0);
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Get Collection", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 200;
|
||||
char test_name[256];
|
||||
snprintf(test_name, sizeof(test_name), "Get Collection '%s'", name);
|
||||
print_test_result(test_name, passed, duration,
|
||||
passed ? NULL : "Collection not found");
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_list_collections(void) {
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("GET", "/collections", NULL, 0);
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("List Collections", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 200;
|
||||
print_test_result("List Collections", passed, duration,
|
||||
passed ? NULL : "Failed to list collections");
|
||||
if (g_verbose && resp->body) {
|
||||
printf(" └─ Response: %s\n", resp->body);
|
||||
}
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static char* test_create_document(const char *collection, const char *doc_json) {
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "/collections/%s/documents", collection);
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("POST", path, doc_json, strlen(doc_json));
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Create Document", false, duration, "Connection failed");
|
||||
return NULL;
|
||||
}
|
||||
bool passed = resp->status_code == 201;
|
||||
char *doc_id = NULL;
|
||||
if (passed && resp->body) {
|
||||
doc_id = json_get_string(resp->body, "_id");
|
||||
}
|
||||
print_test_result("Create Document", passed, duration,
|
||||
passed ? NULL : "Failed to create document");
|
||||
if (g_verbose && resp->body) {
|
||||
printf(" └─ Response: %s\n", resp->body);
|
||||
}
|
||||
http_response_free(resp);
|
||||
return doc_id;
|
||||
}
|
||||
|
||||
static char* test_create_document_with_id(const char *collection, const char *doc_id,
|
||||
const char *doc_json) {
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "/collections/%s/documents", collection);
|
||||
char body[4096];
|
||||
size_t json_len = strlen(doc_json);
|
||||
if (doc_json[json_len - 1] == '}') {
|
||||
snprintf(body, sizeof(body), "{\"_id\":\"%s\",%s", doc_id, doc_json + 1);
|
||||
} else {
|
||||
snprintf(body, sizeof(body), "%s", doc_json);
|
||||
}
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("POST", path, body, strlen(body));
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Create Document with Custom ID", false, duration, "Connection failed");
|
||||
return NULL;
|
||||
}
|
||||
bool passed = resp->status_code == 201;
|
||||
char *returned_id = NULL;
|
||||
if (passed && resp->body) {
|
||||
returned_id = json_get_string(resp->body, "_id");
|
||||
if (returned_id && strcmp(returned_id, doc_id) != 0) {
|
||||
passed = false;
|
||||
free(returned_id);
|
||||
returned_id = NULL;
|
||||
}
|
||||
}
|
||||
char test_name[256];
|
||||
snprintf(test_name, sizeof(test_name), "Create Document with ID '%s'", doc_id);
|
||||
print_test_result(test_name, passed, duration,
|
||||
passed ? NULL : "Custom ID not preserved");
|
||||
http_response_free(resp);
|
||||
return returned_id;
|
||||
}
|
||||
|
||||
static bool test_get_document(const char *collection, const char *doc_id) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/collections/%s/documents/%s", collection, doc_id);
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("GET", path, NULL, 0);
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Get Document", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 200;
|
||||
char test_name[256];
|
||||
snprintf(test_name, sizeof(test_name), "Get Document '%s'", doc_id);
|
||||
print_test_result(test_name, passed, duration,
|
||||
passed ? NULL : "Document not found");
|
||||
if (g_verbose && resp->body) {
|
||||
printf(" └─ Response: %s\n", resp->body);
|
||||
}
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_get_document_not_found(const char *collection) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/collections/%s/documents/nonexistent-id-12345", collection);
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("GET", path, NULL, 0);
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Get Nonexistent Document", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 404;
|
||||
print_test_result("Get Nonexistent Document (expect 404)", passed, duration,
|
||||
passed ? NULL : "Should return 404");
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_update_document(const char *collection, const char *doc_id,
|
||||
const char *update_json) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/collections/%s/documents/%s", collection, doc_id);
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("PUT", path, update_json, strlen(update_json));
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Update Document", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 200;
|
||||
print_test_result("Update Document", passed, duration,
|
||||
passed ? NULL : "Failed to update document");
|
||||
if (g_verbose && resp->body) {
|
||||
printf(" └─ Response: %s\n", resp->body);
|
||||
}
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_delete_document(const char *collection, const char *doc_id) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/collections/%s/documents/%s", collection, doc_id);
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("DELETE", path, NULL, 0);
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Delete Document", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 200;
|
||||
char test_name[256];
|
||||
snprintf(test_name, sizeof(test_name), "Delete Document '%s'", doc_id);
|
||||
print_test_result(test_name, passed, duration,
|
||||
passed ? NULL : "Failed to delete document");
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_list_documents(const char *collection) {
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "/collections/%s/documents", collection);
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("GET", path, NULL, 0);
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("List Documents", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 200;
|
||||
print_test_result("List Documents", passed, duration,
|
||||
passed ? NULL : "Failed to list documents");
|
||||
if (g_verbose && resp->body) {
|
||||
printf(" └─ Response: %s\n", resp->body);
|
||||
}
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_list_documents_with_pagination(const char *collection) {
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "/collections/%s/documents?skip=0&limit=5", collection);
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("GET", path, NULL, 0);
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("List Documents with Pagination", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 200;
|
||||
print_test_result("List Documents with Pagination", passed, duration,
|
||||
passed ? NULL : "Pagination failed");
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_query_documents(const char *collection, const char *query_json) {
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "/collections/%s/query", collection);
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("POST", path, query_json, strlen(query_json));
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Query Documents", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 200;
|
||||
print_test_result("Query Documents", passed, duration,
|
||||
passed ? NULL : "Query failed");
|
||||
if (g_verbose && resp->body) {
|
||||
printf(" └─ Response: %s\n", resp->body);
|
||||
}
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_bulk_insert(const char *collection) {
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "/collections/%s/bulk", collection);
|
||||
const char *body = "{\"documents\":["
|
||||
"{\"name\":\"Bulk User 1\",\"email\":\"bulk1@test.com\",\"score\":85},"
|
||||
"{\"name\":\"Bulk User 2\",\"email\":\"bulk2@test.com\",\"score\":92},"
|
||||
"{\"name\":\"Bulk User 3\",\"email\":\"bulk3@test.com\",\"score\":78},"
|
||||
"{\"name\":\"Bulk User 4\",\"email\":\"bulk4@test.com\",\"score\":95},"
|
||||
"{\"name\":\"Bulk User 5\",\"email\":\"bulk5@test.com\",\"score\":88}"
|
||||
"]}";
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("POST", path, body, strlen(body));
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Bulk Insert (5 documents)", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 201;
|
||||
if (passed && resp->body) {
|
||||
double count = json_get_number(resp->body, "inserted_count");
|
||||
passed = count == 5;
|
||||
}
|
||||
print_test_result("Bulk Insert (5 documents)", passed, duration,
|
||||
passed ? NULL : "Not all documents inserted");
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_bulk_delete(const char *collection) {
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "/collections/%s/bulk", collection);
|
||||
const char *body = "{\"filter\":{\"score\":78}}";
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("DELETE", path, body, strlen(body));
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Bulk Delete by Query", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 200;
|
||||
print_test_result("Bulk Delete by Query", passed, duration,
|
||||
passed ? NULL : "Bulk delete failed");
|
||||
if (g_verbose && resp->body) {
|
||||
printf(" └─ Response: %s\n", resp->body);
|
||||
}
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_delete_collection(const char *name) {
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "/collections/%s", name);
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("DELETE", path, NULL, 0);
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Delete Collection", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 200;
|
||||
char test_name[256];
|
||||
snprintf(test_name, sizeof(test_name), "Delete Collection '%s'", name);
|
||||
print_test_result(test_name, passed, duration,
|
||||
passed ? NULL : "Failed to delete collection");
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static bool test_delete_nonexistent_collection(void) {
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("DELETE", "/collections/nonexistent_collection_xyz", NULL, 0);
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Delete Nonexistent Collection", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 404;
|
||||
print_test_result("Delete Nonexistent Collection (expect 404)", passed, duration,
|
||||
passed ? NULL : "Should return 404");
|
||||
http_response_free(resp);
|
||||
return passed;
|
||||
}
|
||||
|
||||
static void run_basic_tests(void) {
|
||||
print_header("BASIC CONNECTIVITY TESTS");
|
||||
test_health_check();
|
||||
test_stats_endpoint();
|
||||
}
|
||||
|
||||
static void run_collection_tests(void) {
|
||||
print_header("COLLECTION MANAGEMENT TESTS");
|
||||
test_create_collection("test_users");
|
||||
test_create_collection("test_products");
|
||||
test_create_collection("test_orders");
|
||||
test_create_collection_duplicate("test_users");
|
||||
test_get_collection("test_users");
|
||||
test_list_collections();
|
||||
test_delete_nonexistent_collection();
|
||||
}
|
||||
|
||||
static void run_document_crud_tests(void) {
|
||||
print_header("DOCUMENT CRUD TESTS");
|
||||
char *doc1_id = test_create_document("test_users",
|
||||
"{\"name\":\"Alice Smith\",\"email\":\"alice@example.com\",\"age\":28,\"active\":true}");
|
||||
char *doc2_id = test_create_document("test_users",
|
||||
"{\"name\":\"Bob Johnson\",\"email\":\"bob@example.com\",\"age\":35,\"active\":true}");
|
||||
char *doc3_id = test_create_document_with_id("test_users", "custom-id-001",
|
||||
"{\"name\":\"Charlie Brown\",\"email\":\"charlie@example.com\",\"age\":42,\"active\":false}");
|
||||
if (doc1_id) {
|
||||
test_get_document("test_users", doc1_id);
|
||||
test_update_document("test_users", doc1_id, "{\"age\":29,\"verified\":true}");
|
||||
test_get_document("test_users", doc1_id);
|
||||
}
|
||||
test_get_document_not_found("test_users");
|
||||
if (doc2_id) {
|
||||
test_delete_document("test_users", doc2_id);
|
||||
free(doc2_id);
|
||||
}
|
||||
test_list_documents("test_users");
|
||||
free(doc1_id);
|
||||
free(doc3_id);
|
||||
}
|
||||
|
||||
static void run_query_tests(void) {
|
||||
print_header("QUERY AND SEARCH TESTS");
|
||||
test_create_document("test_products",
|
||||
"{\"name\":\"Laptop\",\"category\":\"Electronics\",\"price\":999.99,\"in_stock\":true}");
|
||||
test_create_document("test_products",
|
||||
"{\"name\":\"Phone\",\"category\":\"Electronics\",\"price\":699.99,\"in_stock\":true}");
|
||||
test_create_document("test_products",
|
||||
"{\"name\":\"Desk\",\"category\":\"Furniture\",\"price\":299.99,\"in_stock\":false}");
|
||||
test_create_document("test_products",
|
||||
"{\"name\":\"Chair\",\"category\":\"Furniture\",\"price\":149.99,\"in_stock\":true}");
|
||||
test_query_documents("test_products", "{\"filter\":{\"category\":\"Electronics\"}}");
|
||||
test_query_documents("test_products", "{\"filter\":{\"in_stock\":true}}");
|
||||
test_query_documents("test_products", "{\"filter\":{\"category\":\"Furniture\"},\"limit\":1}");
|
||||
test_list_documents_with_pagination("test_products");
|
||||
}
|
||||
|
||||
static void run_bulk_operation_tests(void) {
|
||||
print_header("BULK OPERATION TESTS");
|
||||
test_bulk_insert("test_orders");
|
||||
test_list_documents("test_orders");
|
||||
test_bulk_delete("test_orders");
|
||||
test_list_documents("test_orders");
|
||||
}
|
||||
|
||||
static void run_cleanup_tests(void) {
|
||||
print_header("CLEANUP TESTS");
|
||||
test_delete_collection("test_users");
|
||||
test_delete_collection("test_products");
|
||||
test_delete_collection("test_orders");
|
||||
}
|
||||
|
||||
static void run_stress_tests(void) {
|
||||
print_header("STRESS TESTS");
|
||||
test_create_collection("stress_test");
|
||||
double start = get_time_ms();
|
||||
int success_count = 0;
|
||||
int total_count = 100;
|
||||
for (int i = 0; i < total_count; i++) {
|
||||
char doc[256];
|
||||
snprintf(doc, sizeof(doc),
|
||||
"{\"index\":%d,\"name\":\"Stress Test Item %d\",\"value\":%d}",
|
||||
i, i, i * 10);
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "/collections/stress_test/documents");
|
||||
http_response_t *resp = http_request("POST", path, doc, strlen(doc));
|
||||
if (resp && resp->status_code == 201) success_count++;
|
||||
http_response_free(resp);
|
||||
}
|
||||
double duration = get_time_ms() - start;
|
||||
double rps = (total_count * 1000.0) / duration;
|
||||
char test_name[256];
|
||||
snprintf(test_name, sizeof(test_name), "Insert %d Documents", total_count);
|
||||
bool passed = success_count == total_count;
|
||||
g_stats.total++;
|
||||
if (passed) g_stats.passed++; else g_stats.failed++;
|
||||
printf(" [%s] %-40s (%.1fms, %.1f req/s)\n",
|
||||
passed ? "PASS" : "FAIL", test_name, duration, rps);
|
||||
if (!passed) {
|
||||
printf(" └─ Only %d/%d succeeded\n", success_count, total_count);
|
||||
}
|
||||
start = get_time_ms();
|
||||
success_count = 0;
|
||||
for (int i = 0; i < total_count; i++) {
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "/collections/stress_test/documents?skip=%d&limit=10", i);
|
||||
http_response_t *resp = http_request("GET", path, NULL, 0);
|
||||
if (resp && resp->status_code == 200) success_count++;
|
||||
http_response_free(resp);
|
||||
}
|
||||
duration = get_time_ms() - start;
|
||||
rps = (total_count * 1000.0) / duration;
|
||||
snprintf(test_name, sizeof(test_name), "Read %d Times with Pagination", total_count);
|
||||
passed = success_count == total_count;
|
||||
g_stats.total++;
|
||||
if (passed) g_stats.passed++; else g_stats.failed++;
|
||||
printf(" [%s] %-40s (%.1fms, %.1f req/s)\n",
|
||||
passed ? "PASS" : "FAIL", test_name, duration, rps);
|
||||
test_delete_collection("stress_test");
|
||||
}
|
||||
|
||||
static void run_edge_case_tests(void) {
|
||||
print_header("EDGE CASE TESTS");
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("POST", "/collections", "{}", 2);
|
||||
double duration = get_time_ms() - start;
|
||||
bool passed = resp && resp->status_code == 400;
|
||||
print_test_result("Create Collection without Name", passed, duration,
|
||||
passed ? NULL : "Should return 400");
|
||||
http_response_free(resp);
|
||||
start = get_time_ms();
|
||||
resp = http_request("POST", "/collections", "{\"name\":\"test!@#$\"}", 22);
|
||||
duration = get_time_ms() - start;
|
||||
passed = resp && resp->status_code == 400;
|
||||
print_test_result("Create Collection with Invalid Name", passed, duration,
|
||||
passed ? NULL : "Should return 400");
|
||||
http_response_free(resp);
|
||||
test_create_collection("edge_cases");
|
||||
start = get_time_ms();
|
||||
resp = http_request("POST", "/collections/edge_cases/documents", "{}", 2);
|
||||
duration = get_time_ms() - start;
|
||||
passed = resp && resp->status_code == 201;
|
||||
print_test_result("Create Empty Document", passed, duration,
|
||||
passed ? NULL : "Should allow empty documents");
|
||||
http_response_free(resp);
|
||||
char large_doc[32768];
|
||||
int pos = snprintf(large_doc, sizeof(large_doc), "{\"data\":\"");
|
||||
for (int i = 0; i < 30000 && pos < (int)sizeof(large_doc) - 10; i++) {
|
||||
large_doc[pos++] = 'A' + (i % 26);
|
||||
}
|
||||
pos += snprintf(large_doc + pos, sizeof(large_doc) - pos, "\"}");
|
||||
start = get_time_ms();
|
||||
resp = http_request("POST", "/collections/edge_cases/documents", large_doc, pos);
|
||||
duration = get_time_ms() - start;
|
||||
passed = resp && resp->status_code == 201;
|
||||
print_test_result("Create Large Document (30KB)", passed, duration,
|
||||
passed ? NULL : "Large document failed");
|
||||
http_response_free(resp);
|
||||
test_delete_collection("edge_cases");
|
||||
}
|
||||
|
||||
static void run_persistence_test(void) {
|
||||
print_header("PERSISTENCE VERIFICATION");
|
||||
test_create_collection("persist_test");
|
||||
char *doc_id = test_create_document("persist_test",
|
||||
"{\"name\":\"Persistent Data\",\"important\":true}");
|
||||
printf("\n NOTE: To verify persistence, restart the server and run:\n");
|
||||
printf(" ./docclient --test-persistence\n");
|
||||
if (doc_id) {
|
||||
printf(" Document ID to verify: %s\n", doc_id);
|
||||
free(doc_id);
|
||||
}
|
||||
}
|
||||
|
||||
static bool test_persistence_verification(void) {
|
||||
print_header("PERSISTENCE VERIFICATION TEST");
|
||||
double start = get_time_ms();
|
||||
http_response_t *resp = http_request("GET", "/collections/persist_test", NULL, 0);
|
||||
double duration = get_time_ms() - start;
|
||||
if (!resp) {
|
||||
print_test_result("Verify Persisted Collection", false, duration, "Connection failed");
|
||||
return false;
|
||||
}
|
||||
bool passed = resp->status_code == 200;
|
||||
print_test_result("Verify Persisted Collection Exists", passed, duration,
|
||||
passed ? NULL : "Collection not found after restart");
|
||||
http_response_free(resp);
|
||||
if (passed) {
|
||||
start = get_time_ms();
|
||||
resp = http_request("GET", "/collections/persist_test/documents", NULL, 0);
|
||||
duration = get_time_ms() - start;
|
||||
passed = resp && resp->status_code == 200;
|
||||
if (passed && resp->body) {
|
||||
double count = json_get_number(resp->body, "count");
|
||||
passed = count >= 1;
|
||||
}
|
||||
print_test_result("Verify Persisted Documents", passed, duration,
|
||||
passed ? NULL : "Documents not found");
|
||||
http_response_free(resp);
|
||||
}
|
||||
test_delete_collection("persist_test");
|
||||
return passed;
|
||||
}
|
||||
|
||||
static void print_summary(void) {
|
||||
printf("\n");
|
||||
print_separator();
|
||||
printf(" TEST SUMMARY\n");
|
||||
print_separator();
|
||||
printf(" Total Tests: %d\n", g_stats.total);
|
||||
printf(" Passed: %d (%.1f%%)\n", g_stats.passed,
|
||||
g_stats.total > 0 ? (g_stats.passed * 100.0 / g_stats.total) : 0);
|
||||
printf(" Failed: %d (%.1f%%)\n", g_stats.failed,
|
||||
g_stats.total > 0 ? (g_stats.failed * 100.0 / g_stats.total) : 0);
|
||||
print_separator();
|
||||
if (g_stats.failed == 0) {
|
||||
printf(" STATUS: ALL TESTS PASSED\n");
|
||||
} else {
|
||||
printf(" STATUS: SOME TESTS FAILED\n");
|
||||
}
|
||||
print_separator();
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
static void print_usage(const char *program) {
|
||||
printf("DocDB Test Client\n\n");
|
||||
printf("Usage: %s [options]\n\n", program);
|
||||
printf("Options:\n");
|
||||
printf(" -h, --host <host> Server host (default: 127.0.0.1)\n");
|
||||
printf(" -p, --port <port> Server port (default: 8080)\n");
|
||||
printf(" -v, --verbose Show response bodies\n");
|
||||
printf(" --test-persistence Run persistence verification test\n");
|
||||
printf(" --stress-only Run only stress tests\n");
|
||||
printf(" --quick Run only basic and CRUD tests\n");
|
||||
printf(" --help Show this help message\n");
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
bool test_persistence = false;
|
||||
bool stress_only = false;
|
||||
bool quick_mode = false;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--host") == 0) {
|
||||
if (++i < argc) strncpy(g_server.host, argv[i], sizeof(g_server.host) - 1);
|
||||
} else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) {
|
||||
if (++i < argc) g_server.port = atoi(argv[i]);
|
||||
} else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
|
||||
g_verbose = true;
|
||||
} else if (strcmp(argv[i], "--test-persistence") == 0) {
|
||||
test_persistence = true;
|
||||
} else if (strcmp(argv[i], "--stress-only") == 0) {
|
||||
stress_only = true;
|
||||
} else if (strcmp(argv[i], "--quick") == 0) {
|
||||
quick_mode = true;
|
||||
} else if (strcmp(argv[i], "--help") == 0) {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
printf("\n");
|
||||
print_separator();
|
||||
printf(" DocDB Test Client v1.0.0\n");
|
||||
printf(" Target: http://%s:%d\n", g_server.host, g_server.port);
|
||||
print_separator();
|
||||
http_response_t *resp = http_request("GET", "/health", NULL, 0);
|
||||
if (!resp || resp->status_code != 200) {
|
||||
printf("\n ERROR: Cannot connect to DocDB server at %s:%d\n",
|
||||
g_server.host, g_server.port);
|
||||
printf(" Make sure the server is running.\n\n");
|
||||
http_response_free(resp);
|
||||
return 1;
|
||||
}
|
||||
http_response_free(resp);
|
||||
if (test_persistence) {
|
||||
test_persistence_verification();
|
||||
print_summary();
|
||||
return g_stats.failed > 0 ? 1 : 0;
|
||||
}
|
||||
if (stress_only) {
|
||||
run_stress_tests();
|
||||
print_summary();
|
||||
return g_stats.failed > 0 ? 1 : 0;
|
||||
}
|
||||
run_basic_tests();
|
||||
run_collection_tests();
|
||||
run_document_crud_tests();
|
||||
if (!quick_mode) {
|
||||
run_query_tests();
|
||||
run_bulk_operation_tests();
|
||||
run_edge_case_tests();
|
||||
run_stress_tests();
|
||||
run_persistence_test();
|
||||
}
|
||||
run_cleanup_tests();
|
||||
print_summary();
|
||||
return g_stats.failed > 0 ? 1 : 0;
|
||||
}
|
||||
1243
docserver.c
Normal file
1243
docserver.c
Normal file
File diff suppressed because it is too large
Load Diff
392
example.c
Normal file
392
example.c
Normal file
@ -0,0 +1,392 @@
|
||||
#include "celerityapi.h"
|
||||
|
||||
typedef struct {
|
||||
int next_id;
|
||||
dict_t *users;
|
||||
} database_t;
|
||||
|
||||
typedef struct {
|
||||
int id;
|
||||
char *name;
|
||||
char *email;
|
||||
int age;
|
||||
} user_t;
|
||||
|
||||
static database_t *g_database = NULL;
|
||||
|
||||
static void user_free(void *ptr) {
|
||||
user_t *user = ptr;
|
||||
if (!user) return;
|
||||
free(user->name);
|
||||
free(user->email);
|
||||
free(user);
|
||||
}
|
||||
|
||||
static void* database_factory(dependency_context_t *ctx) {
|
||||
(void)ctx;
|
||||
if (g_database) return g_database;
|
||||
g_database = calloc(1, sizeof(database_t));
|
||||
if (!g_database) return NULL;
|
||||
g_database->next_id = 1;
|
||||
g_database->users = dict_create(64);
|
||||
return g_database;
|
||||
}
|
||||
|
||||
static void database_cleanup(void *instance) {
|
||||
database_t *db = instance;
|
||||
if (!db) return;
|
||||
dict_free_with_values(db->users, user_free);
|
||||
free(db);
|
||||
g_database = NULL;
|
||||
}
|
||||
|
||||
static http_response_t* handle_root(http_request_t *req, dependency_context_t *deps) {
|
||||
(void)req;
|
||||
(void)deps;
|
||||
http_response_t *resp = http_response_create(200);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "message", json_string("Welcome to CelerityAPI"));
|
||||
json_object_set(body, "version", json_string(CELERITY_VERSION));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
|
||||
static http_response_t* handle_health(http_request_t *req, dependency_context_t *deps) {
|
||||
(void)req;
|
||||
(void)deps;
|
||||
http_response_t *resp = http_response_create(200);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "status", json_string("healthy"));
|
||||
json_object_set(body, "timestamp", json_number((double)time(NULL)));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
|
||||
static http_response_t* handle_get_user(http_request_t *req, dependency_context_t *deps) {
|
||||
database_t *db = dep_resolve(deps, "database");
|
||||
if (!db) {
|
||||
http_response_t *resp = http_response_create(500);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("Database unavailable"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
char *id_str = dict_get(req->path_params, "id");
|
||||
if (!id_str) {
|
||||
http_response_t *resp = http_response_create(400);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("Missing user ID"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
user_t *user = dict_get(db->users, id_str);
|
||||
if (!user) {
|
||||
http_response_t *resp = http_response_create(404);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("User not found"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
http_response_t *resp = http_response_create(200);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "id", json_number(user->id));
|
||||
json_object_set(body, "name", json_string(user->name));
|
||||
json_object_set(body, "email", json_string(user->email));
|
||||
json_object_set(body, "age", json_number(user->age));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
|
||||
static http_response_t* handle_list_users(http_request_t *req, dependency_context_t *deps) {
|
||||
(void)req;
|
||||
database_t *db = dep_resolve(deps, "database");
|
||||
if (!db) {
|
||||
http_response_t *resp = http_response_create(500);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("Database unavailable"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
http_response_t *resp = http_response_create(200);
|
||||
json_value_t *users_array = json_array();
|
||||
size_t key_count;
|
||||
char **keys = dict_keys(db->users, &key_count);
|
||||
for (size_t i = 0; i < key_count; i++) {
|
||||
user_t *user = dict_get(db->users, keys[i]);
|
||||
if (user) {
|
||||
json_value_t *user_obj = json_object();
|
||||
json_object_set(user_obj, "id", json_number(user->id));
|
||||
json_object_set(user_obj, "name", json_string(user->name));
|
||||
json_object_set(user_obj, "email", json_string(user->email));
|
||||
json_object_set(user_obj, "age", json_number(user->age));
|
||||
json_array_append(users_array, user_obj);
|
||||
}
|
||||
free(keys[i]);
|
||||
}
|
||||
free(keys);
|
||||
http_response_set_json(resp, users_array);
|
||||
json_value_free(users_array);
|
||||
return resp;
|
||||
}
|
||||
|
||||
static http_response_t* handle_create_user(http_request_t *req, dependency_context_t *deps) {
|
||||
database_t *db = dep_resolve(deps, "database");
|
||||
if (!db) {
|
||||
http_response_t *resp = http_response_create(500);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("Database unavailable"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
if (!req->json_body) {
|
||||
http_response_t *resp = http_response_create(400);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("Request body required"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
json_value_t *name_val = json_object_get(req->json_body, "name");
|
||||
json_value_t *email_val = json_object_get(req->json_body, "email");
|
||||
json_value_t *age_val = json_object_get(req->json_body, "age");
|
||||
user_t *user = calloc(1, sizeof(user_t));
|
||||
user->id = db->next_id++;
|
||||
user->name = str_duplicate(name_val && name_val->type == JSON_STRING ?
|
||||
name_val->string_value : "Unknown");
|
||||
user->email = str_duplicate(email_val && email_val->type == JSON_STRING ?
|
||||
email_val->string_value : "unknown@example.com");
|
||||
user->age = age_val && age_val->type == JSON_NUMBER ? (int)age_val->number_value : 0;
|
||||
char id_str[32];
|
||||
snprintf(id_str, sizeof(id_str), "%d", user->id);
|
||||
dict_set(db->users, id_str, user);
|
||||
http_response_t *resp = http_response_create(201);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "id", json_number(user->id));
|
||||
json_object_set(body, "name", json_string(user->name));
|
||||
json_object_set(body, "email", json_string(user->email));
|
||||
json_object_set(body, "age", json_number(user->age));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
|
||||
static http_response_t* handle_update_user(http_request_t *req, dependency_context_t *deps) {
|
||||
database_t *db = dep_resolve(deps, "database");
|
||||
if (!db) {
|
||||
http_response_t *resp = http_response_create(500);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("Database unavailable"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
char *id_str = dict_get(req->path_params, "id");
|
||||
if (!id_str) {
|
||||
http_response_t *resp = http_response_create(400);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("Missing user ID"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
user_t *user = dict_get(db->users, id_str);
|
||||
if (!user) {
|
||||
http_response_t *resp = http_response_create(404);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("User not found"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
if (req->json_body) {
|
||||
json_value_t *name_val = json_object_get(req->json_body, "name");
|
||||
json_value_t *email_val = json_object_get(req->json_body, "email");
|
||||
json_value_t *age_val = json_object_get(req->json_body, "age");
|
||||
if (name_val && name_val->type == JSON_STRING) {
|
||||
free(user->name);
|
||||
user->name = str_duplicate(name_val->string_value);
|
||||
}
|
||||
if (email_val && email_val->type == JSON_STRING) {
|
||||
free(user->email);
|
||||
user->email = str_duplicate(email_val->string_value);
|
||||
}
|
||||
if (age_val && age_val->type == JSON_NUMBER) {
|
||||
user->age = (int)age_val->number_value;
|
||||
}
|
||||
}
|
||||
http_response_t *resp = http_response_create(200);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "id", json_number(user->id));
|
||||
json_object_set(body, "name", json_string(user->name));
|
||||
json_object_set(body, "email", json_string(user->email));
|
||||
json_object_set(body, "age", json_number(user->age));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
|
||||
static http_response_t* handle_delete_user(http_request_t *req, dependency_context_t *deps) {
|
||||
database_t *db = dep_resolve(deps, "database");
|
||||
if (!db) {
|
||||
http_response_t *resp = http_response_create(500);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("Database unavailable"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
char *id_str = dict_get(req->path_params, "id");
|
||||
if (!id_str) {
|
||||
http_response_t *resp = http_response_create(400);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("Missing user ID"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
user_t *user = dict_get(db->users, id_str);
|
||||
if (!user) {
|
||||
http_response_t *resp = http_response_create(404);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("User not found"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
user_free(user);
|
||||
dict_remove(db->users, id_str);
|
||||
http_response_t *resp = http_response_create(204);
|
||||
return resp;
|
||||
}
|
||||
|
||||
static http_response_t* handle_openapi(http_request_t *req, dependency_context_t *deps) {
|
||||
(void)req;
|
||||
(void)deps;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static http_response_t* handle_ws_upgrade(http_request_t *req, dependency_context_t *deps) {
|
||||
(void)req;
|
||||
(void)deps;
|
||||
http_response_t *resp = http_response_create(200);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "message", json_string("WebSocket endpoint - use ws:// protocol"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
|
||||
static void ws_echo_handler(websocket_t *ws, const char *message, size_t length) {
|
||||
LOG_INFO("WebSocket received: %.*s", (int)length, message);
|
||||
json_value_t *response = json_object();
|
||||
json_object_set(response, "type", json_string("echo"));
|
||||
json_object_set(response, "message", json_string(message));
|
||||
json_object_set(response, "timestamp", json_number((double)time(NULL)));
|
||||
char *json_str = json_serialize(response);
|
||||
json_value_free(response);
|
||||
if (json_str) {
|
||||
websocket_send_text(ws, json_str, strlen(json_str));
|
||||
free(json_str);
|
||||
}
|
||||
}
|
||||
|
||||
static app_context_t *g_app_ref = NULL;
|
||||
|
||||
static http_response_t* handle_openapi_actual(http_request_t *req, dependency_context_t *deps) {
|
||||
(void)req;
|
||||
(void)deps;
|
||||
if (!g_app_ref || !g_app_ref->openapi) {
|
||||
http_response_t *resp = http_response_create(500);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("OpenAPI not available"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
char *schema = openapi_generate_schema(g_app_ref->openapi);
|
||||
if (!schema) {
|
||||
http_response_t *resp = http_response_create(500);
|
||||
json_value_t *body = json_object();
|
||||
json_object_set(body, "detail", json_string("Failed to generate schema"));
|
||||
http_response_set_json(resp, body);
|
||||
json_value_free(body);
|
||||
return resp;
|
||||
}
|
||||
http_response_t *resp = http_response_create(200);
|
||||
http_response_set_header(resp, "Content-Type", "application/json");
|
||||
http_response_set_body(resp, schema, strlen(schema));
|
||||
free(schema);
|
||||
return resp;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int port = 8000;
|
||||
const char *host = "127.0.0.1";
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) {
|
||||
port = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) {
|
||||
host = argv[++i];
|
||||
}
|
||||
}
|
||||
|
||||
app_context_t *app = app_create("CelerityAPI Example", "1.0.0");
|
||||
if (!app) {
|
||||
LOG_ERROR("Failed to create application");
|
||||
return 1;
|
||||
}
|
||||
g_app_ref = app;
|
||||
|
||||
app_add_dependency(app, "database", database_factory, database_cleanup, DEP_SINGLETON);
|
||||
|
||||
app_add_middleware(app, logging_middleware, NULL);
|
||||
app_add_middleware(app, timing_middleware, NULL);
|
||||
app_add_middleware(app, cors_middleware, NULL);
|
||||
|
||||
field_schema_t *user_schema = schema_create("user", TYPE_OBJECT);
|
||||
|
||||
field_schema_t *name_field = schema_create("name", TYPE_STRING);
|
||||
schema_set_required(name_field, true);
|
||||
schema_set_min(name_field, 3);
|
||||
schema_set_max(name_field, 100);
|
||||
schema_add_field(user_schema, name_field);
|
||||
|
||||
field_schema_t *email_field = schema_create("email", TYPE_STRING);
|
||||
schema_set_required(email_field, true);
|
||||
schema_add_field(user_schema, email_field);
|
||||
|
||||
field_schema_t *age_field = schema_create("age", TYPE_INT);
|
||||
schema_set_required(age_field, false);
|
||||
schema_set_min(age_field, 0);
|
||||
schema_set_max(age_field, 150);
|
||||
schema_add_field(user_schema, age_field);
|
||||
|
||||
ROUTE_GET(app, "/", handle_root);
|
||||
ROUTE_GET(app, "/health", handle_health);
|
||||
ROUTE_GET(app, "/users", handle_list_users);
|
||||
ROUTE_GET(app, "/users/{id}", handle_get_user);
|
||||
ROUTE_POST(app, "/users", handle_create_user, user_schema);
|
||||
ROUTE_PUT(app, "/users/{id}", handle_update_user, NULL);
|
||||
ROUTE_DELETE(app, "/users/{id}", handle_delete_user);
|
||||
ROUTE_GET(app, "/ws", handle_ws_upgrade);
|
||||
ROUTE_GET(app, "/openapi.json", handle_openapi_actual);
|
||||
|
||||
app_set_websocket_handler(app, ws_echo_handler, NULL);
|
||||
|
||||
(void)handle_openapi;
|
||||
|
||||
app_run(app, host, port);
|
||||
|
||||
app_destroy(app);
|
||||
|
||||
return 0;
|
||||
}
|
||||
527
loadtest.c
Normal file
527
loadtest.c
Normal file
@ -0,0 +1,527 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdatomic.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <signal.h>
|
||||
#include <math.h>
|
||||
|
||||
#define MAX_URL_LENGTH 2048
|
||||
#define MAX_BODY_LENGTH 65536
|
||||
#define BUFFER_SIZE 8192
|
||||
#define MAX_LATENCIES 1000000
|
||||
|
||||
typedef struct {
|
||||
char host[256];
|
||||
int port;
|
||||
char path[1024];
|
||||
} parsed_url_t;
|
||||
|
||||
typedef struct {
|
||||
atomic_uint_fast64_t total_requests;
|
||||
atomic_uint_fast64_t successful_requests;
|
||||
atomic_uint_fast64_t failed_requests;
|
||||
atomic_uint_fast64_t bytes_received;
|
||||
atomic_uint_fast64_t connect_errors;
|
||||
atomic_uint_fast64_t timeout_errors;
|
||||
atomic_uint_fast64_t read_errors;
|
||||
atomic_uint_fast64_t status_2xx;
|
||||
atomic_uint_fast64_t status_3xx;
|
||||
atomic_uint_fast64_t status_4xx;
|
||||
atomic_uint_fast64_t status_5xx;
|
||||
double *latencies;
|
||||
atomic_uint_fast64_t latency_count;
|
||||
pthread_mutex_t latency_lock;
|
||||
} stats_t;
|
||||
|
||||
typedef struct {
|
||||
int thread_id;
|
||||
parsed_url_t *url;
|
||||
char *method;
|
||||
char *body;
|
||||
char *headers;
|
||||
int duration_seconds;
|
||||
int timeout_ms;
|
||||
stats_t *stats;
|
||||
volatile bool *running;
|
||||
} worker_config_t;
|
||||
|
||||
static volatile bool g_running = true;
|
||||
static stats_t g_stats;
|
||||
|
||||
static void signal_handler(int sig) {
|
||||
(void)sig;
|
||||
g_running = false;
|
||||
}
|
||||
|
||||
static double get_time_ms(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0;
|
||||
}
|
||||
|
||||
static int parse_url(const char *url, parsed_url_t *parsed) {
|
||||
memset(parsed, 0, sizeof(parsed_url_t));
|
||||
parsed->port = 80;
|
||||
strcpy(parsed->path, "/");
|
||||
const char *start = url;
|
||||
if (strncmp(url, "http://", 7) == 0) {
|
||||
start = url + 7;
|
||||
}
|
||||
const char *path_start = strchr(start, '/');
|
||||
const char *port_start = strchr(start, ':');
|
||||
if (port_start && (!path_start || port_start < path_start)) {
|
||||
size_t host_len = port_start - start;
|
||||
if (host_len >= sizeof(parsed->host)) return -1;
|
||||
strncpy(parsed->host, start, host_len);
|
||||
parsed->port = atoi(port_start + 1);
|
||||
} else if (path_start) {
|
||||
size_t host_len = path_start - start;
|
||||
if (host_len >= sizeof(parsed->host)) return -1;
|
||||
strncpy(parsed->host, start, host_len);
|
||||
} else {
|
||||
strncpy(parsed->host, start, sizeof(parsed->host) - 1);
|
||||
}
|
||||
if (path_start) {
|
||||
strncpy(parsed->path, path_start, sizeof(parsed->path) - 1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int connect_to_server(parsed_url_t *url, int timeout_ms) {
|
||||
struct addrinfo hints, *result, *rp;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
char port_str[16];
|
||||
snprintf(port_str, sizeof(port_str), "%d", url->port);
|
||||
int ret = getaddrinfo(url->host, port_str, &hints, &result);
|
||||
if (ret != 0) {
|
||||
return -1;
|
||||
}
|
||||
int sock = -1;
|
||||
for (rp = result; rp != NULL; rp = rp->ai_next) {
|
||||
sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||
if (sock == -1) continue;
|
||||
int flags = fcntl(sock, F_GETFL, 0);
|
||||
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
|
||||
ret = connect(sock, rp->ai_addr, rp->ai_addrlen);
|
||||
if (ret == 0) {
|
||||
break;
|
||||
}
|
||||
if (errno == EINPROGRESS) {
|
||||
fd_set write_fds;
|
||||
FD_ZERO(&write_fds);
|
||||
FD_SET(sock, &write_fds);
|
||||
struct timeval tv;
|
||||
tv.tv_sec = timeout_ms / 1000;
|
||||
tv.tv_usec = (timeout_ms % 1000) * 1000;
|
||||
ret = select(sock + 1, NULL, &write_fds, NULL, &tv);
|
||||
if (ret > 0) {
|
||||
int error = 0;
|
||||
socklen_t len = sizeof(error);
|
||||
getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len);
|
||||
if (error == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
close(sock);
|
||||
sock = -1;
|
||||
}
|
||||
freeaddrinfo(result);
|
||||
if (sock != -1) {
|
||||
int flags = fcntl(sock, F_GETFL, 0);
|
||||
fcntl(sock, F_SETFL, flags & ~O_NONBLOCK);
|
||||
int opt = 1;
|
||||
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
|
||||
struct timeval tv;
|
||||
tv.tv_sec = timeout_ms / 1000;
|
||||
tv.tv_usec = (timeout_ms % 1000) * 1000;
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
}
|
||||
return sock;
|
||||
}
|
||||
|
||||
static int send_request(int sock, parsed_url_t *url, const char *method,
|
||||
const char *body, const char *extra_headers) {
|
||||
char request[BUFFER_SIZE];
|
||||
int len;
|
||||
if (body && strlen(body) > 0) {
|
||||
len = snprintf(request, sizeof(request),
|
||||
"%s %s HTTP/1.1\r\n"
|
||||
"Host: %s:%d\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Content-Length: %zu\r\n"
|
||||
"%s"
|
||||
"\r\n"
|
||||
"%s",
|
||||
method, url->path, url->host, url->port,
|
||||
strlen(body),
|
||||
extra_headers ? extra_headers : "",
|
||||
body);
|
||||
} else {
|
||||
len = snprintf(request, sizeof(request),
|
||||
"%s %s HTTP/1.1\r\n"
|
||||
"Host: %s:%d\r\n"
|
||||
"Connection: close\r\n"
|
||||
"%s"
|
||||
"\r\n",
|
||||
method, url->path, url->host, url->port,
|
||||
extra_headers ? extra_headers : "");
|
||||
}
|
||||
ssize_t sent = send(sock, request, len, 0);
|
||||
return sent == len ? 0 : -1;
|
||||
}
|
||||
|
||||
static int read_response(int sock, int *status_code, size_t *bytes_read, int timeout_ms) {
|
||||
char buffer[BUFFER_SIZE];
|
||||
*bytes_read = 0;
|
||||
*status_code = 0;
|
||||
bool headers_parsed = false;
|
||||
size_t content_length = 0;
|
||||
size_t header_end_pos = 0;
|
||||
size_t body_received = 0;
|
||||
fd_set read_fds;
|
||||
struct timeval tv;
|
||||
while (1) {
|
||||
FD_ZERO(&read_fds);
|
||||
FD_SET(sock, &read_fds);
|
||||
tv.tv_sec = timeout_ms / 1000;
|
||||
tv.tv_usec = (timeout_ms % 1000) * 1000;
|
||||
int sel = select(sock + 1, &read_fds, NULL, NULL, &tv);
|
||||
if (sel <= 0) {
|
||||
if (sel == 0) return *status_code > 0 ? 0 : -2;
|
||||
return -1;
|
||||
}
|
||||
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
|
||||
if (n < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
if (*status_code > 0) return 0;
|
||||
return -2;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (n == 0) break;
|
||||
*bytes_read += n;
|
||||
buffer[n] = '\0';
|
||||
if (!headers_parsed) {
|
||||
char *status_line = strstr(buffer, "HTTP/");
|
||||
if (status_line) {
|
||||
char *space = strchr(status_line, ' ');
|
||||
if (space) {
|
||||
*status_code = atoi(space + 1);
|
||||
}
|
||||
}
|
||||
char *header_end = strstr(buffer, "\r\n\r\n");
|
||||
if (header_end) {
|
||||
headers_parsed = true;
|
||||
header_end_pos = (header_end - buffer) + 4;
|
||||
body_received = n - header_end_pos;
|
||||
char *cl = strcasestr(buffer, "Content-Length:");
|
||||
if (cl && cl < header_end) {
|
||||
content_length = atoi(cl + 15);
|
||||
}
|
||||
if (content_length > 0 && body_received >= content_length) {
|
||||
break;
|
||||
}
|
||||
if (content_length == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
body_received += n;
|
||||
if (content_length > 0 && body_received >= content_length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void record_latency(stats_t *stats, double latency_ms) {
|
||||
pthread_mutex_lock(&stats->latency_lock);
|
||||
uint64_t idx = atomic_load(&stats->latency_count);
|
||||
if (idx < MAX_LATENCIES) {
|
||||
stats->latencies[idx] = latency_ms;
|
||||
atomic_fetch_add(&stats->latency_count, 1);
|
||||
}
|
||||
pthread_mutex_unlock(&stats->latency_lock);
|
||||
}
|
||||
|
||||
static void* worker_thread(void *arg) {
|
||||
worker_config_t *config = (worker_config_t*)arg;
|
||||
while (*config->running) {
|
||||
double start_time = get_time_ms();
|
||||
int sock = connect_to_server(config->url, config->timeout_ms);
|
||||
if (sock < 0) {
|
||||
atomic_fetch_add(&config->stats->connect_errors, 1);
|
||||
atomic_fetch_add(&config->stats->failed_requests, 1);
|
||||
atomic_fetch_add(&config->stats->total_requests, 1);
|
||||
continue;
|
||||
}
|
||||
if (send_request(sock, config->url, config->method,
|
||||
config->body, config->headers) < 0) {
|
||||
close(sock);
|
||||
atomic_fetch_add(&config->stats->failed_requests, 1);
|
||||
atomic_fetch_add(&config->stats->total_requests, 1);
|
||||
continue;
|
||||
}
|
||||
int status_code;
|
||||
size_t bytes_read;
|
||||
int ret = read_response(sock, &status_code, &bytes_read, config->timeout_ms);
|
||||
close(sock);
|
||||
double end_time = get_time_ms();
|
||||
double latency = end_time - start_time;
|
||||
atomic_fetch_add(&config->stats->total_requests, 1);
|
||||
atomic_fetch_add(&config->stats->bytes_received, bytes_read);
|
||||
if (ret == -2) {
|
||||
atomic_fetch_add(&config->stats->timeout_errors, 1);
|
||||
atomic_fetch_add(&config->stats->failed_requests, 1);
|
||||
} else if (ret < 0) {
|
||||
atomic_fetch_add(&config->stats->read_errors, 1);
|
||||
atomic_fetch_add(&config->stats->failed_requests, 1);
|
||||
} else {
|
||||
atomic_fetch_add(&config->stats->successful_requests, 1);
|
||||
record_latency(config->stats, latency);
|
||||
if (status_code >= 200 && status_code < 300) {
|
||||
atomic_fetch_add(&config->stats->status_2xx, 1);
|
||||
} else if (status_code >= 300 && status_code < 400) {
|
||||
atomic_fetch_add(&config->stats->status_3xx, 1);
|
||||
} else if (status_code >= 400 && status_code < 500) {
|
||||
atomic_fetch_add(&config->stats->status_4xx, 1);
|
||||
} else if (status_code >= 500) {
|
||||
atomic_fetch_add(&config->stats->status_5xx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int compare_double(const void *a, const void *b) {
|
||||
double da = *(const double*)a;
|
||||
double db = *(const double*)b;
|
||||
if (da < db) return -1;
|
||||
if (da > db) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void calculate_percentiles(stats_t *stats, double *p50, double *p90,
|
||||
double *p99, double *min, double *max, double *avg) {
|
||||
uint64_t count = atomic_load(&stats->latency_count);
|
||||
if (count == 0) {
|
||||
*p50 = *p90 = *p99 = *min = *max = *avg = 0;
|
||||
return;
|
||||
}
|
||||
qsort(stats->latencies, count, sizeof(double), compare_double);
|
||||
*min = stats->latencies[0];
|
||||
*max = stats->latencies[count - 1];
|
||||
double sum = 0;
|
||||
for (uint64_t i = 0; i < count; i++) {
|
||||
sum += stats->latencies[i];
|
||||
}
|
||||
*avg = sum / count;
|
||||
*p50 = stats->latencies[(size_t)(count * 0.50)];
|
||||
*p90 = stats->latencies[(size_t)(count * 0.90)];
|
||||
*p99 = stats->latencies[(size_t)(count * 0.99)];
|
||||
}
|
||||
|
||||
static void print_progress(stats_t *stats, double elapsed_seconds) {
|
||||
uint64_t total = atomic_load(&stats->total_requests);
|
||||
uint64_t success = atomic_load(&stats->successful_requests);
|
||||
uint64_t failed = atomic_load(&stats->failed_requests);
|
||||
double rps = elapsed_seconds > 0 ? total / elapsed_seconds : 0;
|
||||
printf("\r[%.1fs] Requests: %lu | Success: %lu | Failed: %lu | RPS: %.1f ",
|
||||
elapsed_seconds, total, success, failed, rps);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static void print_results(stats_t *stats, double total_time_seconds, int concurrency) {
|
||||
uint64_t total = atomic_load(&stats->total_requests);
|
||||
uint64_t success = atomic_load(&stats->successful_requests);
|
||||
uint64_t failed = atomic_load(&stats->failed_requests);
|
||||
uint64_t bytes = atomic_load(&stats->bytes_received);
|
||||
double p50, p90, p99, min, max, avg;
|
||||
calculate_percentiles(stats, &p50, &p90, &p99, &min, &max, &avg);
|
||||
printf("\n\n");
|
||||
printf("╔══════════════════════════════════════════════════════════════╗\n");
|
||||
printf("║ LOAD TEST RESULTS ║\n");
|
||||
printf("╠══════════════════════════════════════════════════════════════╣\n");
|
||||
printf("║ Duration: %10.2f seconds ║\n", total_time_seconds);
|
||||
printf("║ Concurrency: %10d threads ║\n", concurrency);
|
||||
printf("╠══════════════════════════════════════════════════════════════╣\n");
|
||||
printf("║ Total Requests: %10lu ║\n", total);
|
||||
printf("║ Successful: %10lu (%.1f%%) ║\n",
|
||||
success, total > 0 ? (success * 100.0 / total) : 0);
|
||||
printf("║ Failed: %10lu (%.1f%%) ║\n",
|
||||
failed, total > 0 ? (failed * 100.0 / total) : 0);
|
||||
printf("╠══════════════════════════════════════════════════════════════╣\n");
|
||||
printf("║ Requests/sec: %10.2f ║\n",
|
||||
total / total_time_seconds);
|
||||
printf("║ Transfer/sec: %10.2f KB ║\n",
|
||||
(bytes / 1024.0) / total_time_seconds);
|
||||
printf("║ Total Transfer: %10.2f MB ║\n",
|
||||
bytes / (1024.0 * 1024.0));
|
||||
printf("╠══════════════════════════════════════════════════════════════╣\n");
|
||||
printf("║ LATENCY DISTRIBUTION ║\n");
|
||||
printf("║ ───────────────────── ║\n");
|
||||
printf("║ Min: %10.2f ms ║\n", min);
|
||||
printf("║ Avg: %10.2f ms ║\n", avg);
|
||||
printf("║ Max: %10.2f ms ║\n", max);
|
||||
printf("║ p50: %10.2f ms ║\n", p50);
|
||||
printf("║ p90: %10.2f ms ║\n", p90);
|
||||
printf("║ p99: %10.2f ms ║\n", p99);
|
||||
printf("╠══════════════════════════════════════════════════════════════╣\n");
|
||||
printf("║ HTTP STATUS CODES ║\n");
|
||||
printf("║ ───────────────── ║\n");
|
||||
printf("║ 2xx: %10lu ║\n",
|
||||
atomic_load(&stats->status_2xx));
|
||||
printf("║ 3xx: %10lu ║\n",
|
||||
atomic_load(&stats->status_3xx));
|
||||
printf("║ 4xx: %10lu ║\n",
|
||||
atomic_load(&stats->status_4xx));
|
||||
printf("║ 5xx: %10lu ║\n",
|
||||
atomic_load(&stats->status_5xx));
|
||||
printf("╠══════════════════════════════════════════════════════════════╣\n");
|
||||
printf("║ ERROR BREAKDOWN ║\n");
|
||||
printf("║ ─────────────── ║\n");
|
||||
printf("║ Connect Errors: %10lu ║\n",
|
||||
atomic_load(&stats->connect_errors));
|
||||
printf("║ Timeout Errors: %10lu ║\n",
|
||||
atomic_load(&stats->timeout_errors));
|
||||
printf("║ Read Errors: %10lu ║\n",
|
||||
atomic_load(&stats->read_errors));
|
||||
printf("╚══════════════════════════════════════════════════════════════╝\n");
|
||||
}
|
||||
|
||||
static void print_usage(const char *program) {
|
||||
printf("Usage: %s [options] <url>\n\n", program);
|
||||
printf("Options:\n");
|
||||
printf(" -c, --concurrency <n> Number of concurrent connections (default: 10)\n");
|
||||
printf(" -d, --duration <n> Test duration in seconds (default: 10)\n");
|
||||
printf(" -m, --method <method> HTTP method (GET, POST, PUT, DELETE) (default: GET)\n");
|
||||
printf(" -b, --body <data> Request body for POST/PUT\n");
|
||||
printf(" -H, --header <header> Add custom header (can be used multiple times)\n");
|
||||
printf(" -t, --timeout <ms> Request timeout in milliseconds (default: 5000)\n");
|
||||
printf(" -h, --help Show this help message\n");
|
||||
printf("\nExamples:\n");
|
||||
printf(" %s -c 100 -d 30 http://localhost:8000/\n", program);
|
||||
printf(" %s -c 50 -m POST -b '{\"name\":\"test\"}' -H 'Content-Type: application/json' http://localhost:8000/users\n", program);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int concurrency = 10;
|
||||
int duration = 10;
|
||||
int timeout_ms = 5000;
|
||||
char *method = "GET";
|
||||
char *body = NULL;
|
||||
char headers[4096] = "";
|
||||
char *url_str = NULL;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--concurrency") == 0) {
|
||||
if (++i < argc) concurrency = atoi(argv[i]);
|
||||
} else if (strcmp(argv[i], "-d") == 0 || strcmp(argv[i], "--duration") == 0) {
|
||||
if (++i < argc) duration = atoi(argv[i]);
|
||||
} else if (strcmp(argv[i], "-m") == 0 || strcmp(argv[i], "--method") == 0) {
|
||||
if (++i < argc) method = argv[i];
|
||||
} else if (strcmp(argv[i], "-b") == 0 || strcmp(argv[i], "--body") == 0) {
|
||||
if (++i < argc) body = argv[i];
|
||||
} else if (strcmp(argv[i], "-H") == 0 || strcmp(argv[i], "--header") == 0) {
|
||||
if (++i < argc) {
|
||||
strcat(headers, argv[i]);
|
||||
strcat(headers, "\r\n");
|
||||
}
|
||||
} else if (strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "--timeout") == 0) {
|
||||
if (++i < argc) timeout_ms = atoi(argv[i]);
|
||||
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
} else if (argv[i][0] != '-') {
|
||||
url_str = argv[i];
|
||||
}
|
||||
}
|
||||
if (!url_str) {
|
||||
fprintf(stderr, "Error: URL is required\n\n");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
parsed_url_t url;
|
||||
if (parse_url(url_str, &url) < 0) {
|
||||
fprintf(stderr, "Error: Invalid URL format\n");
|
||||
return 1;
|
||||
}
|
||||
if (concurrency < 1) concurrency = 1;
|
||||
if (concurrency > 10000) concurrency = 10000;
|
||||
if (duration < 1) duration = 1;
|
||||
memset(&g_stats, 0, sizeof(g_stats));
|
||||
g_stats.latencies = calloc(MAX_LATENCIES, sizeof(double));
|
||||
if (!g_stats.latencies) {
|
||||
fprintf(stderr, "Error: Failed to allocate memory for latencies\n");
|
||||
return 1;
|
||||
}
|
||||
pthread_mutex_init(&g_stats.latency_lock, NULL);
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
printf("╔══════════════════════════════════════════════════════════════╗\n");
|
||||
printf("║ LOAD TEST STARTING ║\n");
|
||||
printf("╠══════════════════════════════════════════════════════════════╣\n");
|
||||
printf("║ Target: http://%s:%d%s\n", url.host, url.port, url.path);
|
||||
printf("║ Method: %s\n", method);
|
||||
printf("║ Threads: %d\n", concurrency);
|
||||
printf("║ Duration: %d seconds\n", duration);
|
||||
printf("║ Timeout: %d ms\n", timeout_ms);
|
||||
printf("╚══════════════════════════════════════════════════════════════╝\n\n");
|
||||
printf("Running load test... Press Ctrl+C to stop early.\n\n");
|
||||
pthread_t *threads = calloc(concurrency, sizeof(pthread_t));
|
||||
worker_config_t *configs = calloc(concurrency, sizeof(worker_config_t));
|
||||
if (!threads || !configs) {
|
||||
fprintf(stderr, "Error: Failed to allocate memory for threads\n");
|
||||
free(g_stats.latencies);
|
||||
return 1;
|
||||
}
|
||||
double start_time = get_time_ms();
|
||||
for (int i = 0; i < concurrency; i++) {
|
||||
configs[i].thread_id = i;
|
||||
configs[i].url = &url;
|
||||
configs[i].method = method;
|
||||
configs[i].body = body;
|
||||
configs[i].headers = strlen(headers) > 0 ? headers : NULL;
|
||||
configs[i].duration_seconds = duration;
|
||||
configs[i].timeout_ms = timeout_ms;
|
||||
configs[i].stats = &g_stats;
|
||||
configs[i].running = &g_running;
|
||||
pthread_create(&threads[i], NULL, worker_thread, &configs[i]);
|
||||
}
|
||||
double elapsed = 0;
|
||||
while (g_running && elapsed < duration) {
|
||||
usleep(100000);
|
||||
elapsed = (get_time_ms() - start_time) / 1000.0;
|
||||
print_progress(&g_stats, elapsed);
|
||||
}
|
||||
g_running = false;
|
||||
for (int i = 0; i < concurrency; i++) {
|
||||
pthread_join(threads[i], NULL);
|
||||
}
|
||||
double total_time = (get_time_ms() - start_time) / 1000.0;
|
||||
print_results(&g_stats, total_time, concurrency);
|
||||
free(threads);
|
||||
free(configs);
|
||||
free(g_stats.latencies);
|
||||
pthread_mutex_destroy(&g_stats.latency_lock);
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user