tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1574Shell 204Rust 111Markdown 993763 Sutichaykuna

⎇ ca9bde0251d05fbfd34c93033d1aff2eb2ee62b5 crates/hammer-core/src/kernel/plan.rs Rust · 594 siq'ikuna · 21.1 KB Ñawpa willay Pi imata llamkarqan ch'uya

kernel: la clausura contrastada contra un olddefconfig de verdad — 65/68 con 0 falsos positivos ca9bde025 · Sergio ·

1//! `plan` — de una selección de bundles a una **receta derivada**.
2//!
3//! ## Por qué una receta y no un parámetro
4//! Las fases de build entran en `Recipe::hash_inputs` y el config del kernel vive en la fase
5//! `configure` ⇒ **el config ES la identidad del artefacto** (SDD 22 §1). No hay forma de tener un
6//! config variable sin generar recetas: una «perilla» de la UI no es un parámetro de runtime, es
7//! una edición de receta. Fingir lo contrario daría un store que dice que todo está al día mientras
8//! los artefactos ya no corresponden — en un sistema direccionado por contenido, el peor fallo
9//! posible no es fallar, es **mentir**.
10//!
11//! De ahí sale gratis lo que §7 del handoff pedía: el plan **determina** el artefacto, así que el
12//! JSON lleva su `ArtifactHash` y la UI puede decir «esto ya está construido y firmado» sin
13//! construir nada.
14//!
15//! ## Qué se emite y qué no
16//! Se emiten **raíces**, no clausuras. `-d WIRELESS -d WLAN` son dos banderas; los ~400 símbolos
17//! que caen con ellas los calcula el `olddefconfig` **del propio kernel**. hammer sabe cuáles son
18//! (para poder explicarlos), pero no los escribe: la app nunca escribe un `.config`.
19//!
20//! ## Procedencia por símbolo
21//! Cada símbolo emitido carga quién lo pidió ([`PlannedSymbol::from`]). Es el #7 del handoff, y con
22//! él el diff entre dos generaciones deja de ser «347 símbolos cambiaron» para ser «activaste
23//! bluetooth y entraron 12 símbolos».
24
25use std::collections::BTreeMap;
26
27use serde::{Deserialize, Serialize};
28
29use super::catalog::{Catalog, Side};
30use super::kconfig::KconfigTree;
31use crate::recipe::Recipe;
32
33/// Qué se le pide a `scripts/config` para un símbolo.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum Action {
37    Enable,
38    Disable,
39}
40
41impl Action {
42    fn flag(self) -> &'static str {
43        match self {
44            Action::Enable => "-e",
45            Action::Disable => "-d",
46        }
47    }
48}
49
50/// Un símbolo emitido, con su procedencia.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct PlannedSymbol {
53    pub symbol: String,
54    pub action: Action,
55    /// `bundle:sin-wifi`, `knob:tick-1000`.
56    pub from: String,
57    /// Por qué está: raíz declarada, o fuga `select` que hubo que cerrar.
58    pub reason: String,
59}
60
61/// Una perilla que **no** cae del lado de Kconfig: el plan la declara y no la aplica.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct RecipeKnob {
64    pub id: String,
65    pub title: String,
66    pub recipe_field: String,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Plan {
71    pub base_recipe: String,
72    pub base_name: String,
73    pub derived_name: String,
74    pub bundles: Vec<String>,
75    pub knobs: Vec<String>,
76    /// Símbolos que se emiten, con procedencia.
77    pub symbols: Vec<PlannedSymbol>,
78    /// **Diff-back estático**: pedidos que no existen en este árbol. Un `-d` sobre uno de éstos es
79    /// un no-op que nadie ve. Le pasaba a `THUNDERBOLT` y a `REISERFS_FS` en las cuatro recetas de
80    /// kernel del repo hasta que se corrigieron (`2602218`), que es cómo se descubrió el patrón.
81    pub unknown_symbols: Vec<PlannedSymbol>,
82    /// Perillas `side = "recipe"` seleccionadas: declaradas, no aplicadas.
83    pub recipe_side: Vec<RecipeKnob>,
84    /// Cuántos símbolos apaga de verdad la selección, contando la clausura. Es informativo: quien
85    /// la calcula de vuelta, y manda, es `olddefconfig`.
86    pub closure_size: usize,
87    /// La invocación de `scripts/config` que se inserta.
88    pub fragment: String,
89    /// La fase `configure` derivada, entera.
90    pub configure: String,
91    /// El `ArtifactHash` de la receta derivada. `None` si no se pudo calcular (deps sin resolver).
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub artifact_hash: Option<String>,
94}
95
96/// Construye el plan. No toca el disco.
97pub fn make_plan(
98    base: &Recipe,
99    base_path: &str,
100    cat: &Catalog,
101    tree: &KconfigTree,
102    bundle_ids: &[String],
103    knob_ids: &[String],
104) -> crate::Result<Plan> {
105    let mut wanted: BTreeMap<String, PlannedSymbol> = BTreeMap::new();
106    let mut disable_roots: Vec<String> = Vec::new();
107
108    let push = |wanted: &mut BTreeMap<String, PlannedSymbol>,
109                    sym: &str,
110                    action: Action,
111                    from: String,
112                    reason: &str|
113     -> crate::Result<()> {
114        // Dos selecciones que se contradicen sobre el mismo símbolo no se resuelven por orden de
115        // aparición: se rechazan. El orden sería una respuesta plausible y arbitraria.
116        if let Some(prev) = wanted.get(sym) {
117            if prev.action != action {
118                return Err(crate::Error::Recipe(format!(
119                    "conflicto en {sym}: «{}» lo quiere {:?} y «{from}» lo quiere {:?}",
120                    prev.from, prev.action, action
121                )));
122            }
123            return Ok(());
124        }
125        wanted.insert(
126            sym.to_string(),
127            PlannedSymbol {
128                symbol: sym.to_string(),
129                action,
130                from,
131                reason: reason.to_string(),
132            },
133        );
134        Ok(())
135    };
136
137    for id in bundle_ids {
138        let b = cat.bundle(id).ok_or_else(|| {
139            crate::Error::Recipe(format!("no existe el bundle «{id}» en el catálogo"))
140        })?;
141        let from = format!("bundle:{id}");
142        for s in &b.disable {
143            push(&mut wanted, s, Action::Disable, from.clone(), "raíz del bundle")?;
144            disable_roots.push(s.clone());
145        }
146        for s in &b.close_leaks {
147            push(
148                &mut wanted,
149                s,
150                Action::Disable,
151                from.clone(),
152                "fuga `select` cerrada: sin esto el olddefconfig lo reenciende",
153            )?;
154            disable_roots.push(s.clone());
155        }
156        for s in &b.enable {
157            push(&mut wanted, s, Action::Enable, from.clone(), "raíz del bundle")?;
158        }
159    }
160
161    let mut recipe_side = Vec::new();
162    for id in knob_ids {
163        let k = cat.knob(id).ok_or_else(|| {
164            crate::Error::Recipe(format!("no existe la perilla «{id}» en el catálogo"))
165        })?;
166        let from = format!("knob:{id}");
167        if k.side == Side::Recipe {
168            recipe_side.push(RecipeKnob {
169                id: k.id.clone(),
170                title: k.title.clone(),
171                recipe_field: k.recipe_field.clone().unwrap_or_default(),
172            });
173            continue;
174        }
175        for s in &k.enable {
176            push(&mut wanted, s, Action::Enable, from.clone(), "perilla")?;
177        }
178        for s in &k.disable {
179            push(&mut wanted, s, Action::Disable, from.clone(), "perilla")?;
180            disable_roots.push(s.clone());
181        }
182    }
183
184    // Un `enable` que cae DENTRO de lo que otro bundle apaga es un conflicto que el símbolo solo no
185    // delata: `olddefconfig` lo descartaría en silencio por dependencia insatisfecha.
186    let closure = tree.closure_off(&disable_roots);
187    for p in wanted.values() {
188        if p.action == Action::Enable && closure.contains(&p.symbol) {
189            return Err(crate::Error::Recipe(format!(
190                "conflicto: «{}» quiere encender {} pero cae dentro de lo que apaga la selección \
191                 (el olddefconfig lo descartaría sin decir nada)",
192                p.from, p.symbol
193            )));
194        }
195    }
196
197    let (known, unknown_symbols): (Vec<_>, Vec<_>) = wanted
198        .into_values()
199        .partition(|p| tree.symbols.contains_key(&p.symbol));
200
201    let arch = detect_arch(base);
202    let fragment = render_fragment(&known);
203    let base_configure = base
204        .build
205        .phases
206        .configure
207        .as_deref()
208        .ok_or_else(|| {
209            crate::Error::Recipe(format!(
210                "la receta base «{}» no tiene fase `configure`: no hay dónde insertar el config",
211                base.name
212            ))
213        })?
214        .trim_end()
215        .trim_end_matches('\\')
216        .trim_end()
217        .to_string();
218
219    // Se AÑADE una segunda ronda en vez de reescribir la fase base: no hay que parsear el shell de
220    // nadie, la base sigue siendo literalmente la de siempre en el diff, y `olddefconfig` es
221    // idempotente. Lo que la app aporta es un fragmento; resolver sigue siendo del kernel.
222    let configure = format!("{base_configure} && \\\n{fragment} && \\\nmake ARCH={arch} olddefconfig\n");
223
224    let derived_name = derive_name(&base.name, bundle_ids, knob_ids);
225
226    Ok(Plan {
227        base_recipe: base_path.to_string(),
228        base_name: base.name.clone(),
229        derived_name,
230        bundles: bundle_ids.to_vec(),
231        knobs: knob_ids.to_vec(),
232        symbols: known,
233        unknown_symbols,
234        recipe_side,
235        closure_size: closure.len(),
236        fragment,
237        configure,
238        artifact_hash: None,
239    })
240}
241
242/// La receta derivada: la base con la fase `configure` del plan y un nombre propio.
243///
244/// Conserva el `base_dir` de la base a propósito — es lo que hace resolubles sus `deps.build` y,
245/// con ellas, calculable el `ArtifactHash`.
246pub fn derive_recipe(base: &Recipe, plan: &Plan) -> Recipe {
247    let mut r = base.clone();
248    r.name = plan.derived_name.clone();
249    r.build.phases.configure = Some(plan.configure.clone());
250    r
251}
252
253/// Serializa la receta derivada a TOML. Vive acá y no en la CLI porque el formato de receta es del
254/// core: quien lo cambie tiene que cambiarlo en un solo sitio.
255pub fn render_recipe_toml(r: &Recipe) -> crate::Result<String> {
256    toml::to_string_pretty(r).map_err(|e| crate::Error::Serde(e.to_string()))
257}
258
259fn detect_arch(base: &Recipe) -> String {
260    let hay = base
261        .build
262        .phases
263        .configure
264        .as_deref()
265        .unwrap_or_default();
266    for tok in hay.split_whitespace() {
267        if let Some(v) = tok.strip_prefix("ARCH=") {
268            if !v.is_empty() {
269                return v.to_string();
270            }
271        }
272    }
273    "x86_64".to_string()
274}
275
276/// `scripts/config -d A -d B -e C …`, en líneas de ancho razonable y **orden estable**: el texto
277/// entra al hash, así que un orden que dependiera del recorrido daría dos hashes para el mismo plan.
278fn render_fragment(syms: &[PlannedSymbol]) -> String {
279    let mut out = String::from("scripts/config");
280    let mut col = out.len();
281    for p in syms {
282        let pieza = format!(" {} {}", p.action.flag(), p.symbol);
283        if col + pieza.len() > 96 {
284            out.push_str(" \\\n ");
285            col = 1;
286        }
287        col += pieza.len();
288        out.push_str(&pieza);
289    }
290    out
291}
292
293/// Nombre de la receta derivada. Lleva un hash corto de la SELECCIÓN, no del contenido: dos planes
294/// con los mismos bundles dan el mismo nombre, y el `ArtifactHash` sigue siendo quien manda.
295fn derive_name(base: &str, bundles: &[String], knobs: &[String]) -> String {
296    let mut material = String::new();
297    let mut b: Vec<&String> = bundles.iter().collect();
298    b.sort();
299    for x in b {
300        material.push_str("bundle:");
301        material.push_str(x);
302        material.push('\n');
303    }
304    let mut k: Vec<&String> = knobs.iter().collect();
305    k.sort();
306    for x in k {
307        material.push_str("knob:");
308        material.push_str(x);
309        material.push('\n');
310    }
311    let h = crate::hash::ArtifactHash::of_bytes(material.as_bytes());
312    let hex = h.as_str().trim_start_matches("b3:");
313    format!("{base}-plan-{}", &hex[..8])
314}
315
316/// **Diff-back** (§6 del handoff, la mitad que faltaba): ¿sobrevivió cada símbolo pedido al
317/// `olddefconfig`?
318///
319/// Sin esto la UI miente: un `-e FOO` cuya dependencia no se cumple se pierde en silencio.
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct DiffBack {
322    pub honored: Vec<String>,
323    /// Pedidos que el `.config` resultante contradice. **Cada uno es una promesa incumplida.**
324    pub violated: Vec<Violation>,
325    /// Apagados que el `.config` ni menciona. **Esto es éxito, no fallo**: Kconfig no emite un
326    /// símbolo cuyas dependencias no se cumplen, así que un `-d` sobre algo que ya cayó con su
327    /// raíz simplemente no aparece. Contarlo como incumplido —el error que cometí antes de correr
328    /// un `olddefconfig` de verdad— hace que un plan perfecto se reporte como roto.
329    pub off_by_dependency: Vec<String>,
330    /// Símbolos que no existen en este árbol: la bandera no hace nada. Ya se sabía al planear; se
331    /// repite acá porque quien lee el diff-back no siempre vio el plan.
332    pub never_existed: Vec<String>,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct Violation {
337    pub symbol: String,
338    pub asked: Action,
339    /// Cómo quedó de verdad.
340    pub got: String,
341    pub from: String,
342}
343
344impl DiffBack {
345    /// Limpio = ninguna promesa incumplida. Los `off_by_dependency` son el resultado buscado y los
346    /// `never_existed` ya los reportó el plan: ninguno de los dos ensucia el veredicto.
347    pub fn is_clean(&self) -> bool {
348        self.violated.is_empty()
349    }
350}
351
352/// Contrasta el plan contra el `.config` que produjo el kernel.
353pub fn diff_back(plan: &Plan, produced: &super::KernelConfig) -> DiffBack {
354    use super::ConfigValue;
355    let mut d = DiffBack {
356        honored: Vec::new(),
357        violated: Vec::new(),
358        off_by_dependency: Vec::new(),
359        never_existed: Vec::new(),
360    };
361    for p in &plan.symbols {
362        match (p.action, produced.get(&p.symbol)) {
363            // Ausente + se pedía apagar = está apagado. Kconfig omite lo que no puede existir.
364            (Action::Disable, None) => d.off_by_dependency.push(p.symbol.clone()),
365            // Ausente + se pedía encender = NO está. Eso sí es una promesa rota.
366            (Action::Enable, None) => d.violated.push(Violation {
367                symbol: p.symbol.clone(),
368                asked: Action::Enable,
369                got: "ausente del .config".to_string(),
370                from: p.from.clone(),
371            }),
372            (Action::Enable, Some(v)) if v.is_on() => d.honored.push(p.symbol.clone()),
373            (Action::Disable, Some(ConfigValue::No)) => d.honored.push(p.symbol.clone()),
374            (asked, Some(v)) => d.violated.push(Violation {
375                symbol: p.symbol.clone(),
376                asked,
377                got: format!("{v:?}"),
378                from: p.from.clone(),
379            }),
380        }
381    }
382    d.never_existed = plan
383        .unknown_symbols
384        .iter()
385        .map(|p| p.symbol.clone())
386        .collect();
387    d.never_existed.sort();
388    d.never_existed.dedup();
389    d
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use std::collections::BTreeMap;
396
397    fn escenario() -> (tempfile::TempDir, KconfigTree, Catalog, Recipe) {
398        let d = tempfile::tempdir().unwrap();
399        std::fs::write(
400            d.path().join("Kconfig"),
401            r#"
402config WIRELESS
403	bool "Wireless"
404
405config CFG80211
406	tristate "cfg80211"
407	depends on WIRELESS
408
409config WLAN
410	bool "WLAN"
411	select WIRELESS
412
413config AUDIT
414	bool "Audit"
415
416config MAC80211_LEDS
417	bool "leds"
418	depends on CFG80211
419"#,
420        )
421        .unwrap();
422        let tree = KconfigTree::parse(d.path(), &BTreeMap::new()).unwrap();
423        let cat: Catalog = toml::from_str(
424            r#"
425version = 1
426[[bundle]]
427id = "sin-wifi"
428title = "No necesito wifi"
429disable = ["WIRELESS"]
430close_leaks = ["WLAN"]
431
432[[bundle]]
433id = "con-leds"
434title = "Quiero los leds"
435enable = ["MAC80211_LEDS"]
436
437[[knob]]
438id = "auditoria"
439title = "Auditoría"
440side = "kconfig"
441enable = ["AUDIT"]
442
443[[knob]]
444id = "lto"
445title = "LTO"
446side = "recipe"
447recipe_field = "build.compiler"
448"#,
449        )
450        .unwrap();
451        let recipe: Recipe = toml::from_str(
452            r#"
453name = "linux"
454version = "6.16.12"
455[source]
456tarball = "https://x/linux.tar.gz"
457sha256 = "aa"
458[build]
459compiler = "zig-cc"
460target = "x86_64-linux-musl"
461link = "dynamic"
462[build.phases]
463configure = "make ARCH=x86_64 defconfig && make ARCH=x86_64 olddefconfig"
464compile = "make bzImage"
465"#,
466        )
467        .unwrap();
468        (d, tree, cat, recipe)
469    }
470
471    #[test]
472    fn emite_raices_no_clausuras() {
473        let (_d, tree, cat, base) = escenario();
474        let p = make_plan(&base, "recipes/linux.toml", &cat, &tree, &["sin-wifi".into()], &[]).unwrap();
475        // Dos banderas, no cuatro símbolos: la clausura la calcula olddefconfig.
476        assert_eq!(p.symbols.len(), 2);
477        assert!(p.fragment.contains("-d WIRELESS"));
478        assert!(p.fragment.contains("-d WLAN"));
479        assert!(!p.fragment.contains("CFG80211"));
480        // …pero hammer SABE cuántos caen, para poder explicarlo.
481        assert_eq!(p.closure_size, 4);
482    }
483
484    #[test]
485    fn la_fase_derivada_conserva_la_base_y_reresuelve() {
486        let (_d, tree, cat, base) = escenario();
487        let p = make_plan(&base, "recipes/linux.toml", &cat, &tree, &["sin-wifi".into()], &[]).unwrap();
488        assert!(p.configure.starts_with("make ARCH=x86_64 defconfig"));
489        assert!(p.configure.trim_end().ends_with("make ARCH=x86_64 olddefconfig"));
490        // El fragmento va ANTES del olddefconfig final: si fuera después, no se resolvería.
491        let i_frag = p.configure.find("scripts/config").unwrap();
492        let i_last = p.configure.rfind("olddefconfig").unwrap();
493        assert!(i_frag < i_last);
494    }
495
496    #[test]
497    fn el_nombre_derivado_es_estable_e_independiente_del_orden() {
498        let (_d, tree, cat, base) = escenario();
499        let a = make_plan(&base, "r", &cat, &tree, &["sin-wifi".into()], &["auditoria".into()]).unwrap();
500        let b = make_plan(&base, "r", &cat, &tree, &["sin-wifi".into()], &["auditoria".into()]).unwrap();
501        assert_eq!(a.derived_name, b.derived_name);
502        assert!(a.derived_name.starts_with("linux-plan-"));
503        let c = make_plan(&base, "r", &cat, &tree, &["sin-wifi".into()], &[]).unwrap();
504        assert_ne!(a.derived_name, c.derived_name);
505    }
506
507    #[test]
508    fn encender_algo_que_la_seleccion_apaga_es_conflicto() {
509        let (_d, tree, cat, base) = escenario();
510        // MAC80211_LEDS depende de CFG80211, que cae con WIRELESS. olddefconfig lo tiraría callado.
511        let e = make_plan(
512            &base,
513            "r",
514            &cat,
515            &tree,
516            &["sin-wifi".into(), "con-leds".into()],
517            &[],
518        )
519        .unwrap_err()
520        .to_string();
521        assert!(e.contains("MAC80211_LEDS"), "{e}");
522    }
523
524    #[test]
525    fn la_perilla_de_receta_se_declara_y_no_se_aplica() {
526        let (_d, tree, cat, base) = escenario();
527        let p = make_plan(&base, "r", &cat, &tree, &[], &["lto".into()]).unwrap();
528        assert!(p.symbols.is_empty());
529        assert_eq!(p.recipe_side.len(), 1);
530        assert_eq!(p.recipe_side[0].recipe_field, "build.compiler");
531    }
532
533    #[test]
534    fn el_simbolo_inexistente_no_se_pierde_en_silencio() {
535        let (_d, tree, _cat, base) = escenario();
536        let cat: Catalog = toml::from_str(
537            r#"
538version = 1
539[[bundle]]
540id = "fantasma"
541title = "Apaga algo que ya no existe"
542disable = ["REISERFS_FS"]
543"#,
544        )
545        .unwrap();
546        let p = make_plan(&base, "r", &cat, &tree, &["fantasma".into()], &[]).unwrap();
547        assert!(p.symbols.is_empty(), "no se emite una bandera que sería no-op");
548        assert_eq!(p.unknown_symbols.len(), 1);
549        assert_eq!(p.unknown_symbols[0].symbol, "REISERFS_FS");
550    }
551
552    #[test]
553    fn el_diff_back_ve_la_promesa_incumplida() {
554        let (_d, tree, cat, base) = escenario();
555        let p = make_plan(&base, "r", &cat, &tree, &["sin-wifi".into()], &["auditoria".into()]).unwrap();
556        // El kernel produjo un config donde WLAN sobrevivió y AUDIT no se pudo encender.
557        let cfg = super::super::KernelConfig::parse(
558            "# CONFIG_WIRELESS is not set\nCONFIG_WLAN=y\n# CONFIG_AUDIT is not set\n",
559        );
560        let d = diff_back(&p, &cfg);
561        assert!(!d.is_clean());
562        let violados: Vec<&str> = d.violated.iter().map(|v| v.symbol.as_str()).collect();
563        assert_eq!(violados, vec!["AUDIT", "WLAN"]);
564        assert_eq!(d.honored, vec!["WIRELESS"]);
565    }
566
567    /// La lección de correr un `olddefconfig` de verdad (SDD 22 §12): un `-d` sobre un símbolo que
568    /// el `.config` ni menciona **es el resultado buscado**, no una promesa rota. Contarlo como
569    /// fallo hacía que un plan perfecto se reportara roto.
570    #[test]
571    fn apagar_algo_ausente_es_exito_y_encenderlo_es_fallo() {
572        let (_d, tree, cat, base) = escenario();
573        let p = make_plan(&base, "r", &cat, &tree, &["sin-wifi".into()], &["auditoria".into()]).unwrap();
574        // WIRELESS apagado; WLAN ni aparece (cayó con él); AUDIT tampoco aparece, y ése SÍ se pidió
575        // encender.
576        let cfg = super::super::KernelConfig::parse("# CONFIG_WIRELESS is not set\n");
577        let d = diff_back(&p, &cfg);
578        assert_eq!(d.honored, vec!["WIRELESS"]);
579        assert_eq!(d.off_by_dependency, vec!["WLAN"]);
580        assert_eq!(d.violated.len(), 1);
581        assert_eq!(d.violated[0].symbol, "AUDIT");
582        assert!(!d.is_clean(), "falta un símbolo que se pidió ENCENDER");
583    }
584
585    #[test]
586    fn el_diff_back_limpio_es_limpio() {
587        let (_d, tree, cat, base) = escenario();
588        let p = make_plan(&base, "r", &cat, &tree, &["sin-wifi".into()], &[]).unwrap();
589        let cfg = super::super::KernelConfig::parse(
590            "# CONFIG_WIRELESS is not set\n# CONFIG_WLAN is not set\n",
591        );
592        assert!(diff_back(&p, &cfg).is_clean());
593    }
594}