blob: fa68519449010a578bc0df7c2620371b59efa8e3 [file] [log] [blame]
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// devices_handler.rs
//
// Provides the API for the frontend and backend to interact with devices.
//
// The Devices struct is a singleton for the devices collection.
//
// Additional functions are
// -- inactivity instant
// -- vending device identifiers
use super::chip::ChipIdentifier;
use super::device::DeviceIdentifier;
use super::id_factory::IdFactory;
use crate::captures::handlers::update_captures;
use crate::devices::device::AddChipResult;
use crate::devices::device::Device;
use crate::ffi::CxxServerResponseWriter;
use crate::http_server::http_request::HttpHeaders;
use crate::http_server::http_request::HttpRequest;
use crate::http_server::server_response::ResponseWritable;
use crate::CxxServerResponseWriterWrapper;
use cxx::CxxString;
use cxx::UniquePtr;
use frontend_proto::common::ChipKind as ProtoChipKind;
use frontend_proto::frontend::ListDeviceResponse;
use frontend_proto::frontend::PatchDeviceRequest;
use frontend_proto::model::Position as ProtoPosition;
use frontend_proto::model::Scene as ProtoScene;
use lazy_static::lazy_static;
use log::{error, info};
use protobuf_json_mapping::merge_from_str;
use protobuf_json_mapping::print_to_string_with_options;
use protobuf_json_mapping::PrintOptions;
use std::collections::btree_map::Entry;
use std::collections::BTreeMap;
use std::pin::Pin;
use std::sync::RwLock;
use std::sync::RwLockWriteGuard;
use std::time::Instant;
const INITIAL_DEVICE_ID: DeviceIdentifier = 0;
const JSON_PRINT_OPTION: PrintOptions = PrintOptions {
enum_values_int: false,
proto_field_name: false,
always_output_default_values: true,
_future_options: (),
};
lazy_static! {
static ref DEVICES: RwLock<Devices> = RwLock::new(Devices::new());
}
static IDLE_SECS_FOR_SHUTDOWN: u64 = 300;
/// The Device resource is a singleton that manages all devices.
struct Devices {
// BTreeMap allows ListDevice to output devices in order of identifiers.
devices: BTreeMap<DeviceIdentifier, Device>,
id_factory: IdFactory<DeviceIdentifier>,
pub idle_since: Option<Instant>,
}
impl Devices {
fn new() -> Self {
Devices {
devices: BTreeMap::new(),
id_factory: IdFactory::new(INITIAL_DEVICE_ID, 1),
idle_since: Some(Instant::now()),
}
}
}
#[allow(dead_code)]
fn notify_all() {
// TODO
}
/// Returns a Result<AddChipResult, String> after adding chip to resource.
/// add_chip is called by the transport layer when a new chip is attached.
///
/// The guid is a transport layer identifier for the device (host:port)
/// that is adding the chip.
pub fn add_chip(
device_guid: &str,
device_name: &str,
chip_kind: ProtoChipKind,
chip_name: &str,
chip_manufacturer: &str,
chip_product_name: &str,
) -> Result<AddChipResult, String> {
let result = {
let mut resource = DEVICES.write().unwrap();
resource.idle_since = None;
let device_id = get_or_create_device(&mut resource, device_guid, device_name);
// This is infrequent, so we can afford to do another lookup for the device.
resource.devices.get_mut(&device_id).unwrap().add_chip(
device_name,
chip_kind,
chip_name,
chip_manufacturer,
chip_product_name,
)
};
if result.is_ok() {
update_captures();
}
result
}
/// An AddChip function for Rust Device API.
/// The backend gRPC code will be invoking this method.
pub fn add_chip_cxx(
device_guid: &str,
device_name: &str,
chip_kind: &CxxString,
chip_name: &str,
chip_manufacturer: &str,
chip_product_name: &str,
) -> UniquePtr<crate::ffi::AddChipResult> {
let chip_kind_proto = match chip_kind.to_string().as_str() {
"BLUETOOTH" => ProtoChipKind::BLUETOOTH,
"WIFI" => ProtoChipKind::WIFI,
"UWB" => ProtoChipKind::UWB,
_ => ProtoChipKind::UNSPECIFIED,
};
match add_chip(
device_guid,
device_name,
chip_kind_proto,
chip_name,
chip_manufacturer,
chip_product_name,
) {
Ok(result) => {
info!("Rust Device API Add Chip Success");
crate::ffi::new_add_chip_result(
result.device_id as u32,
result.chip_id as u32,
result.facade_id,
)
}
Err(err) => {
error!("Rust Device API Add Chip Error: {err}");
crate::ffi::new_add_chip_result(u32::MAX, u32::MAX, u32::MAX)
}
}
}
/// Get or create a device.
fn get_or_create_device(
resource: &mut RwLockWriteGuard<Devices>,
guid: &str,
name: &str,
) -> DeviceIdentifier {
// Check if a device with the given guid already exists
if let Some(existing_device) = resource.devices.values().find(|d| d.guid == guid) {
// A device with the same guid already exists, return it
existing_device.id
} else {
// No device with the same guid exists, insert the new device
let new_id = resource.id_factory.next_id();
resource.devices.insert(new_id, Device::new(new_id, guid.to_string(), name.to_string()));
new_id
}
}
/// Remove a device from the simulation.
///
/// Called when the last chip for the device is removed.
fn remove_device(
resource: &mut RwLockWriteGuard<Devices>,
id: DeviceIdentifier,
) -> Result<(), String> {
resource.devices.remove(&id).ok_or(format!("Error removing device id {id}"))?;
if resource.devices.is_empty() {
resource.idle_since = Some(Instant::now());
}
Ok(())
}
/// Remove a chip from a device.
///
/// Called when the packet transport for the chip shuts down.
pub fn remove_chip(device_id: DeviceIdentifier, chip_id: ChipIdentifier) -> Result<(), String> {
let result = {
let mut resource = DEVICES.write().unwrap();
let is_empty = match resource.devices.entry(device_id) {
Entry::Occupied(mut entry) => {
let device = entry.get_mut();
device.remove_chip(chip_id)?;
device.chips.is_empty()
}
Entry::Vacant(_) => return Err(format!("RemoveChip device id {device_id} not found")),
};
if is_empty {
remove_device(&mut resource, device_id)?;
}
Ok(())
};
if result.is_ok() {
update_captures();
}
result
}
/// A RemoveChip function for Rust Device API.
/// The backend gRPC code will be invoking this method.
pub fn remove_chip_cxx(device_id: u32, chip_id: u32) {
match remove_chip(device_id as i32, chip_id as i32) {
Ok(_) => info!("Rust Device API Remove Chip Success"),
Err(err) => error!("Rust Device API Remove Chip Failure: {err}"),
}
}
// lock the devices, find the id and call the patch function
#[allow(dead_code)]
fn patch_device(id_option: Option<DeviceIdentifier>, patch_json: &str) -> Result<(), String> {
let mut patch_device_request = PatchDeviceRequest::new();
if merge_from_str(&mut patch_device_request, patch_json).is_ok() {
let mut resource = DEVICES.write().unwrap();
let proto_device = patch_device_request.device;
match id_option {
Some(id) => match resource.devices.get_mut(&id) {
Some(device) => device.patch(&proto_device),
None => Err(format!("No such device with id {id}")),
},
None => {
let mut multiple_matches = false;
let mut target: Option<&mut Device> = None;
for device in resource.devices.values_mut() {
if device.name.contains(&proto_device.name) {
if device.name == proto_device.name {
return device.patch(&proto_device);
}
multiple_matches = target.is_some();
target = Some(device);
}
}
if multiple_matches {
return Err(format!(
"Multiple ambiguous matches were found with substring {}",
proto_device.name
));
}
match target {
Some(device) => device.patch(&proto_device),
None => Err(format!("No such device with name {}", proto_device.name)),
}
}
}
} else {
Err(format!("Incorrect format of patch json {}", patch_json))
}
}
fn distance(a: &ProtoPosition, b: &ProtoPosition) -> f32 {
((b.x - a.x).powf(2.0) + (b.y - a.y).powf(2.0) + (b.z - a.z).powf(2.0)).sqrt()
}
#[allow(dead_code)]
fn get_distance(id: DeviceIdentifier, other_id: DeviceIdentifier) -> Result<f32, String> {
print!("get_distance({:?}, {:?}) = ", id, other_id);
let devices = &DEVICES.read().unwrap().devices;
let a = devices
.get(&id)
.map(|device_ref| device_ref.position.clone())
.ok_or(format!("No such device with id {id}"))?;
let b = devices
.get(&other_id)
.map(|device_ref| device_ref.position.clone())
.ok_or(format!("No such device with id {other_id}"))?;
Ok(distance(&a, &b))
}
/// A GetDistance function for Rust Device API.
/// The backend gRPC code will be invoking this method.
pub fn get_distance_cxx(a: u32, b: u32) -> f32 {
match get_distance(a as i32, b as i32) {
Ok(distance) => distance,
Err(err) => {
error!("Rust Device API Get Distance Error: {err}");
0.0
}
}
}
pub fn get_devices() -> Result<ProtoScene, String> {
let mut scene = ProtoScene::new();
// iterate over the devices and add each to the scene
let resource = DEVICES.read().unwrap();
for device in resource.devices.values() {
scene.devices.push(device.get()?);
}
Ok(scene)
}
#[allow(dead_code)]
fn reset(id: DeviceIdentifier) -> Result<(), String> {
let mut resource = DEVICES.write().unwrap();
match resource.devices.get_mut(&id) {
Some(device) => device.reset(),
None => Err(format!("No such device with id {id}")),
}
}
#[allow(dead_code)]
fn reset_all() -> Result<(), String> {
let mut resource = DEVICES.write().unwrap();
for device in resource.devices.values_mut() {
device.reset()?;
}
Ok(())
}
/// Return true if netsimd is idle for 5 minutes
pub fn is_shutdown_time_cxx() -> bool {
match DEVICES.read().unwrap().idle_since {
Some(idle_since) => {
IDLE_SECS_FOR_SHUTDOWN.checked_sub(idle_since.elapsed().as_secs()).is_none()
}
None => false,
}
}
/// Performs PatchDevice to patch a single device
fn handle_device_patch(writer: ResponseWritable, id: Option<DeviceIdentifier>, patch_json: &str) {
match patch_device(id, patch_json) {
Ok(()) => writer.put_ok("text/plain", "Device Patch Success", &[]),
Err(err) => writer.put_error(404, err.as_str()),
}
}
/// Performs ListDevices to get the list of Devices and write to writer.
fn handle_device_list(writer: ResponseWritable) {
let devices = get_devices().unwrap();
// Instantiate ListDeviceResponse and add Devices
let mut response = ListDeviceResponse::new();
for device in devices.devices {
response.devices.push(device);
}
// Perform protobuf-json-mapping with the given protobuf
if let Ok(json_response) = print_to_string_with_options(&response, &JSON_PRINT_OPTION) {
writer.put_ok("text/json", &json_response, &[])
} else {
writer.put_error(404, "proto to JSON mapping failure")
}
}
/// Performs ResetDevice for all devices
fn handle_device_reset(writer: ResponseWritable) {
match reset_all() {
Ok(()) => writer.put_ok("text/plain", "Device Reset Success", &[]),
Err(err) => writer.put_error(404, err.as_str()),
}
}
/// The Rust device handler used directly by Http frontend or handle_device_cxx for LIST, GET, and PATCH
pub fn handle_device(request: &HttpRequest, param: &str, writer: ResponseWritable) {
// Route handling
if request.uri.as_str() == "/v1/devices" {
// Routes with ID not specified
match request.method.as_str() {
"GET" => {
handle_device_list(writer);
}
"PUT" => {
handle_device_reset(writer);
}
"PATCH" => {
let body = &request.body;
let patch_json = String::from_utf8(body.to_vec()).unwrap();
handle_device_patch(writer, None, patch_json.as_str());
}
_ => writer.put_error(404, "Not found."),
}
} else {
// Routes with ID specified
match request.method.as_str() {
"PATCH" => {
let id = match param.parse::<i32>() {
Ok(num) => num,
Err(_) => {
writer.put_error(404, "Incorrect Id type for devices, ID should be i32.");
return;
}
};
let body = &request.body;
let patch_json = String::from_utf8(body.to_vec()).unwrap();
handle_device_patch(writer, Some(id), patch_json.as_str());
}
_ => writer.put_error(404, "Not found."),
}
}
}
/// Device handler cxx for grpc server to call
pub fn handle_device_cxx(
responder: Pin<&mut CxxServerResponseWriter>,
method: String,
param: String,
body: String,
) {
let mut request = HttpRequest {
method,
uri: String::new(),
headers: HttpHeaders::new(),
version: "1.1".to_string(),
body: body.as_bytes().to_vec(),
};
if param.is_empty() {
request.uri = "/v1/devices".to_string();
} else {
request.uri = format!("/v1/devices/{}", param)
}
handle_device(
&request,
param.as_str(),
&mut CxxServerResponseWriterWrapper { writer: responder },
)
}
/// Get Facade ID from given chip_id
pub fn get_facade_id(chip_id: i32) -> Result<u32, String> {
let resource = DEVICES.read().unwrap();
for device in resource.devices.values() {
for (id, chip) in &device.chips {
if *id == chip_id {
return Ok(chip.facade_id);
}
}
}
Err(format!("Cannot find facade_id for {chip_id}"))
}
#[cfg(test)]
mod tests {
use std::{
io::Cursor,
sync::{Mutex, Once},
time::Duration,
};
use frontend_proto::model::{Device as ProtoDevice, Orientation as ProtoOrientation, State};
use netsim_common::util::netsim_logger::init_for_test;
use protobuf_json_mapping::print_to_string;
use crate::http_server::server_response::ServerResponseWriter;
use super::*;
// Since rust unit tests occur in parallel. We must lock each test case
// to avoid unwanted interleaving operations on DEVICES
lazy_static! {
static ref MUTEX: Mutex<()> = Mutex::new(());
}
// This allows Log init method to be invoked once when running all tests.
static INIT: Once = Once::new();
/// Logger setup function that is only run once, even if called multiple times.
fn logger_setup() {
INIT.call_once(|| {
init_for_test();
});
}
/// TestChipParameters struct to invoke add_chip
/// This struct contains parameters required to invoke add_chip.
/// This will eventually be invoked by the facades.
struct TestChipParameters<'a> {
device_guid: &'a str,
device_name: &'a str,
chip_kind: ProtoChipKind,
chip_name: &'a str,
chip_manufacturer: &'a str,
chip_product_name: &'a str,
}
impl TestChipParameters<'_> {
fn add_chip(&self) -> Result<AddChipResult, String> {
super::add_chip(
self.device_guid,
self.device_name,
self.chip_kind,
self.chip_name,
self.chip_manufacturer,
self.chip_product_name,
)
}
fn get_or_create_device(&self) -> DeviceIdentifier {
let mut resource = DEVICES.write().unwrap();
super::get_or_create_device(&mut resource, self.device_guid, self.device_name)
}
}
/// helper function for test cases to instantiate ProtoPosition
fn new_position(x: f32, y: f32, z: f32) -> ProtoPosition {
ProtoPosition { x, y, z, ..Default::default() }
}
fn new_orientation(yaw: f32, pitch: f32, roll: f32) -> ProtoOrientation {
ProtoOrientation { yaw, pitch, roll, ..Default::default() }
}
/// helper function for test cases to refresh DEVICES
fn refresh_resource() {
let mut resource = DEVICES.write().unwrap();
resource.devices = BTreeMap::new();
resource.id_factory = IdFactory::new(1000, 1);
resource.idle_since = Some(Instant::now());
crate::devices::chip::refresh_resource();
crate::bluetooth::refresh_resource();
crate::wifi::refresh_resource();
}
/// helper function for traveling back n seconds for idle_since
fn travel_back_n_seconds_from_now(n: u64) {
let mut resource = DEVICES.write().unwrap();
resource.idle_since = Some(Instant::now() - Duration::from_secs(n));
}
fn test_chip_1_bt() -> TestChipParameters<'static> {
TestChipParameters {
device_guid: "guid-fs-1",
device_name: "test-device-name-1",
chip_kind: ProtoChipKind::BLUETOOTH,
chip_name: "bt_chip_name",
chip_manufacturer: "netsim",
chip_product_name: "netsim_bt",
}
}
fn test_chip_1_wifi() -> TestChipParameters<'static> {
TestChipParameters {
device_guid: "guid-fs-1",
device_name: "test-device-name-1",
chip_kind: ProtoChipKind::WIFI,
chip_name: "bt_chip_name",
chip_manufacturer: "netsim",
chip_product_name: "netsim_bt",
}
}
fn test_chip_2_bt() -> TestChipParameters<'static> {
TestChipParameters {
device_guid: "guid-fs-2",
device_name: "test-device-name-2",
chip_kind: ProtoChipKind::BLUETOOTH,
chip_name: "bt_chip_name",
chip_manufacturer: "netsim",
chip_product_name: "netsim_bt",
}
}
#[test]
fn test_distance() {
// Pythagorean quadruples
let a = new_position(0.0, 0.0, 0.0);
let mut b = new_position(1.0, 2.0, 2.0);
assert_eq!(distance(&a, &b), 3.0);
b = new_position(2.0, 3.0, 6.0);
assert_eq!(distance(&a, &b), 7.0);
}
#[test]
fn test_add_chip() {
// Avoiding Interleaving Operations
let _lock = MUTEX.lock().unwrap();
// Initializing Logger
logger_setup();
// Adding a chip
refresh_resource();
let chip_params = test_chip_1_bt();
let chip_result = chip_params.add_chip().unwrap();
match get_devices().unwrap().devices.get(0) {
Some(device) => {
let chip = device.chips.get(0).unwrap();
assert_eq!(chip_params.chip_kind, chip.kind.enum_value_or_default());
assert_eq!(chip_params.chip_manufacturer, chip.manufacturer);
assert_eq!(chip_params.chip_name, chip.name);
assert_eq!(chip_params.chip_product_name, chip.product_name);
assert_eq!(chip_params.device_name, device.name);
}
None => unreachable!(),
}
let chip_id = chip_result.chip_id;
// Adding duplicate chip
let chip_result = chip_params.add_chip();
assert!(chip_result.is_err());
assert_eq!(
chip_result.unwrap_err(),
format!("Device::AddChip - duplicate at id {chip_id}, skipping.")
);
}
#[test]
fn test_get_or_create_device() {
// Avoiding Interleaving Operations
let _lock = MUTEX.lock().unwrap();
// Initializing Logger
logger_setup();
// Creating a device and getting device
refresh_resource();
let bt_chip_params = test_chip_1_bt();
let device_id = bt_chip_params.get_or_create_device();
assert_eq!(device_id, 1000);
let wifi_chip_params = test_chip_1_wifi();
let device_id = wifi_chip_params.get_or_create_device();
assert_eq!(device_id, 1000);
}
#[test]
fn test_patch_device() {
// Avoiding Interleaving Operations
let _lock = MUTEX.lock().unwrap();
// Initializing Logger
logger_setup();
// Patching device position and orientation by id
refresh_resource();
let chip_params = test_chip_1_bt();
let chip_result = chip_params.add_chip().unwrap();
let mut patch_device_request = PatchDeviceRequest::new();
let mut proto_device = ProtoDevice::new();
let request_position = new_position(1.1, 2.2, 3.3);
let request_orientation = new_orientation(4.4, 5.5, 6.6);
proto_device.name = chip_params.device_name.into();
proto_device.visible = State::OFF.into();
proto_device.position = Some(request_position.clone()).into();
proto_device.orientation = Some(request_orientation.clone()).into();
patch_device_request.device = Some(proto_device.clone()).into();
let patch_json = print_to_string(&patch_device_request).unwrap();
patch_device(Some(chip_result.device_id), patch_json.as_str()).unwrap();
match get_devices().unwrap().devices.get(0) {
Some(device) => {
assert_eq!(device.position.x, request_position.x);
assert_eq!(device.position.y, request_position.y);
assert_eq!(device.position.z, request_position.z);
assert_eq!(device.orientation.yaw, request_orientation.yaw);
assert_eq!(device.orientation.pitch, request_orientation.pitch);
assert_eq!(device.orientation.roll, request_orientation.roll);
assert_eq!(device.visible.enum_value_or_default(), State::OFF);
}
None => unreachable!(),
}
// Patch device by name with substring match
proto_device.name = "test".into();
patch_device_request.device = Some(proto_device).into();
let patch_json = print_to_string(&patch_device_request).unwrap();
assert!(patch_device(None, patch_json.as_str()).is_ok());
}
#[test]
fn test_patch_error() {
// Avoiding Interleaving Operations
let _lock = MUTEX.lock().unwrap();
// Initializing Logger
logger_setup();
// Patch Error Testing
refresh_resource();
let bt_chip_params = test_chip_1_bt();
let bt_chip2_params = test_chip_2_bt();
let bt_chip_result = bt_chip_params.add_chip().unwrap();
bt_chip2_params.add_chip().unwrap();
// Incorrect value type
let error_json = r#"{"device": {"name": "test-device-name-1", "position": 1.1}}"#;
let patch_result = patch_device(Some(bt_chip_result.device_id), error_json);
assert!(patch_result.is_err());
assert_eq!(
patch_result.unwrap_err(),
format!("Incorrect format of patch json {}", error_json)
);
// Incorrect key
let error_json = r#"{"device": {"name": "test-device-name-1", "hello": "world"}}"#;
let patch_result = patch_device(Some(bt_chip_result.device_id), error_json);
assert!(patch_result.is_err());
assert_eq!(
patch_result.unwrap_err(),
format!("Incorrect format of patch json {}", error_json)
);
// Incorrect Id
let error_json = r#"{"device": {"name": "test-device-name-1"}}"#;
let patch_result = patch_device(Some(INITIAL_DEVICE_ID - 1), error_json);
assert!(patch_result.is_err());
assert_eq!(
patch_result.unwrap_err(),
format!("No such device with id {}", INITIAL_DEVICE_ID - 1)
);
// Incorrect name
let error_json = r#"{"device": {"name": "wrong-name"}}"#;
let patch_result = patch_device(None, error_json);
assert!(patch_result.is_err());
assert_eq!(patch_result.unwrap_err(), "No such device with name wrong-name");
// Multiple ambiguous matching
let error_json = r#"{"device": {"name": "test-device"}}"#;
let patch_result = patch_device(None, error_json);
assert!(patch_result.is_err());
assert_eq!(
patch_result.unwrap_err(),
"Multiple ambiguous matches were found with substring test-device"
);
}
#[test]
fn test_adding_two_chips() {
// Avoiding Interleaving Operations
let _lock = MUTEX.lock().unwrap();
// Initializing Logger
logger_setup();
// Adding two chips of the same device
refresh_resource();
let bt_chip_params = test_chip_1_bt();
let wifi_chip_params = test_chip_1_wifi();
let bt_chip_result = bt_chip_params.add_chip().unwrap();
let wifi_chip_result = wifi_chip_params.add_chip().unwrap();
assert_eq!(bt_chip_result.device_id, wifi_chip_result.device_id);
assert_eq!(get_devices().unwrap().devices.len(), 1);
let scene = get_devices().unwrap();
let device = scene.devices.get(0).unwrap();
assert_eq!(device.id, bt_chip_result.device_id);
assert_eq!(device.name, bt_chip_params.device_name);
assert_eq!(device.visible.enum_value_or_default(), State::ON);
assert!(device.position.is_some());
assert!(device.orientation.is_some());
assert_eq!(device.chips.len(), 2);
for chip in &device.chips {
assert!(chip.id == bt_chip_result.chip_id || chip.id == wifi_chip_result.chip_id);
if chip.id == bt_chip_result.chip_id {
assert!(chip.has_bt());
} else if chip.id == wifi_chip_result.chip_id {
assert!(chip.has_wifi());
} else {
unreachable!();
}
}
}
#[test]
fn test_reset() {
// Avoiding Interleaving Operations
let _lock = MUTEX.lock().unwrap();
// Initializing Logger
logger_setup();
// Patching Device and Resetting scene
refresh_resource();
let chip_params = test_chip_1_bt();
let chip_result = chip_params.add_chip().unwrap();
let mut patch_device_request = PatchDeviceRequest::new();
let mut proto_device = ProtoDevice::new();
let request_position = new_position(10.0, 20.0, 30.0);
let request_orientation = new_orientation(1.0, 2.0, 3.0);
proto_device.name = chip_params.device_name.into();
proto_device.visible = State::OFF.into();
proto_device.position = Some(request_position).into();
proto_device.orientation = Some(request_orientation).into();
patch_device_request.device = Some(proto_device).into();
patch_device(
Some(chip_result.device_id),
print_to_string(&patch_device_request).unwrap().as_str(),
)
.unwrap();
match get_devices().unwrap().devices.get(0) {
Some(device) => {
assert_eq!(device.position.x, 10.0);
assert_eq!(device.orientation.yaw, 1.0);
assert_eq!(device.visible.enum_value_or_default(), State::OFF);
}
None => unreachable!(),
}
reset(chip_result.device_id).unwrap();
match get_devices().unwrap().devices.get(0) {
Some(device) => {
assert_eq!(device.position.x, 0.0);
assert_eq!(device.position.y, 0.0);
assert_eq!(device.position.z, 0.0);
assert_eq!(device.orientation.yaw, 0.0);
assert_eq!(device.orientation.pitch, 0.0);
assert_eq!(device.orientation.roll, 0.0);
assert_eq!(device.visible.enum_value_or_default(), State::ON);
}
None => unreachable!(),
}
}
#[test]
fn test_remove_chip() {
// Avoiding Interleaving Operations
let _lock = MUTEX.lock().unwrap();
// Initializing Logger
logger_setup();
// Add 2 chips of same device and 1 chip of different device
refresh_resource();
let bt_chip_params = test_chip_1_bt();
let wifi_chip_params = test_chip_1_wifi();
let bt_chip_2_params = test_chip_2_bt();
let bt_chip_result = bt_chip_params.add_chip().unwrap();
let wifi_chip_result = wifi_chip_params.add_chip().unwrap();
let bt_chip_2_result = bt_chip_2_params.add_chip().unwrap();
// Remove a bt chip of first device
remove_chip(bt_chip_result.device_id, bt_chip_result.chip_id).unwrap();
assert_eq!(get_devices().unwrap().devices.len(), 2);
for device in get_devices().unwrap().devices {
if device.id == wifi_chip_result.device_id {
assert_eq!(wifi_chip_params.device_name, device.name);
} else if device.id == bt_chip_2_result.device_id {
assert_eq!(bt_chip_2_params.device_name, device.name);
} else {
unreachable!();
}
}
// Remove a wifi chip of first device
remove_chip(wifi_chip_result.device_id, wifi_chip_result.chip_id).unwrap();
assert_eq!(get_devices().unwrap().devices.len(), 1);
match get_devices().unwrap().devices.get(0) {
Some(device) => assert_eq!(bt_chip_2_params.device_name, device.name),
None => unreachable!(),
}
// Remove a bt chip of second device
remove_chip(bt_chip_2_result.device_id, bt_chip_2_result.chip_id).unwrap();
assert!(get_devices().unwrap().devices.is_empty());
}
#[test]
fn test_remove_chip_error() {
// Avoiding Interleaving Operations
let _lock = MUTEX.lock().unwrap();
// Initializing Logger
logger_setup();
// Add 2 chips of same device and 1 chip of different device
refresh_resource();
let bt_chip_params = test_chip_1_bt();
let bt_chip_result = bt_chip_params.add_chip().unwrap();
// Invoke remove_chip with incorrect chip_id.
match remove_chip(bt_chip_result.device_id, 4000) {
Ok(_) => unreachable!(),
Err(err) => assert_eq!(err, "RemoveChip chip id 4000 not found"),
}
// Invoke remove_chip with incorrect device_id
match remove_chip(4000, bt_chip_result.chip_id) {
Ok(_) => unreachable!(),
Err(err) => assert_eq!(err, "RemoveChip device id 4000 not found"),
}
assert_eq!(get_devices().unwrap().devices.len(), 1);
}
#[test]
fn test_get_facade_id() {
// Avoiding Interleaving Operations
let _lock = MUTEX.lock().unwrap();
// Initializing Logger
logger_setup();
// Add bt, wifi chips of the same device and bt chip of second device
refresh_resource();
let bt_chip_params = test_chip_1_bt();
let bt_chip_result = bt_chip_params.add_chip().unwrap();
let wifi_chip_params = test_chip_1_wifi();
let wifi_chip_result = wifi_chip_params.add_chip().unwrap();
let bt_chip_2_params = test_chip_2_bt();
let bt_chip_2_result = bt_chip_2_params.add_chip().unwrap();
// Invoke get_facade_id from first bt chip
match get_facade_id(bt_chip_result.chip_id) {
Ok(facade_id) => assert_eq!(facade_id, 0),
Err(err) => {
error!("{err}");
unreachable!();
}
}
// Invoke get_facade_id from first wifi chip
match get_facade_id(wifi_chip_result.chip_id) {
Ok(facade_id) => assert_eq!(facade_id, 0),
Err(err) => {
error!("{err}");
unreachable!();
}
}
// Invoke get_facade_id from second bt chip
match get_facade_id(bt_chip_2_result.chip_id) {
Ok(facade_id) => assert_eq!(facade_id, 1),
Err(err) => {
error!("{err}");
unreachable!();
}
}
}
#[test]
fn test_is_shutdown_time_cxx() {
// Avoiding Interleaving Operations
let _lock = MUTEX.lock().unwrap();
// Initializing Logger
logger_setup();
// Refresh Resource
refresh_resource();
// Set the idle_since value to more than 5 minutes before current time
travel_back_n_seconds_from_now(301);
assert!(is_shutdown_time_cxx());
// Set the idle_since value to less than 5 minutes before current time
travel_back_n_seconds_from_now(299);
assert!(!is_shutdown_time_cxx());
// Refresh Resource again
refresh_resource();
// Add a device and check if idle_since is None
let _ = test_chip_1_bt().add_chip();
assert!(DEVICES.read().unwrap().idle_since.is_none());
assert!(!is_shutdown_time_cxx());
}
fn list_request() -> HttpRequest {
HttpRequest {
method: "GET".to_string(),
uri: "/v1/devices".to_string(),
version: "1.1".to_string(),
headers: HttpHeaders::new(),
body: b"".to_vec(),
}
}
#[test]
fn test_handle_device() {
// Avoiding Interleaving Operations
let _lock = MUTEX.lock().unwrap();
// Initializing Logger
logger_setup();
// Refresh Resource
refresh_resource();
// Add bt, wifi chips of the same device and bt chip of second device
let bt_chip_params = test_chip_1_bt();
let _bt_chip_result = bt_chip_params.add_chip().unwrap();
let wifi_chip_params = test_chip_1_wifi();
let _wifi_chip_result = wifi_chip_params.add_chip().unwrap();
let bt_chip_2_params = test_chip_2_bt();
let _bt_chip_2_result = bt_chip_2_params.add_chip().unwrap();
// ListDevice Testing
// Initialize request for ListDevice
let request = list_request();
// Initialize writer
let mut stream = Cursor::new(Vec::new());
let mut writer = ServerResponseWriter::new(&mut stream);
// Perform ListDevice
handle_device(&request, "", &mut writer);
// Check the response for ListDevice
let expected = include_bytes!("test/initial.txt");
let actual = stream.get_ref();
assert_eq!(actual, expected);
// PatchDevice Testing
// Initialize request for PatchDevice
// The patch body will change the visibility and position of the first device.
let request = HttpRequest {
method: "PATCH".to_string(),
uri: "/v1/devices".to_string(),
version: "1.1".to_string(),
headers: HttpHeaders::new(),
body: include_bytes!("test/patch_body.txt").to_vec(),
};
// Initialize writer
let mut stream = Cursor::new(Vec::new());
let mut writer = ServerResponseWriter::new(&mut stream);
// Perform PatchDevice
handle_device(&request, "", &mut writer);
// Initialize writer
let mut stream = Cursor::new(Vec::new());
let mut writer = ServerResponseWriter::new(&mut stream);
// Perform ListDevice
let request = list_request();
handle_device(&request, "", &mut writer);
// Check the response for ListDevice
let expected = include_bytes!("test/post_patch.txt");
let actual = stream.get_ref();
assert_eq!(actual, expected);
// ResetDevice Testing
// Initialize request for ResetDevice
let request = HttpRequest {
method: "PUT".to_string(),
uri: "/v1/devices".to_string(),
version: "1.1".to_string(),
headers: HttpHeaders::new(),
body: b"".to_vec(),
};
// Initialize writer
let mut stream = Cursor::new(Vec::new());
let mut writer = ServerResponseWriter::new(&mut stream);
// Perform ResetDevice
handle_device(&request, "", &mut writer);
// Initialize writer
let mut stream = Cursor::new(Vec::new());
let mut writer = ServerResponseWriter::new(&mut stream);
// Perform ResetDevice
let request = list_request();
handle_device(&request, "", &mut writer);
// Check the response for ListDevice
let expected = include_bytes!("test/initial.txt");
let actual = stream.get_ref();
assert_eq!(actual, expected);
}
// Helper function for regenerating golden files
fn regenerate_golden_files_helper() {
use std::io::Write;
// Write initial state of the test case (2 bt chip and 1 wifi chip)
let mut file = std::fs::File::create("src/devices/test/initial.txt").unwrap();
let initial = b"HTTP/1.1 200\r\nContent-Type: text/json\r\nContent-Length: 783\r\n\r\n{\"devices\": [{\"id\": 1000, \"name\": \"test-device-name-1\", \"visible\": \"ON\", \"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}, \"orientation\": {\"yaw\": 0.0, \"pitch\": 0.0, \"roll\": 0.0}, \"chips\": [{\"kind\": \"BLUETOOTH\", \"id\": 1000, \"name\": \"bt_chip_name\", \"manufacturer\": \"netsim\", \"productName\": \"netsim_bt\", \"bt\": {}}, {\"kind\": \"WIFI\", \"id\": 1001, \"name\": \"bt_chip_name\", \"manufacturer\": \"netsim\", \"productName\": \"netsim_bt\", \"wifi\": {\"state\": \"UNKNOWN\", \"range\": 0.0, \"txCount\": 0, \"rxCount\": 0}}]}, {\"id\": 1001, \"name\": \"test-device-name-2\", \"visible\": \"ON\", \"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}, \"orientation\": {\"yaw\": 0.0, \"pitch\": 0.0, \"roll\": 0.0}, \"chips\": [{\"kind\": \"BLUETOOTH\", \"id\": 1002, \"name\": \"bt_chip_name\", \"manufacturer\": \"netsim\", \"productName\": \"netsim_bt\", \"bt\": {}}]}]}";
file.write_all(initial).unwrap();
// Write the body of the patch request
let mut file = std::fs::File::create("src/devices/test/patch_body.txt").unwrap();
let patch_body = b"{\"device\": {\"name\": \"test-device-name-1\", \"visible\": \"OFF\", \"position\": {\"x\": 1.0, \"y\": 1.0, \"z\": 1.0}}}";
file.write_all(patch_body).unwrap();
// Write post-patch state of the test case (after PatchDevice)
let mut file = std::fs::File::create("src/devices/test/post_patch.txt").unwrap();
let post_patch = b"HTTP/1.1 200\r\nContent-Type: text/json\r\nContent-Length: 784\r\n\r\n{\"devices\": [{\"id\": 1000, \"name\": \"test-device-name-1\", \"visible\": \"OFF\", \"position\": {\"x\": 1.0, \"y\": 1.0, \"z\": 1.0}, \"orientation\": {\"yaw\": 0.0, \"pitch\": 0.0, \"roll\": 0.0}, \"chips\": [{\"kind\": \"BLUETOOTH\", \"id\": 1000, \"name\": \"bt_chip_name\", \"manufacturer\": \"netsim\", \"productName\": \"netsim_bt\", \"bt\": {}}, {\"kind\": \"WIFI\", \"id\": 1001, \"name\": \"bt_chip_name\", \"manufacturer\": \"netsim\", \"productName\": \"netsim_bt\", \"wifi\": {\"state\": \"UNKNOWN\", \"range\": 0.0, \"txCount\": 0, \"rxCount\": 0}}]}, {\"id\": 1001, \"name\": \"test-device-name-2\", \"visible\": \"ON\", \"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}, \"orientation\": {\"yaw\": 0.0, \"pitch\": 0.0, \"roll\": 0.0}, \"chips\": [{\"kind\": \"BLUETOOTH\", \"id\": 1002, \"name\": \"bt_chip_name\", \"manufacturer\": \"netsim\", \"productName\": \"netsim_bt\", \"bt\": {}}]}]}";
file.write_all(post_patch).unwrap();
}
/// This is not a test function
/// Uncomment the helper function and run the test to regenerate golden files.
#[test]
fn regenerate_golden_files() {
// Avoiding Interleaving Operations
let _lock = MUTEX.lock().unwrap();
// regenerate_golden_files_helper();
}
}