diff --git a/.gitea/workflows/sandbox-cmake.yml b/.gitea/workflows/sandbox-cmake.yml new file mode 100644 index 0000000..b010c52 --- /dev/null +++ b/.gitea/workflows/sandbox-cmake.yml @@ -0,0 +1,61 @@ +name: Sandbox - cmake + ccache on the pool +on: + push: + branches: [ci/sandbox] + workflow_dispatch: + +env: + # Inside the workspace so actions/cache can store it. The path is identical on + # every run, which matters because ccache hashes the compiler command line. + CCACHE_DIR: ${{ github.workspace }}/.ccache + +jobs: + build: + runs-on: cmake + defaults: + run: + shell: bash + working-directory: sandbox_c + steps: + - uses: actions/checkout@v4 + + - name: Toolchain + run: | + set -euo pipefail + command -v cmake >/dev/null && command -v ccache >/dev/null && exit 0 + apt-get update -qq + apt-get install -y -qq --no-install-recommends \ + cmake ninja-build gcc libc6-dev ccache ca-certificates >/dev/null + cmake --version | head -1 + ccache --version | head -1 + + # Cache the compiler cache, never build/: CMake bakes absolute paths into + # CMakeCache.txt, so a restored build tree breaks if the workspace moves. + - name: Cache ccache + uses: actions/cache@v4 + with: + path: ${{ env.CCACHE_DIR }} + key: ccache-${{ hashFiles('sandbox_c/**') }} + restore-keys: ccache- + + - name: ccache before build + run: ccache --show-stats | grep -E "cache (hits|misses|read)" || ccache --show-stats + + - name: Configure + run: | + cmake -S . -B build -G Ninja \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest --test-dir build --output-on-failure + + - name: ccache after build + if: always() + run: | + ccache --show-stats + set -o pipefail + ctest --test-dir build 2>&1 | grep -E "tests passed" diff --git a/sandbox_c/.gitignore b/sandbox_c/.gitignore new file mode 100644 index 0000000..b569585 --- /dev/null +++ b/sandbox_c/.gitignore @@ -0,0 +1,4 @@ +/build +/.ccache +CMakeCache.txt +CMakeFiles/ diff --git a/sandbox_c/CMakeLists.txt b/sandbox_c/CMakeLists.txt new file mode 100644 index 0000000..3e564ad --- /dev/null +++ b/sandbox_c/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.22) +project(sandbox_c C) +set(CMAKE_C_STANDARD 11) + +add_library(tempsense STATIC src/temperature.c src/convert.c src/parse.c src/format.c) +target_include_directories(tempsense PUBLIC include) + +enable_testing() +add_executable(test_tempsense tests/test_temperature.c) +target_link_libraries(test_tempsense PRIVATE tempsense) +add_test(NAME tempsense COMMAND test_tempsense) diff --git a/sandbox_c/include/temperature.h b/sandbox_c/include/temperature.h new file mode 100644 index 0000000..8e052be --- /dev/null +++ b/sandbox_c/include/temperature.h @@ -0,0 +1,12 @@ +#ifndef TEMPSENSE_H +#define TEMPSENSE_H +#include + +typedef struct { char sensor[32]; double celsius; } Reading; + +double to_fahrenheit(double celsius); +int parse_reading(const char *json, Reading *out); /* 0 on success */ +int format_reading(const Reading *in, char *buf, size_t len); +double clamp_to_absolute_zero(double celsius); + +#endif diff --git a/sandbox_c/src/convert.c b/sandbox_c/src/convert.c new file mode 100644 index 0000000..a32d2b9 --- /dev/null +++ b/sandbox_c/src/convert.c @@ -0,0 +1,2 @@ +#include "temperature.h" +double to_fahrenheit(double celsius) { return celsius * 9.0 / 5.0 + 32.0; } diff --git a/sandbox_c/src/format.c b/sandbox_c/src/format.c new file mode 100644 index 0000000..fa84586 --- /dev/null +++ b/sandbox_c/src/format.c @@ -0,0 +1,7 @@ +#include "temperature.h" +#include +int format_reading(const Reading *in, char *buf, size_t len) { + if (!in || !buf || len < 16) return 1; + int n = snprintf(buf, len, "{\"sensor\":\"%s\",\"celsius\":%.2f}", in->sensor, in->celsius); + return (n < 0 || (size_t)n >= len) ? 2 : 0; +} diff --git a/sandbox_c/src/parse.c b/sandbox_c/src/parse.c new file mode 100644 index 0000000..2db55e2 --- /dev/null +++ b/sandbox_c/src/parse.c @@ -0,0 +1,34 @@ +#include "temperature.h" +#include +#include + +/* Deliberately tiny parser: finds "celsius": and an optional + "sensor":"..." so the test can assert on both fields. */ +int parse_reading(const char *json, Reading *out) { + if (!json || !out) return 1; + const char *c = strstr(json, "\"celsius\""); + const char *s = strstr(json, "\"sensor\""); + if (!c) return 2; + c = strchr(c, ':'); + if (!c) return 3; + char *end = NULL; + out->celsius = strtod(c + 1, &end); + if (end == c + 1) return 4; + out->sensor[0] = '\0'; + if (s) { + s = strchr(s, ':'); + if (s) { + const char *open = strchr(s, '"'); + if (open) { + const char *close = strchr(open + 1, '"'); + if (close) { + size_t n = (size_t)(close - open - 1); + if (n >= sizeof out->sensor) n = sizeof out->sensor - 1; + memcpy(out->sensor, open + 1, n); + out->sensor[n] = '\0'; + } + } + } + } + return 0; +} diff --git a/sandbox_c/src/temperature.c b/sandbox_c/src/temperature.c new file mode 100644 index 0000000..5919ac7 --- /dev/null +++ b/sandbox_c/src/temperature.c @@ -0,0 +1,3 @@ +#include "temperature.h" +#define ABSOLUTE_ZERO_C (-273.15) +double clamp_to_absolute_zero(double celsius) { return celsius < ABSOLUTE_ZERO_C ? ABSOLUTE_ZERO_C : celsius; } diff --git a/sandbox_c/tests/test_temperature.c b/sandbox_c/tests/test_temperature.c new file mode 100644 index 0000000..d67737a --- /dev/null +++ b/sandbox_c/tests/test_temperature.c @@ -0,0 +1,26 @@ +#include "temperature.h" +#include +#include +#include +#include + +int main(void) { + assert(fabs(to_fahrenheit(0.0) - 32.0) < 1e-9); + assert(fabs(to_fahrenheit(100.0) - 212.0) < 1e-9); + + Reading r; + assert(parse_reading("{\"sensor\":\"attic\",\"celsius\":21.50}", &r) == 0); + assert(strcmp(r.sensor, "attic") == 0); + assert(fabs(r.celsius - 21.5) < 1e-9); + assert(parse_reading("{\"celsius\":-300.0}", &r) == 0); + assert(fabs(clamp_to_absolute_zero(r.celsius) + 273.15) < 1e-9); + assert(parse_reading("nonsense", &r) != 0); + + char buf[128]; + Reading in = { "boiler", 65.25 }; + assert(format_reading(&in, buf, sizeof buf) == 0); + assert(strstr(buf, "\"sensor\":\"boiler\"") != NULL); + + puts("all tempsense assertions passed"); + return 0; +}