Skip to content

Commit 6b7d96d

Browse files
committed
close testing gaps
1 parent 147967b commit 6b7d96d

File tree

10 files changed

+1721
-23
lines changed

10 files changed

+1721
-23
lines changed

.github/codecov.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,25 @@ coverage:
1717
precision: 2
1818
round: down
1919
range: "85...100"
20+
status:
21+
project:
22+
default:
23+
target: auto
24+
threshold: 1%
25+
patch:
26+
default:
27+
target: 80%
28+
29+
flags:
30+
native:
31+
paths:
32+
- core/src/lib/
33+
- core/src/include/
34+
carryforward: true
35+
client:
36+
paths:
37+
- packages/client/src/
38+
carryforward: true
2039

2140
parsers:
2241
gcov:
@@ -33,4 +52,9 @@ comment:
3352

3453
ignore:
3554
- core/src/examples
36-
- core/src/tests/integration/generate_file.cpp
55+
- core/src/tests
56+
- packages/client/tests
57+
- server/
58+
- "**/node_modules"
59+
- "**/*.test.*"
60+
- "**/*.spec.*"

.github/workflows/tests.yml

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,126 @@ jobs:
106106
# os-name: ubuntu-22.04-arm64
107107
# docker-image: ghcr.io/ctc-oss/omega-edit-build-arm64:ubuntu-22.04
108108
# library-filename: libomega_edit.so
109+
110+
coverage:
111+
name: Code Coverage 📊
112+
runs-on: ubuntu-24.04
113+
steps:
114+
- name: Checkout 🛎️
115+
uses: actions/checkout@v6
116+
117+
- name: Setup cmake 🔧
118+
uses: lukka/get-cmake@latest
119+
120+
- name: Install lcov 📦
121+
run: sudo apt-get update && sudo apt-get install -y lcov
122+
123+
# ── C/C++ coverage ──────────────────────────────────────────────────
124+
- name: Build with coverage instrumentation 🔨
125+
run: |
126+
cmake --preset ci-coverage
127+
cmake --build --preset ci-coverage --config Debug
128+
129+
- name: Run C/C++ tests 🧪
130+
run: ctest -C Debug --test-dir build-coverage/core --output-on-failure
131+
132+
- name: Collect C/C++ coverage with lcov 📈
133+
run: |
134+
lcov --capture --directory build-coverage --output-file coverage-native.info \
135+
--rc branch_coverage=1 --ignore-errors mismatch
136+
lcov --remove coverage-native.info \
137+
'*/src/tests/*' '*/src/examples/*' '*/_deps/*' '/usr/*' '*/catch2/*' \
138+
--output-file coverage-native.info --rc branch_coverage=1 --ignore-errors unused
139+
lcov --list coverage-native.info
140+
141+
- name: Upload C/C++ coverage to Codecov 🔺
142+
uses: codecov/codecov-action@v5
143+
with:
144+
files: coverage-native.info
145+
flags: native
146+
name: omega-edit-native
147+
fail_ci_if_error: false
148+
149+
# ── TypeScript client coverage ──────────────────────────────────────
150+
- name: Setup Node.js 🟢
151+
uses: actions/setup-node@v6
152+
with:
153+
node-version: 20
154+
cache: yarn
155+
cache-dependency-path: yarn.lock
156+
157+
- name: Setup Python 🐍
158+
uses: actions/setup-python@v5
159+
with:
160+
python-version: '3.12'
161+
162+
- name: Install Conan 📦
163+
run: pip install conan
164+
165+
- name: Install uuid-dev 🔧
166+
run: sudo apt-get install -y uuid-dev
167+
168+
- name: Detect Conan profile 🔧
169+
run: conan profile detect --force
170+
171+
- name: Cache Conan packages 📦
172+
uses: actions/cache@v4
173+
with:
174+
path: ~/.conan2
175+
key: conan-coverage-${{ hashFiles('server/cpp/conanfile.py') }}
176+
restore-keys: |
177+
conan-coverage-
178+
179+
- name: Build C++ gRPC server for client tests 🔨
180+
run: |
181+
cmake -G Ninja -S . -B _build_core \
182+
-DCMAKE_BUILD_TYPE=Release \
183+
-DBUILD_SHARED_LIBS=OFF \
184+
-DBUILD_DOCS=OFF \
185+
-DBUILD_EXAMPLES=OFF \
186+
-DBUILD_TESTS=OFF
187+
cmake --build _build_core --config Release
188+
cmake --install _build_core/packages/core --prefix "$(pwd)/_install_core" --config Release
189+
export OE_LIB_DIR="$(pwd)/_install_core/lib"
190+
export OE_PREFIX="$(pwd)/_install_core"
191+
cd server/cpp
192+
conan install . --output-folder=build \
193+
--build=missing \
194+
-s build_type=Release \
195+
-s compiler.cppstd=17 \
196+
-c tools.cmake.cmaketoolchain:generator=Ninja
197+
cmake --preset conan-release \
198+
-DOE_LIB_DIR="$OE_LIB_DIR" \
199+
-DCMAKE_PREFIX_PATH="$OE_PREFIX" \
200+
|| cmake -G Ninja -S . -B build \
201+
-DCMAKE_BUILD_TYPE=Release \
202+
-DCMAKE_TOOLCHAIN_FILE="build/conan_toolchain.cmake" \
203+
-DOE_LIB_DIR="$OE_LIB_DIR" \
204+
-DCMAKE_PREFIX_PATH="$OE_PREFIX"
205+
cmake --build build --config Release
206+
cd ../..
207+
echo "CPP_SERVER_BINARY=$(pwd)/server/cpp/build/omega-edit-grpc-server" >> $GITHUB_ENV
208+
209+
- name: Yarn Install 🏗️
210+
run: yarn
211+
212+
- name: Yarn Package - Server 📦
213+
run: yarn workspace @omega-edit/server package
214+
env:
215+
CPP_SERVER_BINARY: ${{ env.CPP_SERVER_BINARY }}
216+
217+
- name: Yarn Prepare - Client
218+
run: yarn workspace @omega-edit/client prepare
219+
220+
- name: Run client tests with coverage 🧪
221+
run: yarn workspace @omega-edit/client test:coverage
222+
env:
223+
CPP_SERVER_BINARY: ''
224+
225+
- name: Upload TypeScript coverage to Codecov 🔺
226+
uses: codecov/codecov-action@v5
227+
with:
228+
files: packages/client/coverage/lcov.info
229+
flags: client
230+
name: omega-edit-client
231+
fail_ci_if_error: false

CMakePresets.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,21 @@
201201
"BUILD_SHARED_LIBS": "ON",
202202
"BUILD_TESTS": "OFF"
203203
}
204+
},
205+
{
206+
"name": "ci-coverage",
207+
"displayName": "CI Coverage",
208+
"description": "CI build with code coverage instrumentation (Debug, static, GCC/Clang)",
209+
"generator": "Ninja",
210+
"binaryDir": "${sourceDir}/build-coverage",
211+
"cacheVariables": {
212+
"CMAKE_BUILD_TYPE": "Debug",
213+
"BUILD_DOCS": "OFF",
214+
"BUILD_EXAMPLES": "OFF",
215+
"BUILD_SHARED_LIBS": "OFF",
216+
"BUILD_TESTS": "ON",
217+
"BUILD_COVERAGE": "ON"
218+
}
204219
}
205220
],
206221
"buildPresets": [
@@ -273,6 +288,11 @@
273288
"name": "ci-docs",
274289
"displayName": "CI Documentation",
275290
"configurePreset": "ci-docs"
291+
},
292+
{
293+
"name": "ci-coverage",
294+
"displayName": "CI Coverage",
295+
"configurePreset": "ci-coverage"
276296
}
277297
]
278298
}

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,15 @@ update-version:
4646
@echo " git push origin v$(version)"
4747
@echo "------------------------------------------------------------------------"
4848

49+
coverage:
50+
cmake -G $(GENERATOR) -S . -B _build-coverage -DBUILD_SHARED_LIBS=NO -DBUILD_DOCS=NO -DBUILD_EXAMPLES=NO -DBUILD_COVERAGE=YES -DCMAKE_BUILD_TYPE=Debug
51+
cmake --build _build-coverage --config Debug
52+
ctest -C Debug --test-dir _build-coverage/core --output-on-failure
53+
@echo "Coverage data generated in _build-coverage/"
54+
@echo "Run 'lcov --capture --directory _build-coverage --output-file coverage.info' to generate a report"
55+
4956
clean:
50-
rm -rf _build _install lib/$(LIBNAME)
57+
rm -rf _build _build-coverage _install lib/$(LIBNAME) coverage.info
5158

5259
all: lib/$(LIBNAME)
5360
@echo $<

core/CMakeLists.txt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,25 @@ option(BUILD_SHARED_LIBS "Build shared libraries instead of static ones" ON)
1313
option(BUILD_TESTS "build tests" ON)
1414
option(BUILD_DOCS "build documentation" ON)
1515
option(BUILD_EXAMPLES "build examples" ON)
16+
option(BUILD_COVERAGE "build with code coverage instrumentation (GCC/Clang)" OFF)
1617

1718
## Let omega_edit_SHARED_LIBS override BUILD_SHARED_LIBS
1819
if (DEFINED omega_edit_SHARED_LIBS)
1920
set(BUILD_SHARED_LIBS "${omega_edit_SHARED_LIBS}")
2021
endif ()
2122

22-
message(STATUS "Building ${PROJECT_NAME} ${PROJECT_VERSION} (shared: ${BUILD_SHARED_LIBS}, tests: ${BUILD_TESTS}, docs: ${BUILD_DOCS}, examples: ${BUILD_EXAMPLES})")
23+
message(STATUS "Building ${PROJECT_NAME} ${PROJECT_VERSION} (shared: ${BUILD_SHARED_LIBS}, tests: ${BUILD_TESTS}, docs: ${BUILD_DOCS}, examples: ${BUILD_EXAMPLES}, coverage: ${BUILD_COVERAGE})")
24+
25+
# Code coverage instrumentation (GCC/Clang only)
26+
if (BUILD_COVERAGE)
27+
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
28+
message(STATUS "Enabling code coverage instrumentation")
29+
add_compile_options(--coverage -fprofile-arcs -ftest-coverage)
30+
add_link_options(--coverage)
31+
else ()
32+
message(WARNING "BUILD_COVERAGE is only supported with GCC or Clang, ignoring")
33+
endif ()
34+
endif ()
2335

2436
# Common configurations
2537
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)

core/src/lib/check.cpp

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
**********************************************************************************************************************/
1414

1515
#include "../include/omega_edit/check.h"
16+
#include "../include/omega_edit/session.h"
1617
#include "impl_/change_def.hpp"
1718
#include "impl_/internal_fun.hpp"
1819
#include "impl_/macros.h"
@@ -21,30 +22,62 @@
2122

2223
int omega_check_model(const omega_session_t *session_ptr) {
2324
if (!session_ptr) { return -1; }
24-
int64_t expected_offset = 0;
25-
if (!session_ptr->models_.empty()) {
26-
for (auto &&model_ptr: session_ptr->models_) {
27-
assert(model_ptr);
28-
for (const auto &segment: model_ptr->model_segments) {
29-
assert(segment->change_ptr);
30-
if (expected_offset != segment->computed_offset ||
31-
(segment->change_offset + segment->computed_length) > segment->change_ptr->length) {
32-
print_model_segments_(session_ptr->models_.back().get(), CLOG);
33-
return -1;
34-
}
35-
expected_offset += segment->computed_length;
25+
if (session_ptr->models_.empty()) { return -1; }// session must have at least one model
26+
27+
for (size_t model_index = 0; model_index < session_ptr->models_.size(); ++model_index) {
28+
const auto &model_ptr = session_ptr->models_[model_index];
29+
if (!model_ptr) { return -1; }
30+
31+
int64_t expected_offset = 0;
32+
for (const auto &segment: model_ptr->model_segments) {
33+
// Each segment must reference a valid change
34+
if (!segment || !segment->change_ptr) { return -1; }
35+
36+
// Segment offsets must be non-negative
37+
if (segment->computed_offset < 0 || segment->computed_length < 0 || segment->change_offset < 0) {
38+
print_model_segments_(model_ptr.get(), CLOG);
39+
return -1;
3640
}
37-
}
38-
if (!session_ptr->models_.front()->model_segments.empty()) {
39-
const auto first_serial =
40-
session_ptr->models_.front()->model_segments.front()->change_ptr->serial;
41-
// First segment should be either the initial READ (serial 0) or the first user change (serial 1)
42-
if ((first_serial != 0 && first_serial != 1) ||
43-
0 != (session_ptr->models_.front()->model_segments.front()->change_ptr->kind &
44-
OMEGA_CHANGE_TRANSACTION_BIT)) {
41+
42+
// Segments must have positive length (zero-length segments are not valid in the model)
43+
if (segment->computed_length == 0) {
44+
print_model_segments_(model_ptr.get(), CLOG);
4545
return -1;
4646
}
47+
48+
// Segments must be contiguous (no gaps or overlaps)
49+
if (expected_offset != segment->computed_offset) {
50+
print_model_segments_(model_ptr.get(), CLOG);
51+
return -1;
52+
}
53+
54+
// Segment must not extend beyond its parent change data
55+
if (segment->change_offset + segment->computed_length > segment->change_ptr->length) {
56+
print_model_segments_(model_ptr.get(), CLOG);
57+
return -1;
58+
}
59+
60+
// change_offset must not exceed the change length
61+
if (segment->change_offset >= segment->change_ptr->length) {
62+
print_model_segments_(model_ptr.get(), CLOG);
63+
return -1;
64+
}
65+
66+
expected_offset += segment->computed_length;
4767
}
68+
69+
// Checkpoint models (index > 0) must have a backing file
70+
if (model_index > 0 && !model_ptr->file_ptr) { return -1; }
4871
}
72+
73+
// Cross-check: the back model's total segment length must match the session's computed file size
74+
const auto computed_file_size = omega_session_get_computed_file_size(session_ptr);
75+
const auto &back_model = session_ptr->models_.back();
76+
const int64_t model_total_length = back_model->model_segments.empty()
77+
? 0
78+
: back_model->model_segments.back()->computed_offset +
79+
back_model->model_segments.back()->computed_length;
80+
if (model_total_length != computed_file_size) { return -1; }
81+
4982
return 0;
5083
}

0 commit comments

Comments
 (0)