Skip to content

Commit

Permalink
Merge pull request #1 from himaren/develop
Browse files Browse the repository at this point in the history
Merge sync feature and add some tests
  • Loading branch information
himaren authored May 8, 2024
2 parents 1a6a060 + 2e76b75 commit 21b9cfa
Show file tree
Hide file tree
Showing 11 changed files with 1,026 additions and 5 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/hella_world.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop

jobs:
compile_and_run:
Expand All @@ -16,7 +18,7 @@ jobs:
uses: actions/checkout@v4
with:
submodules: true

- name: get-cmake
uses: lukka/get-cmake@v3.29.3

Expand Down
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ endmacro()

fetch_content_from_submodule(Catch2 vendor/Catch2)

add_executable(address_book src/main.cpp src/address_book.cpp)
add_executable(address_book src/main.cpp src/address_book.cpp src/synchronization.cpp)
target_include_directories(address_book PRIVATE vendor)

add_executable(address_book_tests test/address_book_tests.cpp src/address_book.cpp)
add_executable(address_book_tests test/address_book_tests.cpp src/address_book.cpp src/synchronization.cpp)
target_include_directories(address_book_tests PRIVATE src vendor)
target_link_libraries(address_book_tests Catch2::Catch2WithMain)
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ This is a small example application for which we want to write unit tests using
To build and run the application and tests, the following are required:
- CMake
- A modern C++ compiler (a few C++20 features are used, GCC 11.4 is sufficient)
- Python 3 (to run the HTTP synchronization server)

Configure the build using `cmake -S . -B build`, then use `cmake --build build` to compile.
This produces two executables, `address_book` and `address_book_tests` inside the `build` directory.
Both executables can be called without any arguments; the tests support various options for how to run them, run `address_book_tests --help` for more information.

To enable the synchronization feature, start the `simple_sync_server.py`.

## Specification

The `address_book` class defined in `src/address_book.hpp|.cpp` adheres to the following specification:
Expand All @@ -25,3 +28,6 @@ The `address_book` class defined in `src/address_book.hpp|.cpp` adheres to the f
- A phone number and birthday can be set for each entry.
- Attempting to set a phone number or birthday on a non-existent entry throws an exception.
- Attempting to set an invalid date as birthday throws an exception.
- The address book can be synchronized with a remote location using a *synchronization provider*.
- The synchronization provider offers a single function `synchronize`, that is given a list of all local entries, serialized as `name,number,MM/DD`, for example `jane m doe,123456789,11/30`.
It then returns a list in the same format after synchronization.
32 changes: 32 additions & 0 deletions simple_sync_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
import http.server
import socketserver

PORT = 3333

address_book_data = b''

class AddressBookRequestHandler(http.server.BaseHTTPRequestHandler):
def __init__(self, request, client_address, server):
super().__init__(request, client_address, server)

def do_GET(self):
if self.path == '/address-book':
self.send_response(200)
self.send_header('Content-type', 'application/text')
self.end_headers()
global address_book_data
self.wfile.write(address_book_data)

def do_POST(self):
if self.path == '/address-book':
self.send_response(200)
self.send_header('Content-type', 'application/text')
self.end_headers()
content_length = int(self.headers['Content-Length'])
global address_book_data
address_book_data = self.rfile.read(content_length)

with socketserver.TCPServer(("", PORT), AddressBookRequestHandler) as httpd:
print("Serving on port", PORT)
httpd.serve_forever()
24 changes: 24 additions & 0 deletions src/address_book.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,30 @@ std::string address_book::get_next_birthday() const {
return prettify_name(name);
}

void address_book::synchronize(synchronization_provider& provider) {
std::vector<std::string> serialized_entries;
for(const auto& [name, entry] : m_entries) {
std::string serialized_entry = name + "," + std::to_string(entry.phone_number) + ","
+ std::to_string(static_cast<unsigned>(entry.birthday.month())) + "/"
+ std::to_string(static_cast<unsigned>(entry.birthday.day()));
serialized_entries.push_back(serialized_entry);
}
std::vector<std::string> merged_entries = provider.synchronize(serialized_entries);
m_entries.clear();
for(const std::string& serialized_entry : merged_entries) {
std::string name(max_name_length, '\0');
std::uint64_t phone_number;
unsigned month;
unsigned day;
char format[32] = {0};
snprintf(format, sizeof(format), "%%%zu[^,],%%lu,%%u/%%u", max_name_length);
sscanf(serialized_entry.c_str(), format, name.data(), &phone_number, &month, &day);
name.erase(name.find('\0'));
m_entries.try_emplace(normalize_name(name),
entry{phone_number, std::chrono::month_day{std::chrono::month{month}, std::chrono::day{day}}});
}
}

address_book::entry& address_book::get_entry(std::string name) {
auto it = m_entries.find(normalize_name(name));
if(it == m_entries.end()) { throw std::invalid_argument("Entry not found"); }
Expand Down
5 changes: 5 additions & 0 deletions src/address_book.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include "synchronization.hpp"

#include <chrono>
#include <cstdint>
#include <string>
Expand Down Expand Up @@ -46,6 +48,9 @@ class address_book {
/// Throws if the address book is empty.
std::string get_next_birthday() const;

/// Synchronizes the address book with a remote provider.
void synchronize(synchronization_provider& provider);

private:
struct entry {
std::uint64_t phone_number;
Expand Down
11 changes: 10 additions & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,18 @@ void next_birthday(address_book& ab) {
printf("%s's birthday is on %u/%u\n", name.c_str(), (unsigned)birthday.month(), (unsigned)birthday.day());
}

void synchronize(address_book& ab) {
http_synchronization_provider provider{"http://localhost:3333"};
ab.synchronize(provider);
printf("Synchronized %zu entries\n", ab.get_entries().size());
}

int main() {
address_book ab;

printf("Address Book\n");
while(true) {
printf("\n(A)dd entry, (R)emove entry, (L)ist entries, (N)ext birthday, (Q)uit\n");
printf("\n(A)dd entry, (R)emove entry, (L)ist entries, (N)ext birthday, (S)ynchronize, (Q)uit\n");

try {
char choice;
Expand All @@ -76,6 +82,9 @@ int main() {
case 'N':
case 'n': next_birthday(ab); break;

case 'S':
case 's': synchronize(ab); break;

case 'Q':
case 'q': return 0;

Expand Down
91 changes: 91 additions & 0 deletions src/synchronization.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#include "synchronization.hpp"

#define HTTP_IMPLEMENTATION
#include <http.h>

#include <algorithm>
#include <fstream>
#include <sstream>

std::vector<std::string> synchronization_provider::merge_entries(
std::vector<std::string> local_entries, std::vector<std::string> remote_entries) {
std::vector<std::string> merged_entries = local_entries;
for(std::string remote_entry : remote_entries) {
bool found = false;
std::string name = remote_entry.substr(0, remote_entry.find(','));
if(std::none_of(merged_entries.begin(), merged_entries.end(),
[&name](std::string entry) { return entry.substr(0, entry.find(',')) == name; })) {
merged_entries.push_back(remote_entry);
}
}
return merged_entries;
}

http_synchronization_provider::http_synchronization_provider(std::string url) : m_url(url) {}

void await_request(http_t* request) {
http_status_t status = HTTP_STATUS_PENDING;
while(status == HTTP_STATUS_PENDING) {
status = http_process(request);
}
if(status == HTTP_STATUS_FAILED) {
printf("HTTP request failed (%d): %s.\n", request->status_code, request->reason_phrase);
http_release(request);
throw std::runtime_error("HTTP request failed");
}
}

std::vector<std::string> http_synchronization_provider::synchronize(std::vector<std::string> serialized_entries) {
http_t* get_request = http_get((m_url + "/address-book").c_str(), NULL);
if(!get_request) { throw std::runtime_error("Invalid request"); }
await_request(get_request);
std::string response((const char*)get_request->response_data);
http_release(get_request);

std::vector<std::string> remote_entries;
std::stringstream ss{response};
std::string line;
while(std::getline(ss, line)) {
remote_entries.push_back(line);
}

std::vector<std::string> merged_entries = merge_entries(serialized_entries, remote_entries);

std::string merged_entries_str;
for(std::string entry : merged_entries) {
merged_entries_str += entry + '\n';
}

http_t* post_request =
http_post((m_url + "/address-book").c_str(), merged_entries_str.c_str(), merged_entries_str.size(), NULL);
if(!post_request) { throw std::runtime_error("Invalid request"); }
await_request(post_request);
http_release(post_request);

return merged_entries;
}

file_synchronization_provider::file_synchronization_provider(std::string path) : m_path(path) {}

std::vector<std::string> file_synchronization_provider::synchronize(std::vector<std::string> serialized_entries) {
std::vector<std::string> file_entries;
{
std::fstream file{m_path, std::ios::in | std::ios::binary};
if(file.is_open()) {
std::string line;
while(std::getline(file, line)) {
file_entries.push_back(line);
}
}
}
std::vector<std::string> merged_entries = merge_entries(serialized_entries, file_entries);
{
std::fstream file{m_path, std::ios::out | std::ios::binary | std::ios::trunc};
if(!file.is_open()) { throw std::runtime_error("Could not open file " + m_path); }
for(std::string entry : merged_entries) {
file << entry << '\n';
}
}

return merged_entries;
}
35 changes: 35 additions & 0 deletions src/synchronization.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include <string>
#include <vector>

/// Abstract base class for synchronization providers.
class synchronization_provider {
public:
virtual std::vector<std::string> synchronize(std::vector<std::string> serialized_entries) = 0;
virtual ~synchronization_provider() = default;

protected:
std::vector<std::string> merge_entries(
std::vector<std::string> local_entries, std::vector<std::string> remote_entries);
};

// Synchronization provider that synchronizes with a HTTP server.
class http_synchronization_provider : public synchronization_provider {
public:
http_synchronization_provider(std::string url);
std::vector<std::string> synchronize(std::vector<std::string> serialized_entries) override;

private:
std::string m_url;
};

// Synchronization provider that synchronizes with a file.
class file_synchronization_provider : public synchronization_provider {
public:
file_synchronization_provider(std::string path);
std::vector<std::string> synchronize(std::vector<std::string> serialized_entries) override;

private:
std::string m_path;
};
Loading

0 comments on commit 21b9cfa

Please sign in to comment.