1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
// TEST.rs
// by Lut99
//
// Created:
// 21 Sep 2022, 16:23:37
// Last edited:
// 25 May 2023, 20:12:59
// Auto updated?
// Yes
//
// Description:
//! Contains functions for testing package functions.
//
use std::fs;
use std::path::PathBuf;
use brane_ast::ParserOptions;
use brane_exe::FullValue;
use brane_tsk::docker::DockerOptions;
use brane_tsk::input::prompt_for_input;
use console::style;
use specifications::data::DataIndex;
use specifications::package::PackageInfo;
use specifications::version::Version;
use crate::errors::TestError;
use crate::run::{OfflineVmState, initialize_offline_vm, run_offline_vm};
use crate::utils::{ensure_datasets_dir, ensure_package_dir};
/***** HELPER FUNCTIONS *****/
/// Writes the given FullValue to a string in such a way that it's valid BraneScript.
///
/// # Arguments
/// - `value`: The FullValue to write.
///
/// # Returns
/// The string that may be written to, say, phony workflow files.
fn write_value(value: FullValue) -> String {
match value {
FullValue::Array(values) => {
// Write them all in an array
format!("[ {} ]", values.into_iter().map(write_value).collect::<Vec<String>>().join(", "))
},
FullValue::Instance(name, props) => {
// Write them all in an instance expression
format!("new {}{{ {} }}", name, props.into_iter().map(|(n, v)| format!("{n} := {v}")).collect::<Vec<String>>().join(", "))
},
FullValue::Data(name) => {
// Write it as a new Data declaration
format!("new Data{{ name := \"{name}\" }}")
},
FullValue::IntermediateResult(name) => {
// Also write it as a new Data declaration
format!("new Data{{ name := \"{name}\" }}")
},
FullValue::Boolean(value) => {
if value {
"true".into()
} else {
"false".into()
}
},
FullValue::Integer(value) => format!("{value}"),
FullValue::Real(value) => format!("{value}"),
FullValue::String(value) => format!("\"{}\"", value.replace('\\', "\\\\").replace('\"', "\\\"")),
FullValue::Void => String::new(),
}
}
/***** LIBRARY *****/
/// Handles the `brane test`-command.
///
/// # Arguments
/// - `name`: The name of the package to test.
/// - `version`: The version of the package to test.
/// - `show_result`: Whether or not to `cat` the resulting file if any.
/// - `docker_opts`: The options we use to connect to the local Docker daemon.
/// - `keep_containers`: Whether to keep containers after execution or not.
///
/// # Returns
/// Nothing, but does do a whole dance of querying the user and executing a package based on that.
///
/// # Errors
/// This function errors if any part of that dance failed.
pub async fn handle(
name: impl Into<String>,
version: Version,
show_result: Option<PathBuf>,
docker_opts: DockerOptions,
keep_containers: bool,
) -> Result<(), TestError> {
let name: String = name.into();
// Read the package info of the given package
let package_dir = match ensure_package_dir(&name, Some(&version), false) {
Ok(dir) => dir,
Err(err) => {
return Err(TestError::PackageDirError { name, version, err });
},
};
let package_info = match PackageInfo::from_path(package_dir.join("package.yml")) {
Ok(info) => info,
Err(err) => {
return Err(TestError::PackageInfoError { name, version, err });
},
};
// Run the test for this info
let output: FullValue = test_generic(package_info, show_result, docker_opts, keep_containers).await?;
// Print it, done
println!("Result: {} [{}]", style(format!("{output}")).bold().cyan(), style(format!("{}", output.data_type())).bold());
Ok(())
}
/// Tests the package in the given PackageInfo.
///
/// # Arguments
/// - `info`: The PackageInfo that describes the package to test.
/// - `show_result`: Whether or not to `cat` the resulting file if any.
/// - `docker_opts`: The options we use to connect to the local Docker daemon.
/// - `keep_containers`: Whether to keep containers after execution or not.
///
/// # Returns
/// The value of the chosen function in that package (which may be Void this time).
pub async fn test_generic(
info: PackageInfo,
show_result: Option<PathBuf>,
docker_opts: DockerOptions,
keep_containers: bool,
) -> Result<FullValue, TestError> {
// Get the local datasets directory
let datasets_dir: PathBuf = match ensure_datasets_dir(true) {
Ok(dir) => dir,
Err(err) => {
return Err(TestError::DatasetsDirError { err });
},
};
// Collect the local data index
let data_index: DataIndex = match brane_tsk::local::get_data_index(datasets_dir) {
Ok(index) => index,
Err(err) => {
return Err(TestError::DataIndexError { err });
},
};
// Query the user what they'd like to do (we quickly convert the common Type to a ClassDef)
let (function, mut args) = match prompt_for_input(&data_index, &info) {
Ok(res) => res,
Err(err) => {
return Err(TestError::InputError { err });
},
};
// Build a phony workflow with that
let workflow: String = format!(
"import {}[{}]; return {}({});",
info.name,
info.version,
function,
// We iterate over the function arguments to resolve them in the args
info.functions
.get(&function)
.unwrap()
.parameters
.iter()
.map(|p| { write_value(args.remove(&p.name).unwrap()) })
.collect::<Vec<String>>()
.join(", "),
);
// We run it by spinning up an offline VM
let mut state: OfflineVmState = match initialize_offline_vm(ParserOptions::bscript(), docker_opts, keep_containers) {
Ok(state) => state,
Err(err) => {
return Err(TestError::InitializeError { err });
},
};
let result: FullValue = match run_offline_vm(&mut state, "<test task>", workflow).await {
Ok(result) => result,
Err(err) => {
return Err(TestError::RunError { err });
},
};
// Write the intermediate result if told to do so
if let Some(file) = show_result {
if let FullValue::IntermediateResult(name) = &result {
let name: String = name.into();
// Write the result
println!();
println!("{}", (0..80).map(|_| '-').collect::<String>());
println!("Contents of intermediate result '{name}':");
let path: PathBuf = state.results_dir.path().join(name).join(file);
let contents: String = match fs::read_to_string(&path) {
Ok(contents) => contents,
Err(err) => {
return Err(TestError::IntermediateResultFileReadError { path, err });
},
};
if !contents.is_empty() {
println!("{contents}");
}
println!("{}", (0..80).map(|_| '-').collect::<String>());
println!();
}
}
// Return the result
Ok(result)
}