Repository containing Ben Ryan's official submissions for Synspective's Embedded Software Engineer coding test.
- The user is running a Linux or Unix-based operating system.
- Alternatively, the user is working from a Linux shell on Windows via WSL
- Git CLI is installed on the system (usually by default)
- You are performing this build either on the command line/terminal or in VSCode.
sudo apt install build-essential cmake libgtest-dev
sudo dnf install code gcc gcc-c++ kernel-devel make cmake gtest
If Homebrew is not installed yet
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
username=$(whoami)
echo >> /Users/$username/.zprofile
echo 'eval "$(/opt/homebrew/bin/brew shellenv zsh)"' >> /Users/$username/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv zsh)"
xcode-select --install
brew install cmake googletest
Via your command line/terminal, clone the repository via one of the following commands:
- If you have not already, set up your public SSH key with your GitHub account.
- Run command:
git clone git@github.com:bpryan99/synspective-coding-test.git
- Run command:
git clone https://github.com/bpryan99/synspective-coding-test.git
- If you prefer, download the project via ZIP archive and extract.
CMake is utilized to make the build as system and compiler agnostic as possible.
This project does not produce executables for the source files. This is due to there being no requested main() logic for either implementation question. By omitting main(), I can more easily package the source and header files for each question into a library that the unit tests can use, therefore avoiding double compiling of the source. If the libraries contained source files with main() definitions, they would override gtest_main and result in no tests being discovered.
From the root of the project directory (where the CMakeLists.txt file resides), set build folder to build and then build/compile.
cd ~/<cloned_dir>
cmake -B build && cmake --build buildThe build should occur automatically when opening the project. If it does not, click 'Build' along the bottom ribbon menu.
After successful build, executables for unit tests of questions 2 and 3 will have populated in the build folder as question-*_unittest. The sources for these tests are found in tests/. The testing framework used is GoogleTest (GTest).
cd build && ctest
cd build
ctest -R RingBufferTest
ctest -R LinkedListTest
ctest -R CarTest
ctest -R NodeTest
My testing methodology for unit tests can be broken down into 3 tenants:
- Unit tests are testing the public interface for expected behavior
- Unit tests should produce as close to 100% coverage of code/logic as possible to be effective
- Unit tests should test edge cases where logic is more likely to be fragile
Unit tests can only access the public members of a class, be it vars or functions. Unit tests should test all public members.
Unit tests should be sufficient enough to 'touch' each line of code (or at least each line changing logic state) for the given source so as to verify static functionality completely. Even if some members are private, public members invoking private methods should allow for sufficient coverage. This way, if a new engineer goes to update a piece of logic they falsely assume inconsequential, there is already a unit test that will preemptively catch the mistake introduced by the bad assumption. The test implementor must not just test what they consider to be the likely scenarios in which the source might break, but should strive to cover each line so that subsequent changes do not silently introduce bugs.
A source file may have full code coverage by unit tests with certain input conditions, but that may not reveal errors introduced by less common input conditions. For example, if the LinkedList to be merged has only 1 node, this is an edge case which needs to be tested. Hypothetically, a source file with total coverage via unit tests would already have tested this logic since there is code for this special base case, but what if that code were not present? Edge cases where input is most likely to break the functionality should be covered by unit tests for testing completeness.
I made a few assumptions when designing unit tests for these two questions:
- The source files do not need to generate executables, there just need to be executable tests that can access source/headers as libraries.
- The compiler/ctest will handle constructor contract breaks, such as providing a completely incorrect type for a given var/function arg.
- Since no system limitations were provided, the resources of the hypothetical system these functions would be run on have sufficient resources that I need not test edge cases where memory size is a concern.
If I were to be asked how I would take either of these implementations further, I would answer with the following:
- Further generalize my LinkedList implementation to be more extensible.
- Updating
mergeSortByYear()to be agnostic of the key of the data to sort by would be my first priority. Providing a key type and key name and attempting to sort based on those two args would improve the extensibility of the implementation.
- Updating
- Consider updating the implementation as a doubly-linked list if node deletion would be happening frequently. While the implementation is more complex, the time complexity of deletion in a doubly-linked list is
O(1)vs singly-linked list'sO(n). - Add functionality to search the list for a specific Car, insert in a specific location, etc.
- Potentially sort on insertion of a new node so that the sort function just returns a copy of the already sorted list. This would be most useful if insertion time complexity was not a major concern but retrieval complexity being as low as possible was paramount.
- Update implementation to allow specification for an overwriting buffer
- If the user set a constant value
bool overwrite;as true upon buffer instantiation, the buffer would move the head to the tail, increment the tail, and overwrite the value at the head (old tail), thus maintaining First In First Out in overwriting the oldest element in the buffer.
- If the user set a constant value