Compare commits

...
22 Commits
Author SHA1 Message Date
druppy c1293bda4c webhook test: fresh hook, correct URL
Sandbox - ephemeral runner + shared cache / cargo (push) Successful in 10s
Build RPi image / build (push) Successful in 10s
2026-10-24 22:12:09 +02:00
druppy f4db1e7013 scale B
Sandbox - ephemeral runner + shared cache / cargo (push) Successful in 10s
Build RPi image / build (push) Successful in 10s
2026-10-24 21:46:49 +02:00
druppy 79ea99e939 scale A
Build RPi image / build (push) Successful in 10s
Sandbox - ephemeral runner + shared cache / cargo (push) Successful in 10s
2026-10-24 21:46:49 +02:00
druppy 45d3d3c91f Show full cargo test output
Sandbox - ephemeral runner + shared cache / cargo (push) Successful in 10s
Build RPi image / build (push) Successful in 10s
2026-10-24 21:43:55 +02:00
druppy 92f6412539 trigger: prove cache survives the runner that wrote it
Build RPi image / build (push) Successful in 10s
Sandbox - ephemeral runner + shared cache / cargo (push) Successful in 9s
2026-10-24 21:40:48 +02:00
druppy 07b072e5b7 Use absolute cargo path in toolchain step too
Build RPi image / build (push) Successful in 10s
Sandbox - ephemeral runner + shared cache / cargo (push) Successful in 32s
2026-10-24 21:37:28 +02:00
druppy 1acdc44f88 Call cargo by absolute path (PATH did not carry over between steps)
Build RPi image / build (push) Successful in 10s
Sandbox - ephemeral runner + shared cache / cargo (push) Failing after 10s
2026-10-24 21:33:57 +02:00
druppy 393bac993d Export cargo bin PATH directly; GITHUB_PATH did not propagate
Build RPi image / build (push) Successful in 10s
Sandbox - ephemeral runner + shared cache / cargo (push) Failing after 10s
2026-10-24 21:30:48 +02:00
druppy ff2270a64d Cache rustup+registry+target, reach cache server via LAN IP, fail on pipe errors
Build RPi image / build (push) Successful in 10s
Sandbox - ephemeral runner + shared cache / cargo (push) Failing after 10s
2026-10-24 21:27:35 +02:00
druppy f45ca8ff95 trigger: exercise ephemeral podman runner
Build RPi image / build (push) Successful in 10s
Sandbox - ephemeral runner + shared cache / cargo (push) Successful in 12s
2026-10-24 21:25:46 +02:00
druppy 37d70c40c5 Add sandbox crate + workflow to test ephemeral podman runners
Build RPi image / build (push) Successful in 10s
Sandbox - ephemeral runner + shared cache / cargo (push) Failing after 2s
2026-10-24 21:23:30 +02:00
druppy 1c596cf097 Keep image globs outside quotes so they expand
Build RPi image / build (push) Successful in 10s
Build RPi image / build (pull_request) Successful in 10s
2026-10-24 19:41:59 +02:00
druppy 16d63d95cb Refactor CI into composite actions + repo variables
Build RPi image / build (push) Failing after 9s
Build RPi image / build (pull_request) Failing after 8s
The workflow was mostly inline bash. Moves the logic into reusable
composite actions under .gitea/actions/, keeps host paths in the
YOCTO_HOME repo variable, adds a workflow_dispatch skip-build option
and publishes step outputs (build dir, image list, kas path).
2026-10-24 19:39:31 +02:00
druppy d4ca8fe7c5 Add pull_request trigger so PRs into main post a status check
Build RPi image / build (push) Successful in 10s
Build RPi image / build (pull_request) Successful in 10s
2026-10-24 19:31:07 +02:00
druppy 6f8456f2b6 Trigger CI on ci/* branches so main can stay protected
Build RPi image / build (push) Successful in 10s
2026-10-24 19:30:09 +02:00
druppy 571c8ad70c Fix artifact collection: workspace-relative dist, canonical image names, checksums
Build RPi image / build (push) Successful in 11s
2026-10-24 19:14:09 +02:00
druppy 5396dd1d7e Give each kas repo its own path (all three shared extern/, clobbering each other)
Build RPi image / build (push) Failing after 19m24s
2026-10-24 18:52:23 +02:00
druppy a71b188289 Use https for meta-openembedded (git:// port 9418 unreachable)
Build RPi image / build (push) Failing after 1s
2026-10-24 18:47:37 +02:00
druppy 31264c17c9 Separate runner dir from persistent build dir; guard against self-sync
Build RPi image / build (push) Failing after 4m34s
2026-10-24 18:35:05 +02:00
druppy 4a645bab2d Target dedicated host runner (yocto label); YOCTO_HOME overridable via runner env 2026-10-24 18:18:01 +02:00
druppy 439b5c53a8 Match instance runner label; probe environment on first run
Build RPi image / build (push) Failing after 12s
2026-10-24 17:58:43 +02:00
druppy 25dd8e1f28 Add Gitea Actions build workflow for rpilinux-image
Host-based kas build on a dedicated 'yocto:host' runner with a
persistent project dir ($HOME/yocto-ci) for incremental sstate reuse.
Uploads deploy images from raspberrypi4-64 as artifacts.
2026-10-24 17:48:47 +02:00
10 changed files with 602 additions and 6 deletions

No files matched your search

+8 -6
View File
@@ -12,21 +12,21 @@ target: rpilinux-image
repos:
meta-raspberrypi:
url: http://git.yoctoproject.org/git/meta-raspberrypi
path: extern/
url: https://git.yoctoproject.org/git/meta-raspberrypi
path: extern/meta-raspberrypi
tag: scarthgap
poky:
url: http://git.yoctoproject.org/git/poky
url: https://git.yoctoproject.org/git/poky
tag: scarthgap
path: extern/
path: extern/poky
layers:
meta:
meta-poky:
meta-yocto-bsp:
meta-openembedded:
url: git://git.openembedded.org/meta-openembedded
url: https://git.openembedded.org/meta-openembedded
tag: scarthgap
path: extern/
path: extern/meta-openembedded
layers:
meta-oe:
meta-multimedia:
@@ -34,6 +34,8 @@ repos:
meta-python:
local:
path: meta-rpilinux
local:
path: meta-rpilinux
local_conf_header:
ipk: |
+118
View File
@@ -0,0 +1,118 @@
name: build-image
description: >-
Build a kas project in a persistent directory and expose the flashable
images. The workspace is synced into the build dir instead of built in
place, because checkout cleans ignored files (build/, extern/) and bitbake
reuse depends on sstate-cache surviving between runs.
inputs:
project:
description: kas configuration file, relative to the repository
required: false
default: .config.yaml
machine:
description: Machine the images are deployed for
required: false
default: raspberrypi4-64
image-prefix:
description: Artifact filename prefix in the deploy dir
required: false
default: rpilinux-image
skip-build:
description: Set to true to only collect images from an existing build dir
required: false
default: "false"
outputs:
build-dir:
description: Persistent project directory on the runner
value: ${{ steps.locate.outputs.build-dir }}
image-dir:
description: Deploy dir bitbake wrote to
value: ${{ steps.locate.outputs.image-dir }}
images:
description: Newline separated artifact filenames
value: ${{ steps.collect.outputs.images }}
runs:
using: composite
steps:
- name: Sync workspace into persistent build dir
if: ${{ inputs.skip-build != 'true' }}
shell: bash
run: |
set -euo pipefail
SRC="$(pwd)"
DST="${YOCTO_HOME:-$HOME/yocto-build}/$(basename "$GITHUB_REPOSITORY")"
# checkout deletes ignored files, so the build tree must live elsewhere
if [ "$SRC" = "$DST" ]; then
echo "::error::workspace == build dir ($SRC); point YOCTO_HOME somewhere else"
exit 1
fi
echo "syncing $SRC -> $DST"
mkdir -p "$DST"
# .git stays (kas requires a repo); extern/ + build/ only exist in DST,
# so --delete cannot reach them
rsync -a --delete --exclude /build/ --exclude /extern/ ./ "$DST/"
- name: kas build
if: ${{ inputs.skip-build != 'true' }}
shell: bash
run: |
set -euo pipefail
cd "${YOCTO_HOME:-$HOME/yocto-build}/$(basename "$GITHUB_REPOSITORY")"
git config --global --add safe.directory "$PWD" || true
"${YOCTO_HOME:-$HOME/yocto-build}/venv/bin/kas" build
- name: Locate deploy dir
id: locate
shell: bash
run: |
set -euo pipefail
BUILD_DIR="${YOCTO_HOME:-$HOME/yocto-build}/$(basename "$GITHUB_REPOSITORY")"
IMAGE_DIR="$(find "$BUILD_DIR/build" -type d -path "*/tmp/deploy/images/${{ inputs.machine }}" | head -1)"
if [ -z "$IMAGE_DIR" ]; then
echo "::error::no deploy dir for ${{ inputs.machine }} under $BUILD_DIR/build"
exit 1
fi
echo "build-dir=$BUILD_DIR" >> "$GITHUB_OUTPUT"
echo "image-dir=$IMAGE_DIR" >> "$GITHUB_OUTPUT"
- name: Collect artifacts
id: collect
shell: bash
run: |
set -euo pipefail
IMAGE_DIR="${{ steps.locate.outputs.image-dir }}"
shopt -s nullglob
# only the timestamped names: the short ones are hardlinks to the same
# inode and would upload every image twice.
# NOTE: the glob must stay OUTSIDE the quotes - "$DIR/pat*" never expands.
FILES=("${IMAGE_DIR}/${{ inputs.image-prefix }}."*.rootfs-[0-9]*.wic.* \
"${IMAGE_DIR}/${{ inputs.image-prefix }}-"*.rootfs-[0-9]*.wic.*)
if [ "${#FILES[@]}" -eq 0 ]; then
echo "::error::no flashable .wic image for ${{ inputs.machine }} in $IMAGE_DIR"
exit 1
fi
mkdir -p dist
cp -v "${FILES[@]}" dist/
( cd dist && sha256sum * > SHA256SUMS )
ls -lh dist
printf 'images=%s\n' "$(ls dist | tr '\n' ' ')" >> "$GITHUB_OUTPUT"
- name: Summarise
shell: bash
run: |
set -euo pipefail
SUMMARY="${GITHUB_STEP_SUMMARY:-}"
[ -n "$SUMMARY" ] || exit 0
{
echo "## ${{ inputs.image-prefix }} for ${{ inputs.machine }}"
echo
echo "| artifact | sha256 |"
echo "| --- | --- |"
( cd dist && while read -r sum file; do
[ "$file" = SHA256SUMS ] || echo "| \`$file\` | \`${sum:0:16}\` |"
done < SHA256SUMS )
echo
echo "Flash with \`bzcat <image> | sudo dd of=/dev/sdX bs=4M status=progress\`."
echo
echo "Incremental build tree: \`${{ steps.locate.outputs.build-dir }}\`"
} >> "$SUMMARY"
@@ -0,0 +1,37 @@
name: check-build-host
description: >-
Report the build host and fail fast when executables, CPU or disk that the
build needs are missing - a mistake should cost seconds, not hours.
inputs:
tools:
description: Space separated executables that must be on PATH
required: false
default: git python3
min-disk-gb:
description: Warn when the build filesystem has less than this much free
required: false
default: "100"
runs:
using: composite
steps:
- name: Host report
shell: bash
run: |
set -euo pipefail
echo "os : $(. /etc/os-release && echo "$PRETTY_NAME") ($(uname -m), $(uname -r))"
echo "user : $(id -un) home=$HOME"
echo "cpu : $(nproc) cores / $(awk '/MemTotal/{printf "%.0fG", $2/1048576}' /proc/meminfo) ram"
df -h . | tail -1 | sed 's/^/disk : /'
missing=""
for tool in ${{ inputs.tools }}; do
command -v "$tool" >/dev/null 2>&1 || missing="$missing $tool"
done
if [ -n "$missing" ]; then
echo "::error::Host is missing:$missing"
echo "Install them on the runner host, e.g. sudo apt-get install -y $missing"
exit 1
fi
free_gb="$(df --output=avail -BG . | tail -1 | tr -d ' ')"
if [ "$free_gb" -lt "${{ inputs.min-disk-gb }}" ]; then
echo "::warning::only ${free_gb}G free on $(df --output=target . | tail -1); expected >= ${{ inputs.min-disk-gb }}G"
fi
+34
View File
@@ -0,0 +1,34 @@
name: setup-kas
description: Create or reuse the persistent python venv that holds kas.
inputs:
requirement:
description: pip requirement for kas (pinned, e.g. kas==5.5)
required: false
default: kas
outputs:
kas:
description: Absolute path of the kas executable
value: ${{ steps.venv.outputs.kas }}
reused:
description: "true when the venv already existed (warm run)"
value: ${{ steps.venv.outputs.reused }}
runs:
using: composite
steps:
- name: Prepare venv
id: venv
shell: bash
run: |
set -euo pipefail
BUILD_ROOT="${YOCTO_HOME:-$HOME/yocto-build}"
VENV="$BUILD_ROOT/venv"
mkdir -p "$BUILD_ROOT"
if [ -d "$VENV" ]; then
echo "reused=true" >> "$GITHUB_OUTPUT"
else
echo "reused=false" >> "$GITHUB_OUTPUT"
python3 -m venv "$VENV"
fi
"$VENV/bin/pip" install --quiet --upgrade "${{ inputs.requirement }}"
echo "kas=$VENV/bin/kas" >> "$GITHUB_OUTPUT"
echo "kas $($VENV/bin/kas --version) in $VENV"
+76
View File
@@ -0,0 +1,76 @@
name: Build RPi image
# Yocto (kas) build of rpilinux-image. Steps live in .gitea/actions/* so this
# file stays a description of the pipeline, not a shell script.
#
# Host-specific paths come from repo settings (Settings -> Actions ->
# Variables), so the workflow is not tied to one machine:
# YOCTO_HOME persistent build dir on the runner
# Order of precedence for the build dir: this variable, then the runner's
# own YOCTO_HOME (runner.envs), then $HOME/yocto-build.
on:
push:
# main is release protected, ci/* branches are free scratch space
branches: [main, 'ci/*']
pull_request:
# gates merges into main and posts the commit status that branch
# protection waits for
branches: [main]
workflow_dispatch:
inputs:
skip-build:
description: Collect images from the existing build dir instead of rebuilding
type: boolean
default: false
concurrency:
group: yocto-rpi-build
cancel-in-progress: false
permissions:
contents: read
env:
YOCTO_HOME: ${{ vars.YOCTO_HOME }}
MACHINE: raspberrypi4-64
IMAGE_PREFIX: rpilinux-image
defaults:
run:
shell: bash
jobs:
build:
runs-on: yocto
timeout-minutes: 600
steps:
- name: Check out sources
uses: builtin:checkout
- name: Verify build host
uses: $/.gitea/actions/check-build-host
with:
tools: git python3 curl wget rsync gawk xz diffstat chrpath bison flex node
min-disk-gb: "200"
- name: Set up kas
id: kas
uses: $/.gitea/actions/setup-kas
with:
requirement: kas==5.5
- name: Build image
id: image
uses: $/.gitea/actions/build-image
with:
machine: ${{ env.MACHINE }}
image-prefix: ${{ env.IMAGE_PREFIX }}
skip-build: ${{ github.event.inputs.skip-build }}
- name: Upload image
uses: actions/upload-artifact@v4
with:
name: ${{ env.IMAGE_PREFIX }}-${{ env.MACHINE }}
path: dist/
retention-days: 14
+49
View File
@@ -0,0 +1,49 @@
name: Sandbox - ephemeral runner + shared cache
on:
push:
branches: [ci/sandbox]
workflow_dispatch:
jobs:
cargo:
runs-on: sandbox
defaults:
run:
working-directory: sandbox
shell: bash
steps:
- uses: actions/checkout@v4
- name: Where am I?
run: |
echo "runner : ${RUNNER_NAME}"
echo "uid : $(id -u) cgroup: $(cut -d/ -f3 /proc/self/cgroup 2>/dev/null || echo n/a)"
echo "cache : ${ACTIONS_CACHE_URL:-unset}"
# Restore first, so a warm cache also skips the toolchain download.
- name: Cache rust toolchain, registry and target
uses: actions/cache@v4
with:
path: |
~/.cargo
~/.rustup
sandbox/target
key: cargo-${{ hashFiles('sandbox/Cargo.lock') }}
restore-keys: cargo-
- name: Toolchain
run: |
set -euo pipefail
if ! command -v cargo >/dev/null && [ ! -x "$HOME/.cargo/bin/cargo" ]; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
fi
# runner v5.2.2 does not apply GITHUB_PATH to later steps, so use absolute paths
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
"$HOME/.cargo/bin/cargo" --version
- name: Test
run: |
set -o pipefail # without this a failing build looks green
CARGO="$HOME/.cargo/bin/cargo"
echo "HOME=$HOME cargo there: $([ -x "$CARGO" ] && echo yes || echo no)"
"$CARGO" test 2>&1 | tail -22
+5
View File
@@ -0,0 +1,5 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{}
+214
View File
@@ -0,0 +1,214 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "ci-sandbox"
version = "0.1.0"
dependencies = [
"anyhow",
"rand",
"serde",
"serde_json",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "zerocopy"
version = "0.8.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "ci-sandbox"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
rand = "0.8"
[dev-dependencies]
+49
View File
@@ -0,0 +1,49 @@
//! Tiny crate used to exercise CI caching: enough deps to make a warm
//! build obviously faster than a cold one.
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Reading {
pub sensor: String,
pub celsius: f64,
}
pub fn to_fahrenheit(celsius: f64) -> f64 {
celsius * 9.0 / 5.0 + 32.0
}
pub fn parse(json: &str) -> Result<Reading> {
let r: Reading = serde_json::from_str(json)?;
if r.celsius < -273.15 {
bail!("{} C is below absolute zero", r.celsius);
}
Ok(r)
}
pub fn jitter(amount: f64) -> f64 {
use rand::Rng;
rand::thread_rng().gen_range(-amount..=amount)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_units() {
assert!((to_fahrenheit(0.0) - 32.0).abs() < 1e-9);
assert!((to_fahrenheit(100.0) - 212.0).abs() < 1e-9);
}
#[test]
fn parses_reading() {
let r = parse(r#"{"sensor":"attic","celsius":21.5}"#).unwrap();
assert_eq!(r.sensor, "attic");
}
#[test]
fn rejects_impossible_temperature() {
assert!(parse(r#"{"sensor":"x","celsius":-300.0}"#).is_err());
}
}