Skip to content

Commit 6cd2bf1

Browse files
authored
Refactor the automated tests and structure them in a better way (#89)
Signed-off-by: Guvenc Gulce <[email protected]>
1 parent 69b8882 commit 6cd2bf1

File tree

6 files changed

+960
-904
lines changed

6 files changed

+960
-904
lines changed

feos/tests/integration/fixtures.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use anyhow::Result;
5+
use feos_proto::vm_service::{VmEvent, VmState, VmStateChangedEvent};
6+
use log::{error, info, warn};
7+
use nix::sys::signal::{kill, Signal};
8+
use nix::unistd::Pid;
9+
use prost::Message;
10+
use std::path::Path;
11+
use tokio_stream::StreamExt;
12+
use vm_service::VM_API_SOCKET_DIR;
13+
14+
pub struct VmGuard {
15+
pub vm_id: String,
16+
pub pid: Option<Pid>,
17+
pub cleanup_disabled: bool,
18+
}
19+
20+
impl VmGuard {
21+
pub fn new(vm_id: String) -> Self {
22+
Self {
23+
vm_id,
24+
pid: None,
25+
cleanup_disabled: false,
26+
}
27+
}
28+
29+
pub fn disable_cleanup(&mut self) {
30+
self.cleanup_disabled = true;
31+
}
32+
33+
pub fn set_pid(&mut self, pid: i32) {
34+
self.pid = Some(Pid::from_raw(pid));
35+
}
36+
}
37+
38+
impl Drop for VmGuard {
39+
fn drop(&mut self) {
40+
if self.cleanup_disabled {
41+
return;
42+
}
43+
info!("Cleaning up VM '{}'...", self.vm_id);
44+
if let Some(pid) = self.pid {
45+
info!("Killing process with PID: {}", pid);
46+
let _ = kill(pid, Signal::SIGKILL);
47+
}
48+
let socket_path = format!("{}/{}", VM_API_SOCKET_DIR, self.vm_id);
49+
if let Err(e) = std::fs::remove_file(&socket_path) {
50+
if e.kind() != std::io::ErrorKind::NotFound {
51+
warn!("Could not remove socket file '{}': {}", socket_path, e);
52+
}
53+
} else {
54+
info!("Removed socket file '{}'", socket_path);
55+
}
56+
}
57+
}
58+
59+
pub async fn wait_for_target_state(
60+
stream: &mut tonic::Streaming<VmEvent>,
61+
target_state: VmState,
62+
) -> Result<()> {
63+
while let Some(event_res) = stream.next().await {
64+
let event = event_res?;
65+
let any_data = event.data.expect("Event should have data payload");
66+
if any_data.type_url == "type.googleapis.com/feos.vm.vmm.api.v1.VmStateChangedEvent" {
67+
let state_change = VmStateChangedEvent::decode(&*any_data.value)?;
68+
let new_state =
69+
VmState::try_from(state_change.new_state).unwrap_or(VmState::Unspecified);
70+
71+
info!(
72+
"Received VM state change event: new_state={:?}, reason='{}'",
73+
new_state, state_change.reason
74+
);
75+
76+
if new_state == target_state {
77+
return Ok(());
78+
}
79+
80+
if new_state == VmState::Crashed {
81+
let err_msg = format!("VM entered Crashed state. Reason: {}", state_change.reason);
82+
error!("{}", &err_msg);
83+
return Err(anyhow::anyhow!(err_msg));
84+
}
85+
}
86+
}
87+
Err(anyhow::anyhow!(
88+
"Event stream ended before VM reached {:?} state.",
89+
target_state
90+
))
91+
}
92+
93+
pub fn verify_vm_socket_cleanup(vm_id: &str) {
94+
let socket_path = format!("{}/{}", VM_API_SOCKET_DIR, vm_id);
95+
assert!(
96+
!Path::new(&socket_path).exists(),
97+
"Socket file '{}' should not exist after DeleteVm",
98+
socket_path
99+
);
100+
info!("Verified VM API socket is deleted: {}", socket_path);
101+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use super::{ensure_server, get_public_clients};
5+
use anyhow::{Context, Result};
6+
use feos_proto::host_service::{
7+
GetCpuInfoRequest, GetNetworkInfoRequest, HostnameRequest, MemoryRequest,
8+
};
9+
use log::info;
10+
use nix::unistd;
11+
use std::fs::File;
12+
use std::io::{BufRead, BufReader};
13+
14+
#[tokio::test]
15+
async fn test_hostname_retrieval() -> Result<()> {
16+
ensure_server().await;
17+
let (_, mut host_client) = get_public_clients().await?;
18+
19+
let response = host_client.hostname(HostnameRequest {}).await?;
20+
let remote_hostname = response.into_inner().hostname;
21+
let local_hostname = unistd::gethostname()?
22+
.into_string()
23+
.expect("Hostname is not valid UTF-8");
24+
25+
info!(
26+
"Hostname from API: '{}', Hostname from local call: '{}'",
27+
remote_hostname, local_hostname
28+
);
29+
assert_eq!(
30+
remote_hostname, local_hostname,
31+
"The hostname from the API should match the local system's hostname"
32+
);
33+
34+
Ok(())
35+
}
36+
37+
#[tokio::test]
38+
async fn test_get_memory_info() -> Result<()> {
39+
ensure_server().await;
40+
let (_, mut host_client) = get_public_clients().await?;
41+
42+
let file = File::open("/proc/meminfo")?;
43+
let reader = BufReader::new(file);
44+
let mut local_memtotal = 0;
45+
for line in reader.lines() {
46+
let line = line?;
47+
if line.starts_with("MemTotal:") {
48+
let parts: Vec<&str> = line.split_whitespace().collect();
49+
if parts.len() >= 2 {
50+
local_memtotal = parts[1].parse::<u64>()?;
51+
}
52+
break;
53+
}
54+
}
55+
56+
assert!(
57+
local_memtotal > 0,
58+
"Failed to parse MemTotal from local /proc/meminfo"
59+
);
60+
info!("Local MemTotal from /proc/meminfo: {} kB", local_memtotal);
61+
62+
info!("Sending GetMemory request");
63+
let response = host_client.get_memory(MemoryRequest {}).await?.into_inner();
64+
65+
let mem_info = response
66+
.mem_info
67+
.context("MemoryInfo was not present in the response")?;
68+
info!(
69+
"Remote MemTotal from gRPC response: {} kB",
70+
mem_info.memtotal
71+
);
72+
73+
assert_eq!(
74+
mem_info.memtotal, local_memtotal,
75+
"MemTotal from API should match the local system's MemTotal"
76+
);
77+
assert!(
78+
mem_info.memfree <= mem_info.memtotal,
79+
"MemFree should not be greater than MemTotal"
80+
);
81+
82+
Ok(())
83+
}
84+
85+
#[tokio::test]
86+
async fn test_get_cpu_info() -> Result<()> {
87+
ensure_server().await;
88+
let (_, mut host_client) = get_public_clients().await?;
89+
90+
let file = File::open("/proc/cpuinfo")?;
91+
let reader = BufReader::new(file);
92+
let mut local_processor_count = 0;
93+
let mut local_vendor_id = String::new();
94+
for line in reader.lines() {
95+
let line = line?;
96+
if line.starts_with("processor") {
97+
local_processor_count += 1;
98+
}
99+
if line.starts_with("vendor_id") && local_vendor_id.is_empty() {
100+
let parts: Vec<&str> = line.splitn(2, ':').collect();
101+
if parts.len() == 2 {
102+
local_vendor_id = parts[1].trim().to_string();
103+
}
104+
}
105+
}
106+
107+
assert!(
108+
local_processor_count > 0,
109+
"Failed to parse processor count from /proc/cpuinfo"
110+
);
111+
assert!(
112+
!local_vendor_id.is_empty(),
113+
"Failed to parse vendor_id from /proc/cpuinfo"
114+
);
115+
info!(
116+
"Local data from /proc/cpuinfo: {} processors, vendor_id: {}",
117+
local_processor_count, local_vendor_id
118+
);
119+
120+
info!("Sending GetCPUInfo request");
121+
let response = host_client
122+
.get_cpu_info(GetCpuInfoRequest {})
123+
.await?
124+
.into_inner();
125+
126+
let remote_cpu_info = response.cpu_info;
127+
info!(
128+
"Remote data from gRPC: {} processors",
129+
remote_cpu_info.len()
130+
);
131+
132+
assert_eq!(
133+
remote_cpu_info.len(),
134+
local_processor_count,
135+
"Processor count from API should match local count"
136+
);
137+
138+
let first_cpu = remote_cpu_info
139+
.first()
140+
.context("CPU info list should not be empty")?;
141+
info!("Remote vendor_id: {}", first_cpu.vendor_id);
142+
143+
assert_eq!(
144+
first_cpu.vendor_id, local_vendor_id,
145+
"Vendor ID of first CPU should match"
146+
);
147+
assert!(
148+
first_cpu.cpu_mhz > 0.0,
149+
"CPU MHz should be a positive value"
150+
);
151+
152+
Ok(())
153+
}
154+
155+
#[tokio::test]
156+
async fn test_get_network_info() -> Result<()> {
157+
ensure_server().await;
158+
let (_, mut host_client) = get_public_clients().await?;
159+
160+
info!("Sending GetNetworkInfo request");
161+
let response = host_client
162+
.get_network_info(GetNetworkInfoRequest {})
163+
.await?
164+
.into_inner();
165+
166+
assert!(
167+
!response.devices.is_empty(),
168+
"The list of network devices should not be empty"
169+
);
170+
info!("Received {} network devices", response.devices.len());
171+
172+
let lo = response
173+
.devices
174+
.iter()
175+
.find(|d| d.name == "lo")
176+
.context("Could not find the loopback interface 'lo'")?;
177+
178+
info!("Found loopback interface 'lo'");
179+
assert_eq!(lo.name, "lo");
180+
assert!(
181+
lo.rx_packets > 0 || lo.tx_packets > 0,
182+
"Loopback interface should have some packets transferred"
183+
);
184+
185+
Ok(())
186+
}

0 commit comments

Comments
 (0)