Skip to content

Commit 2d4e342

Browse files
Copilotguoweikang
andauthored
[WIP] Fix TUI menuconfig not displaying choice inside if blocks (#7)
* Initial plan * Initial plan Co-authored-by: guoweikang <18571063+guoweikang@users.noreply.github.com> * Fix: Handle Entry::If blocks correctly in menu tree building - Refactored process_entries to use collect_items helper method - If blocks now inline-process their entries into parent's items vector - Prevents menu_tree overwrites that caused entries to be lost - Added comprehensive tests for if block handling Co-authored-by: guoweikang <18571063+guoweikang@users.noreply.github.com> * Address code review: Add documentation and improve test assertions - Added doc comment for collect_items() helper method - Improved test assertions to be more specific about platform configs - Tests now check for unique identifiers from if blocks Co-authored-by: guoweikang <18571063+guoweikang@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: guoweikang <18571063+guoweikang@users.noreply.github.com>
1 parent c388a1b commit 2d4e342

2 files changed

Lines changed: 216 additions & 6 deletions

File tree

xtask/xconfig/src/ui/state/mod.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,20 @@ impl ConfigState {
202202
fn process_entries(&mut self, entries: &[Entry], depth: usize, parent_id: &str) {
203203
let mut items = Vec::new();
204204

205+
// Process entries and collect them into items
206+
self.collect_items(entries, depth, parent_id, &mut items);
207+
208+
self.menu_tree.insert(parent_id.to_string(), items.clone());
209+
self.all_items.extend(items);
210+
}
211+
212+
/// Recursively collects menu items from entries and appends them to the provided items vector.
213+
///
214+
/// This helper function is used to handle inline processing of `if` blocks, ensuring that
215+
/// entries within if blocks are collected into the same items vector as their siblings.
216+
/// This prevents menu tree overwrites that would occur if if-blocks were processed via
217+
/// separate calls to `process_entries()`.
218+
fn collect_items(&mut self, entries: &[Entry], depth: usize, parent_id: &str, items: &mut Vec<MenuItem>) {
205219
for entry in entries {
206220
match entry {
207221
Entry::Config(config) => {
@@ -220,7 +234,7 @@ impl ConfigState {
220234
let menu_id = item.id.clone();
221235
items.push(item);
222236

223-
// Process menu children
237+
// Process menu children with new parent_id and depth
224238
self.process_entries(&menu.entries, depth + 1, &menu_id);
225239
}
226240
Entry::Choice(choice) => {
@@ -238,8 +252,9 @@ impl ConfigState {
238252
items.push(item);
239253
}
240254
Entry::If(if_entry) => {
241-
// Process if block entries
242-
self.process_entries(&if_entry.entries, depth, parent_id);
255+
// Process if block entries inline - they belong to the same menu level
256+
// The if condition is already part of each entry's depends_on field
257+
self.collect_items(&if_entry.entries, depth, parent_id, items);
243258
}
244259
Entry::MainMenu(_title) => {
245260
// Skip mainmenu for now
@@ -249,9 +264,6 @@ impl ConfigState {
249264
}
250265
}
251266
}
252-
253-
self.menu_tree.insert(parent_id.to_string(), items.clone());
254-
self.all_items.extend(items);
255267
}
256268

257269
pub fn get_items_for_path(&self, path: &[String]) -> Vec<MenuItem> {
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
use xconfig::kconfig::Parser;
2+
use xconfig::ui::state::ConfigState;
3+
use std::fs;
4+
use tempfile::TempDir;
5+
6+
/// Test that entries inside `if` blocks are properly processed and displayed in menu tree
7+
#[test]
8+
fn test_if_block_choice_visibility() {
9+
// Create a temporary directory for test files
10+
let temp_dir = TempDir::new().unwrap();
11+
let kconfig_path = temp_dir.path().join("Kconfig");
12+
13+
// Create a test Kconfig with choice inside if block (similar to platform selection)
14+
let kconfig_content = r#"
15+
mainmenu "Test Config"
16+
17+
choice
18+
prompt "Target Architecture"
19+
default ARCH_AARCH64
20+
21+
config ARCH_AARCH64
22+
bool "AArch64"
23+
24+
config ARCH_RISCV64
25+
bool "RISC-V 64-bit"
26+
27+
endchoice
28+
29+
menu "Platform Selection"
30+
31+
if ARCH_AARCH64
32+
33+
choice
34+
prompt "AArch64 Platform"
35+
default PLATFORM_QEMU
36+
37+
config PLATFORM_QEMU
38+
bool "QEMU virt machine"
39+
40+
config PLATFORM_CROSVM
41+
bool "Crosvm virt machine"
42+
43+
endchoice
44+
45+
endif
46+
47+
if ARCH_RISCV64
48+
49+
config PLATFORM_RISCV_QEMU
50+
bool "RISC-V QEMU"
51+
52+
endif
53+
54+
endmenu
55+
"#;
56+
57+
fs::write(&kconfig_path, kconfig_content).unwrap();
58+
59+
// Parse the Kconfig
60+
let mut parser = Parser::new(&kconfig_path, temp_dir.path()).unwrap();
61+
let ast = parser.parse().unwrap();
62+
63+
// Build ConfigState
64+
let config_state = ConfigState::build_from_entries(&ast.entries);
65+
66+
// Verify that all items are collected (including those in if blocks)
67+
let all_item_ids: Vec<String> = config_state.all_items.iter().map(|i| i.id.clone()).collect();
68+
69+
// Should contain the architecture choice and its options
70+
assert!(all_item_ids.contains(&"choice".to_string()), "Should contain architecture choice");
71+
assert!(all_item_ids.contains(&"ARCH_AARCH64".to_string()), "Should contain ARCH_AARCH64");
72+
assert!(all_item_ids.contains(&"ARCH_RISCV64".to_string()), "Should contain ARCH_RISCV64");
73+
74+
// Should contain the menu
75+
assert!(all_item_ids.contains(&"menu_Platform Selection".to_string()), "Should contain Platform Selection menu");
76+
77+
// CRITICAL: Should contain the platform choice that's inside the if block
78+
// We check for the specific platform configs which are unique to the if block
79+
assert!(all_item_ids.contains(&"PLATFORM_QEMU".to_string()),
80+
"Should contain PLATFORM_QEMU from inside if ARCH_AARCH64 block");
81+
assert!(all_item_ids.contains(&"PLATFORM_CROSVM".to_string()),
82+
"Should contain PLATFORM_CROSVM from inside if ARCH_AARCH64 block");
83+
84+
// Should contain platform configs from inside other if blocks
85+
assert!(all_item_ids.contains(&"PLATFORM_RISCV_QEMU".to_string()),
86+
"Should contain PLATFORM_RISCV_QEMU from inside if ARCH_RISCV64 block");
87+
88+
// Verify menu tree structure - the Platform Selection menu should contain items from if blocks
89+
let platform_menu_items = config_state.menu_tree.get("menu_Platform Selection");
90+
assert!(platform_menu_items.is_some(), "Platform Selection menu should exist in menu_tree");
91+
92+
let platform_items = platform_menu_items.unwrap();
93+
let platform_item_ids: Vec<String> = platform_items.iter().map(|i| i.id.clone()).collect();
94+
95+
// The menu should contain the platform-specific configs from inside the if blocks
96+
assert!(platform_item_ids.contains(&"PLATFORM_QEMU".to_string()),
97+
"Platform Selection menu should contain PLATFORM_QEMU from if ARCH_AARCH64 block. Found: {:?}",
98+
platform_item_ids);
99+
assert!(platform_item_ids.contains(&"PLATFORM_CROSVM".to_string()),
100+
"Platform Selection menu should contain PLATFORM_CROSVM from if ARCH_AARCH64 block. Found: {:?}",
101+
platform_item_ids);
102+
assert!(platform_item_ids.contains(&"PLATFORM_RISCV_QEMU".to_string()),
103+
"Platform Selection menu should contain PLATFORM_RISCV_QEMU from if ARCH_RISCV64 block. Found: {:?}",
104+
platform_item_ids);
105+
}
106+
107+
/// Test that nested menus inside if blocks work correctly
108+
#[test]
109+
fn test_nested_menu_in_if_block() {
110+
let temp_dir = TempDir::new().unwrap();
111+
let kconfig_path = temp_dir.path().join("Kconfig");
112+
113+
let kconfig_content = r#"
114+
config FEATURE_A
115+
bool "Enable Feature A"
116+
117+
if FEATURE_A
118+
119+
menu "Feature A Options"
120+
121+
config OPTION_1
122+
bool "Option 1"
123+
124+
config OPTION_2
125+
bool "Option 2"
126+
127+
endmenu
128+
129+
endif
130+
"#;
131+
132+
fs::write(&kconfig_path, kconfig_content).unwrap();
133+
134+
let mut parser = Parser::new(&kconfig_path, temp_dir.path()).unwrap();
135+
let ast = parser.parse().unwrap();
136+
137+
let config_state = ConfigState::build_from_entries(&ast.entries);
138+
139+
// Verify the menu inside if block is processed
140+
let all_item_ids: Vec<String> = config_state.all_items.iter().map(|i| i.id.clone()).collect();
141+
142+
assert!(all_item_ids.contains(&"FEATURE_A".to_string()), "Should contain FEATURE_A");
143+
assert!(all_item_ids.contains(&"menu_Feature A Options".to_string()),
144+
"Should contain menu from inside if block");
145+
assert!(all_item_ids.contains(&"OPTION_1".to_string()),
146+
"Should contain OPTION_1 from menu inside if block");
147+
assert!(all_item_ids.contains(&"OPTION_2".to_string()),
148+
"Should contain OPTION_2 from menu inside if block");
149+
}
150+
151+
/// Test that multiple if blocks at the same level are all processed
152+
#[test]
153+
fn test_multiple_if_blocks() {
154+
let temp_dir = TempDir::new().unwrap();
155+
let kconfig_path = temp_dir.path().join("Kconfig");
156+
157+
let kconfig_content = r#"
158+
menu "Options"
159+
160+
if ARCH_A
161+
config OPTION_A1
162+
bool "Option A1"
163+
endif
164+
165+
if ARCH_B
166+
config OPTION_B1
167+
bool "Option B1"
168+
endif
169+
170+
if ARCH_C
171+
config OPTION_C1
172+
bool "Option C1"
173+
endif
174+
175+
endmenu
176+
"#;
177+
178+
fs::write(&kconfig_path, kconfig_content).unwrap();
179+
180+
let mut parser = Parser::new(&kconfig_path, temp_dir.path()).unwrap();
181+
let ast = parser.parse().unwrap();
182+
183+
let config_state = ConfigState::build_from_entries(&ast.entries);
184+
185+
// All options from different if blocks should be in the menu
186+
let menu_items = config_state.menu_tree.get("menu_Options").unwrap();
187+
let item_ids: Vec<String> = menu_items.iter().map(|i| i.id.clone()).collect();
188+
189+
assert!(item_ids.contains(&"OPTION_A1".to_string()),
190+
"Should contain OPTION_A1 from first if block");
191+
assert!(item_ids.contains(&"OPTION_B1".to_string()),
192+
"Should contain OPTION_B1 from second if block");
193+
assert!(item_ids.contains(&"OPTION_C1".to_string()),
194+
"Should contain OPTION_C1 from third if block");
195+
196+
// All should be at the same depth (within the menu)
197+
assert_eq!(menu_items.len(), 3, "Should have 3 items in the menu");
198+
}

0 commit comments

Comments
 (0)