Skip to content

Commit dcaf56f

Browse files
committed
feat: add unit test infrastructure with mock RocketMQ library
Introduce a comprehensive testing framework that enables unit testing without requiring the actual RocketMQ C++ library. This includes: - Mock RocketMQ headers and stub implementation for isolated testing - CMake options for coverage and stub library configuration - Jest test suites for Producer, PushConsumer, ConsumerAck, and init - Test helper for conditional native binding loading - Coverage file patterns added to .gitignore - AddonData class for thread-safe instance data management
1 parent 385a103 commit dcaf56f

26 files changed

Lines changed: 2930 additions & 156 deletions

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,11 @@ deps/rocketmq/test/bin
208208
deps/rocketmq/tmp_*
209209
deps/rocketmq/libs/signature/lib
210210
-
211+
212+
### Coverage ###
213+
coverage.html
214+
coverage.css
215+
coverage.*.html
216+
*.gcda
217+
*.gcno
218+
*.gcov

CMakeLists.txt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ add_definitions(-DNODE_RUNTIME=node)
77
add_definitions(-DBUILDING_NODE_EXTENSION)
88

99
option(ROCKETMQ_FORCE_STATIC_MUSL "Link against a provided static musl libc archive" OFF)
10+
option(ROCKETMQ_ENABLE_COVERAGE "Enable coverage flags" OFF)
11+
option(ROCKETMQ_USE_STUB "Use RocketMQ stub library for tests" OFF)
1012

1113
set(NPX_CMD npx)
1214

@@ -19,7 +21,20 @@ execute_process(
1921
message(STATUS "CMake.js configurations: INC=${CMAKE_JS_INC}")
2022

2123
include_directories(${CMAKE_JS_INC})
22-
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/deps/rocketmq/include)
24+
25+
if(ROCKETMQ_USE_STUB)
26+
add_definitions(-DROCKETMQ_USE_STUB=1)
27+
set(ROCKETMQ_STUB_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/test/mocks/rocketmq")
28+
file(GLOB ROCKETMQ_STUB_SOURCES "${ROCKETMQ_STUB_ROOT}/src/*.cpp")
29+
add_library(rocketmq_stub STATIC ${ROCKETMQ_STUB_SOURCES})
30+
set_target_properties(rocketmq_stub PROPERTIES POSITION_INDEPENDENT_CODE ON)
31+
target_include_directories(rocketmq_stub PUBLIC "${ROCKETMQ_STUB_ROOT}/include")
32+
include_directories("${ROCKETMQ_STUB_ROOT}/include")
33+
set(ROCKETMQ_LIB rocketmq_stub)
34+
else()
35+
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/deps/rocketmq/include)
36+
set(ROCKETMQ_LIB ${CMAKE_CURRENT_SOURCE_DIR}/deps/rocketmq/bin/librocketmq.a)
37+
endif()
2338

2439
file(GLOB SOURCE_FILES "src/*.cpp")
2540
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES})
@@ -29,7 +44,7 @@ set_target_properties(${PROJECT_NAME}
2944
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/build"
3045
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/build"
3146
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/build")
32-
target_link_libraries(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/deps/rocketmq/bin/librocketmq.a)
47+
target_link_libraries(${PROJECT_NAME} ${ROCKETMQ_LIB})
3348

3449
set(CMAKE_DEPENDS_USE_COMPILER FALSE)
3550
set(CMAKE_SKIP_DEPENDENCY_TRACKING TRUE)
@@ -52,3 +67,9 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
5267
else()
5368
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11 -stdlib=libc++")
5469
endif()
70+
71+
if(ROCKETMQ_ENABLE_COVERAGE)
72+
add_definitions(-DROCKETMQ_COVERAGE=1)
73+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g -fprofile-arcs -ftest-coverage")
74+
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fprofile-arcs -ftest-coverage")
75+
endif()

lib/push_consumer.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,24 @@ class RocketMQPushConsumer extends EventEmitter {
4646
options.logLevel = OPTIONS_LOG_LEVEL[options.logLevel.toUpperCase()] || OPTIONS_LOG_LEVEL.INFO;
4747
}
4848
this.core = new binding.PushConsumer(groupId, instanceName, options);
49-
this.core.setListener(this.emit.bind(this, "message"));
49+
this.core.setListener((msg, ack) => {
50+
try {
51+
this.emit("message", msg, ack);
52+
} catch (err) {
53+
try {
54+
if (ack && typeof ack.done === "function") {
55+
ack.done(false);
56+
}
57+
} catch (_) {
58+
}
59+
if (this.listenerCount("error") > 0) {
60+
try {
61+
this.emit("error", err, msg, ack);
62+
} catch (_) {
63+
}
64+
}
65+
}
66+
});
5067
this.status = START_STATUS.STOPPED;
5168
}
5269

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
10
2525
]
2626
},
27+
"scripts": {
28+
"build": "cmake-js build",
29+
"build:coverage": "cmake-js rebuild --CDROCKETMQ_ENABLE_COVERAGE=ON --CDROCKETMQ_USE_STUB=ON",
30+
"test": "node --expose-gc --test --test-force-exit",
31+
"test:coverage": "npm run build:coverage && node --expose-gc --test --test-force-exit && gcovr -r . --filter src --exclude test --print-summary"
32+
},
2733
"dependencies": {
2834
"bindings": "^1.5.0"
2935
},

src/addon_data.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
#ifndef __ROCKETMQ_ADDON_DATA_H__
18+
#define __ROCKETMQ_ADDON_DATA_H__
19+
20+
#include <napi.h>
21+
22+
namespace __node_rocketmq__ {
23+
24+
struct AddonData {
25+
Napi::FunctionReference producer_constructor;
26+
Napi::FunctionReference push_consumer_constructor;
27+
Napi::FunctionReference consumer_ack_constructor;
28+
};
29+
30+
AddonData* GetAddonData(Napi::Env env);
31+
32+
} // namespace __node_rocketmq__
33+
34+
#endif

src/consumer_ack.cpp

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,49 @@
1515
* limitations under the License.
1616
*/
1717
#include "consumer_ack.h"
18+
19+
#include <cstdlib>
1820
#include <exception>
19-
#include "napi.h"
21+
22+
#include <napi.h>
23+
24+
#include "addon_data.h"
2025

2126
namespace __node_rocketmq__ {
2227

23-
static void DeleteFunctionReference(Napi::Env, Napi::FunctionReference* data) {
24-
delete data;
28+
#if defined(ROCKETMQ_COVERAGE) || defined(ROCKETMQ_USE_STUB)
29+
namespace {
30+
bool IsEnvEnabled(const char* name) {
31+
const char* value = std::getenv(name);
32+
if (value == nullptr) {
33+
return false;
34+
}
35+
return value[0] != '\0' && value[0] != '0';
36+
}
2537
}
38+
#endif
2639

27-
Napi::Object ConsumerAck::Init(Napi::Env env, Napi::Object exports) {
40+
Napi::Object ConsumerAck::Init(Napi::Env env, Napi::Object exports, AddonData* addon_data) {
2841
Napi::Function func = DefineClass(
2942
env, "ConsumerAck", {InstanceMethod<&ConsumerAck::Done>("done")});
3043

31-
// Store the constructor in env to avoid memory leak
32-
Napi::FunctionReference* constructor = new Napi::FunctionReference();
33-
*constructor = Napi::Persistent(func);
34-
env.SetInstanceData<Napi::FunctionReference, DeleteFunctionReference>(constructor);
44+
addon_data->consumer_ack_constructor = Napi::Persistent(func);
3545

3646
exports.Set("ConsumerAck", func);
3747
return exports;
3848
}
3949

4050
Napi::Object ConsumerAck::NewInstance(Napi::Env env) {
41-
Napi::Object obj = env.GetInstanceData<Napi::FunctionReference>()->New({});
42-
return obj;
51+
AddonData* addon_data = GetAddonData(env);
52+
#if defined(ROCKETMQ_COVERAGE) || defined(ROCKETMQ_USE_STUB)
53+
if (addon_data == nullptr || IsEnvEnabled("ROCKETMQ_STUB_CONSUMER_ACK_NULL_ADDON_DATA")) {
54+
#else
55+
if (addon_data == nullptr) {
56+
#endif
57+
Napi::Error::New(env, "ConsumerAck constructor not initialized").ThrowAsJavaScriptException();
58+
return Napi::Object();
59+
}
60+
return addon_data->consumer_ack_constructor.New({});
4361
}
4462

4563
ConsumerAck::ConsumerAck(const Napi::CallbackInfo& info)
@@ -49,34 +67,39 @@ void ConsumerAck::SetPromise(std::promise<bool>&& promise) {
4967
promise_ = std::move(promise);
5068
}
5169

52-
void ConsumerAck::Done(bool ack) {
53-
try {
54-
promise_.set_value(ack);
55-
} catch (const std::future_error& e) {
56-
// ignore
57-
}
58-
}
59-
6070
void ConsumerAck::Done(std::exception_ptr exception) {
61-
try {
62-
promise_.set_exception(exception);
63-
} catch (const std::future_error& e) {
64-
// ignore
71+
if (!done_called_.exchange(true)) {
72+
try {
73+
promise_.set_exception(exception);
74+
} catch (const std::future_error&) {
75+
}
6576
}
6677
}
6778

6879
Napi::Value ConsumerAck::Done(const Napi::CallbackInfo& info) {
69-
try {
70-
if (info.Length() >= 1) {
71-
Napi::Value ack = info[0];
72-
if (ack.IsBoolean() && !ack.ToBoolean()) {
73-
Done(false);
74-
return info.Env().Undefined();
75-
}
80+
if (done_called_.exchange(true)) {
81+
return info.Env().Undefined();
82+
}
83+
84+
bool ack = true;
85+
if (info.Length() >= 1) {
86+
Napi::Value ack_value = info[0];
87+
if (ack_value.IsBoolean() && !ack_value.ToBoolean()) {
88+
ack = false;
7689
}
77-
Done(true);
78-
} catch (const std::exception& e) {
79-
// ignore
90+
}
91+
92+
#if defined(ROCKETMQ_COVERAGE) || defined(ROCKETMQ_USE_STUB)
93+
if (IsEnvEnabled("ROCKETMQ_STUB_CONSUMER_ACK_FORCE_FUTURE_ERROR")) {
94+
try {
95+
promise_.set_value(true);
96+
} catch (const std::future_error&) {
97+
}
98+
}
99+
#endif
100+
try {
101+
promise_.set_value(ack);
102+
} catch (const std::future_error&) {
80103
}
81104
return info.Env().Undefined();
82105
}

src/consumer_ack.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,32 @@
1717
#ifndef __ROCKETMQ_CONSUMER_ACK_H__
1818
#define __ROCKETMQ_CONSUMER_ACK_H__
1919

20+
#include <atomic>
2021
#include <future>
2122

2223
#include <napi.h>
2324

2425
namespace __node_rocketmq__ {
2526

27+
struct AddonData;
28+
2629
class ConsumerAck : public Napi::ObjectWrap<ConsumerAck> {
2730
public:
28-
static Napi::Object Init(Napi::Env env, Napi::Object exports);
31+
static Napi::Object Init(Napi::Env env, Napi::Object exports, AddonData* addon_data);
2932
static Napi::Object NewInstance(Napi::Env env);
3033

3134
ConsumerAck(const Napi::CallbackInfo& info);
3235

3336
void SetPromise(std::promise<bool>&& promise);
3437

35-
void Done(bool ack);
3638
void Done(std::exception_ptr exception);
3739

3840
private:
3941
Napi::Value Done(const Napi::CallbackInfo& info);
4042

4143
private:
4244
std::promise<bool> promise_;
45+
std::atomic<bool> done_called_{false};
4346
};
4447

4548
} // namespace __node_rocketmq__

0 commit comments

Comments
 (0)