# kache-e2e fixture: minimal C project built with make.
#
# C/C++ caching is live: the Makefile's `-c` compile step
# (`cc -O0 -g -Isrc -c src/foo.c -o build/foo.o`) is cache-eligible;
# the link step (`cc build/foo.o -o build/foo`) is link-mode and
# passes through. So each build produces exactly one cacheable
# event — the object compile.

name = "e2e-c-hello"
tags = ["suite:e2e", "lang:cc"]

[source]
kind = "fixture"
path = "source"

[env]
CC = "$KACHE cc"

[commands]
build = "make"
clean = "make clean"

[verify]
run = "./build/foo"
expected_stdout_contains = ["hello from bar"]

# Differential test: the object a cache hit restores must be
# byte-for-byte identical to the one the real compiler produced on the
# cold build. Runtime verify only proves the program behaves right; a
# subtly wrong .o that still prints "hello from bar" would slip past it.
[diff]
artifacts = ["build/foo.o"]

# Invalidation test: drives the relocate-modified phase. The project is
# relocated AND bar.h's greeting is edited — changing the macro value,
# so the preprocessor expansion (and thus the cache key) diverges. The
# rebuild MUST miss; a hit here would be a stale-restore bug.
[modify]
file = "src/bar.h"
find = "hello from bar"
replace = "edited greeting"

# Cold: the object compile is a miss → one entry lands in the cache.
# Op-counts: kache runs the preprocessor once (cache key) and, on the
# miss, the compiler once.
[assertions.cold]
min_entries_after = 1
max_compiler_runs = 1
max_preprocessor_runs = 1
# The compiler probe (`cc --version` / `cc -###`) runs once for the
# build: a single object compile, a single probe.
max_probe_runs = 1

# Warm: clean wipes build/, the rebuild's compile hits the cache.
# `max_compiler_runs = 0` is the deterministic proof the hit skipped
# the compile entirely. The preprocessor still runs once (it derives
# the key) — that's the overhead a future direct mode would remove.
[assertions.warm]
min_hits = 1
max_compiler_runs = 0
max_preprocessor_runs = 1
# Probe memoized on disk from the cold build — the warm rebuild reads
# the record instead of re-running `cc --version` / `cc -###`.
max_probe_runs = 0

# Noop: `make` sees build/foo.o + build/foo up to date and does
# nothing. No `cc` invocation, no kache event. make emits no
# "Compiling"-style marker, so the contract is just "the phase
# runs clean" — `should_not_recompile = false` (no marker to
# assert against; make's own incrementality is what's exercised).
[assertions.noop]
should_not_recompile = false

# Relocate: same source built at a different absolute path with the
# shared cache. The cc cache key is the preprocessor expansion
# (`cc -E -P`, line markers stripped) — path-independent by
# construction — so the relocated compile HITS. This is the C/C++
# parallel of multi-dep's relocate contract.
[assertions.relocate]
min_hits = 1
max_compiler_runs = 0
max_preprocessor_runs = 1

# Relocate-modified: relocated AND edited (see [modify]). The inverse
# of relocate — the edited source MUST miss and recompile. `min_misses`
# is the headline; the op-counts confirm exactly one compile ran.
[assertions.relocate-modified]
min_misses = 1
max_compiler_runs = 1
max_preprocessor_runs = 1
