Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions picotest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ picotest_macros = { path = "../picotest_macros", version = "1.8.1" }
picotest_helpers = { path = "../picotest_helpers", version = "1.8.1" }
anyhow.workspace = true
ctor = "0.6.0"
libloading = "0.8.9"
base64 = "0.22.1"
rstest.workspace = true
serde.workspace = true

Expand Down
16 changes: 15 additions & 1 deletion picotest/src/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ pub fn lua_ffi_call_unit_test(test_fn_name: &str, plugin_dylib_path: &str) -> St
"[*] Running unit-test '{test_fn_name}'"

ffi = require("ffi")
ffi.cdef[[void {test_fn_name}();]]
ffi.cdef[[void ({test_fn_name})();]]
dylib = "{plugin_dylib_path}"
ffi.load(dylib).{test_fn_name}()

Expand All @@ -114,6 +114,20 @@ true"#
)
}

/// Replaces original test implementation in test binary, executes its payload on picodata
/// instance and passes back result of test run.
///
/// ### Arguments
/// - `package_name` - name of plugin to load into picodata, on picotest_unit side
/// it should be provided via env!(CARGO_PKG_NAME)
/// - `test_locator_name` - FFI-exposed function, which stores information about test.
/// As of now, it calls the test by itself.
/// - `test_display_name` - fully-qualified test name.
pub fn execute_test(package_name: &str, test_locator_name: &str, test_display_name: &str) {
let runner = crate::runner::get_test_runner(package_name);
runner.execute_unit(test_display_name, test_locator_name);
}

pub fn verify_unit_test_output(output: &str) -> anyhow::Result<()> {
if output.contains("cannot open shared object file") {
bail!("failed to open plugin shared library")
Expand Down
1 change: 1 addition & 0 deletions picotest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub use rstest::*;
pub use std::{panic, path::PathBuf, sync::OnceLock, time::Duration};

pub mod internal;
pub mod runner;

pub static SESSION_CLUSTER: OnceLock<Cluster> = OnceLock::new();

Expand Down
106 changes: 106 additions & 0 deletions picotest/src/runner/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use std::path::PathBuf;

use super::{PicotestRunner, TestResult, TestStatus };
use crate::get_or_create_session_cluster;
use crate::internal::verify_unit_test_output;
use crate::internal::{get_or_create_unit_test_topology, plugin_dylib_path, plugin_root_dir};
use picotest_helpers::Cluster;

struct RemotePicotestRunner {
#[allow(unused)]
package_name: String,
plugin_dylib_path: PathBuf,
cluster: &'static Cluster,
}

impl PicotestRunner for RemotePicotestRunner {
fn execute_unit(&self, name: &str, locator_name: &str) -> TestResult {
let package_name = &self.package_name;
let dylib_path = self.plugin_dylib_path.to_str().unwrap();
let call_server_side = format!(
r#"
"[*] Running unit-test '{name}'"

ffi = require("ffi")
_G.__picotest = _G.__picotest or {{ }}
_G.__picotest_result = _G.__picotest_result or ffi.cdef[[ typedef struct {{ uint8_t fail; char* data; size_t len; size_t cap; }} picounit_result; ]]
_G.__picotest_exec_unit = _G.__picotest_exec_unit or ffi.cdef[[ picounit_result (picotest_execute_unit)(const char*, const char*, const char*); ]]
_G.__picotest_free_unit = _G.__picotest_free_unit or ffi.cdef[[ void (picotest_free_unit_result)(picounit_result); ]]
_G.__picotest["{package_name}"] = _G.__picotest["{package_name}"] or {{ lib = ffi.load("{dylib_path}") }}

result = _G.__picotest["{package_name}"].lib.picotest_execute_unit("{package_name}","{name}","{locator_name}")

"[*] Test '{name}' has been finished"
("picotest_unit|{name}|fail=%s"):format(result.fail)
("picotest_unit|{name}|data=%s"):format(ffi.string(result.data,result.len))

_G.__picotest["{package_name}"].lib.picotest_free_unit_result(result)
true"#
);

let output = self.cluster
.run_lua(call_server_side)
.expect("Failed to execute query");

let test_out_prefix = format!("- picotest_unit|{name}|");
let mut fail = false;
let (mut payload,mut location,mut backtrace): (String,Option<String>,Option<String>) = (String::new(),None,None);
for line in output.split("\n") {
if !line.starts_with(&test_out_prefix) {
continue
}
let line = line.strip_prefix(&test_out_prefix).unwrap();
if !line.contains("=") {
continue;
}

let (key,value) = line.split_once("=").unwrap();
if key == "fail" && value == "1" {
fail = true
}
if key == "data" {
(payload, location, backtrace) = super::server::PicotestPanicInfo::decode_with_base64(value);
}
}

if fail {
let data = {
let mut out = String::with_capacity(backtrace.as_ref().map(|b| b.len()).unwrap_or(0)+200);
let location = location.unwrap_or(String::from("<?>"));
out += &format!("remote fiber panicked at {}:\n{}",location,payload);
if let Some(backtrace) = backtrace {
out += "\nremote stack backtrace:\n";
out += &backtrace;
}
out
};
panic!("{}",data);
}
if let Err(err) = verify_unit_test_output(&output) {
for l in output.split("----") {
println!("[Lua] {l}")
}
panic!("Test '{name}' exited with failure: {}", err);
}
TestResult { status: TestStatus::Success }
}
}

pub fn create_test_runner(package_name: &str) -> impl PicotestRunner {
let package_name = package_name.to_string();
let plugin_path = plugin_root_dir();
let plugin_dylib_path = plugin_dylib_path(&plugin_path, &package_name);
let plugin_topology = get_or_create_unit_test_topology();

let cluster = get_or_create_session_cluster(
plugin_path.to_str().unwrap().into(),
plugin_topology.into(),
0,
);

RemotePicotestRunner {
package_name,
cluster,
plugin_dylib_path,
}
}
46 changes: 46 additions & 0 deletions picotest/src/runner/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use std::collections::HashMap;
use std::sync::{Arc, LazyLock, Mutex, OnceLock};

mod client;
mod server;

static IS_SERVER_SIDE: OnceLock<bool> = OnceLock::new();
static RUNNERS_MAP: LazyLock<Mutex<HashMap<String,Arc<dyn PicotestRunner>>>> = LazyLock::new(|| {
Mutex::new(HashMap::with_capacity(1))
});

pub fn running_as_server() -> bool {
*IS_SERVER_SIDE.get_or_init(detect_run_as_server)
}

fn detect_run_as_server() -> bool {
let exe_path = std::env::current_exe().unwrap();
exe_path.ends_with("picodata")
}

pub type UnitTestLocator = extern "C" fn();

pub enum TestStatus {
Success,
Failure,
}

pub struct TestResult {
pub status: TestStatus,
}

pub trait PicotestRunner: Sync + Send {
fn execute_unit(&self, name: &str, locator_name: &str) -> TestResult;
}

pub fn get_test_runner(package_name: &str) -> Arc<dyn PicotestRunner> {
assert!(!running_as_server());

let mut runners_map = RUNNERS_MAP.lock().unwrap();
if let Some(package_test_runner) = runners_map.get(package_name) {
return Arc::clone(package_test_runner)
}
let new_runner = Arc::new(client::create_test_runner(package_name)) as Arc<dyn PicotestRunner>;
runners_map.insert(String::from(package_name), Arc::clone(&new_runner));
new_runner
}
Loading
Loading