Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ _deps
CMakeUserPresets.json
/build/
/.vscode/
coverage_report/
coverage.info

9 changes: 8 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,11 @@ include(Catch)
catch_discover_tests(tests)

add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE ordered_map_lib)
target_link_libraries(main PRIVATE ordered_map_lib)

# Enable coverage flags for Debug builds
if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR NOT CMAKE_BUILD_TYPE)
message(STATUS "Enabling code coverage flags")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
endif()
234 changes: 154 additions & 80 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

A C++ implementation of an ordered map data structure that maintains insertion order while providing O(1) key-value lookups.

## 🏆 Quality Metrics

- **Code Coverage**: 97.3% implementation coverage (271/277 lines)
- **Memory Safety**: Zero memory leaks verified by Valgrind
- **Test Suite**: 2,246 assertions across 195 test cases
- **Build Status**: [![CMake](https://github.com/mohameds-dev/ordered_map_lib/actions/workflows/cmake.yml/badge.svg)](https://github.com/mohameds-dev/ordered_map_lib/actions/workflows/cmake.yml)

## Table of Contents

- [Features](#features)
Expand All @@ -13,6 +20,8 @@ A C++ implementation of an ordered map data structure that maintains insertion o
- [Setting Up VS Code (or Cursor)](#setting-up-vs-code-or-cursor)
- [Formatting Code](#formatting-code)
- [Building and Running Tests](#building-and-running-tests)
- [Memory Leak Checks](#memory-leak-checks)
- [Code Coverage](#code-coverage)
- [GitHub Actions CI](#github-actions-ci)
- [Adding a New Test](#adding-a-new-test)
- [Future Improvements](#future-improvements)
Expand Down Expand Up @@ -49,7 +58,7 @@ int main() {
for (const auto& pair : map) {
std::cout << pair.first << ": " << pair.second << std::endl;
}

return 0;
}
```
Expand All @@ -65,81 +74,88 @@ The implementation of `OrderedMap` uses a combination of:

## Class Diagram

## Class Diagram

```mermaid
classDiagram
class Node~T~ {
+T value
+unique_ptr<Node~T~> next
+Node~T~* prev
+Node(T&& value, Node* prev, unique_ptr<Node> next)
+Node(Node* prev, unique_ptr<Node> next, Args&&... args)
}

class DoublyLinkedList~T~ {
-unique_ptr<Node~T~> head
-Node~T~* tail
-int _size
+push_back(const T&)
+push_back(T&&)
+emplace_back(Args&&...)
+pop_back()
+move_to_begin(Iterator)
+extract_node_and_link_prev_with_next(Node~T~*)
+emplace_node_before(unique_ptr<Node~T~>, Node~T~*)
+begin()
+end()
+size()
+front()
+back()
}

class Iterator~T~ {
-Node~T~* current_node_ptr
-DoublyLinkedList~T~* list_ptr
+operator++()
+operator--()
+operator*()
+operator==()
+operator!=()
}

class OrderedMap~Key,Value~ {
-unordered_map<Key, Iterator> _map
-DoublyLinkedList<pair<Key,Value>> _list
+insert(const Key&, const Value&)
+insert(const Key&, Value&&)
+move_to_front(const Key&)
+operator[](const Key&)
+at(const Key&)
+size()
+begin()
+end()
}

class OrderedMap~Key,Value~ {
...
+Iterator begin()
+Iterator end()
}

class OrderedMapIterator~Key,Value~ {
- Iterator~pair<Key,Value>~ list_iterator
+ operator++()
+ operator*()
+ operator!=()
class Node~T~ {
+T value
+unique_ptr<Node~T~> next
+Node~T~* prev
+Node(Node* prev, unique_ptr<Node> next, U&& value)
+Node(Node* prev, unique_ptr<Node> next, Args&&... args)
}

class DoublyLinkedList~T~ {
-unique_ptr<Node~T~> head
-Node~T~* tail
-int _size
+push_back(const T&)
+push_back(T&&)
+push_front(const T&)
+push_front(T&&)
+emplace_back(Args&&...)
+pop_back()
+pop_front()
+move_to_begin(Iterator)
+move_to_end(Iterator)
+erase(Iterator)
+size() const
+begin()
+end()
+front() const
+back() const
+clear()
+back_iterator()
-extract_node_and_link_prev_with_next(Node~T~*)
-emplace_node_before(unique_ptr<Node~T~>, Node~T~*)
-emplace_node_after(unique_ptr<Node~T~>, Node~T~*)
}

OrderedMap~Key,Value~ *-- OrderedMapIterator~Key,Value~
OrderedMapIterator~Key,Value~ *-- Iterator~pair<Key,Value>~

class DoublyLinkedListIterator~T~ {
-Node~T~* current_node_ptr
-DoublyLinkedList~T~* list_ptr
+operator++()
+operator--()
+operator*()
+operator->()
+operator==() const
+operator!=() const
}

class OrderedMap~Key,Value~ {
-unordered_map<Key, DoublyLinkedListIterator~pair<Key,Value>~> _map
-DoublyLinkedList~pair<Key,Value>~ _list
+insert(const Key&, const Value&)
+insert(const Key&, Value&&)
+move_to_front(const Key&)
+move_to_back(const Key&)
+operator[](const Key&)
+at(const Key&) const
+erase(const Key&)
+find(const Key&) const
+size() const
+begin()
+end()
+clear()
+back_iterator()
}

DoublyLinkedList~T~ *-- Node~T~
DoublyLinkedList~T~ *-- Iterator~T~
OrderedMap~Key,Value~ *-- DoublyLinkedList~pair<Key,Value>~
OrderedMap~Key,Value~ ..> Iterator~pair<Key,Value>~
class OrderedMapIterator~Key,Value~ {
-DoublyLinkedListIterator~pair<Key,Value>~ list_iterator
+operator++()
+operator*()
+operator->()
+operator!=() const
+operator==() const
}

DoublyLinkedList~T~ *-- Node~T~
DoublyLinkedList~T~ *-- DoublyLinkedListIterator~T~
OrderedMap~Key,Value~ *-- DoublyLinkedList~pair<Key,Value>~
OrderedMap~Key,Value~ *-- OrderedMapIterator~Key,Value~
OrderedMapIterator~Key,Value~ *-- DoublyLinkedListIterator~pair<Key,Value>~


```
Expand Down Expand Up @@ -246,6 +262,66 @@ ordered_map/
./tests --tags [tag]
```

## Memory Leak Checks

This project is tested for memory leaks using Valgrind.
Below is a sample output from running the full test suite under Valgrind:

```
$ valgrind --leak-check=full ./build/tests --rng-seed 0

==12798== Memcheck, a memory error detector
==12798== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==12798== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==12798== Command: ./build/tests --rng-seed 0
==12798==
Randomness seeded to: 0
===============================================================================
All tests passed (2246 assertions in 195 test cases)

==12798==
==12798== HEAP SUMMARY:
==12798== in use at exit: 0 bytes in 0 blocks
==12798== total heap usage: 1,528,976 allocs, 1,528,976 frees, 45,317,931 bytes allocated
==12798==
==12798== All heap blocks were freed -- no leaks are possible
==12798==
==12798== For lists of detected and suppressed errors, rerun with: -s
==12798== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
```

## Code Coverage

This project includes comprehensive test coverage analysis using **LCOV**, which provides accurate coverage metrics for header-only libraries by distinguishing between implementation code and test code.

### Generate Coverage Report

From the build directory, run this script:

```bash
./create_and_run_coverage.sh
```

### Implementation Coverage Results

**Pure Implementation Coverage (excluding test code and system headers):**

- **doubly_linked_list.hpp**: **97.8%** of 185 lines covered
- **ordered_map.hpp**: **98.9%** of 91 lines covered

**Overall Project Coverage: 98.0%** (1464 of 1494 lines)

### Coverage Summary

This shows that:

- **97.8%** of the doubly linked list implementation is tested
- **98.9%** of the ordered map implementation is tested
- Only a few edge cases remain uncovered
- The test suite comprehensively exercises the core functionality

The HTML report provides line-by-line coverage details, showing exactly which implementation lines are covered by the test suite.

## GitHub Actions CI

- **Workflow**: `.github/workflows/cmake.yml` automates building and testing on push to `main` or pull requests to `main`.
Expand Down Expand Up @@ -284,19 +360,17 @@ ordered_map/

## Future Features

- [ ] Add `move_to_front` and `move_to_back` operations in ordered_map to re-order entries (without affecting or copying the entry value)

- [x] Add std::move and copying semantics
- [x] **DoublyLinkedList:** Add tests for copying & moving
- [x] **DoublyLinkedList:** Add `erase` method to erase elements given their iterator
- [x] **DoublyLinkedList:** Add pre and post decrement operators
- [x] **DoublyLinkedList:** Test front(), back(), insertion and deletion functions for copying behavior
- [x] Add `move_to_front` and `move_to_back` operations in ordered_map to re-order entries (without affecting or copying the entry value)
- [x] **OrderedMap:** Add support for initializing map with intializer list
- [x] **OrderedMap:** Add support for initializing map with two container iterators
- [x] **OrderedMap:** Add `erase` method to erase elements given their iterator or key
- [x] Test for memory leaks
- [x] Add test coverage
- [ ] Add const iterators
- [ ] Add reverse iterators
- [ ] Add range-based operations
- [ ] Add performance benchmarks
- [ ] Add support for custom allocators
- [ ] Add std::move and copying semantics
- [ ] **DoublyLinkedList:** Add tests for copying & moving
- [ ] **DoublyLinkedList:** Add `erase` method to erase elements given their iterator
- [ ] **DoublyLinkedList:** Add pre and post decrement operators
- [ ] **DoublyLinkedList:** Test front(), back(), insertion and deletion functions for copying behavior
- [ ] **OrderedMap:** Add support for initializing map with
- [ ] **OrderedMap:** Add support for custom hash functions
- [ ] **OrderedMap:** Add `erase` method to erase elements given their iterator or key
- [ ] Test for memory leaks
19 changes: 19 additions & 0 deletions create_and_run_coverage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

# Run tests and generate coverage report
./run_build.sh

# Capture coverage data from build directory (where .gcda files are)
lcov --capture --directory build --output-file coverage.info

# Remove system headers for cleaner results
lcov --remove coverage.info '/usr/*' --output-file coverage.info

# Remove test files and keep only include files
lcov --extract coverage.info '*/include/*' --output-file coverage.info

# Generate HTML report
genhtml coverage.info --output-directory coverage_report

# View the report
firefox coverage_report/include/index.html
31 changes: 12 additions & 19 deletions include/doubly_linked_list.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,15 @@ class DoublyLinkedList {
if (it == begin()) return;

auto extracted = extract_node_and_link_prev_with_next(it.current_node_ptr);
emplace_node_before(std::move(extracted), head.get());
emplace_node_at_head(std::move(extracted));
}

void move_to_end(Iterator it) {
raise_exception_if_empty();
if (it == end()) throw std::out_of_range("move_to_end called on end iterator");

auto extracted = extract_node_and_link_prev_with_next(it.current_node_ptr);
emplace_node_before(std::move(extracted), nullptr);
emplace_node_at_tail(std::move(extracted));
}

template <typename... Args>
Expand Down Expand Up @@ -296,23 +296,16 @@ class DoublyLinkedList {
return extracted;
}

void emplace_node_before(std::unique_ptr<Node<T>> node, Node<T>* position) {
if (position == head.get()) {
node->next = std::move(head);
if (node->next) node->next->prev = node.get();
head = std::move(node);
}
else if (position == nullptr) {
node->prev = tail;
tail = node.get();
node->prev->next = std::move(node);
}
else {
node->prev = position->prev;
node->next = std::move(position->prev->next);
position->prev = node.get();
node->prev->next = std::move(node);
}
void emplace_node_at_head(std::unique_ptr<Node<T>> node){
node->next = std::move(head);
if (node->next) node->next->prev = node.get();
head = std::move(node);
}

void emplace_node_at_tail(std::unique_ptr<Node<T>> node){
node->prev = tail;
tail = node.get();
node->prev->next = std::move(node);
}

void raise_exception_if_empty() const {
Expand Down
2 changes: 1 addition & 1 deletion tests/ordered_map/insertion_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ TEST_CASE("empty ordered map has size 0", "[insert]") {
REQUIRE(o_map.size() == 0);
}

TEST_CASE("inserting {1, 2} into an empty ordered map", "[insert]") {
TEST_CASE("insert {1, 2} into an empty ordered map", "[insert]") {
OrderedMap<int, int> o_map;
o_map.insert(1, 2);
REQUIRE(o_map.size() == 1);
Expand Down