SDK Host Unit Testing

This guide describes the reusable host-side unit test harness for SDK code and app example libraries.

Layout

SDK/Tests/Host/
β”œβ”€β”€ CMakeLists.txt
β”œβ”€β”€ cmake/
β”‚   └── FetchGoogleTest.cmake
β”œβ”€β”€ support/
β”‚   β”œβ”€β”€ KernelTestDoubles.hpp
β”‚   └── KernelTestDoubles.cpp
└── apps/
    └── Running/
        β”œβ”€β”€ Settings_test.cpp
        └── SettingsSerializer_test.cpp
  • cmake/: third-party bootstrap (GoogleTest).

  • support/: reusable fakes and fixtures (kernel facade dependencies, in-memory filesystem).

  • apps/<AppName>/: app-specific test cases.

Build and Run

From SDK root:

cmake -S Tests/Host -B build-host -DCMAKE_BUILD_TYPE=Debug
cmake --build build-host
ctest --test-dir build-host --output-on-failure

On Windows multi-config generators:

ctest --test-dir build-host -C Debug --output-on-failure

Expected: una-sdk-host-tests passes.

Add Tests For Another App

  1. Create a folder under Tests/Host/apps/<YourApp>/.

  2. Add one or more *_test.cpp files.

  3. Add test files and required app sources to Tests/Host/CMakeLists.txt executable source list.

  4. Reuse support doubles from support/KernelTestDoubles.* when the code requires SDK::Kernel or filesystem interactions.

Serializer/Settings Test Pattern

Use SDK::TestSupport::KernelFixture:

  • fixture.kernel provides a ready SDK::Kernel with stubbed dependencies.

  • fixture.fileSystem.seedFile(path, json) seeds in-memory input files.

  • Instantiate serializer with SettingsSerializer(fixture.kernel, "settings.json").

  • Verify load/save behavior without device firmware or simulator runtime.

Testing Code That Scans A Directory

InMemoryFileSystem enumerates what you seed, so code that walks a directory (kernel.fs.dir(path) then readNext()) can be tested directly. Seeding a file implies the directories along its path, with no mkdir needed:

KernelFixture fixture;
fixture.fileSystem.seedFile("Activity/morning.fit", contents);
fixture.fileSystem.seedFile("Activity/archive/old.fit", contents);

auto dir = fixture.kernel.fs.dir("Activity");
ASSERT_TRUE(dir->open());
SDK::Interface::IFileSystem::ObjectInfo item{};
while (dir->readNext(item)) {
    // "morning.fit" (isDir false), then "archive" (isDir true)
}
dir->close();

Worth knowing:

  • Direct children only. Activity/archive/old.fit shows up as the directory archive when listing Activity.

  • mkdir() creates parents and succeeds if the directory already exists; IDirectory::create() does not create parents.

  • A listing never reports the same name twice, for every name that fits ObjectInfo::name.

  • Opening a directory that does not exist fails, so assert on open().

Prove the scan is live

A test whose expectation is β€œnothing was found” cannot tell a correct decision from a scan that never ran. Where the expected outcome is a negative, assert first that the directory really enumerates what you seeded, or pair the test with a positive control that differs in exactly the one property under test.

Divergences from a real backend

Check these before writing a test that leans on one:

  • Entries are listed in the order they were created, which is not name order, so seed in the order a test needs. Overwriting a file keeps its place; a rename, or a file removed and created again, goes last, where FatFs would reuse the first free slot. An implied directory sits where its oldest remaining content does.

  • A listing is a snapshot taken at open(). Files seeded mid-scan appear after readNext(item, /*reset=*/true).

  • An implied directory is only as durable as its contents. It stops existing when the last thing under it is removed. Call mkdir() if a test needs it to outlive its children.

  • Leading and trailing slashes are not significant, and runs of / collapse: /a.txt, a.txt, a.txt/ are one object, as are a//b and a/b.

  • closeGate is passed the path without leading or trailing slashes. openHandles and flushCounts read the same for either spelling.

  • Names are case-sensitive. Walk.uapp and WALK.UAPP are two files.

  • A file may be created under a directory that does not exist. mkdir() the parents first if a test needs that failure.

  • No . / .. resolution. Both are ordinary path segments.

  • Directory rename is not modelled; it returns false.

  • isHidden is derived from a leading . in the name.

  • A name longer than ObjectInfo::name comes back clipped, and two names differing only past that length come back as identical entries. FatFs caps a long file name at 255 characters, which fits, so only a test can seed such a name. A 255-character name is real, and cannot be joined onto any directory within skMaxPathLen.

The fake refuses: a mkdir(), IDirectory::create(), write-open() or seedFile() where the other kind of object already holds the name; a removed path as the source of rename() or copy(); a destination that is the root, an existing directory, or under a file; a rename() onto an existing file, which FatFs f_rename also refuses; removing a directory that is not empty; and close() on a handle that is not open, so EXPECT_TRUE(h->close()) is a real assertion. failRenames makes every rename() fail.

Troubleshooting

  • If core_json.h is missing, confirm include path: SDK/ThirdParty/coreJSON/source/include.

  • If linker errors mention logger symbols, ensure SDK/Libs/Source/UnaLogger/Logger.cpp is linked.

  • If app headers are not found, add app lib header paths to target_include_directories(...) for the test target.