1#![allow(dead_code)]
9
10#[cfg(feature = "alloc")]
11use alloc::vec::Vec;
12use core::{panic::PanicInfo, time::Duration};
13
14use crate::{error::KernelError, serial_print, serial_println};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[repr(u32)]
18pub enum QemuExitCode {
19 Success = 0x10,
20 Failed = 0x11,
21}
22
23pub trait Testable {
25 fn run(&self) -> Result<(), KernelError>;
26}
27
28impl<T> Testable for T
29where
30 T: Fn() -> Result<(), KernelError>,
31{
32 fn run(&self) -> Result<(), KernelError> {
33 serial_print!("{}...\t", core::any::type_name::<T>());
34 match self() {
35 Ok(()) => {
36 serial_println!("[ok]");
37 Ok(())
38 }
39 Err(e) => {
40 serial_println!("[failed]: {}", e);
41 Err(e)
42 }
43 }
44 }
45}
46
47#[cfg(test)]
49pub fn test_runner(tests: &[&dyn Testable]) -> ! {
50 serial_println!("Running {} tests", tests.len());
51 let mut passed = 0;
52 let mut failed = 0;
53
54 for test in tests {
55 match test.run() {
56 Ok(()) => passed += 1,
57 Err(e) => {
58 failed += 1;
59 serial_println!("[ERROR] Test failed: {}", e);
60 }
61 }
62 }
63
64 serial_println!("\nTest Results: {} passed, {} failed", passed, failed);
65
66 if failed == 0 {
67 exit_qemu(QemuExitCode::Success);
68 } else {
69 exit_qemu(QemuExitCode::Failed);
70 }
71}
72
73pub fn test_panic_handler(info: &PanicInfo) -> ! {
75 serial_println!("[failed]\n");
76 serial_println!("Error: {}\n", info);
77 exit_qemu(QemuExitCode::Failed);
78}
79
80pub fn exit_qemu(_exit_code: QemuExitCode) -> ! {
82 #[cfg(target_arch = "x86_64")]
83 unsafe {
88 use x86_64::instructions::port::Port;
89 let mut port = Port::new(0xf4);
90 port.write(_exit_code as u32);
91 core::hint::unreachable_unchecked();
92 }
93
94 #[cfg(target_arch = "aarch64")]
95 {
96 const PSCI_SYSTEM_OFF: u32 = 0x84000008;
98 unsafe {
102 core::arch::asm!(
103 "mov w0, {psci_off:w}",
104 "hvc #0",
105 psci_off = in(reg) PSCI_SYSTEM_OFF,
106 options(noreturn)
107 );
108 }
109 }
110
111 #[cfg(target_arch = "riscv64")]
112 {
113 const SBI_SHUTDOWN: usize = 8;
115 unsafe {
120 core::arch::asm!(
121 "li a7, {sbi_shutdown}",
122 "ecall",
123 sbi_shutdown = const SBI_SHUTDOWN,
124 options(noreturn)
125 );
126 }
127 }
128
129 #[cfg(not(any(
130 target_arch = "x86_64",
131 target_arch = "aarch64",
132 target_arch = "riscv64"
133 )))]
134 loop {
135 core::hint::spin_loop();
136 }
137}
138
139#[macro_export]
141macro_rules! kernel_test {
142 ($name:ident, $test:expr) => {
143 #[test_case]
144 const $name: &dyn $crate::test_framework::Testable =
145 &|| -> Result<(), $crate::error::KernelError> { $test };
146 };
147}
148
149#[macro_export]
151macro_rules! test_module {
152 ($name:ident, $($test_name:ident => $test_fn:expr),* $(,)?) => {
153 #[cfg(test)]
154 mod $name {
155 use super::*;
156
157 $(
158 kernel_test!($test_name, $test_fn);
159 )*
160 }
161 };
162}
163
164#[macro_export]
166macro_rules! kernel_assert {
167 ($cond:expr) => {
168 if !$cond {
169 serial_println!("Assertion failed: {}", stringify!($cond));
170 panic!("Assertion failed");
171 }
172 };
173 ($cond:expr, $($arg:tt)*) => {
174 if !$cond {
175 serial_println!($($arg)*);
176 panic!("Assertion failed");
177 }
178 };
179}
180
181#[macro_export]
182macro_rules! kernel_assert_eq {
183 ($left:expr, $right:expr) => {
184 if $left != $right {
185 serial_println!(
186 "Assertion failed: {} != {}\n left: {:?}\n right: {:?}",
187 stringify!($left),
188 stringify!($right),
189 $left,
190 $right
191 );
192 panic!("Assertion failed: not equal");
193 }
194 };
195}
196
197#[macro_export]
198macro_rules! kernel_assert_ne {
199 ($left:expr, $right:expr) => {
200 if $left == $right {
201 serial_println!(
202 "Assertion failed: {} == {}\n left: {:?}\n right: {:?}",
203 stringify!($left),
204 stringify!($right),
205 $left,
206 $right
207 );
208 panic!("Assertion failed: equal");
209 }
210 };
211}
212
213pub trait Benchmark {
219 fn run(&self, iterations: u64) -> Duration;
220 fn warmup(&self, iterations: u64);
221 fn name(&self) -> &'static str;
222}
223
224#[derive(Debug, Clone, Copy)]
226pub struct BenchmarkResult {
227 pub name: &'static str,
228 pub iterations: u64,
229 pub total_time: Duration,
230 pub avg_time_ns: u64,
231 pub min_time_ns: u64,
232 pub max_time_ns: u64,
233}
234
235#[inline(always)]
241pub fn read_timestamp() -> u64 {
242 crate::arch::entropy::read_timestamp()
243}
244
245#[inline(always)]
247pub fn cycles_to_ns(cycles: u64) -> u64 {
248 const CPU_FREQ_GHZ: u64 = 2;
250 cycles / CPU_FREQ_GHZ
251}
252
253pub struct BenchmarkRunner {
255 iterations: u64,
256 warmup_iterations: u64,
257}
258
259impl Default for BenchmarkRunner {
260 fn default() -> Self {
261 Self::new()
262 }
263}
264
265impl BenchmarkRunner {
266 pub const fn new() -> Self {
267 Self {
268 iterations: 1000,
269 warmup_iterations: 100,
270 }
271 }
272
273 pub fn run_benchmark<F>(&self, name: &'static str, mut f: F) -> BenchmarkResult
274 where
275 F: FnMut(),
276 {
277 serial_print!("{}...\t", name);
278
279 for _ in 0..self.warmup_iterations {
281 f();
282 }
283
284 let mut min_cycles = u64::MAX;
286 let mut max_cycles = 0u64;
287 let mut total_cycles = 0u64;
288
289 for _ in 0..self.iterations {
290 let start = read_timestamp();
291 f();
292 let end = read_timestamp();
293 let elapsed = end.saturating_sub(start);
294
295 total_cycles += elapsed;
296 min_cycles = min_cycles.min(elapsed);
297 max_cycles = max_cycles.max(elapsed);
298 }
299
300 let avg_cycles = total_cycles / self.iterations;
301 let result = BenchmarkResult {
302 name,
303 iterations: self.iterations,
304 total_time: Duration::from_nanos(cycles_to_ns(total_cycles)),
305 avg_time_ns: cycles_to_ns(avg_cycles),
306 min_time_ns: cycles_to_ns(min_cycles),
307 max_time_ns: cycles_to_ns(max_cycles),
308 };
309
310 serial_println!("[ok] avg: {} ns", result.avg_time_ns);
311 result
312 }
313}
314
315#[macro_export]
317macro_rules! kernel_bench {
318 ($name:ident, $body:expr) => {
319 #[test]
320 fn $name() {
321 use $crate::test_framework::{cycles_to_ns, read_timestamp, BenchmarkRunner};
322 let runner = BenchmarkRunner::new();
323 let result = runner.run_benchmark(stringify!($name), || $body);
324 serial_println!(
325 " Min: {} ns, Max: {} ns",
326 result.min_time_ns,
327 result.max_time_ns
328 );
329 }
330 };
331}
332
333#[cfg(feature = "alloc")]
340pub struct TestRegistry {
341 tests: Vec<(&'static str, fn())>,
342 benchmarks: Vec<(&'static str, fn())>,
343}
344
345#[cfg(feature = "alloc")]
346impl TestRegistry {
347 pub const fn new() -> Self {
348 Self {
349 tests: Vec::new(),
350 benchmarks: Vec::new(),
351 }
352 }
353
354 pub fn register_test(&mut self, name: &'static str, test: fn()) {
355 self.tests.push((name, test));
356 }
357
358 pub fn register_benchmark(&mut self, name: &'static str, bench: fn()) {
359 self.benchmarks.push((name, bench));
360 }
361
362 pub fn run_all(&self) -> (usize, usize) {
363 let mut passed = 0;
364 let failed = 0;
365
366 serial_println!("Running {} tests", self.tests.len());
367 for (name, test) in &self.tests {
368 serial_print!("{}...\t", name);
369 test();
370 serial_println!("[ok]");
371 passed += 1;
372 }
373
374 if !self.benchmarks.is_empty() {
375 serial_println!("\nRunning {} benchmarks", self.benchmarks.len());
376 for (_name, bench) in &self.benchmarks {
377 bench();
378 }
379 }
380
381 (passed, failed)
382 }
383}
384
385#[cfg(feature = "alloc")]
386static TEST_REGISTRY: spin::Mutex<Option<TestRegistry>> = spin::Mutex::new(None);
387
388#[cfg(feature = "alloc")]
390pub fn init_test_registry() {
391 *TEST_REGISTRY.lock() = Some(TestRegistry::new());
392}
393
394#[cfg(feature = "alloc")]
396pub fn with_test_registry<R, F: FnOnce(&mut TestRegistry) -> R>(f: F) -> Option<R> {
397 TEST_REGISTRY.lock().as_mut().map(f)
398}
399
400#[cfg(feature = "alloc")]
401#[macro_export]
402macro_rules! register_test {
403 ($name:ident) => {
404 #[allow(non_snake_case)]
405 #[used]
406 #[link_section = ".test_registry"]
407 static $name: fn() = || {
408 $crate::test_framework::with_test_registry(|registry| {
409 registry.register_test(stringify!($name), $name);
410 });
411 };
412 };
413}
414
415#[cfg(feature = "alloc")]
423pub fn test_package_install_remove() -> Result<(), KernelError> {
424 use alloc::string::String;
425
426 use crate::pkg::{format::SignaturePolicy, PackageManager, Version};
427
428 let mut pm = PackageManager::new();
429
430 pm.set_signature_policy(SignaturePolicy {
432 require_signatures: false,
433 require_post_quantum: false,
434 ..SignaturePolicy::default()
435 });
436
437 pm.search("nonexistent"); let mut resolver = crate::pkg::resolver::DependencyResolver::new();
442 resolver.register_package(
443 String::from("test-pkg"),
444 Version::new(1, 0, 0),
445 alloc::vec![],
446 alloc::vec![],
447 );
448
449 let mut pm2 = PackageManager::new();
451 pm2.set_signature_policy(SignaturePolicy {
452 require_signatures: false,
453 require_post_quantum: false,
454 ..SignaturePolicy::default()
455 });
456
457 let pkg_id = String::from("test-pkg");
461 let _metadata = crate::pkg::PackageMetadata {
462 name: pkg_id.clone(),
463 version: Version::new(1, 0, 0),
464 author: String::from("test"),
465 description: String::from("test package"),
466 license: String::from("MIT"),
467 dependencies: alloc::vec![],
468 conflicts: alloc::vec![],
469 };
470
471 if pm2.is_installed(&pkg_id) {
473 return Err(KernelError::InvalidState {
474 expected: "package not installed",
475 actual: "package found before install",
476 });
477 }
478
479 let installed_list_before = pm2.list_installed();
485 if !installed_list_before.is_empty() {
486 return Err(KernelError::InvalidState {
487 expected: "empty installed list",
488 actual: "non-empty installed list",
489 });
490 }
491
492 Ok(())
493}
494
495#[cfg(feature = "alloc")]
500pub fn test_package_dependency_resolution() -> Result<(), KernelError> {
501 use alloc::string::String;
502
503 use crate::pkg::{resolver::DependencyResolver, Dependency, Version};
504
505 let mut resolver = DependencyResolver::new();
506
507 resolver.register_package(
509 String::from("app"),
510 Version::new(1, 0, 0),
511 alloc::vec![Dependency {
512 name: String::from("lib-a"),
513 version_req: String::from(">=1.0.0"),
514 }],
515 alloc::vec![],
516 );
517 resolver.register_package(
518 String::from("lib-a"),
519 Version::new(1, 2, 0),
520 alloc::vec![Dependency {
521 name: String::from("lib-b"),
522 version_req: String::from("^1.0"),
523 }],
524 alloc::vec![],
525 );
526 resolver.register_package(
527 String::from("lib-b"),
528 Version::new(1, 1, 0),
529 alloc::vec![],
530 alloc::vec![],
531 );
532
533 let deps = alloc::vec![Dependency {
534 name: String::from("app"),
535 version_req: String::from("*"),
536 }];
537
538 let result = resolver
539 .resolve(&deps)
540 .map_err(|_| KernelError::InvalidState {
541 expected: "successful resolution",
542 actual: "dependency resolution failed",
543 })?;
544
545 if result.len() != 3 {
547 return Err(KernelError::InvalidState {
548 expected: "3 packages in resolution",
549 actual: "wrong number of packages",
550 });
551 }
552
553 let lib_b_pos = result.iter().position(|(p, _)| p == "lib-b");
555 let lib_a_pos = result.iter().position(|(p, _)| p == "lib-a");
556 if let (Some(b_pos), Some(a_pos)) = (lib_b_pos, lib_a_pos) {
557 if b_pos >= a_pos {
558 return Err(KernelError::InvalidState {
559 expected: "lib-b before lib-a",
560 actual: "incorrect dependency order",
561 });
562 }
563 }
564
565 Ok(())
566}
567
568#[cfg(feature = "alloc")]
573pub fn test_package_transaction_rollback() -> Result<(), KernelError> {
574 use crate::pkg::PackageManager;
575
576 let mut pm = PackageManager::new();
577
578 pm.begin_transaction()?;
580
581 let second_begin = pm.begin_transaction();
583 if second_begin.is_ok() {
584 return Err(KernelError::InvalidState {
585 expected: "error on double begin",
586 actual: "double begin succeeded",
587 });
588 }
589
590 pm.rollback_transaction()?;
592
593 let bad_rollback = pm.rollback_transaction();
595 if bad_rollback.is_ok() {
596 return Err(KernelError::InvalidState {
597 expected: "error on rollback without transaction",
598 actual: "rollback succeeded without transaction",
599 });
600 }
601
602 pm.begin_transaction()?;
604 pm.commit_transaction()?;
605
606 Ok(())
607}
608
609#[cfg(feature = "alloc")]
614pub fn test_toml_parsing() -> Result<(), KernelError> {
615 use crate::pkg::toml_parser::{parse_toml, TomlValue};
616
617 let toml_content = "\
618name = \"test-package\"\nversion = \"1.2.3\"\nenabled = true\ncount = 42\n\n[build]\ntype = \
619 \"cmake\"\njobs = 4\n";
620
621 let parsed = parse_toml(toml_content)?;
622
623 match parsed.get("name") {
625 Some(TomlValue::String(s)) if s == "test-package" => {}
626 _ => {
627 return Err(KernelError::InvalidState {
628 expected: "name = test-package",
629 actual: "missing or wrong name",
630 });
631 }
632 }
633
634 match parsed.get("enabled") {
636 Some(TomlValue::Boolean(true)) => {}
637 _ => {
638 return Err(KernelError::InvalidState {
639 expected: "enabled = true",
640 actual: "missing or wrong enabled",
641 });
642 }
643 }
644
645 match parsed.get("count") {
647 Some(TomlValue::Integer(42)) => {}
648 _ => {
649 return Err(KernelError::InvalidState {
650 expected: "count = 42",
651 actual: "missing or wrong count",
652 });
653 }
654 }
655
656 match parsed.get("build") {
658 Some(TomlValue::Table(table)) => match table.get("type") {
659 Some(TomlValue::String(s)) if s == "cmake" => {}
660 _ => {
661 return Err(KernelError::InvalidState {
662 expected: "build.type = cmake",
663 actual: "missing or wrong build.type",
664 });
665 }
666 },
667 _ => {
668 return Err(KernelError::InvalidState {
669 expected: "[build] section",
670 actual: "missing build section",
671 });
672 }
673 }
674
675 Ok(())
676}
677
678#[cfg(feature = "alloc")]
683pub fn test_package_search() -> Result<(), KernelError> {
684 use alloc::string::String;
685
686 use crate::pkg::{resolver::DependencyResolver, Version};
687
688 let mut resolver = DependencyResolver::new();
689
690 resolver.register_package(
691 String::from("libfoo"),
692 Version::new(1, 0, 0),
693 alloc::vec![],
694 alloc::vec![],
695 );
696 resolver.register_package(
697 String::from("libbar"),
698 Version::new(2, 0, 0),
699 alloc::vec![],
700 alloc::vec![],
701 );
702 resolver.register_package(
703 String::from("my-app"),
704 Version::new(0, 1, 0),
705 alloc::vec![],
706 alloc::vec![],
707 );
708
709 let results = resolver.search("lib");
711 if results.len() != 2 {
712 return Err(KernelError::InvalidState {
713 expected: "2 search results for 'lib'",
714 actual: "wrong number of results",
715 });
716 }
717
718 let results = resolver.search("app");
720 if results.len() != 1 {
721 return Err(KernelError::InvalidState {
722 expected: "1 search result for 'app'",
723 actual: "wrong number of results",
724 });
725 }
726
727 let results = resolver.search("nonexistent");
729 if !results.is_empty() {
730 return Err(KernelError::InvalidState {
731 expected: "0 search results",
732 actual: "unexpected results found",
733 });
734 }
735
736 Ok(())
737}
738
739#[cfg(feature = "alloc")]
744pub fn test_version_comparison() -> Result<(), KernelError> {
745 use crate::pkg::Version;
746
747 let v100 = Version::new(1, 0, 0);
748 let v110 = Version::new(1, 1, 0);
749 let v111 = Version::new(1, 1, 1);
750 let v200 = Version::new(2, 0, 0);
751 let v010 = Version::new(0, 1, 0);
752
753 if v100 >= v110 {
755 return Err(KernelError::InvalidState {
756 expected: "1.0.0 < 1.1.0",
757 actual: "ordering failed",
758 });
759 }
760 if v110 >= v111 {
761 return Err(KernelError::InvalidState {
762 expected: "1.1.0 < 1.1.1",
763 actual: "ordering failed",
764 });
765 }
766 if v111 >= v200 {
767 return Err(KernelError::InvalidState {
768 expected: "1.1.1 < 2.0.0",
769 actual: "ordering failed",
770 });
771 }
772 if v010 >= v100 {
773 return Err(KernelError::InvalidState {
774 expected: "0.1.0 < 1.0.0",
775 actual: "ordering failed",
776 });
777 }
778
779 let v100_dup = Version::new(1, 0, 0);
781 if v100 != v100_dup {
782 return Err(KernelError::InvalidState {
783 expected: "1.0.0 == 1.0.0",
784 actual: "equality failed",
785 });
786 }
787
788 let v900 = Version::new(9, 0, 0);
790 if v200 >= v900 {
791 return Err(KernelError::InvalidState {
792 expected: "2.0.0 < 9.0.0",
793 actual: "major version ordering failed",
794 });
795 }
796
797 Ok(())
798}
799
800#[cfg(feature = "alloc")]
806pub fn test_pkg_delta_compute_apply() -> Result<(), KernelError> {
807 use crate::pkg::delta::{apply_delta, compute_delta};
808
809 let old_data = b"Hello, World! This is the original content of the package.";
810 let new_data = b"Hello, World! This is the updated content of the package.";
811
812 let delta = compute_delta(old_data, new_data);
813 let result = apply_delta(old_data, &delta)?;
814
815 if result.as_slice() != new_data {
816 return Err(KernelError::InvalidState {
817 expected: "delta apply matches new data",
818 actual: "delta apply produced different output",
819 });
820 }
821
822 if delta.operations.is_empty() {
824 return Err(KernelError::InvalidState {
825 expected: "non-empty delta operations",
826 actual: "empty delta",
827 });
828 }
829
830 Ok(())
831}
832
833#[cfg(feature = "alloc")]
839pub fn test_pkg_reproducible_manifest() -> Result<(), KernelError> {
840 use alloc::string::String;
841
842 use crate::pkg::reproducible::{verify_reproducible, BuildInputs, BuildManifest, BuildOutputs};
843
844 let mut inputs = BuildInputs::new();
846 inputs
847 .source_hashes
848 .push((String::from("main.rs"), [0xAB; 32]));
849
850 let mut outputs = BuildOutputs::new();
851 outputs
852 .file_hashes
853 .push((String::from("output.bin"), [0xCD; 32]));
854 outputs.total_size = 1024;
855 outputs.file_count = 1;
856
857 let manifest_a = BuildManifest {
858 port_name: String::from("test-port"),
859 port_version: String::from("1.0.0"),
860 inputs: inputs.clone(),
861 outputs: outputs.clone(),
862 build_duration_ms: 100,
863 };
864 let manifest_b = BuildManifest {
865 port_name: String::from("test-port"),
866 port_version: String::from("1.0.0"),
867 inputs,
868 outputs,
869 build_duration_ms: 200, };
871
872 let result = verify_reproducible(&manifest_a, &manifest_b);
873 if !result.is_reproducible() {
874 return Err(KernelError::InvalidState {
875 expected: "builds are reproducible",
876 actual: "reproducibility check failed",
877 });
878 }
879
880 Ok(())
881}
882
883#[cfg(feature = "alloc")]
889pub fn test_pkg_license_detection() -> Result<(), KernelError> {
890 use crate::pkg::compliance::{detect_license, License, LicenseCompatibility};
891
892 let mit_text = "Permission is hereby granted, free of charge, to any person obtaining a copy \
894 of this software and associated documentation files";
895 let detected = detect_license(mit_text);
896 if detected != License::MIT {
897 return Err(KernelError::InvalidState {
898 expected: "MIT license detected",
899 actual: "wrong license detected",
900 });
901 }
902
903 let gpl_text = "This program is free software: you can redistribute it and/or modify it under \
905 the terms of the GNU General Public License";
906 let detected = detect_license(gpl_text);
907 if detected != License::GPL2 && detected != License::GPL3 {
909 return Err(KernelError::InvalidState {
910 expected: "GPL license detected",
911 actual: "wrong license detected",
912 });
913 }
914
915 if !LicenseCompatibility::is_compatible(&License::MIT, &License::Apache2) {
917 return Err(KernelError::InvalidState {
918 expected: "MIT compatible with Apache-2.0",
919 actual: "compatibility check failed",
920 });
921 }
922
923 Ok(())
924}
925
926#[cfg(feature = "alloc")]
932pub fn test_pkg_security_scan() -> Result<(), KernelError> {
933 use crate::pkg::testing::{PackageSecurityScanner, ScanSeverity};
934
935 let scanner = PackageSecurityScanner::new();
936
937 let paths = ["/etc/shadow", "/usr/bin/app", "/dev/mem"];
939 let findings = scanner.scan_paths(&paths);
940
941 let has_high = findings
943 .iter()
944 .any(|f| matches!(f.severity, ScanSeverity::High | ScanSeverity::Critical));
945
946 if !has_high {
947 return Err(KernelError::InvalidState {
948 expected: "high severity finding for suspicious paths",
949 actual: "no high severity findings",
950 });
951 }
952
953 let caps = ["CAP_SYS_ADMIN", "CAP_NET_RAW"];
955 let cap_findings = scanner.scan_capabilities(&caps);
956
957 if cap_findings.is_empty() {
958 return Err(KernelError::InvalidState {
959 expected: "findings for excessive capabilities",
960 actual: "no capability findings",
961 });
962 }
963
964 Ok(())
965}
966
967#[cfg(feature = "alloc")]
973pub fn test_pkg_ecosystem_definitions() -> Result<(), KernelError> {
974 use crate::pkg::ecosystem::{
975 get_base_system_packages, get_driver_packages, get_essential_apps,
976 };
977
978 let base = get_base_system_packages();
980 if base.len() < 3 {
981 return Err(KernelError::InvalidState {
982 expected: "at least 3 base system package sets",
983 actual: "too few base package sets",
984 });
985 }
986
987 let apps = get_essential_apps();
989 if apps.is_empty() {
990 return Err(KernelError::InvalidState {
991 expected: "non-empty essential apps list",
992 actual: "empty essential apps",
993 });
994 }
995
996 let drivers = get_driver_packages("x86_64");
998 if drivers.is_empty() {
999 return Err(KernelError::InvalidState {
1000 expected: "non-empty x86_64 driver packages",
1001 actual: "empty driver packages",
1002 });
1003 }
1004
1005 for set in &base {
1007 if set.name.is_empty() {
1008 return Err(KernelError::InvalidState {
1009 expected: "non-empty package set name",
1010 actual: "empty name",
1011 });
1012 }
1013 if set.packages.is_empty() {
1014 return Err(KernelError::InvalidState {
1015 expected: "non-empty packages in set",
1016 actual: "empty packages",
1017 });
1018 }
1019 }
1020
1021 Ok(())
1022}
1023
1024pub fn test_shell_ansi_parser() -> Result<(), KernelError> {
1031 use crate::services::shell::ansi::{AnsiEvent, AnsiParser};
1032
1033 let mut parser = AnsiParser::new();
1034
1035 let r1 = parser.feed(0x1B); let r2 = parser.feed(b'[');
1038 let r3 = parser.feed(b'A');
1039
1040 if r1.is_some() || r2.is_some() {
1042 return Err(KernelError::InvalidState {
1043 expected: "no event for partial escape sequence",
1044 actual: "event emitted too early",
1045 });
1046 }
1047 match r3 {
1048 Some(AnsiEvent::ArrowUp) => {}
1049 _ => {
1050 return Err(KernelError::InvalidState {
1051 expected: "ArrowUp event",
1052 actual: "wrong or missing event",
1053 });
1054 }
1055 }
1056
1057 parser.feed(0x1B);
1059 parser.feed(b'[');
1060 match parser.feed(b'B') {
1061 Some(AnsiEvent::ArrowDown) => {}
1062 _ => {
1063 return Err(KernelError::InvalidState {
1064 expected: "ArrowDown event",
1065 actual: "wrong or missing event",
1066 });
1067 }
1068 }
1069
1070 match parser.feed(b'x') {
1072 Some(AnsiEvent::Char(b'x')) => {}
1073 _ => {
1074 return Err(KernelError::InvalidState {
1075 expected: "Char('x') event",
1076 actual: "wrong or missing event",
1077 });
1078 }
1079 }
1080
1081 Ok(())
1082}
1083
1084#[cfg(feature = "alloc")]
1089pub fn test_shell_variable_expansion() -> Result<(), KernelError> {
1090 use alloc::{collections::BTreeMap, string::String};
1091
1092 use crate::services::shell::expand::expand_variables;
1093
1094 let mut env = BTreeMap::new();
1095 env.insert(String::from("HOME"), String::from("/root"));
1096 env.insert(String::from("USER"), String::from("admin"));
1097
1098 let result = expand_variables("hello $USER", &env, 0);
1100 if !result.contains("admin") {
1101 return Err(KernelError::InvalidState {
1102 expected: "hello admin",
1103 actual: "variable not expanded",
1104 });
1105 }
1106
1107 let result = expand_variables("${MISSING:-fallback}", &env, 0);
1109 if !result.contains("fallback") {
1110 return Err(KernelError::InvalidState {
1111 expected: "fallback value",
1112 actual: "default not applied",
1113 });
1114 }
1115
1116 let result = expand_variables("~/docs", &env, 0);
1118 if !result.contains("/root") {
1119 return Err(KernelError::InvalidState {
1120 expected: "/root/docs",
1121 actual: "tilde not expanded",
1122 });
1123 }
1124
1125 Ok(())
1126}
1127
1128pub fn test_shell_glob_match() -> Result<(), KernelError> {
1133 use crate::services::shell::glob::glob_match;
1134
1135 if !glob_match("*.txt", "hello.txt") {
1137 return Err(KernelError::InvalidState {
1138 expected: "*.txt matches hello.txt",
1139 actual: "no match",
1140 });
1141 }
1142
1143 if !glob_match("?.rs", "a.rs") {
1145 return Err(KernelError::InvalidState {
1146 expected: "?.rs matches a.rs",
1147 actual: "no match",
1148 });
1149 }
1150 if glob_match("?.rs", "ab.rs") {
1151 return Err(KernelError::InvalidState {
1152 expected: "?.rs should NOT match ab.rs",
1153 actual: "false match",
1154 });
1155 }
1156
1157 if !glob_match("[abc].rs", "b.rs") {
1159 return Err(KernelError::InvalidState {
1160 expected: "[abc].rs matches b.rs",
1161 actual: "no match",
1162 });
1163 }
1164
1165 if glob_match("*.txt", "hello.rs") {
1167 return Err(KernelError::InvalidState {
1168 expected: "*.txt should NOT match hello.rs",
1169 actual: "false match",
1170 });
1171 }
1172
1173 Ok(())
1174}
1175
1176#[cfg(feature = "alloc")]
1181pub fn test_shell_pipe_roundtrip() -> Result<(), KernelError> {
1182 use crate::fs::pipe::create_pipe;
1183
1184 let (reader, writer) = create_pipe()?;
1185
1186 let data = b"hello pipe";
1188 let written = writer.write(data)?;
1189 if written != data.len() {
1190 return Err(KernelError::InvalidState {
1191 expected: "wrote all bytes",
1192 actual: "partial write",
1193 });
1194 }
1195
1196 let mut buf = [0u8; 32];
1198 let read_count = reader.read(&mut buf)?;
1199 if read_count != data.len() {
1200 return Err(KernelError::InvalidState {
1201 expected: "read all bytes",
1202 actual: "wrong read count",
1203 });
1204 }
1205
1206 if &buf[..read_count] != data {
1207 return Err(KernelError::InvalidState {
1208 expected: "data matches",
1209 actual: "data mismatch",
1210 });
1211 }
1212
1213 Ok(())
1214}
1215
1216#[cfg(feature = "alloc")]
1221pub fn test_shell_redirect_parse() -> Result<(), KernelError> {
1222 use alloc::{string::String, vec};
1223
1224 use crate::services::shell::redirect::{parse_redirections, Redirection};
1225
1226 let tokens = vec![
1227 String::from("echo"),
1228 String::from("hello"),
1229 String::from(">"),
1230 String::from("/tmp/out.txt"),
1231 ];
1232
1233 let (cmd_tokens, redirections) = parse_redirections(&tokens);
1234
1235 if cmd_tokens.len() != 2 {
1237 return Err(KernelError::InvalidState {
1238 expected: "2 command tokens",
1239 actual: "wrong token count",
1240 });
1241 }
1242
1243 if redirections.len() != 1 {
1245 return Err(KernelError::InvalidState {
1246 expected: "1 redirection",
1247 actual: "wrong redirection count",
1248 });
1249 }
1250
1251 match &redirections[0] {
1252 Redirection::StdoutTo(path) if path == "/tmp/out.txt" => {}
1253 _ => {
1254 return Err(KernelError::InvalidState {
1255 expected: "StdoutTo(/tmp/out.txt)",
1256 actual: "wrong redirection type",
1257 });
1258 }
1259 }
1260
1261 Ok(())
1262}
1263
1264pub fn run_with_timeout<F>(f: F, timeout_cycles: u64) -> Result<(), KernelError>
1270where
1271 F: FnOnce(),
1272{
1273 let start = read_timestamp();
1274 f();
1275 let end = read_timestamp();
1276
1277 if end.saturating_sub(start) > timeout_cycles {
1278 Err(KernelError::Timeout {
1279 operation: "test execution",
1280 duration_ms: timeout_cycles / 2_000_000, })
1282 } else {
1283 Ok(())
1284 }
1285}
1286
1287#[macro_export]
1288macro_rules! test_timeout {
1289 ($timeout_ms:expr, $body:expr) => {{
1290 use $crate::test_framework::run_with_timeout;
1291 let timeout_cycles = $timeout_ms * 2_000_000; run_with_timeout(|| $body, timeout_cycles)
1294 }};
1295}