#![cfg(test)]
use super::*;
#[test]
fn resolve_current_exe_happy_path() {
let exe = resolve_current_exe().unwrap();
let std_exe = std::env::current_exe().unwrap();
if std_exe.exists() {
assert_eq!(exe, std_exe);
} else {
assert_eq!(exe, std::path::PathBuf::from("/proc/self/exe"));
}
}
#[test]
fn errno_name_known_values() {
assert_eq!(errno_name(libc::EPERM), Some("EPERM"));
assert_eq!(errno_name(libc::ENOENT), Some("ENOENT"));
assert_eq!(errno_name(libc::EINVAL), Some("EINVAL"));
assert_eq!(errno_name(libc::ENOMEM), Some("ENOMEM"));
assert_eq!(errno_name(libc::EBUSY), Some("EBUSY"));
assert_eq!(errno_name(libc::EACCES), Some("EACCES"));
assert_eq!(errno_name(libc::EAGAIN), Some("EAGAIN"));
assert_eq!(errno_name(libc::ENOSYS), Some("ENOSYS"));
assert_eq!(errno_name(libc::ETIMEDOUT), Some("ETIMEDOUT"));
}
#[test]
fn errno_name_unknown() {
assert_eq!(errno_name(9999), None);
assert_eq!(errno_name(0), None);
assert_eq!(errno_name(-1), None);
}
#[test]
fn find_kernel_preserves_untracked_cache_entries() {
use crate::cache::{CacheArtifacts, CacheDir, KernelMetadata, KernelSource};
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _kernel_guard = EnvVarGuard::remove("KTSTR_KERNEL");
let tmp = tempfile::TempDir::new().unwrap();
let cache_root = tmp.path().join("cache");
let _cache_guard = EnvVarGuard::set("KTSTR_CACHE_DIR", &cache_root);
let cache = CacheDir::with_root(cache_root.clone());
let src_dir = tempfile::TempDir::new().unwrap();
let image = src_dir.path().join("bzImage");
std::fs::write(&image, b"fake kernel image").unwrap();
let meta = KernelMetadata::new(
KernelSource::Tarball,
"x86_64",
"bzImage",
"2026-04-12T10:00:00Z",
)
.with_version("6.14.2");
assert!(
meta.ktstr_kconfig_hash.is_none(),
"test fixture must have no recorded kconfig hash to exercise the \
Untracked branch of kconfig_status"
);
let entry = cache
.store("untracked-entry", &CacheArtifacts::new(&image), &meta)
.unwrap();
let expected_image = entry.image_path();
assert!(
expected_image.exists(),
"fixture image must exist on disk so find_kernel's image.exists() \
check passes — got {expected_image:?}"
);
let resolved = find_kernel().unwrap();
assert_eq!(
resolved,
Some(expected_image),
"find_kernel dropped an Untracked cache entry — the kconfig-hash \
filter at lib.rs must treat `Untracked` as keep, not stale"
);
}
#[test]
fn find_kernel_skips_stale_cache_entry() {
use crate::cache::{CacheArtifacts, CacheDir, KernelMetadata, KernelSource};
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _kernel_guard = EnvVarGuard::remove("KTSTR_KERNEL");
let tmp = tempfile::TempDir::new().unwrap();
let cache_root = tmp.path().join("cache");
let _cache_guard = EnvVarGuard::set("KTSTR_CACHE_DIR", &cache_root);
let current_hash = crate::kconfig_hash();
let stale_hash = format!("{current_hash}-stale");
let cache = CacheDir::with_root(cache_root.clone());
let src_dir = tempfile::TempDir::new().unwrap();
let current_image = src_dir.path().join("current.bzImage");
std::fs::write(¤t_image, b"current kernel image").unwrap();
let current_meta = KernelMetadata::new(
KernelSource::Tarball,
"x86_64",
"current.bzImage",
"2026-04-01T00:00:00Z",
)
.with_version("6.14.2")
.with_ktstr_kconfig_hash(current_hash.clone());
let current_entry = cache
.store(
"current-entry",
&CacheArtifacts::new(¤t_image),
¤t_meta,
)
.unwrap();
let stale_image = src_dir.path().join("stale.bzImage");
std::fs::write(&stale_image, b"stale kernel image").unwrap();
let stale_meta = KernelMetadata::new(
KernelSource::Tarball,
"x86_64",
"stale.bzImage",
"2026-04-20T00:00:00Z",
)
.with_version("6.14.3")
.with_ktstr_kconfig_hash(stale_hash);
cache
.store(
"stale-entry",
&CacheArtifacts::new(&stale_image),
&stale_meta,
)
.unwrap();
let resolved = find_kernel().unwrap();
assert_eq!(
resolved,
Some(current_entry.image_path()),
"find_kernel must skip the newer stale entry and return the \
current-hash entry — regression of the KconfigStatus::Stale \
skip branch in find_kernel's cache-scan loop",
);
}
#[test]
fn worker_ready_marker_path_format_is_stable() {
use crate::worker_ready::{WORKER_READY_MARKER_PREFIX, worker_ready_marker_path};
assert_eq!(WORKER_READY_MARKER_PREFIX, "/tmp/ktstr-worker-ready-");
assert_eq!(worker_ready_marker_path(0), "/tmp/ktstr-worker-ready-0");
assert_eq!(
worker_ready_marker_path(12345),
"/tmp/ktstr-worker-ready-12345"
);
assert_eq!(
worker_ready_marker_path(u32::MAX),
"/tmp/ktstr-worker-ready-4294967295"
);
}
#[test]
fn ktstr_kernel_env_round_trips_absolute_path() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _lock = lock_env();
let tmp = tempfile::TempDir::new().unwrap();
let canonical = std::fs::canonicalize(tmp.path()).unwrap();
let _guard = EnvVarGuard::set(KTSTR_KERNEL_ENV, &canonical);
let read_back = ktstr_kernel_env().expect("env is set");
assert_eq!(
std::path::PathBuf::from(&read_back),
canonical,
"writer-reader round-trip must preserve the exact path; \
drift between parent's canonicalize output and child's \
ktstr_kernel_env read breaks every downstream resolver",
);
}
#[test]
fn ktstr_kernel_env_unset_is_none() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _lock = lock_env();
let _guard = EnvVarGuard::remove(KTSTR_KERNEL_ENV);
assert!(
ktstr_kernel_env().is_none(),
"unset KTSTR_KERNEL must read as None so fallback resolvers activate",
);
}
#[test]
fn ktstr_kernel_env_empty_is_none() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _lock = lock_env();
let _guard = EnvVarGuard::set(KTSTR_KERNEL_ENV, "");
assert!(
ktstr_kernel_env().is_none(),
"empty KTSTR_KERNEL must collapse to None; CI flows routinely \
pass empty strings for unused variables",
);
}
#[test]
fn ktstr_kernel_env_whitespace_is_none() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _lock = lock_env();
let _guard = EnvVarGuard::set(KTSTR_KERNEL_ENV, " \t\n ");
assert!(
ktstr_kernel_env().is_none(),
"whitespace-only KTSTR_KERNEL must collapse to None via trim \
+ empty-filter; no caller parses a whitespace-only value \
meaningfully",
);
}
#[test]
fn ktstr_kernel_env_trims_surrounding_whitespace() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _lock = lock_env();
let _guard = EnvVarGuard::set(KTSTR_KERNEL_ENV, " ../linux ");
let read_back = ktstr_kernel_env().expect("env is set");
assert_eq!(
read_back, "../linux",
"surrounding whitespace must be trimmed but the interior \
preserved verbatim",
);
}
#[test]
fn find_kernel_inverted_range_env_surfaces_swap_diagnostic() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let tmp = tempfile::TempDir::new().unwrap();
let cache_root = tmp.path().join("cache");
let _cache_guard = EnvVarGuard::set("KTSTR_CACHE_DIR", &cache_root);
let _kernel_guard = EnvVarGuard::set(KTSTR_KERNEL_ENV, "6.16..6.12");
let err = find_kernel().expect_err("inverted range must error");
let msg = format!("{err:#}");
assert!(
msg.contains("inverted kernel range"),
"validate() diagnostic must surface ahead of the generic \
env-form bail; got: {msg}",
);
assert!(
msg.contains("6.12..6.16"),
"swap suggestion must appear in the error; got: {msg}",
);
assert!(
!msg.contains("not supported in env-var form"),
"validate() must short-circuit before the generic bail; got: {msg}",
);
}
#[test]
fn ktstr_kernel_env_constant_is_literal() {
assert_eq!(KTSTR_KERNEL_ENV, "KTSTR_KERNEL");
}
#[test]
fn cache_key_suffix_with_extra_none_matches_bare_suffix() {
assert_eq!(cache_key_suffix_with_extra(None), cache_key_suffix());
}
#[test]
fn cache_key_suffix_with_extra_some_has_two_segment_shape() {
let suffix = cache_key_suffix_with_extra(Some("CONFIG_FOO=y\n"));
let baked = kconfig_hash();
assert!(
suffix.starts_with(&baked),
"Some suffix must start with bare baked-in hash {baked:?}, got {suffix:?}"
);
let after = &suffix[baked.len()..];
assert!(
after.starts_with("-xkc"),
"after the baked-in segment, the next bytes must be `-xkc`, got {after:?}"
);
let extra_segment = &after["-xkc".len()..];
assert_eq!(
extra_segment.len(),
8,
"extra-hash segment must be 8 hex chars, got {extra_segment:?}"
);
assert!(
extra_segment.chars().all(|c| c.is_ascii_hexdigit()),
"extra-hash segment must be lowercase hex, got {extra_segment:?}"
);
}
#[test]
fn cache_key_suffix_with_extra_some_differs_from_bare_suffix() {
let suffix = cache_key_suffix_with_extra(Some("CONFIG_FOO=y\n"));
assert_ne!(suffix, cache_key_suffix());
let baked = kconfig_hash();
let after = &suffix[baked.len()..];
assert_eq!(
after.len(),
"-xkc".len() + 8,
"suffix tail must be `-xkc{{8 hex chars}}`, got {after:?}"
);
}
#[test]
fn cache_key_suffix_with_extra_matches_production_format_string() {
let extra = "CONFIG_FOO=y\n";
let baked = kconfig_hash();
let extra_h = extra_kconfig_hash(extra);
let helper = cache_key_suffix_with_extra(Some(extra));
let expected = format!("{baked}-xkc{extra_h}");
assert_eq!(
helper, expected,
"helper output must match production format `{{baked}}-xkc{{extra}}` \
(cargo-ktstr.rs builds the tarball cache key with the same shape \
via `{{ver}}-tarball-{{arch}}-kc{{cache_key_suffix_with_extra(...)}}`)"
);
}
#[test]
fn cache_key_suffix_with_extra_empty_differs_from_none() {
let with_empty = cache_key_suffix_with_extra(Some(""));
let without = cache_key_suffix_with_extra(None);
assert_ne!(with_empty, without);
}
#[test]
fn cache_key_suffix_with_extra_same_content_same_suffix() {
let extra = "CONFIG_FOO=y\nCONFIG_BAR=n\n";
let a = cache_key_suffix_with_extra(Some(extra));
let b = cache_key_suffix_with_extra(Some(extra));
assert_eq!(a, b, "same fragment must produce same suffix");
}
#[test]
fn cache_key_suffix_with_extra_different_content_different_suffix() {
let a = cache_key_suffix_with_extra(Some("CONFIG_FOO=y\n"));
let b = cache_key_suffix_with_extra(Some("CONFIG_FOO=n\n"));
assert_ne!(a, b, "distinct fragments must produce distinct suffixes");
let baked = kconfig_hash();
assert!(a.starts_with(&baked) && b.starts_with(&baked));
}
#[test]
fn extra_kconfig_hash_is_8_hex_chars() {
for content in ["", "CONFIG_X=y\n", "# CONFIG_BPF is not set\n"] {
let h = extra_kconfig_hash(content);
assert_eq!(h.len(), 8, "expected 8 hex chars, got {h}");
assert!(
h.chars().all(|c| c.is_ascii_hexdigit()),
"expected lowercase hex, got {h}",
);
}
}
#[test]
fn extra_kconfig_hash_is_byte_sensitive() {
let lf = "CONFIG_FOO=y\n";
let crlf = "CONFIG_FOO=y\r\n";
assert_ne!(
extra_kconfig_hash(lf),
extra_kconfig_hash(crlf),
"CRLF and LF must hash differently — raw-byte hashing is intentional for byte-deterministic discrimination"
);
let with_comment = "# user note\nCONFIG_FOO=y\n";
let without_comment = "CONFIG_FOO=y\n";
assert_ne!(
extra_kconfig_hash(with_comment),
extra_kconfig_hash(without_comment),
"comments must affect the hash — raw-byte hashing is intentional"
);
}
#[test]
fn cache_key_suffix_with_extra_crlf_differs_from_lf() {
let lf = "CONFIG_FOO=y\n";
let crlf = "CONFIG_FOO=y\r\n";
let lf_suffix = cache_key_suffix_with_extra(Some(lf));
let crlf_suffix = cache_key_suffix_with_extra(Some(crlf));
assert_ne!(
lf_suffix, crlf_suffix,
"LF and CRLF user fragments must produce distinct cache \
keys (no CRLF canonicalization). A Windows operator and \
a Unix operator who supplied 'the same' fragment land at \
distinct cache slots; this is the documented \
byte-deterministic cache contract."
);
}
#[test]
fn legacy_bare_suffix_is_proper_prefix_of_extras_suffix() {
let bare = cache_key_suffix();
let extras = cache_key_suffix_with_extra(Some("CONFIG_FOO=y\n"));
assert!(
extras.starts_with(&bare),
"extras suffix must extend bare suffix — bare={bare:?} extras={extras:?}",
);
assert!(
extras.len() > bare.len(),
"extras suffix must be strictly longer than bare suffix",
);
assert_ne!(
bare, extras,
"structural distinction: legacy entries (key ending in `kc{{bare}}`) cannot \
collide with extras entries (key ending in `kc{{bare}}-xkc{{...}}`) on \
exact-match cache lookup."
);
}
#[test]
fn extra_kconfig_hash_empty_is_crc32_zero_not_sentinel() {
let empty = extra_kconfig_hash("");
assert_eq!(
empty, "00000000",
"CRC32 of zero bytes is 0x00000000 by spec. This value is \
a legitimate hash output, not a sentinel — readers that \
want to detect 'no extras' must check the metadata's \
`extra_kconfig_hash: Option<String>` for None, not for \
this string."
);
}
#[test]
fn merge_user_extra_appears_after_baked_in_for_conflict_resolution() {
let user = "# CONFIG_BPF is not set\n";
let merged = merge_kconfig_fragments(EMBEDDED_KCONFIG, Some(user));
let baked_pos = merged
.find("CONFIG_BPF=y")
.expect("baked-in CONFIG_BPF=y must be present in merged fragment");
let user_pos = merged
.find("# CONFIG_BPF is not set")
.expect("user override must be present in merged fragment");
assert!(
baked_pos < user_pos,
"baked-in line must appear BEFORE user override so kbuild's \
last-wins rule (confdata.c::conf_read_simple) keeps the user \
value (baked_pos={baked_pos}, user_pos={user_pos})",
);
let mut last = None;
for line in merged.lines() {
let trimmed = line.trim();
if trimmed == "CONFIG_BPF=y" || trimmed == "# CONFIG_BPF is not set" {
last = Some(trimmed.to_string());
}
}
assert_eq!(
last.as_deref(),
Some("# CONFIG_BPF is not set"),
"the LAST occurrence of CONFIG_BPF in the merged content must \
be the user override; kbuild's `conf_read_simple` walks lines \
top-to-bottom and keeps the last assignment, so the user line \
determines the final config value",
);
}
#[test]
fn merge_user_extra_combines_with_baked_in_for_disjoint_symbols() {
let novel = "CONFIG_KTSTR_TEST_NOVEL_SYMBOL_FOR_MERGE_TEST=y\n";
assert!(
!EMBEDDED_KCONFIG.contains("CONFIG_KTSTR_TEST_NOVEL_SYMBOL_FOR_MERGE_TEST"),
"test fixture must use a symbol absent from EMBEDDED_KCONFIG"
);
let merged = merge_kconfig_fragments(EMBEDDED_KCONFIG, Some(novel));
assert!(
merged.contains("CONFIG_KTSTR_TEST_NOVEL_SYMBOL_FOR_MERGE_TEST=y"),
"user-novel line must appear in merged fragment",
);
assert!(
merged.contains("CONFIG_BPF=y"),
"baked-in CONFIG_BPF=y must still appear in merged fragment",
);
}
#[test]
fn merge_kconfig_fragments_none_returns_baked_unchanged() {
let merged = merge_kconfig_fragments(EMBEDDED_KCONFIG, None);
assert_eq!(
merged, EMBEDDED_KCONFIG,
"merge with None must return the baked fragment unchanged"
);
}
#[test]
fn merge_kconfig_fragments_some_empty_appends_separator_newline() {
let merged = merge_kconfig_fragments(EMBEDDED_KCONFIG, Some(""));
let expected = format!("{EMBEDDED_KCONFIG}\n");
assert_eq!(merged, expected);
}
#[test]
fn merge_kconfig_fragments_user_line_appears_after_baked_for_overrides() {
let baked = "CONFIG_FOO=y\nCONFIG_BAR=m\n";
let user = "CONFIG_FOO=n\n";
let merged = merge_kconfig_fragments(baked, Some(user)).into_owned();
let baked_idx = merged
.find("CONFIG_FOO=y")
.expect("baked CONFIG_FOO=y must be present");
let user_idx = merged
.find("CONFIG_FOO=n")
.expect("user CONFIG_FOO=n override must be present");
assert!(
baked_idx < user_idx,
"baked-in CONFIG_FOO=y must precede user override CONFIG_FOO=n so \
kbuild's last-wins rule picks the user value: {merged}"
);
}
#[test]
fn merge_kconfig_fragments_disjoint_symbols_both_present() {
let baked = "CONFIG_FOO=y\n";
let user = "CONFIG_DISJOINT_TEST_SYMBOL=m\n";
let merged = merge_kconfig_fragments(baked, Some(user)).into_owned();
assert!(
merged.contains("CONFIG_FOO=y"),
"baked symbol must survive merge: {merged}"
);
assert!(
merged.contains("CONFIG_DISJOINT_TEST_SYMBOL=m"),
"user-added disjoint symbol must survive merge: {merged}"
);
}
#[test]
fn cache_lookup_same_extras_hits_planted_entry() {
use crate::cache::{CacheArtifacts, CacheDir, KernelMetadata, KernelSource};
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _kernel_guard = EnvVarGuard::remove("KTSTR_KERNEL");
let tmp = tempfile::TempDir::new().unwrap();
let cache_root = tmp.path().join("cache");
let _cache_guard = EnvVarGuard::set("KTSTR_CACHE_DIR", &cache_root);
let extra = "CONFIG_KTSTR_CACHE_ROUNDTRIP_TEST_A=y\n";
let extra_hash = extra_kconfig_hash(extra);
let cache_key = format!("test-roundtrip-{}-xkc{}", kconfig_hash(), extra_hash);
let cache = CacheDir::with_root(cache_root.clone());
let src_dir = tempfile::TempDir::new().unwrap();
let image = src_dir.path().join("bzImage");
std::fs::write(&image, b"fake kernel image").unwrap();
let meta = KernelMetadata::new(
KernelSource::Tarball,
"x86_64",
"bzImage",
"2026-04-12T10:00:00Z",
)
.with_extra_kconfig_hash(extra_hash.clone());
cache
.store(&cache_key, &CacheArtifacts::new(&image), &meta)
.unwrap();
let hit = cache.lookup(&cache_key);
assert!(
hit.is_some(),
"cache lookup with same extras must return planted entry; \
cache_key={cache_key}"
);
assert_eq!(
hit.as_ref().unwrap().metadata.extra_kconfig_hash.as_deref(),
Some(extra_hash.as_str()),
"retrieved entry must carry the planted extra_kconfig_hash"
);
}
#[test]
fn cache_lookup_different_extras_misses_planted_entry() {
use crate::cache::{CacheArtifacts, CacheDir, KernelMetadata, KernelSource};
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _kernel_guard = EnvVarGuard::remove("KTSTR_KERNEL");
let tmp = tempfile::TempDir::new().unwrap();
let cache_root = tmp.path().join("cache");
let _cache_guard = EnvVarGuard::set("KTSTR_CACHE_DIR", &cache_root);
let extra_a = "CONFIG_KTSTR_CACHE_DISCRIMINATE_A=y\n";
let extra_b = "CONFIG_KTSTR_CACHE_DISCRIMINATE_B=y\n";
let key_a = format!(
"test-disc-{}-xkc{}",
kconfig_hash(),
extra_kconfig_hash(extra_a)
);
let key_b = format!(
"test-disc-{}-xkc{}",
kconfig_hash(),
extra_kconfig_hash(extra_b)
);
assert_ne!(
key_a, key_b,
"extras A and B must produce distinct cache keys (precondition)"
);
let cache = CacheDir::with_root(cache_root.clone());
let src_dir = tempfile::TempDir::new().unwrap();
let image = src_dir.path().join("bzImage");
std::fs::write(&image, b"fake kernel image A").unwrap();
let meta = KernelMetadata::new(
KernelSource::Tarball,
"x86_64",
"bzImage",
"2026-04-12T10:00:00Z",
)
.with_extra_kconfig_hash(extra_kconfig_hash(extra_a));
cache
.store(&key_a, &CacheArtifacts::new(&image), &meta)
.unwrap();
let hit_b = cache.lookup(&key_b);
assert!(
hit_b.is_none(),
"lookup with extras=B's key must miss when only extras=A is planted; \
key_a={key_a} key_b={key_b}"
);
assert!(
cache.lookup(&key_a).is_some(),
"planted entry must be reachable via its own key"
);
}
#[test]
fn cache_lookup_bare_and_extras_keys_segregated() {
use crate::cache::{CacheArtifacts, CacheDir, KernelMetadata, KernelSource};
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _kernel_guard = EnvVarGuard::remove("KTSTR_KERNEL");
let tmp = tempfile::TempDir::new().unwrap();
let cache_root = tmp.path().join("cache");
let _cache_guard = EnvVarGuard::set("KTSTR_CACHE_DIR", &cache_root);
let baked = kconfig_hash();
let extra = "CONFIG_KTSTR_CACHE_SEGREGATE=y\n";
let bare_key = format!("test-seg-{baked}");
let extras_key = format!("test-seg-{baked}-xkc{}", extra_kconfig_hash(extra));
assert_ne!(
bare_key, extras_key,
"bare and extras-suffix keys must be distinct (precondition)"
);
let cache = CacheDir::with_root(cache_root.clone());
let src_dir = tempfile::TempDir::new().unwrap();
let image = src_dir.path().join("bzImage");
std::fs::write(&image, b"bare kernel").unwrap();
let bare_meta = KernelMetadata::new(
KernelSource::Tarball,
"x86_64",
"bzImage",
"2026-04-12T10:00:00Z",
);
assert!(
bare_meta.extra_kconfig_hash.is_none(),
"bare entry fixture must not carry extras hash"
);
cache
.store(&bare_key, &CacheArtifacts::new(&image), &bare_meta)
.unwrap();
assert!(
cache.lookup(&extras_key).is_none(),
"extras lookup must NOT serve the bare entry — operator built with \
--extra-kconfig and would silently get a kernel without their \
user symbols if this regressed"
);
let extras_image = src_dir.path().join("bzImage-extras");
std::fs::write(&extras_image, b"extras kernel").unwrap();
let extras_meta = KernelMetadata::new(
KernelSource::Tarball,
"x86_64",
"bzImage",
"2026-04-13T10:00:00Z",
)
.with_extra_kconfig_hash(extra_kconfig_hash(extra));
cache
.store(
&extras_key,
&CacheArtifacts::new(&extras_image),
&extras_meta,
)
.unwrap();
let bare_hit = cache.lookup(&bare_key).expect("bare entry");
let extras_hit = cache.lookup(&extras_key).expect("extras entry");
assert!(
bare_hit.metadata.extra_kconfig_hash.is_none(),
"bare entry must report None extras hash"
);
assert!(
extras_hit.metadata.extra_kconfig_hash.is_some(),
"extras entry must report Some(hash)"
);
}
#[test]
fn cache_entry_has_extra_kconfig_reflects_metadata() {
use crate::cache::{CacheArtifacts, CacheDir, KernelMetadata, KernelSource};
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _kernel_guard = EnvVarGuard::remove("KTSTR_KERNEL");
let tmp = tempfile::TempDir::new().unwrap();
let cache_root = tmp.path().join("cache");
let _cache_guard = EnvVarGuard::set("KTSTR_CACHE_DIR", &cache_root);
let cache = CacheDir::with_root(cache_root.clone());
let src_dir = tempfile::TempDir::new().unwrap();
let image = src_dir.path().join("bzImage");
std::fs::write(&image, b"img").unwrap();
let bare_meta = KernelMetadata::new(
KernelSource::Tarball,
"x86_64",
"bzImage",
"2026-04-12T10:00:00Z",
);
let bare = cache
.store("test-has-bare", &CacheArtifacts::new(&image), &bare_meta)
.unwrap();
assert!(
!bare.has_extra_kconfig(),
"bare entry (extra_kconfig_hash = None) must report has_extra_kconfig() = false"
);
let extras_meta = KernelMetadata::new(
KernelSource::Tarball,
"x86_64",
"bzImage",
"2026-04-13T10:00:00Z",
)
.with_extra_kconfig_hash("deadbeef");
let extras = cache
.store(
"test-has-extras",
&CacheArtifacts::new(&image),
&extras_meta,
)
.unwrap();
assert!(
extras.has_extra_kconfig(),
"entry with extra_kconfig_hash = Some(...) must report has_extra_kconfig() = true"
);
}