use std::path::Path;
use anyhow::{Context, Result, bail};
use super::super::kernel_cmd::EMBEDDED_KCONFIG;
use super::make::run_make;
pub(super) fn all_fragment_lines_present(fragment: &str, config: &str) -> bool {
let existing: std::collections::HashSet<&str> = config.lines().map(str::trim).collect();
fragment
.lines()
.map(str::trim)
.filter(|t| is_kconfig_semantic_line(t))
.all(|t| existing.contains(t))
}
pub(super) fn is_kconfig_semantic_line(trimmed: &str) -> bool {
if trimmed.is_empty() {
return false;
}
if let Some(rest) = trimmed.strip_prefix('#') {
let rest = rest.trim_start();
return rest.starts_with("CONFIG_") && rest.ends_with(" is not set");
}
true
}
pub fn read_extra_kconfig(path: &Path, cli_label: &str) -> std::result::Result<String, String> {
let display = path.display();
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(e) => {
let msg = match e.kind() {
std::io::ErrorKind::NotFound => {
format!(
"--extra-kconfig {display}: file not found; check the \
path spelling and that the file exists"
)
}
std::io::ErrorKind::IsADirectory => {
format!(
"--extra-kconfig {display}: is a directory; pass a \
regular file containing kconfig fragment lines"
)
}
std::io::ErrorKind::PermissionDenied => {
format!(
"--extra-kconfig {display}: permission denied; check \
file ownership and mode (the kconfig fragment must \
be readable by the current user)"
)
}
_ => format!("--extra-kconfig {display}: {e}"),
};
return Err(msg);
}
};
if bytes.is_empty() {
let path_str = display.to_string();
tracing::warn!(
cli_label = cli_label,
path = %path_str,
"--extra-kconfig file is empty; the build will land at a \
distinct cache slot but no user symbols will merge into the \
configuration. Did you mean to populate {path_str} with \
CONFIG_X=... lines?",
);
}
String::from_utf8(bytes).map_err(|_| {
format!(
"--extra-kconfig {display}: file is not valid UTF-8; kconfig \
fragments must be ASCII text"
)
})
}
pub fn append_extra_kconfig_suffix(cache_key: &mut String, extra: Option<&str>) {
if let Some(content) = extra {
cache_key.push_str("-xkc");
cache_key.push_str(&crate::extra_kconfig_hash(content));
}
}
pub(super) fn warn_extra_kconfig_overrides_baked_in(extra: &str, cli_label: &str) {
let mut baked: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
for raw in EMBEDDED_KCONFIG.lines() {
let line = raw.trim();
if let Some(sym) = parse_kconfig_symbol(line) {
baked.insert(sym, line);
}
}
for raw in extra.lines() {
let line = raw.trim();
let Some(user_sym) = parse_kconfig_symbol(line) else {
continue;
};
let Some(baked_line) = baked.get(user_sym) else {
continue;
};
if *baked_line == line {
continue;
}
tracing::warn!(
cli_label = cli_label,
symbol = user_sym,
was = *baked_line,
now = line,
"--extra-kconfig overrides baked-in {user_sym} (was {}, now {})",
render_kconfig_value(baked_line, user_sym),
render_kconfig_value(line, user_sym),
);
}
}
pub(super) fn parse_kconfig_symbol(line: &str) -> Option<&str> {
if let Some(rest) = line.strip_prefix("# ")
&& let Some(sym) = rest.strip_suffix(" is not set")
&& sym.starts_with("CONFIG_")
{
return Some(sym);
}
if line.starts_with("CONFIG_")
&& let Some((sym, _)) = line.split_once('=')
{
return Some(sym);
}
None
}
pub(super) fn render_kconfig_value<'a>(line: &'a str, sym: &str) -> &'a str {
if let Some(value) = line.strip_prefix(sym)
&& value.starts_with('=')
{
return value;
}
if line == format!("# {sym} is not set") {
return "is not set";
}
line
}
pub(super) fn warn_dropped_extra_kconfig_lines(kernel_dir: &Path, extra: &str, cli_label: &str) {
let config_path = kernel_dir.join(".config");
let Ok(final_config) = std::fs::read_to_string(&config_path) else {
return;
};
let final_lines: std::collections::HashSet<&str> =
final_config.lines().map(str::trim).collect();
for raw_line in extra.lines() {
let line = raw_line.trim();
if line.is_empty() {
continue;
}
let is_disable_directive = line.starts_with("# CONFIG_") && line.ends_with(" is not set");
let is_assignment = line.starts_with("CONFIG_") && line.contains('=');
if !is_disable_directive && !is_assignment {
continue;
}
if final_lines.contains(line) {
continue;
}
let sym_name = if is_assignment {
line.split('=').next().unwrap_or(line)
} else {
line.trim_start_matches("# ")
.trim_end_matches(" is not set")
};
let final_state = final_config
.lines()
.find(|l| {
let t = l.trim();
t.starts_with(&format!("{sym_name}=")) || t == format!("# {sym_name} is not set")
})
.map(str::trim)
.unwrap_or(
"(absent — symbol not present in .config; likely \
disabled or unrecognized by kconfig)",
);
tracing::warn!(
cli_label = cli_label,
requested = line,
final_state = final_state,
"--extra-kconfig line did not survive `make olddefconfig` (likely an \
unmet dependency or unrecognized symbol)"
);
}
}
pub fn configure_kernel(kernel_dir: &Path, fragment: &str) -> Result<()> {
let config_path = kernel_dir.join(".config");
if !config_path.exists() {
run_make(kernel_dir, &["defconfig"])?;
}
let config_content = std::fs::read_to_string(&config_path)?;
if all_fragment_lines_present(fragment, &config_content) {
return Ok(());
}
let mut config = std::fs::OpenOptions::new()
.append(true)
.open(&config_path)?;
std::io::Write::write_all(&mut config, fragment.as_bytes())?;
run_make(kernel_dir, &["olddefconfig"])?;
Ok(())
}
pub fn has_sched_ext(kernel_dir: &std::path::Path) -> bool {
let config = kernel_dir.join(".config");
std::fs::read_to_string(config)
.map(|s| s.lines().any(|l| l == "CONFIG_SCHED_CLASS_EXT=y"))
.unwrap_or(false)
}
const VALIDATE_CONFIG_CRITICAL: &[(&str, &str)] = &[
(
"CONFIG_SCHED_CLASS_EXT",
"depends on CONFIG_DEBUG_INFO_BTF — ensure pahole >= 1.16 is installed (dwarves package)",
),
(
"CONFIG_DEBUG_INFO_BTF",
"requires pahole >= 1.16 (dwarves package)",
),
("CONFIG_BPF_SYSCALL", "required for BPF program loading"),
(
"CONFIG_FTRACE",
"gate for all tracing infrastructure — arm64 defconfig disables it, \
silently dropping KPROBE_EVENTS and BPF_EVENTS",
),
(
"CONFIG_KPROBE_EVENTS",
"required for ktstr probe pipeline (depends on FTRACE + KPROBES)",
),
(
"CONFIG_BPF_EVENTS",
"required for BPF kprobe/tracepoint attachment (depends on KPROBE_EVENTS + PERF_EVENTS)",
),
(
"CONFIG_VIRTIO_BLK",
"required for ktstr DiskConfig — backs /dev/vd* in the guest. Depends on VIRTIO + BLOCK; \
a user --extra-kconfig that strips BLOCK would silently disable this and disk-IO WorkTypes \
would fail with a confusing 'no /dev/vda' inside the guest instead of a clear build error",
),
];
pub fn validate_kernel_config(kernel_dir: &std::path::Path) -> Result<()> {
let config_path = kernel_dir.join(".config");
let config = std::fs::read_to_string(&config_path)
.with_context(|| format!("read {}", config_path.display()))?;
let existing: std::collections::HashSet<&str> = config.lines().map(str::trim).collect();
let mut missing = Vec::new();
for &(option, hint) in VALIDATE_CONFIG_CRITICAL {
let enabled = format!("{option}=y");
if !existing.contains(enabled.as_str()) {
missing.push((option, hint));
}
}
if !missing.is_empty() {
let mut msg =
String::from("kernel build completed but critical config options are missing:\n");
for (option, hint) in &missing {
msg.push_str(&format!(" {option} not set — {hint}\n"));
}
msg.push_str(
"\nThe kernel build system silently disables options whose dependencies \
are not met. Install missing tools and rebuild with --force.",
);
bail!("{msg}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_has_sched_ext_present() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(
tmp.path().join(".config"),
"CONFIG_SOMETHING=y\nCONFIG_SCHED_CLASS_EXT=y\nCONFIG_OTHER=m\n",
)
.unwrap();
assert!(has_sched_ext(tmp.path()));
}
#[test]
fn cli_has_sched_ext_absent() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(
tmp.path().join(".config"),
"CONFIG_SOMETHING=y\nCONFIG_OTHER=m\n",
)
.unwrap();
assert!(!has_sched_ext(tmp.path()));
}
#[test]
fn cli_has_sched_ext_module_not_builtin() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join(".config"), "CONFIG_SCHED_CLASS_EXT=m\n").unwrap();
assert!(!has_sched_ext(tmp.path()));
}
#[test]
fn cli_has_sched_ext_commented_out() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(
tmp.path().join(".config"),
"# CONFIG_SCHED_CLASS_EXT is not set\n",
)
.unwrap();
assert!(!has_sched_ext(tmp.path()));
}
#[test]
fn cli_has_sched_ext_no_config_file() {
let tmp = tempfile::TempDir::new().unwrap();
assert!(!has_sched_ext(tmp.path()));
}
#[test]
fn cli_has_sched_ext_empty_config() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join(".config"), "").unwrap();
assert!(!has_sched_ext(tmp.path()));
}
#[test]
fn critical_options_are_in_embedded_kconfig() {
let fragment = crate::EMBEDDED_KCONFIG;
for &(option, _) in VALIDATE_CONFIG_CRITICAL {
let enabled = format!("{option}=y");
assert!(
fragment.lines().any(|l| l.trim() == enabled),
"VALIDATE_CONFIG_CRITICAL lists {option:?} but ktstr.kconfig does not \
enable it; either add `{option}=y` to the fragment or drop the entry \
from VALIDATE_CONFIG_CRITICAL",
);
}
}
#[test]
fn validate_kernel_config_all_present() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(
dir.path().join(".config"),
"CONFIG_SCHED_CLASS_EXT=y\n\
CONFIG_DEBUG_INFO_BTF=y\n\
CONFIG_BPF_SYSCALL=y\n\
CONFIG_FTRACE=y\n\
CONFIG_KPROBE_EVENTS=y\n\
CONFIG_BPF_EVENTS=y\n\
CONFIG_VIRTIO_BLK=y\n",
)
.unwrap();
assert!(validate_kernel_config(dir.path()).is_ok());
}
#[test]
fn validate_kernel_config_missing_btf() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(
dir.path().join(".config"),
"CONFIG_SCHED_CLASS_EXT=y\n\
CONFIG_BPF_SYSCALL=y\n\
CONFIG_FTRACE=y\n\
CONFIG_KPROBE_EVENTS=y\n\
CONFIG_BPF_EVENTS=y\n",
)
.unwrap();
let err = validate_kernel_config(dir.path()).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("CONFIG_DEBUG_INFO_BTF"), "got: {msg}");
}
#[test]
fn validate_kernel_config_missing_multiple() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join(".config"), "CONFIG_BPF_SYSCALL=y\n").unwrap();
let err = validate_kernel_config(dir.path()).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("CONFIG_SCHED_CLASS_EXT"), "got: {msg}");
assert!(msg.contains("CONFIG_DEBUG_INFO_BTF"), "got: {msg}");
}
#[test]
fn validate_kernel_config_no_config_file() {
let dir = tempfile::TempDir::new().unwrap();
assert!(validate_kernel_config(dir.path()).is_err());
}
#[test]
fn validate_kernel_config_trim_handles_crlf_and_trailing_whitespace() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(
dir.path().join(".config"),
"CONFIG_SCHED_CLASS_EXT=y\r\n\
CONFIG_DEBUG_INFO_BTF=y \n\
CONFIG_BPF_SYSCALL=y\r\n\
CONFIG_FTRACE=y \n\
CONFIG_KPROBE_EVENTS=y\r\n\
CONFIG_BPF_EVENTS=y \n\
CONFIG_VIRTIO_BLK=y\r\n",
)
.unwrap();
let result = validate_kernel_config(dir.path());
assert!(
result.is_ok(),
"validate_kernel_config must trim per-line whitespace \
before the HashSet probe; got: {result:?}",
);
}
#[test]
fn parse_kconfig_symbol_assignment() {
assert_eq!(parse_kconfig_symbol("CONFIG_FOO=y"), Some("CONFIG_FOO"));
assert_eq!(parse_kconfig_symbol("CONFIG_FOO=m"), Some("CONFIG_FOO"));
assert_eq!(parse_kconfig_symbol("CONFIG_FOO=n"), Some("CONFIG_FOO"));
assert_eq!(
parse_kconfig_symbol("CONFIG_BAR=\"value\""),
Some("CONFIG_BAR")
);
}
#[test]
fn parse_kconfig_symbol_disable_directive() {
assert_eq!(
parse_kconfig_symbol("# CONFIG_FOO is not set"),
Some("CONFIG_FOO")
);
}
#[test]
fn parse_kconfig_symbol_rejects_free_text_comment() {
assert!(parse_kconfig_symbol("# user note about foo").is_none());
assert!(parse_kconfig_symbol("#").is_none());
assert!(parse_kconfig_symbol("# this is a doc line").is_none());
}
#[test]
fn parse_kconfig_symbol_rejects_blank_and_non_config() {
assert!(parse_kconfig_symbol("").is_none());
assert!(parse_kconfig_symbol("not a kconfig line").is_none());
assert!(parse_kconfig_symbol("FOO=y").is_none());
}
#[test]
fn render_kconfig_value_assignment_returns_value_with_equals() {
assert_eq!(render_kconfig_value("CONFIG_FOO=y", "CONFIG_FOO"), "=y");
assert_eq!(render_kconfig_value("CONFIG_FOO=n", "CONFIG_FOO"), "=n");
assert_eq!(
render_kconfig_value("CONFIG_BAR=\"value\"", "CONFIG_BAR"),
"=\"value\""
);
}
#[test]
fn render_kconfig_value_disable_returns_is_not_set() {
assert_eq!(
render_kconfig_value("# CONFIG_FOO is not set", "CONFIG_FOO"),
"is not set"
);
}
#[test]
fn render_kconfig_value_falls_back_to_full_line_on_unknown_shape() {
let s = "CONFIG_FOO without equals";
assert_eq!(render_kconfig_value(s, "CONFIG_FOO"), s);
}
#[test]
fn warn_extra_kconfig_overrides_does_not_panic_on_empty_fragment() {
warn_extra_kconfig_overrides_baked_in("", "test");
}
#[test]
fn warn_extra_kconfig_overrides_does_not_panic_on_no_overrides() {
let novel = "CONFIG_KTSTR_TEST_NOVEL_SYMBOL_OVERRIDE_TEST=y\n";
assert!(
!EMBEDDED_KCONFIG.contains("CONFIG_KTSTR_TEST_NOVEL_SYMBOL_OVERRIDE_TEST"),
"test fixture must use a symbol absent from EMBEDDED_KCONFIG"
);
warn_extra_kconfig_overrides_baked_in(novel, "test");
}
#[test]
fn warn_extra_kconfig_overrides_does_not_panic_on_actual_override() {
let user = "# CONFIG_BPF is not set\n";
warn_extra_kconfig_overrides_baked_in(user, "test");
}
#[test]
fn warn_extra_kconfig_overrides_skips_matching_assignments() {
let user = "CONFIG_BPF=y\n";
warn_extra_kconfig_overrides_baked_in(user, "test");
}
#[test]
fn warn_extra_kconfig_overrides_skips_free_text_comments() {
let user = "# this is a comment about something\n# another comment\n";
warn_extra_kconfig_overrides_baked_in(user, "test");
}
#[test]
fn read_extra_kconfig_returns_content_for_valid_file() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("frag.kconfig");
let content = "CONFIG_FOO=y\nCONFIG_BAR=m\n";
std::fs::write(&path, content).unwrap();
let got = read_extra_kconfig(&path, "test").unwrap();
assert_eq!(got, content, "content must round-trip byte-for-byte");
}
#[test]
fn read_extra_kconfig_not_found_arm_names_path_and_intent() {
let dir = tempfile::TempDir::new().unwrap();
let missing = dir.path().join("does-not-exist.kconfig");
let display = missing.display().to_string();
let err = read_extra_kconfig(&missing, "cargo ktstr").expect_err("missing file must Err");
assert!(
err.contains(&display),
"ENOENT message must name the literal path: {err}"
);
assert!(
err.contains("--extra-kconfig"),
"ENOENT message must name the flag: {err}"
);
assert!(
err.contains("file not found"),
"ENOENT arm must surface a `file not found` token: {err}"
);
}
#[test]
fn read_extra_kconfig_directory_arm_distinguishes_from_not_found() {
let dir = tempfile::TempDir::new().unwrap();
let display = dir.path().display().to_string();
let err =
read_extra_kconfig(dir.path(), "cargo ktstr").expect_err("directory path must Err");
assert!(
err.contains(&display),
"EISDIR message must name the path: {err}"
);
assert!(
err.contains("is a directory"),
"EISDIR arm must surface its specific token: {err}"
);
}
#[test]
fn read_extra_kconfig_invalid_utf8_arm_names_constraint() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("binary.bin");
std::fs::write(&path, [0xFFu8, 0xFE, 0x00, 0x42]).unwrap();
let err = read_extra_kconfig(&path, "cargo ktstr").expect_err("non-UTF-8 file must Err");
assert!(
err.contains("not valid UTF-8"),
"UTF-8 arm must surface the constraint name: {err}"
);
assert!(
err.contains("ASCII text"),
"UTF-8 arm must mention the kconfig content constraint: {err}"
);
}
#[test]
fn read_extra_kconfig_empty_file_returns_empty_string() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("empty.kconfig");
std::fs::write(&path, "").unwrap();
let got = read_extra_kconfig(&path, "cargo ktstr").unwrap();
assert_eq!(
got, "",
"empty file must round-trip as empty String, not Err"
);
}
#[test]
#[cfg(unix)]
fn read_extra_kconfig_follows_symlink_chain() {
use std::os::unix::fs::symlink;
let dir = tempfile::TempDir::new().unwrap();
let target = dir.path().join("real.kconfig");
let link = dir.path().join("link.kconfig");
std::fs::write(&target, "CONFIG_BPF=y\n").unwrap();
symlink(&target, &link).unwrap();
let got = read_extra_kconfig(&link, "test").unwrap();
assert_eq!(
got, "CONFIG_BPF=y\n",
"symlink must resolve to target content"
);
}
#[test]
fn warn_dropped_extra_kconfig_lines_silent_when_config_missing() {
let dir = tempfile::TempDir::new().unwrap();
let extra = "CONFIG_FOO=y\n";
warn_dropped_extra_kconfig_lines(dir.path(), extra, "test");
}
#[test]
fn warn_dropped_extra_kconfig_lines_silent_when_all_present() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join(".config"), "CONFIG_FOO=y\nCONFIG_BAR=m\n").unwrap();
let extra = "CONFIG_FOO=y\nCONFIG_BAR=m\n";
warn_dropped_extra_kconfig_lines(dir.path(), extra, "test");
}
#[test]
fn warn_dropped_extra_kconfig_lines_does_not_panic_on_dropped_line() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join(".config"), "CONFIG_BPF=y\n").unwrap();
let extra = "CONFIG_KTSTR_DROPPED_TEST_NOVEL=y\n";
warn_dropped_extra_kconfig_lines(dir.path(), extra, "test");
}
#[test]
fn warn_dropped_extra_kconfig_lines_does_not_panic_on_rewritten_line() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(
dir.path().join(".config"),
"# CONFIG_KTSTR_REWRITE_TEST is not set\n",
)
.unwrap();
let extra = "CONFIG_KTSTR_REWRITE_TEST=y\n";
warn_dropped_extra_kconfig_lines(dir.path(), extra, "test");
}
#[test]
fn warn_dropped_extra_kconfig_lines_skips_free_text_comments() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join(".config"), "CONFIG_BPF=y\n").unwrap();
let extra = "# decorative header\nCONFIG_BPF=y\n";
warn_dropped_extra_kconfig_lines(dir.path(), extra, "test");
}
#[test]
fn configure_kernel_appends_missing() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join(".config"), "CONFIG_BPF=y\n").unwrap();
std::fs::write(dir.path().join("Makefile"), "olddefconfig:\n\t@true\n").unwrap();
let fragment = "CONFIG_EXTRA=y\n";
configure_kernel(dir.path(), fragment).unwrap();
let config = std::fs::read_to_string(dir.path().join(".config")).unwrap();
assert!(config.contains("CONFIG_EXTRA=y"));
assert!(config.contains("CONFIG_BPF=y"));
}
#[test]
fn configure_kernel_skips_when_present() {
let dir = tempfile::TempDir::new().unwrap();
let initial = "CONFIG_BPF=y\nCONFIG_EXTRA=y\n";
std::fs::write(dir.path().join(".config"), initial).unwrap();
let fragment = "CONFIG_EXTRA=y\n";
configure_kernel(dir.path(), fragment).unwrap();
let config = std::fs::read_to_string(dir.path().join(".config")).unwrap();
assert_eq!(config, initial);
}
#[test]
fn configure_kernel_rejects_numeric_prefix_false_match() {
let dir = tempfile::TempDir::new().unwrap();
let initial = "CONFIG_NR_CPUS=128\n";
std::fs::write(dir.path().join(".config"), initial).unwrap();
std::fs::write(dir.path().join("Makefile"), "olddefconfig:\n\t@true\n").unwrap();
let fragment = "CONFIG_NR_CPUS=1\n";
configure_kernel(dir.path(), fragment).unwrap();
let config = std::fs::read_to_string(dir.path().join(".config")).unwrap();
assert!(
config.lines().any(|l| l.trim() == "CONFIG_NR_CPUS=1"),
"CONFIG_NR_CPUS=1 must be appended as its own line: {config:?}"
);
assert!(
config.lines().any(|l| l.trim() == "CONFIG_NR_CPUS=128"),
"original CONFIG_NR_CPUS=128 must be preserved: {config:?}"
);
}
#[test]
fn all_fragment_lines_present_exact_match() {
let config = "CONFIG_FOO=y\nCONFIG_BAR=m\n";
assert!(all_fragment_lines_present("CONFIG_FOO=y\n", config));
assert!(all_fragment_lines_present("CONFIG_BAR=m\n", config));
assert!(all_fragment_lines_present(
"CONFIG_FOO=y\nCONFIG_BAR=m\n",
config
));
}
#[test]
fn all_fragment_lines_present_numeric_prefix_not_present() {
let config = "CONFIG_NR_CPUS=128\n";
assert!(!all_fragment_lines_present("CONFIG_NR_CPUS=1\n", config));
assert!(!all_fragment_lines_present("CONFIG_NR_CPUS=12\n", config));
}
#[test]
fn all_fragment_lines_present_disable_directive_participates() {
let config = "CONFIG_BPF=y\n";
assert!(!all_fragment_lines_present(
"# CONFIG_BPF is not set\n",
config
));
}
#[test]
fn all_fragment_lines_present_empty_lines_skipped() {
let config = "CONFIG_FOO=y\n";
assert!(all_fragment_lines_present("\n\nCONFIG_FOO=y\n\n", config));
}
#[test]
fn all_fragment_lines_present_free_text_comment_stripped() {
let config = "CONFIG_FOO=y\n";
let fragment = "# Build for testing scx schedulers\nCONFIG_FOO=y\n";
assert!(
all_fragment_lines_present(fragment, config),
"free-text comment must not block the present-in-config check"
);
}
#[test]
fn all_fragment_lines_present_disable_directive_still_participates() {
let config = "CONFIG_FOO=y\n# CONFIG_BAR is not set\n";
let fragment_present = "# CONFIG_BAR is not set\n";
assert!(
all_fragment_lines_present(fragment_present, config),
"disable directive present in config must satisfy probe"
);
let config_missing = "CONFIG_FOO=y\n";
assert!(
!all_fragment_lines_present(fragment_present, config_missing),
"disable directive missing from config must fail probe"
);
}
#[test]
fn all_fragment_lines_present_section_header_comment_stripped() {
let config = "CONFIG_FOO=y\nCONFIG_BAR=m\n";
let fragment = "\
# == BPF support ==\n\
CONFIG_FOO=y\n\
# == Tracing ==\n\
CONFIG_BAR=m\n";
assert!(all_fragment_lines_present(fragment, config));
}
#[test]
fn is_kconfig_semantic_line_classifies_assignment_disable_and_comment() {
assert!(is_kconfig_semantic_line("CONFIG_FOO=y"));
assert!(is_kconfig_semantic_line("CONFIG_NR_CPUS=128"));
assert!(is_kconfig_semantic_line("# CONFIG_BPF is not set"));
assert!(is_kconfig_semantic_line("# CONFIG_BPF is not set"));
assert!(!is_kconfig_semantic_line("# Build for testing"));
assert!(!is_kconfig_semantic_line("# == Section header =="));
assert!(!is_kconfig_semantic_line(""));
assert!(!is_kconfig_semantic_line("# CONFIG_FOO is enabled"));
assert!(!is_kconfig_semantic_line("# CONFIG_FOO"));
}
}