Introduce a test framework for Asterinas NixOS

This commit is contained in:
Chen Chengjun 2026-01-13 09:35:53 +00:00 committed by Tate, Hongliang Tian
parent bae5de9e8f
commit c439df3d02
20 changed files with 1309 additions and 10 deletions

3
.gitignore vendored
View File

@ -35,3 +35,6 @@ distro/result
# cachix package list # cachix package list
cachix.list cachix.list
# temporary configuration file for NixOS tests
distro/etc_nixos/_config_for_test.nix

View File

@ -1,6 +1,6 @@
{ disable-systemd ? "false", stage-2-hook ? "/bin/sh -l", log-level ? "error" { disable-systemd ? "false", stage-2-hook ? "/bin/sh -l", log-level ? "error"
, console ? "hvc0", test-command ? "", extra-substituters ? "" , console ? "hvc0", extra-substituters ? "", extra-trusted-public-keys ? ""
, extra-trusted-public-keys ? "", pkgs ? import <nixpkgs> { } }: , config-file-name ? "configuration.nix", pkgs ? import <nixpkgs> { } }:
let let
aster-kernel = builtins.path { aster-kernel = builtins.path {
name = "aster-kernel-osdk-bin"; name = "aster-kernel-osdk-bin";
@ -16,7 +16,6 @@ let
aster-stage-2-hook = stage-2-hook; aster-stage-2-hook = stage-2-hook;
aster-log-level = log-level; aster-log-level = log-level;
aster-console = console; aster-console = console;
aster-test-command = test-command;
aster-substituters = extra-substituters; aster-substituters = extra-substituters;
aster-trusted-public-keys = extra-trusted-public-keys; aster-trusted-public-keys = extra-trusted-public-keys;
}; };
@ -38,7 +37,7 @@ in pkgs.stdenv.mkDerivation {
mkdir -p $out/{bin,etc_nixos} mkdir -p $out/{bin,etc_nixos}
cp ${install_aster_nixos} $out/bin/install_aster_nixos.sh cp ${install_aster_nixos} $out/bin/install_aster_nixos.sh
cp -L ${aster_configuration} $out/etc_nixos/aster_configuration.nix cp -L ${aster_configuration} $out/etc_nixos/aster_configuration.nix
cp -L ${etc-nixos}/configuration.nix $out/etc_nixos/configuration.nix cp -L ${etc-nixos}/${config-file-name} $out/etc_nixos/configuration.nix
cp -r ${etc-nixos}/modules $out/etc_nixos/modules cp -r ${etc-nixos}/modules $out/etc_nixos/modules
cp -r ${etc-nixos}/overlays $out/etc_nixos/overlays cp -r ${etc-nixos}/overlays $out/etc_nixos/overlays
ln -s ${aster-kernel} $out/kernel ln -s ${aster-kernel} $out/kernel

View File

@ -1,8 +1,9 @@
{ pkgs ? import <nixpkgs> { }, autoInstall ? false, test-command ? "" { pkgs ? import <nixpkgs> { }, autoInstall ? false, extra-substituters ? ""
, extra-substituters ? "", extra-trusted-public-keys ? "", version ? "", ... }: , config-file-name ? "configuration.nix", extra-trusted-public-keys ? ""
, version ? "", ... }:
let let
installer = pkgs.callPackage ../aster_nixos_installer { installer = pkgs.callPackage ../aster_nixos_installer {
inherit test-command extra-substituters extra-trusted-public-keys; inherit extra-substituters extra-trusted-public-keys config-file-name;
}; };
configuration = { configuration = {
imports = [ imports = [

20
test/README.md Normal file
View File

@ -0,0 +1,20 @@
# Test Suites
This directory contains the testing infrastructure for Asterinas, organized into two complementary testing approaches.
## Test Types
### Initramfs-Based Tests ([`initramfs/`](initramfs/))
Tests running in a minimal initramfs environment. Best for:
- System call validation
- Core functionality testing
- Performance benchmarks
See [`initramfs/README.md`](initramfs/README.md) for details.
### NixOS-Based Tests ([`nixos/`](nixos/))
Tests running in NixOS environments.
See [`nixos/README.md`](nixos/README.md) for details.

332
test/nixos/Cargo.lock generated Normal file
View File

@ -0,0 +1,332 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "comma"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "inventory"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e"
dependencies = [
"rustversion",
]
[[package]]
name = "libc"
version = "0.2.179"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nixos-test-framework"
version = "0.1.0"
dependencies = [
"inventory",
"nixos-test-macro",
"rexpect",
"strip-ansi-escapes",
]
[[package]]
name = "nixos-test-macro"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "proc-macro2"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rexpect"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c1bcd4ac488e9d2d726d147031cceff5cff6425011ff1914049739770fa4726"
dependencies = [
"comma",
"nix",
"regex",
"tempfile",
"thiserror",
]
[[package]]
name = "rustix"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "strip-ansi-escapes"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025"
dependencies = [
"vte",
]
[[package]]
name = "syn"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "test-hello"
version = "0.1.0"
dependencies = [
"nixos-test-framework",
]
[[package]]
name = "test-nix"
version = "0.1.0"
dependencies = [
"nixos-test-framework",
]
[[package]]
name = "test-podman"
version = "0.1.0"
dependencies = [
"nixos-test-framework",
]
[[package]]
name = "thiserror"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "vte"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077"
dependencies = [
"memchr",
]
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"

19
test/nixos/Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[workspace]
resolver = "2"
members = [
"common/framework", "common/test_macro",
"tests/*",
]
[workspace.package]
version = "0.1.0"
repository = "https://github.com/asterinas/asterinas"
license = "MPL-2.0"
edition = "2024"
[workspace.dependencies]
inventory = "0.3"
rexpect = "0.6"
strip-ansi-escapes = "0.2"
nixos-test-framework = { path = "common/framework" }
nixos-test-macro = { path = "common/test_macro" }

97
test/nixos/Makefile Normal file
View File

@ -0,0 +1,97 @@
# SPDX-License-Identifier: MPL-2.0
NIXOS_TEST_SUITE ?=
NIXOS_TEST_CASE ?=
NIXOS_TEST_TIMEOUT ?= 5min
# Generated configuration file name
TEST_CONFIG_FILE_NAME := _configuration_for_test.nix
# Paths
MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
ASTERINAS_ROOT := $(MAKEFILE_DIR)/../..
NIXOS_CONFIG_DIR := $(ASTERINAS_ROOT)/distro/etc_nixos
BASE_CONFIG := $(NIXOS_CONFIG_DIR)/configuration.nix
TEST_CONFIG := $(NIXOS_CONFIG_DIR)/$(TEST_CONFIG_FILE_NAME)
MERGE_SCRIPT := $(ASTERINAS_ROOT)/test/nixos/common/merge_nixos_config.sh
# NixOS build and run scripts
BUILD_ISO_SCRIPT := $(ASTERINAS_ROOT)/tools/nixos/build_iso.sh
BUILD_NIXOS_SCRIPT := $(ASTERINAS_ROOT)/tools/nixos/build_nixos.sh
RUN_SCRIPT := $(ASTERINAS_ROOT)/tools/nixos/run.sh
# Test directory structure
TEST_DIR := tests/$(NIXOS_TEST_SUITE)
EXTRA_CONFIG := $(TEST_DIR)/extra_config.nix
.PHONY: check-test-name
check-test-name:
@if [ -z "$(NIXOS_TEST_SUITE)" ]; then \
echo "Error: NIXOS_TEST_SUITE is not set"; \
echo "Usage: make [target] NIXOS_TEST_SUITE=<test_name>"; \
exit 1; \
fi
@if [ ! -d "$(TEST_DIR)" ]; then \
echo "Error: Test directory '$(TEST_DIR)' does not exist"; \
exit 1; \
fi
.PHONY: prepare
prepare: check-test-name
@echo "==> Preparing configuration for test '$(NIXOS_TEST_SUITE)'..."
@if [ -f "$(EXTRA_CONFIG)" ]; then \
echo " Found extra config: $(EXTRA_CONFIG)"; \
echo " Merging configurations..."; \
bash "$(MERGE_SCRIPT)" "$(BASE_CONFIG)" "$(EXTRA_CONFIG)" "$(TEST_CONFIG)"; \
if [ $$? -ne 0 ]; then \
echo "Error: Configuration merge failed"; \
rm "$(TEST_CONFIG)"; \
exit 1; \
fi; \
else \
echo " No extra config found, using base configuration"; \
cp "$(BASE_CONFIG)" "$(TEST_CONFIG)"; \
fi
.PHONY: iso
iso: prepare
@echo "==> Building ISO installer for test '$(NIXOS_TEST_NAME)'..."
@echo " Building ISO installer...";
@bash "$(BUILD_ISO_SCRIPT)" "$(TEST_CONFIG_FILE_NAME)";
@if [ $$? -ne 0 ]; then \
echo "Error: ISO build failed"; \
rm "$(TEST_CONFIG)"; \
exit 1; \
fi
@rm "$(TEST_CONFIG)";
.PHONY: nixos
nixos: prepare
@echo "==> Building NixOS image for test '$(NIXOS_TEST_NAME)'..."
@bash "$(BUILD_NIXOS_SCRIPT)" "$(TEST_CONFIG_FILE_NAME)";
@if [ $$? -ne 0 ]; then \
echo "Error: NixOS build failed"; \
rm "$(TEST_CONFIG)"; \
exit 1; \
fi
@rm "$(TEST_CONFIG)";
.PHONY: run_nixos
run_nixos: check-test-name
@cd "$(TEST_DIR)" && \
TEST_CASE_ARG=""; \
if [ -n "$(NIXOS_TEST_CASE)" ]; then \
TEST_CASE_ARG="--test $(NIXOS_TEST_CASE)"; \
fi; \
QEMU_CMD="bash $(RUN_SCRIPT) nixos"; \
cargo run -- --qemu-cmd "$$QEMU_CMD" $$TEST_CASE_ARG || \
exit 1
.PHONY: check
check:
@cargo clippy -- -D warnings
.PHONY: format
format:
@cargo fmt
@nixfmt .

137
test/nixos/README.md Normal file
View File

@ -0,0 +1,137 @@
# NixOS-Based Test Suites
This directory contains NixOS-based tests and a framework for writing and running them. The framework executes tests by interacting with a live instance of the operating system in a virtual environment. Thanks to this interactive design, the framework can test virtually any behavior that a real user could trigger through a terminal. It also offers a simple, imperative API, making it easy to write and maintain these interactive test scenarios.
## Directory Structure
```
test/nixos/
├── common/
│ ├── template/ # Template for creating new tests
│ └── ... # Core implementation of the framework
├── tests/
│ ├── podman/ # A real test crate
│ │ ├── Cargo.toml
│ │ ├── extra_config.nix # (Optional) Additional NixOS configuration
│ │ └── src/
│ │ └── main.rs
│ └── ... # Other tests
└── Makefile
```
## Creating a New Test
### Step 1: Copy the Template
```bash
cd test/nixos
cp -r common/template tests/my-test
```
### Step 2: Update `Cargo.toml`
Replace `<test_name>` with your test name:
### Step 3: Implement Your Tests
Edit `src/main.rs`:
```rust
// SPDX-License-Identifier: MPL-2.0
use nixos_test_framework::*;
use nixos_test_macro::nixos_test;
// This macro generates the main function that runs all registered tests
nixos_test_main!();
// Register a test case using the #[nixos_test] attribute
#[nixos_test]
fn basic_command_test(nixos_shell: &mut Session) -> Result<(), Error> {
nixos_shell.run_cmd("echo 'Hello, World!'")?;
nixos_shell.run_cmd_and_expect("cat /etc/os-release", "NixOS")?;
Ok(())
}
// You can define multiple test cases in the same file
#[nixos_test]
fn file_operations_test(nixos_shell: &mut Session) -> Result<(), Error> {
nixos_shell.run_cmd("touch /tmp/test.txt")?;
nixos_shell.run_cmd_and_expect("ls /tmp", "test.txt")?;
Ok(())
}
```
The `Session` type provides APIs for interacting with the VM. See the [Session API documentation](common/framework/src/session.rs) for details.
### Step 4: (Optional) Configure NixOS
If your test requires additional packages or system configuration, edit `extra_config.nix`:
```nix
{ config, lib, pkgs, ... }:
{
environment.systemPackages = with pkgs; [
# Add required packages here
vim
git
];
# Configure system services
virtualisation.podman.enable = true;
}
```
This content of this file will be merged with the [default configuration file](../../distro/etc_nixos/configuration.nix) to generate the final configuration file for the testing Asterinas NixOS system.
## Running Tests
The following commands should be run under the project root.
### Build Test Image
```bash
# Build NixOS image for a test suite
make nixos NIXOS_TEST_SUITE=my-test
# Or build using ISO installer workflow
make iso NIXOS_TEST_SUITE=my-test
make run_iso
```
### Run Tests
```bash
# Run all tests in the suite
make run_nixos NIXOS_TEST_SUITE=my-test
# Run a specific test case
make run_nixos NIXOS_TEST_SUITE=my-test NIXOS_TEST_CASE=basic_command_test
# Customize timeout with units (default: 5min)
make run_nixos NIXOS_TEST_SUITE=my-test NIXOS_TEST_TIMEOUT=10min # 10 minutes
make run_nixos NIXOS_TEST_SUITE=my-test NIXOS_TEST_TIMEOUT=600s # 600 seconds
make run_nixos NIXOS_TEST_SUITE=my-test NIXOS_TEST_TIMEOUT=600000ms # 600000 milliseconds
```
### Complete Workflow Examples
```bash
# Quick test
make nixos NIXOS_TEST_SUITE=my-test && make run_nixos NIXOS_TEST_SUITE=my-test
# Test with ISO installer
make iso NIXOS_TEST_SUITE=my-test && make run_iso && make run_nixos NIXOS_TEST_SUITE=my-test
# Run specific test with custom timeout (10 minutes)
make nixos NIXOS_TEST_SUITE=podman
make run_nixos NIXOS_TEST_SUITE=podman NIXOS_TEST_CASE=container_basic_test NIXOS_TEST_TIMEOUT=10min
```
## Environment Variables
- **`NIXOS_TEST_SUITE`**: Name of the test suite to run (required for test mode)
- **`NIXOS_TEST_CASE`**: Specific test case to run (optional, runs all if not specified)
- **`NIXOS_TEST_TIMEOUT`**: Timeout for command execution with unit suffix (optional, default: 5min)
- Supported formats: `<number>ms` (milliseconds), `<number>s` (seconds), `<number>min` (minutes)
- Examples: `300000ms`, `300s`, `5min`

View File

@ -0,0 +1,12 @@
[package]
name = "nixos-test-framework"
version.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
[dependencies]
inventory.workspace = true
nixos-test-macro.workspace = true
rexpect.workspace = true
strip-ansi-escapes.workspace = true

View File

@ -0,0 +1,232 @@
// SPDX-License-Identifier: MPL-2.0
//! An imperative testing framework for NixOS-based tests.
//!
//! # Core Concepts
//!
//! ## Test Registration
//!
//! Use the `#[nixos_test]` attribute to register test cases. The framework
//! automatically discovers and runs all registered tests.
//!
//! ## Session Interaction
//!
//! Tests are implemented by interacting with a [`Session`] object. The [`Session`] type
//! provides methods for executing commands and verifying output. It supports nested execution
//! contexts (containers, SSH, etc.) with automatic cleanup.
//!
//! See the [template crate](https://github.com/asterinas/asterinas/tree/main/test/nixos/common/template)
//! for a usage example.
//!
//! See the [project README](https://github.com/asterinas/asterinas/tree/main/test/nixos)
//! for complete documentation on creating and running test suites.
use std::env;
pub use inventory;
pub use nixos_test_macro::nixos_test;
pub use rexpect::error::Error;
pub use session::{Session, SessionDesc};
mod session;
/// A test case definition.
pub struct TestCase {
pub name: &'static str,
pub test_fn: fn(&mut Session) -> Result<(), Error>,
}
inventory::collect!(TestCase);
/// Generates the main function that runs all test cases.
#[macro_export]
macro_rules! nixos_test_main {
() => {
fn main() -> Result<(), Box<dyn std::error::Error>> {
$crate::__nixos_test_main()
}
};
}
#[doc(hidden)]
pub fn __nixos_test_main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
// Check for --help flag
if args.contains(&"--help".to_string()) || args.contains(&"-h".to_string()) {
print_help();
return Ok(());
}
// Parse --qemu-cmd argument
let qemu_cmd = parse_arg(&args, "--qemu-cmd").ok_or("Missing --qemu-cmd argument")?;
// Parse optional --test argument
let test_filter = parse_arg(&args, "--test");
// Parse timeout from NIXOS_TEST_TIMEOUT environment variable
let timeout_ms = env::var("NIXOS_TEST_TIMEOUT")
.ok()
.map(|v| parse_timeout(&v))
.transpose()?
.unwrap_or(300_000); // Default: 5 minutes
let all_test_cases: Vec<&TestCase> = inventory::iter::<TestCase>().collect();
if all_test_cases.is_empty() {
return Err("No test cases found".into());
}
// Filter test cases if --test is specified
let test_cases: Vec<&TestCase> = if let Some(ref filter) = test_filter {
let filtered: Vec<&TestCase> = all_test_cases
.into_iter()
.filter(|tc| tc.name == filter)
.collect();
if filtered.is_empty() {
return Err(format!("Test case '{}' not found", filter).into());
}
filtered
} else {
all_test_cases
};
if let Some(ref filter) = test_filter {
println!("=== Running single test case: {} ===", filter);
} else {
println!("=== Found {} test case(s) ===", test_cases.len());
for tc in &test_cases {
println!(" - test_{}", tc.name);
}
println!();
}
let mut session = rexpect::spawn(&qemu_cmd, Some(timeout_ms))?;
println!("--> Waiting for login prompt...");
let init_prompt = "root@asterinas:";
session.exp_string(init_prompt)?;
let desc = SessionDesc::new()
.expect_prompt(init_prompt)
.cmd_to_enter("")
.cmd_to_exit("poweroff");
let mut session = Session::new(desc, session);
let mut passed = 0;
let mut failed = 0;
let mut failed_tests = Vec::new();
for test_case in test_cases {
println!("=== Running test case: {} ===", test_case.name);
match session.run(test_case.test_fn) {
Ok(_) => {
println!("✓ Test case 'test_{}' passed\n", test_case.name);
passed += 1;
}
Err(_) => {
println!("✗ Test case 'test_{}' failed\n", test_case.name);
failed += 1;
failed_tests.push(test_case.name);
}
}
}
println!("=== Test Summary ===");
println!("Passed: {}", passed);
println!("Failed: {}", failed);
let res = if !failed_tests.is_empty() {
println!("\nFailed tests:");
for name in failed_tests {
println!(" - test_{}", name);
}
Err("Some tests failed")
} else {
Ok(())
};
let shutdown_res = session.shutdown();
res?;
shutdown_res?;
Ok(())
}
/// Parses timeout string with units into milliseconds.
///
/// Supports formats:
/// - `<number>ms` - milliseconds
/// - `<number>s` - seconds
/// - `<number>min` - minutes
///
/// # Examples
///
/// ```rust
/// parse_timeout("300000ms") // Ok(300000)
/// parse_timeout("300s") // Ok(300000)
/// parse_timeout("5min") // Ok(300000)
/// ```
fn parse_timeout(timeout_str: &str) -> Result<u64, Box<dyn std::error::Error>> {
let timeout_str = timeout_str.trim();
if let Some(ms) = timeout_str.strip_suffix("ms") {
return Ok(ms.trim().parse()?);
}
if let Some(s) = timeout_str.strip_suffix('s') {
let seconds: u64 = s.trim().parse()?;
return Ok(seconds * 1000);
}
if let Some(m) = timeout_str.strip_suffix("min") {
let minutes: u64 = m.trim().parse()?;
return Ok(minutes * 60000);
}
Err(format!(
"Invalid timeout format '{}'. Use: <number>ms, <number>s, or <number>m",
timeout_str
)
.into())
}
/// Parse command line argument in the form --flag <value>
fn parse_arg(args: &[String], flag: &str) -> Option<String> {
for i in 0..args.len() {
if args[i] == flag {
return args.get(i + 1).cloned();
}
}
None
}
fn print_help() {
println!(
"\
NixOS-Based Test Framework
USAGE:
<test-binary> --qemu-cmd <COMMAND> [OPTIONS]
REQUIRED ARGUMENTS:
--qemu-cmd <COMMAND> Command to launch QEMU with the test environment
OPTIONS:
--test <TEST_NAME> Run only the specified test case
-h, --help Print this help message
ENVIRONMENT VARIABLES:
NIXOS_TEST_TIMEOUT Timeout for command execution
Supports: <number>ms, <number>s, <number>min
Examples: 300000ms, 300s, 5min
(default: 5min = 300000ms)
"
);
}

View File

@ -0,0 +1,285 @@
// SPDX-License-Identifier: MPL-2.0
use rexpect::session::PtySession;
use super::Error;
/// Describes a runtime session configuration.
///
/// A session descriptor defines how to interact with a particular execution context,
/// such as a shell environment, container, or remote session. It specifies the prompt
/// pattern (by `expect_prompt`) and the commands needed to enter (by `cmd_to_enter`)
/// and exit (by `cmd_to_exit`) the context.
///
/// # Example
///
/// The session descriptor uses a fluent builder API:
///
/// ```rust
/// use nixos_test_framework::SessionDesc;
///
/// let desc = SessionDesc::new()
/// .expect_prompt("/ #")
/// .cmd_to_enter("podman run -it alpine")
/// .cmd_to_exit("exit");
/// ```
pub struct SessionDesc {
prompt: &'static str,
enter_command: &'static str,
exit_cmd: &'static str,
}
impl SessionDesc {
/// Creates a new session descriptor with empty fields.
///
/// Use the builder methods to configure the session before use.
pub fn new() -> Self {
Self {
prompt: "",
enter_command: "",
exit_cmd: "",
}
}
/// Sets the expected prompt pattern for this session.
pub fn expect_prompt(mut self, prompt: &'static str) -> Self {
self.prompt = prompt;
self
}
/// Sets the command used to enter this session.
pub fn cmd_to_enter(mut self, enter_command: &'static str) -> Self {
self.enter_command = enter_command;
self
}
/// Sets the command used to exit this session.
pub fn cmd_to_exit(mut self, exit_cmd: &'static str) -> Self {
self.exit_cmd = exit_cmd;
self
}
}
impl Default for SessionDesc {
fn default() -> Self {
Self::new()
}
}
/// An interactive session for running commands in a test environment.
///
/// `Session` provides a high-level interface for interacting with the test environment.
/// It manages execution contexts and handles nested environments automatically.
///
/// It uses a [`SessionDesc`] to track the current prompt and the correct command
/// to exit the current context.
pub struct Session {
desc: SessionDesc,
pty_session: PtySession,
}
impl Session {
/// Creates a new session with the given PTY session and descriptor.
pub(super) fn new(desc: SessionDesc, pty_session: PtySession) -> Self {
Self { desc, pty_session }
}
fn output_error(error: &Error) {
match error {
Error::EOF {
expected,
got,
exit_code,
} => {
println!("=== EOF Error Details ===");
println!("Expected: {}", expected);
println!(
"Got: {}",
String::from_utf8_lossy(&strip_ansi_escapes::strip(got))
);
println!("Exit code: {:?}", exit_code);
println!("========================");
}
Error::Timeout {
expected,
got,
timeout,
} => {
println!("=== Timeout Error Details ===");
println!("Expected: {}", expected);
println!(
"Got: {}",
String::from_utf8_lossy(&strip_ansi_escapes::strip(got))
);
println!("Timeout: {:?}", timeout);
println!("============================");
}
_ => {}
}
}
/// Executes a command in the current session.
///
/// This method runs the specified command and waits for it to complete.
/// The command is considered complete when the session prompt reappears.
///
/// Returns an error if the command times out or the session terminates unexpectedly.
///
/// # Example
///
/// ```rust
/// use nixos_test_framework::*;
///
/// fn example(nixos_shell: &mut Session) -> Result<(), Error> {
/// // Execute simple commands
/// nixos_shell.run_cmd("ls -la")?;
/// nixos_shell.run_cmd("cd /tmp")?;
/// nixos_shell.run_cmd("mkdir test_dir")?;
///
/// Ok(())
/// }
/// ```
pub fn run_cmd(&mut self, command: &str) -> Result<(), Error> {
println!("--> Running: {}", command);
self.pty_session.send_line(command)?;
// Read and consume the echoed command line
self.pty_session.exp_string(command).unwrap();
if let Err(e) = self.pty_session.exp_string(self.desc.prompt) {
Self::output_error(&e);
return Err(e);
}
Ok(())
}
/// Executes a command and verifies its output contains expected text.
///
/// This method runs the command and checks that the specified string appears
/// in the output. This is useful for validating command results.
///
/// Returns an error if:
/// - The expected string is not found in the output
/// - The command times out
/// - The session terminates unexpectedly
///
/// # Example
///
/// ```rust
/// use nixos_test_framework::*;
///
/// fn example(nixos_shell: &mut Session) -> Result<(), Error> {
/// // Verify output contains expected string
/// nixos_shell.run_cmd_and_expect("echo 'Hello, World!'", "Hello")?;
///
/// // Check if a file exists
/// nixos_shell.run_cmd_and_expect("ls /etc/hostname", "/etc/hostname")?;
///
/// // Verify system information
/// nixos_shell.run_cmd_and_expect("cat /etc/os-release", "NixOS")?;
///
/// Ok(())
/// }
/// ```
pub fn run_cmd_and_expect(&mut self, command: &str, expected: &str) -> Result<(), Error> {
println!("--> Running: {} (expecting: {})", command, expected);
self.pty_session.send_line(command)?;
// Read and consume the echoed command line
self.pty_session.exp_string(command).unwrap();
match self.pty_session.exp_string(self.desc.prompt) {
Ok(unread) => {
let cleaned_unread =
String::from_utf8_lossy(&strip_ansi_escapes::strip(&unread)).to_string();
if !cleaned_unread.contains(expected) {
println!("=== Unexpected Output ===");
println!("Expected: {}", expected);
println!("Output before prompt:\n{}", cleaned_unread);
println!("=========================");
return Err(Error::EOF {
expected: expected.to_string(),
got: cleaned_unread,
exit_code: None,
});
}
}
Err(e) => {
Self::output_error(&e);
return Err(e);
}
}
Ok(())
}
pub(super) fn run<F>(&mut self, test_ops: F) -> Result<(), Error>
where
F: FnOnce(&mut Session) -> Result<(), Error>,
{
(test_ops)(self)
}
/// Enters a nested session, runs operations, and automatically exits.
///
/// This method is used to work with nested environments like containers, SSH sessions,
/// or any interactive shell.
///
/// Returns an error if entering, running operations, or exiting fails.
///
/// # Example
///
/// ```rust
/// use nixos_test_framework::*;
///
/// fn container_test(nixos_shell: &mut Session) -> Result<(), Error> {
/// // Define the container session
/// let container_session_desc = SessionDesc::new()
/// .expect_prompt("/ #")
/// .cmd_to_enter("podman run -it docker.io/library/alpine")
/// .cmd_to_exit("exit");
///
/// // Enter container, run tests, and automatically exit
/// nixos_shell.enter_session_and_run(container_session_desc, |alpine_shell| {
/// alpine_shell.run_cmd_and_expect(
/// "cat /etc/os-release",
/// "Alpine"
/// )?;
/// Ok(())
/// })?;
///
/// // Back in the host - container has been exited
/// nixos_shell.run_cmd("echo 'Back on host'")?;
/// Ok(())
/// }
/// ```
pub fn enter_session_and_run<F>(&mut self, desc: SessionDesc, test_ops: F) -> Result<(), Error>
where
F: FnOnce(&mut Session) -> Result<(), Error>,
{
let old_desc = std::mem::replace(&mut self.desc, desc);
if let Err(e) = self.run_cmd(self.desc.enter_command) {
self.desc = old_desc;
return Err(e);
}
let res = self.run(test_ops);
let exit_cmd = self.desc.exit_cmd;
self.desc = old_desc;
let exit_res = self.run_cmd(exit_cmd);
res?;
exit_res?;
Ok(())
}
pub(super) fn shutdown(&mut self) -> Result<(), Error> {
self.pty_session.send_line(self.desc.exit_cmd)?;
self.pty_session.process.wait()?;
Ok(())
}
}

View File

@ -0,0 +1,69 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
# Usage: merge_nixos_config.sh <base_file> <extra_file> <merged_file>
#
# This script takes two NixOS configuration files, <base_file> and <extra_file>,
# as inputs and produces a new NixOS configuration file whose content is the
# combination of the two inputs. If the same key is set by both <base_file>
# and <extra_file>, then the value provided by <extra_file> takes precedence.
#
# A NixOS configuration file, usually named `configuration.nix`, is written in
# the following form:
#
# { config, lib, pkgs, ... }: {
# key1 = value1;
# key2 = value2;
# }
set -e
# Check for the correct number of arguments
if [ "$#" -ne 3 ]; then
echo "Usage: $0 <base_file> <extra_file> <merged_file>"
exit 1
fi
BASE_FILE="$1"
EXTRA_FILE="$2"
MERGED_FILE="$3"
# Check if input files exist
if [ ! -f "$BASE_FILE" ]; then
echo "Error: Base file not found at '$BASE_FILE'"
exit 1
fi
if [ ! -f "$EXTRA_FILE" ]; then
echo "Error: Extra file not found at '$EXTRA_FILE'"
exit 1
fi
BASE_CONTENT=$(cat "$BASE_FILE")
EXTRA_CONTENT=$(cat "$EXTRA_FILE")
# Create the merged configuration file by embedding the file contents directly.
cat > "$MERGED_FILE" <<EOF
# This file is generated by merge_nixos_config.sh
# It merges two configuration files. Do not edit directly.
{ config, lib, pkgs, ... }:
let
# The content of the original modules is embedded here.
baseModule = (
$BASE_CONTENT
);
extraModule = (
$EXTRA_CONTENT
);
# Evaluate each module with the standard NixOS arguments
base = baseModule { inherit config lib pkgs; };
extra = extraModule { inherit config lib pkgs; };
in
# Deeply merge the base and extra configurations.
# The value from extra_file takes precedence.
lib.recursiveUpdate base extra
EOF

View File

@ -0,0 +1,9 @@
[package]
name = "test-<test_name>"
version.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
[dependencies]
nixos-test-framework.workspace = true

View File

@ -0,0 +1,3 @@
{ config, lib, pkgs, ... }:
{ }

View File

@ -0,0 +1,22 @@
// SPDX-License-Identifier: MPL-2.0
//! The test suite for <TargetAppName> on Asterinas NixOS.
//!
//! # Document maintenance
//!
//! An application's test suite and its "Verified Usage" section in Asterinas Book
//! should always be kept in sync.
//! So whenever you modify the test suite,
//! review the documentation and see if should be updated accordingly.
use nixos_test_framework::*;
nixos_test_main!();
#[nixos_test]
fn hello_world(nixos_shell: &mut Session) -> Result<(), Error> {
nixos_shell.run_cmd("echo 'Hello, World!' > out.txt")?;
nixos_shell.run_cmd_and_expect("ls out.txt", "out.txt")?;
Ok(())
}

View File

@ -0,0 +1,14 @@
[package]
name = "nixos-test-macro"
version.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }

View File

@ -0,0 +1,39 @@
// SPDX-License-Identifier: MPL-2.0
//! A procedural macro to register NixOS test cases.
//!
//! This crate should work together with `nixos_test_framework` crate. The
//! registered test cases will be collected and run by the test framework.
use proc_macro::TokenStream;
use quote::quote;
use syn::{ItemFn, parse_macro_input};
/// Registers a function as a NixOS test case.
///
/// # Example
/// ```rust
/// #[nixos_test]
/// fn my_test() {
/// // test code here
/// }
/// ```
#[proc_macro_attribute]
pub fn nixos_test(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let fn_name = &input.sig.ident;
let fn_name_str = fn_name.to_string();
let expanded = quote! {
#input
::nixos_test_framework::inventory::submit! {
::nixos_test_framework::TestCase {
name: #fn_name_str,
test_fn: #fn_name,
}
}
};
TokenStream::from(expanded)
}

View File

@ -9,12 +9,14 @@ ASTERINAS_DIR=$(realpath ${SCRIPT_DIR}/../..)
DISTRO_DIR=$(realpath ${ASTERINAS_DIR}/distro) DISTRO_DIR=$(realpath ${ASTERINAS_DIR}/distro)
TARGET_DIR=${ASTERINAS_DIR}/target/nixos TARGET_DIR=${ASTERINAS_DIR}/target/nixos
VERSION=$(cat ${ASTERINAS_DIR}/VERSION) VERSION=$(cat ${ASTERINAS_DIR}/VERSION)
# Accept config file name as parameter, default to "configuration.nix"
CONFIG_FILE_NAME=${1:-"configuration.nix"}
mkdir -p ${TARGET_DIR} mkdir -p ${TARGET_DIR}
nix-build ${DISTRO_DIR}/iso_image \ nix-build ${DISTRO_DIR}/iso_image \
--arg autoInstall ${AUTO_INSTALL} \ --arg autoInstall ${AUTO_INSTALL} \
--argstr test-command "${NIXOS_TEST_COMMAND}" \ --argstr config-file-name "${CONFIG_FILE_NAME}" \
--argstr extra-substituters "${RELEASE_SUBSTITUTER} ${DEV_SUBSTITUTER}" \ --argstr extra-substituters "${RELEASE_SUBSTITUTER} ${DEV_SUBSTITUTER}" \
--argstr extra-trusted-public-keys "${RELEASE_TRUSTED_PUBLIC_KEY} ${DEV_TRUSTED_PUBLIC_KEY}" \ --argstr extra-trusted-public-keys "${RELEASE_TRUSTED_PUBLIC_KEY} ${DEV_TRUSTED_PUBLIC_KEY}" \
--argstr version ${VERSION} \ --argstr version ${VERSION} \

View File

@ -8,7 +8,9 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
ASTERINAS_DIR=$(realpath ${SCRIPT_DIR}/../..) ASTERINAS_DIR=$(realpath ${SCRIPT_DIR}/../..)
ASTER_IMAGE_PATH=${ASTERINAS_DIR}/target/nixos/asterinas.img ASTER_IMAGE_PATH=${ASTERINAS_DIR}/target/nixos/asterinas.img
DISTRO_DIR=$(realpath ${ASTERINAS_DIR}/distro) DISTRO_DIR=$(realpath ${ASTERINAS_DIR}/distro)
CONFIG_PATH=${DISTRO_DIR}/etc_nixos/configuration.nix # Accept config file name as parameter, default to "configuration.nix"
CONFIG_FILE_NAME=${1:-"configuration.nix"}
CONFIG_PATH=${DISTRO_DIR}/etc_nixos/${CONFIG_FILE_NAME}
pushd $DISTRO_DIR pushd $DISTRO_DIR
nix-build aster_nixos_installer/default.nix \ nix-build aster_nixos_installer/default.nix \
@ -16,7 +18,6 @@ nix-build aster_nixos_installer/default.nix \
--argstr stage-2-hook "${NIXOS_STAGE_2_INIT}" \ --argstr stage-2-hook "${NIXOS_STAGE_2_INIT}" \
--argstr log-level "${LOG_LEVEL}" \ --argstr log-level "${LOG_LEVEL}" \
--argstr console "${CONSOLE}" \ --argstr console "${CONSOLE}" \
--argstr test-command "${NIXOS_TEST_COMMAND}" \
--argstr extra-substituters "${RELEASE_SUBSTITUTER} ${DEV_SUBSTITUTER}" \ --argstr extra-substituters "${RELEASE_SUBSTITUTER} ${DEV_SUBSTITUTER}" \
--argstr extra-trusted-public-keys "${RELEASE_TRUSTED_PUBLIC_KEY} ${DEV_TRUSTED_PUBLIC_KEY}" --argstr extra-trusted-public-keys "${RELEASE_TRUSTED_PUBLIC_KEY} ${DEV_TRUSTED_PUBLIC_KEY}"
popd popd

View File

@ -21,6 +21,9 @@ MODE=$1
SCRIPT_DIR=$(dirname "$0") SCRIPT_DIR=$(dirname "$0")
ASTERINAS_DIR=$(realpath "${SCRIPT_DIR}/../..") ASTERINAS_DIR=$(realpath "${SCRIPT_DIR}/../..")
# Change to Asterinas root directory to ensure all scripts run from the correct location.
cd "${ASTERINAS_DIR}"
# Base QEMU arguments # Base QEMU arguments
BASE_QEMU_ARGS="qemu-system-x86_64 \ BASE_QEMU_ARGS="qemu-system-x86_64 \
-bios /root/ovmf/release/OVMF.fd \ -bios /root/ovmf/release/OVMF.fd \