blob: dd90a90de763d9903cc01316a97161b8a64600e7 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#![cfg(test)]
use argh::FromArgs;
use fuchsia_async as fasync;
use fuchsia_component_test::{Capability, ChildOptions, RealmBuilder, RealmInstance, Ref, Route};
use fuchsia_zircon::{self as zx, DurationNum};
use iquery::{command_line::CommandLine, commands::*, types::Error};
use pretty_assertions::assert_eq;
use regex::Regex;
use serde::ser::Serialize;
use serde_json::ser::{PrettyFormatter, Serializer};
use std::{fmt, fs, path::Path};
pub const BASIC_COMPONENT_URL: &'static str =
"fuchsia-pkg://fuchsia.com/iquery-tests#meta/basic_component.cm";
pub const TEST_COMPONENT_URL: &'static str =
"fuchsia-pkg://fuchsia.com/iquery-tests#meta/test_component.cm";
pub struct TestBuilder {
builder: RealmBuilder,
}
impl TestBuilder {
pub async fn new() -> Self {
Self { builder: RealmBuilder::new().await.expect("Created realm builder") }
}
pub async fn add_basic_component(mut self, name: &str) -> Self {
self.add_child(name, BASIC_COMPONENT_URL).await;
self
}
pub async fn add_test_component(mut self, name: &str) -> Self {
self.add_child(name, TEST_COMPONENT_URL).await;
self
}
pub async fn start(self) -> TestInExecution {
let instance = self.builder.build().await.expect("create instance");
TestInExecution { instance }
}
async fn add_child(&mut self, name: &str, url: &str) -> &mut Self {
let child_ref =
self.builder.add_child(name, url, ChildOptions::new().eager()).await.unwrap();
self.builder
.add_route(
Route::new()
.capability(Capability::protocol_by_name("fuchsia.logger.LogSink"))
.from(Ref::parent())
.to(&child_ref),
)
.await
.unwrap();
self
}
}
pub enum AssertionOption {
Retry,
}
#[derive(Clone, Copy, Debug)]
pub enum IqueryCommand {
List,
ListAccessors,
Selectors,
Show,
}
impl fmt::Display for IqueryCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::List => "list",
Self::ListAccessors => "list-accessors",
Self::Selectors => "selectors",
Self::Show => "show",
};
write!(f, "{}", s)
}
}
pub struct AssertionParameters<'a> {
pub command: IqueryCommand,
pub golden_basename: &'static str,
pub iquery_args: Vec<&'a str>,
pub opts: Vec<AssertionOption>,
}
#[derive(Clone, Copy, Debug)]
pub enum Format {
Json,
Text,
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Json => "json",
Self::Text => "text",
};
write!(f, "{}", s)
}
}
impl Format {
fn all() -> impl Iterator<Item = Format> {
vec![Format::Json, Format::Text].into_iter()
}
}
pub struct TestInExecution {
instance: RealmInstance,
}
impl TestInExecution {
pub async fn assert(&self, params: AssertionParameters<'_>) {
for format in Format::all() {
let expected = self.read_golden(params.golden_basename, format);
let mut assertion = CommandAssertion::new(
params.command,
&expected,
format,
&params.iquery_args,
self.instance.root.child_name(),
);
for opt in &params.opts {
match opt {
AssertionOption::Retry => assertion.with_retries(),
}
}
assertion.assert().await;
}
}
pub fn instance_child_name(&self) -> &str {
self.instance.root.child_name()
}
fn read_golden(&self, golden_basename: &str, format: Format) -> String {
let path = format!("/pkg/data/goldens/{}.{}", golden_basename, format);
fs::read_to_string(Path::new(&path))
.unwrap_or_else(|e| panic!("loaded golden {}: {:?}", path, e))
}
}
#[derive(Clone, Debug)]
pub struct CommandAssertion<'a> {
command: IqueryCommand,
iquery_args: &'a [&'a str],
max_retry_time_seconds: i64,
expected: &'a str,
format: Format,
instance_child_name: &'a str,
}
impl<'a> CommandAssertion<'a> {
pub fn new(
command: IqueryCommand,
expected: &'a str,
format: Format,
iquery_args: &'a [&'a str],
instance_child_name: &'a str,
) -> Self {
Self {
command,
iquery_args,
max_retry_time_seconds: 0,
expected: expected.into(),
format,
instance_child_name,
}
}
pub fn with_retries(&mut self) {
self.max_retry_time_seconds = 120;
}
pub(crate) async fn assert(self) {
let started = zx::Time::get_monotonic().into_nanos();
let format_str = self.format.to_string();
let command_str = self.command.to_string();
let mut command_line = vec!["--format", &format_str, &command_str];
command_line.append(&mut self.iquery_args.iter().map(|s| s.as_ref()).collect::<Vec<_>>());
loop {
match execute_command(&command_line[..]).await {
Ok(mut result) => {
result = self.cleanup_unrelated_components(result);
let now = zx::Time::get_monotonic().into_nanos();
if now >= started + self.max_retry_time_seconds.seconds().into_nanos() {
self.assert_result(&result, &self.expected);
break;
}
if self.result_equals_expected(&result, &self.expected) {
break;
}
}
Err(e) => {
let now = zx::Time::get_monotonic().into_nanos();
if now >= started + self.max_retry_time_seconds.seconds().into_nanos() {
assert!(false, "Error: {:?}", e);
}
}
}
fasync::Timer::new(fasync::Time::after(100.millis())).await;
}
}
fn cleanup_unrelated_components(&self, result: String) -> String {
match self.format {
Format::Json => {
// Removes the entry in the vector for the archivist
let mut result_json: serde_json::Value =
serde_json::from_str(&result).expect("expected json");
self.retain_with_moniker_match_in_json(&mut result_json, |val| {
// Also retain paths (for accessor and files lists)
val.starts_with("/")
|| val.starts_with("./")
|| val.starts_with("archivist")
|| val.ends_with("archivist")
|| val == "realm_builder_server"
|| val.starts_with(&format!("realm_builder\\:{}", self.instance_child_name))
|| val.starts_with(&format!("realm_builder:{}", self.instance_child_name))
});
// Use 4 spaces for indentation since `fx format-code` enforces that in the
// goldens.
let mut buf = Vec::new();
let mut ser =
Serializer::with_formatter(&mut buf, PrettyFormatter::with_indent(b" "));
result_json.serialize(&mut ser).unwrap();
String::from_utf8(buf).unwrap()
}
Format::Text => self.retain_with_moniker_match_in_text(result),
}
}
fn retain_with_moniker_match_in_json<F>(&self, result_json: &mut serde_json::Value, f: F)
where
F: Fn(&str) -> bool,
{
if let Some(arr) = result_json.as_array_mut() {
arr.retain(|value| value.as_str().map(&f).unwrap_or(true));
arr.retain(|value| {
value.get("moniker").and_then(|val| val.as_str()).map(&f).unwrap_or(true)
});
}
}
// We want to filter out results from other tests. So we only include the components under our
// test case root realm.
fn retain_with_moniker_match_in_text(&self, initial: String) -> String {
let mut result = String::new();
let mut include_data = true;
for line in initial.lines() {
if line.starts_with(" ") {
if include_data {
result.push_str(line);
result.push_str("\n");
}
continue;
}
if line.starts_with("realm_builder\\:") {
if !line.starts_with(&format!("realm_builder\\:{}", self.instance_child_name)) {
include_data = false;
continue;
} else {
include_data = true;
}
}
if line.starts_with("realm_builder:") {
if !line.starts_with(&format!("realm_builder:{}", self.instance_child_name)) {
include_data = false;
continue;
} else {
include_data = true;
}
}
result.push_str(line);
result.push_str("\n");
}
result.trim().to_string()
}
/// Validates that a command result matches the expected json string
fn assert_result(&self, result: &str, expected: &str) {
let clean_result = self.cleanup_variable_strings(result);
assert_eq!(&clean_result, expected.trim());
}
/// Checks that the result string (cleaned) and the expected string are equal
fn result_equals_expected(&self, result: &str, expected: &str) -> bool {
let clean_result = self.cleanup_variable_strings(result);
clean_result.trim() == expected.trim()
}
/// Cleans-up instances of:
/// - RealmBuilder collection child names by CHILD_NAME
/// - `"start_timestamp_nanos": 7762005786231` by `"start_timestamp_nanos": TIMESTAMP`
/// - process IDs and thread IDs
/// - instance IDs in monikers
/// - timestamps in log strings
/// - process and thread IDs in log strings
fn cleanup_variable_strings(&self, string: impl Into<String>) -> String {
// Replace start_timestamp_nanos in fuchsia.inspect.Health entries and
// timestamp in metadatas.
let mut string: String = string.into();
// Replace the autogenerated realm builder collection child name (TEXT)
let re = Regex::new(r#"realm_builder\\:auto-[[:xdigit:]]+/"#).unwrap();
let replacement = "realm_builder\\:CHILD_NAME/";
string = re.replace_all(&string, replacement).to_string();
// Replace the autogenerated realm builder collection child name (JSON)
let re = Regex::new(r#"realm_builder\\\\:auto-[[:xdigit:]]+/"#).unwrap();
let replacement = "realm_builder\\\\:CHILD_NAME/";
string = re.replace_all(&string, replacement).to_string();
// Replace the autogenerated realm builder collection child name (path)
let re = Regex::new(r#"realm_builder:auto-[[:xdigit:]]+/"#).unwrap();
let replacement = "realm_builder:CHILD_NAME/";
string = re.replace_all(&string, replacement).to_string();
// Moniker in log metadatas requires special treatment to remove instance ids.
let re = Regex::new(r#""moniker": "(.+:)(\d+)""#).unwrap();
let replacement = "\"moniker\": \"${1}INSTANCE_ID";
string = re.replace_all(&string, replacement).to_string();
// Timestamp, pid, instance id and tid in log text.
let re = Regex::new(r#"\[\d+\.\d+\]\[\d+\]\[\d+\]\[(.+)\]"#).unwrap();
string = re.replace_all(&string, "[TIMESTAMP][PID][TID][${1}]").to_string();
// Make PID and TID constant in JSON outputs.
for value in &["pid", "tid"] {
let re = Regex::new(&format!("\"{}\": \\d+", value)).unwrap();
let replacement = format!("\"{}\": \"{}\"", value, value.to_string().to_uppercase());
string = re.replace_all(&string, replacement.as_str()).to_string();
}
for value in &["timestamp", "start_timestamp_nanos"] {
let re = Regex::new(&format!("\"{}\": \\d+", value)).unwrap();
let replacement = format!("\"{}\": \"TIMESTAMP\"", value);
string = re.replace_all(&string, replacement.as_str()).to_string();
let re = Regex::new(&format!("{} = \\d+", value)).unwrap();
let replacement = format!("{} = TIMESTAMP", value);
string = re.replace_all(&string, replacement.as_str()).to_string();
}
// Remove tab characters.
let re = Regex::new(r#"\t"#).unwrap();
string = re.replace_all(&string, "").to_string();
string
}
}
/// Execute a command: [command, flags, and, iquery_args]
pub async fn execute_command(command: &[&str]) -> Result<String, Error> {
let provider = ArchiveAccessorProvider::default();
let command_line = CommandLine::from_args(&["iquery"], command).expect("create command line");
command_line.execute(&provider).await
}