diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..689eae5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.o +*.vscode +*tests/output +*googletest +tests/circular_buffer_tests diff --git a/README.md b/README.md index 7fb2175..0fbcc6e 100644 --- a/README.md +++ b/README.md @@ -17,3 +17,11 @@ 3. Generic data type (e.g. uint8_t) 4. Two put(data) overflow behaviors: overwrite and discard data. 2. Write tests showing your circular buffer in action. + +## Build instructions + +This solution is using the googletest framework for C++ unit tests. + +1. Clone googletest into the root folder of this project via + `git clone https://github.com/google/googletest.git` +2. Run `make check` inside the `tests` folder in order to compile and run the tests. diff --git a/include/circularbuffer/circular_buffer.hpp b/include/circularbuffer/circular_buffer.hpp new file mode 100644 index 0000000..335862f --- /dev/null +++ b/include/circularbuffer/circular_buffer.hpp @@ -0,0 +1,110 @@ +/* + * Copyright 2020 by Alexander Dahmen + */ +#ifndef CIRCULARBUFFER_CIRCULAR_BUFFER_HPP_ +#define CIRCULARBUFFER_CIRCULAR_BUFFER_HPP_ + +#include + +namespace circularbuffer { + +/** + * CircularBuffer class + * + * Declaration and definition and of a ring buffer with a fixed size. + * + */ +template class CircularBuffer { + public: + /** + * Reads the next item from the buffer if not empty. + * Returns true if successfully. + * + */ + bool get(T *item); + /** + * Method to write a new item into the buffer. + * If the buffer is full the oldest item will be overwritten. + * + */ + void putOverwrite(T const &item); + /** + * Method to write a new item into the buffer. + * If the buffer is full the new item won't be written. + * + */ + void putDiscard(T const &item); + /** + * Returns true if the buffer is full or false if not. + * + */ + bool full() const; + /** + * Returns true if the buffer is empty or false if not. + */ + bool empty() const; + + private: + void put(T const &item); + + size_t read_index_; + size_t write_index_; + size_t data_available_; + std::array buf_; + std::mutex mutex_; +}; + +template +void CircularBuffer::put(T const &item) { + buf_[write_index_] = item; + write_index_ = (write_index_ + 1) % buffer_size; +} + +template +void CircularBuffer::putOverwrite(T const &item) { + std::lock_guard lock(mutex_); + + put(item); + // Since this method overrides existing data + // the maximum data which can be available is buffer_size. + // Only increase data_available_ when it is smaller than buffer_size. + if (!full()) + data_available_++; +} + +template +void CircularBuffer::putDiscard(T const &item) { + std::lock_guard lock(mutex_); + + if (full()) + return; + put(item); + data_available_++; +} + +template +bool CircularBuffer::get(T *item) { + std::lock_guard lock(mutex_); + + if (empty()) + return false; + // read one item, return it and move index + *item = buf_[read_index_]; + read_index_ = (read_index_ + 1) % buffer_size; + data_available_--; + return true; +} + +template +bool CircularBuffer::full() const { + return data_available_ == buffer_size; +} + +template +bool CircularBuffer::empty() const { + return data_available_ == 0; +} + +} // namespace circularbuffer + +#endif // CIRCULARBUFFER_CIRCULAR_BUFFER_HPP_ diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..6c2d2d3 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,56 @@ +# A Makefile for fusing Google Test and building a sample test against it. +# +# SYNOPSIS: +# +# make [all] - makes everything. +# make TARGET - makes the given target. +# make check - makes everything and runs the built test. +# make clean - removes all files generated by make. + +# googletest root dir +GOOGLE_TEST_ROOT = ../googletest/googletest + +# Points to the root of fused Google Test, relative to where this file is. +FUSED_GTEST_DIR = output + +# Paths to the fused gtest files. +FUSED_GTEST_H = $(FUSED_GTEST_DIR)/gtest/gtest.h +FUSED_GTEST_ALL_CC = $(FUSED_GTEST_DIR)/gtest/gtest-all.cc + +# Where to find gtest_main.cc. +GTEST_MAIN_CC = $(GOOGLE_TEST_ROOT)/src/gtest_main.cc + +# Flags passed to the preprocessor. +# We have no idea here whether pthreads is available in the system, so +# disable its use. +CPPFLAGS += -I$(FUSED_GTEST_DIR) -I../include -DGTEST_HAS_PTHREAD=0 + +# Flags passed to the C++ compiler. +CXXFLAGS += -g + +all : circular_buffer_tests + +check : all + ./circular_buffer_tests + +clean : + rm -rf $(FUSED_GTEST_DIR) circular_buffer_tests *.o + +$(FUSED_GTEST_H) : + $(GOOGLE_TEST_ROOT)/scripts/fuse_gtest_files.py $(FUSED_GTEST_DIR) + +$(FUSED_GTEST_ALL_CC) : + $(GOOGLE_TEST_ROOT)/scripts/fuse_gtest_files.py $(FUSED_GTEST_DIR) + +gtest-all.o : $(FUSED_GTEST_H) $(FUSED_GTEST_ALL_CC) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(FUSED_GTEST_DIR)/gtest/gtest-all.cc + +gtest_main.o : $(FUSED_GTEST_H) $(GTEST_MAIN_CC) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(GTEST_MAIN_CC) + +circular_buffer_tests.o : circular_buffer_tests.cpp \ + ../include/circularbuffer/circular_buffer.hpp $(FUSED_GTEST_H) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c circular_buffer_tests.cpp + +circular_buffer_tests : circular_buffer_tests.o gtest-all.o gtest_main.o + $(CXX) $(CPPFLAGS) $(CXXFLAGS) $^ -o $@ diff --git a/tests/circular_buffer_tests.cpp b/tests/circular_buffer_tests.cpp new file mode 100644 index 0000000..4ca5d8b --- /dev/null +++ b/tests/circular_buffer_tests.cpp @@ -0,0 +1,91 @@ +/* + * Copyright 2020 by Alexander Dahmen + */ +#include +#include + +using circularbuffer::CircularBuffer; + +TEST(CircularBufferTest, overwriteTest) { + CircularBuffer circular_buffer{}; + + // check if buffer is empty + ASSERT_TRUE(circular_buffer.empty()); + ASSERT_FALSE(circular_buffer.full()); + + // fill the buffer with data till it is full + circular_buffer.putOverwrite(10); + circular_buffer.putOverwrite(8); + circular_buffer.putOverwrite(6); + + // check if full and empty are set correctly + ASSERT_TRUE(circular_buffer.full()); + ASSERT_FALSE(circular_buffer.empty()); + + circular_buffer.putOverwrite(4); + + uint8_t read_item; + bool result = circular_buffer.get(&read_item); + ASSERT_EQ(read_item, 4); + ASSERT_FALSE(circular_buffer.full()); + ASSERT_FALSE(circular_buffer.empty()); +} + +TEST(CircularBufferTest, discardTest) { + CircularBuffer circular_buffer{}; + + // check if buffer is empty + ASSERT_TRUE(circular_buffer.empty()); + ASSERT_FALSE(circular_buffer.full()); + + // fill the buffer with data till it is full + circular_buffer.putDiscard(10); + circular_buffer.putDiscard(8); + circular_buffer.putDiscard(6); + + // check if full and empty are set correctly + ASSERT_TRUE(circular_buffer.full()); + ASSERT_FALSE(circular_buffer.empty()); + + circular_buffer.putDiscard(4); + + // read first item + uint8_t read_item; + bool result = circular_buffer.get(&read_item); + ASSERT_EQ(read_item, 10); + ASSERT_FALSE(circular_buffer.full()); + ASSERT_FALSE(circular_buffer.empty()); +} + +TEST(CircularBufferTest, getTest) { + CircularBuffer circular_buffer{}; + + // check if buffer is empty + ASSERT_TRUE(circular_buffer.empty()); + ASSERT_FALSE(circular_buffer.full()); + + uint8_t read_value; + // try to read from an empty buffer + bool result = circular_buffer.get(&read_value); + ASSERT_FALSE(result); + + // fill buffer with data + for (int i = 0; i < 3; ++i) { + circular_buffer.putDiscard(i); + } + + // check if buffer is full + ASSERT_FALSE(circular_buffer.empty()); + ASSERT_TRUE(circular_buffer.full()); + + // read from buffer + for (int i = 0; i < 3; ++i) { + bool result = circular_buffer.get(&read_value); + ASSERT_TRUE(result); + ASSERT_EQ(read_value, i) << "read item differs at " << i; + } + + // now buffer should be empty + ASSERT_TRUE(circular_buffer.empty()); + ASSERT_FALSE(circular_buffer.full()); +}