#[cfg(feature = "ffi")]
use std::ffi::{CStr, CString};
#[cfg(feature = "ffi")]
use std::panic;
#[cfg(feature = "ffi")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "ffi")]
unsafe fn read_json(ptr: *const libc::c_char) -> Result<String, i32> {
if ptr.is_null() {
return Err(-1);
}
let cstr = unsafe { CStr::from_ptr(ptr) };
cstr.to_str().map(|s| s.to_string()).map_err(|_| -2)
}
#[cfg(feature = "ffi")]
fn write_json<T: Serialize>(result_ptr: *mut *mut libc::c_char, value: &T) -> i32 {
if result_ptr.is_null() {
return -1;
}
match serde_json::to_string(value) {
Ok(json) => match CString::new(json) {
Ok(cstr) => {
unsafe { *result_ptr = cstr.into_raw() };
0
}
Err(_) => -3,
},
Err(_) => -3,
}
}
#[cfg(feature = "ffi")]
const ERR_PARSE: i32 = -2;
#[cfg(feature = "ffi")]
const ERR_COMPUTE: i32 = -3;
#[cfg(feature = "ffi")]
fn write_error(
result_ptr: *mut *mut libc::c_char,
status: i32,
error: impl Into<crate::wire::WireError>,
) -> i32 {
let e = error.into();
let mut err = serde_json::json!({
"error": e.message, "code": e.code, "index": e.index, "parameter": e.parameter,
});
for (key, value) in &e.details {
err[*key] = value.to_json();
}
match write_json(result_ptr, &err) {
0 => status,
write_failure => write_failure,
}
}
#[cfg(feature = "ffi")]
fn parse_request<T: serde::de::DeserializeOwned>(
json: &str,
result_ptr: *mut *mut libc::c_char,
) -> Result<T, i32> {
let refuse = |message: String, site: Option<(String, Option<usize>)>| {
let mut error =
crate::wire::WireError::new(crate::wire::code::MALFORMED_INPUT, None, message);
if let Some((parameter, index)) = site {
error.index = index;
error = error.about(parameter);
}
write_error(result_ptr, ERR_PARSE, error)
};
let mut de = serde_json::Deserializer::from_str(json);
match serde_path_to_error::deserialize(&mut de) {
Ok(value) => match de.end() {
Ok(()) => Ok(value),
Err(e) => Err(refuse(format!("Invalid JSON: {e}"), None)),
},
Err(e) => {
let site = (e.inner().is_data() && e.path().iter().next().is_some())
.then(|| crate::wire::failure_site("", e.path()));
Err(refuse(format!("Invalid JSON: {e}"), site))
}
}
}
#[cfg(feature = "ffi")]
fn ffi_catch(
result_ptr: *mut *mut libc::c_char,
f: impl FnOnce() -> i32 + panic::UnwindSafe,
) -> i32 {
if !result_ptr.is_null() {
unsafe { *result_ptr = std::ptr::null_mut() };
}
match panic::catch_unwind(f) {
Ok(code) => code,
Err(_) => write_error(
result_ptr,
-4,
crate::wire::WireError::new(crate::wire::code::INTERNAL, None, "internal panic"),
),
}
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct SpcChartRequest {
subgroups: Vec<Vec<f64>>,
#[serde(default)]
rules: Option<serde_json::Value>,
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_xbar_r_chart(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: SpcChartRequest = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
let rules = match rules_of(req.rules) {
Ok(r) => r,
Err(e) => return write_error(result_ptr, ERR_COMPUTE, &e),
};
match crate::wire::xbar_r_dto(req.subgroups, rules) {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
})
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ValuesRequest {
values: Vec<f64>,
#[serde(default)]
rules: Option<serde_json::Value>,
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RunRulesRequest {
values: Vec<f64>,
limits: crate::wire::LimitsInputDto,
#[serde(default)]
rules: Option<serde_json::Value>,
}
#[cfg(feature = "ffi")]
fn rules_of(
rules: Option<serde_json::Value>,
) -> Result<crate::spc::RuleSet, crate::wire::WireError> {
crate::wire::rules_from_json(rules.map(|r| serde_json::json!({ "rules": r })))
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_xbar_s_chart(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: SpcChartRequest = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
let rules = match rules_of(req.rules) {
Ok(r) => r,
Err(e) => return write_error(result_ptr, ERR_COMPUTE, &e),
};
match crate::wire::xbar_s_dto(req.subgroups, rules) {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
})
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_imr_chart(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: ValuesRequest = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
let rules = match rules_of(req.rules) {
Ok(r) => r,
Err(e) => return write_error(result_ptr, ERR_COMPUTE, &e),
};
match crate::wire::imr_dto(req.values, rules) {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
})
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_run_rules(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: RunRulesRequest = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
let rules = match rules_of(req.rules) {
Ok(r) => r,
Err(e) => return write_error(result_ptr, ERR_COMPUTE, &e),
};
match crate::wire::run_rules_dto(req.values, req.limits, rules) {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
})
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct PChartRequest {
samples: serde_json::Value,
#[serde(default)]
p_bar: Option<f64>,
}
#[cfg(feature = "ffi")]
impl PChartRequest {
fn standard(&self) -> crate::wire::AttributeStandardDto {
crate::wire::AttributeStandardDto {
p_bar: self.p_bar,
u_bar: None,
phi: None,
}
}
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct LaneyPChartRequest {
samples: serde_json::Value,
#[serde(default)]
p_bar: Option<f64>,
#[serde(default)]
phi: Option<f64>,
}
#[cfg(feature = "ffi")]
impl LaneyPChartRequest {
fn standard(&self) -> crate::wire::AttributeStandardDto {
crate::wire::AttributeStandardDto {
p_bar: self.p_bar,
u_bar: None,
phi: self.phi,
}
}
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_p_chart(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: PChartRequest = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
match crate::wire::count_pairs(&req.samples, "samples")
.and_then(|s| crate::wire::p_chart_dto(&s, &req.standard()))
{
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
})
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_laney_p_chart(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: LaneyPChartRequest = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
match crate::wire::count_pairs(&req.samples, "samples")
.and_then(|s| crate::wire::laney_p_dto(&s, &req.standard()))
{
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
})
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct NpChartRequest {
defectives: serde_json::Value,
sample_size: serde_json::Value,
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct DefectsRequest {
defects: serde_json::Value,
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct UChartRequest {
samples: serde_json::Value,
#[serde(default)]
u_bar: Option<f64>,
}
#[cfg(feature = "ffi")]
impl UChartRequest {
fn standard(&self) -> crate::wire::AttributeStandardDto {
crate::wire::AttributeStandardDto {
p_bar: None,
u_bar: self.u_bar,
phi: None,
}
}
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct LaneyUChartRequest {
samples: serde_json::Value,
#[serde(default)]
u_bar: Option<f64>,
#[serde(default)]
phi: Option<f64>,
}
#[cfg(feature = "ffi")]
impl LaneyUChartRequest {
fn standard(&self) -> crate::wire::AttributeStandardDto {
crate::wire::AttributeStandardDto {
p_bar: None,
u_bar: self.u_bar,
phi: self.phi,
}
}
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct GapsRequest {
gaps: Vec<f64>,
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct TimesRequest {
times: Vec<f64>,
}
#[cfg(feature = "ffi")]
unsafe fn chart_entry<R, T, F>(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
compute: F,
) -> i32
where
R: serde::de::DeserializeOwned,
T: Serialize,
F: FnOnce(R) -> Result<T, crate::wire::WireError>,
{
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: R = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
match compute(req) {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_np_chart(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || unsafe {
chart_entry(request_json, result_ptr, |req: NpChartRequest| {
let defectives = crate::wire::count_rows(&req.defectives, "defectives")?;
let size = crate::wire::sample_size_value(&req.sample_size, "sample_size")?;
crate::wire::np_chart_dto(&defectives, size)
})
})
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_c_chart(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || unsafe {
chart_entry(request_json, result_ptr, |req: DefectsRequest| {
let defects = crate::wire::count_rows(&req.defects, "defects")?;
crate::wire::c_chart_dto(&defects)
})
})
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_u_chart(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || unsafe {
chart_entry(request_json, result_ptr, |req: UChartRequest| {
let samples = crate::wire::rate_pairs(&req.samples, "samples")?;
crate::wire::u_chart_dto(&samples, &req.standard())
})
})
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_laney_u_chart(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || unsafe {
chart_entry(request_json, result_ptr, |req: LaneyUChartRequest| {
let samples = crate::wire::rate_pairs(&req.samples, "samples")?;
crate::wire::laney_u_dto(&samples, &req.standard())
})
})
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_g_chart(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || unsafe {
chart_entry(request_json, result_ptr, |req: GapsRequest| {
crate::wire::g_chart_dto(&req.gaps)
})
})
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_t_chart(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || unsafe {
chart_entry(request_json, result_ptr, |req: TimesRequest| {
crate::wire::t_chart_dto(&req.times)
})
})
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_process_capability(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: crate::wire::CapabilityInputDto = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
match crate::wire::capability_dto(req) {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
})
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_percentile_capability(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: crate::wire::PercentileCapabilityInputDto = match parse_request(&json, result_ptr)
{
Ok(r) => r,
Err(status) => return status,
};
match crate::wire::percentile_capability_dto(req) {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
})
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_gage_rr_xbar_r(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: crate::wire::GageRRInputDto = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
match crate::wire::gage_rr_xbar_r_dto(req) {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
})
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_gage_rr_anova(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: crate::wire::GageRRInputDto = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
match crate::wire::gage_rr_anova_dto(req) {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
})
}
macro_rules! wire_export {
($(#[$doc:meta])* $name:ident, $req:ty, |$r:ident| $body:expr) => {
$(#[$doc])*
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn $name(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let $r: $req = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
match $body {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, e),
}
})
}
};
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct SigmaRequest {
sigma: f64,
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct PpmRequest {
ppm: f64,
}
wire_export!(
uanalytics_weibull_mle,
crate::wire::reliability::WeibullMleRequestDto,
|r| crate::wire::reliability::weibull_mle_dto(
&r.failure_times,
&r.suspension_times,
r.confidence,
r.method.as_deref(),
&r.fractions_failed
)
);
wire_export!(
uanalytics_counter_increments,
crate::wire::counter::CounterRequestDto,
|r| crate::wire::counter::increments_dto(
&r.times,
&r.values,
&crate::wire::counter::options_of(r.modulus, r.max_rate, &r.discontinuities)
)
);
wire_export!(
uanalytics_counter_totals,
crate::wire::counter::CounterTotalsRequestDto,
|r| crate::wire::counter::totals_dto(
&r.times,
&r.values,
&r.edges,
&crate::wire::counter::options_of(r.modulus, r.max_rate, &r.discontinuities)
)
);
wire_export!(
uanalytics_weibull_mrr,
crate::wire::reliability::WeibullMrrRequestDto,
|r| crate::wire::reliability::weibull_mrr_dto(&r.failure_times, &r.suspension_times)
);
wire_export!(
uanalytics_weibull_reliability,
crate::wire::reliability::WeibullReliabilityInputDto,
|r| crate::wire::reliability::weibull_reliability_dto(r)
);
wire_export!(
uanalytics_boxcox_capability,
crate::wire::reliability::BoxcoxCapabilityInputDto,
|r| crate::wire::reliability::boxcox_capability_dto(r)
);
wire_export!(
uanalytics_sigma_to_ppm,
SigmaRequest,
|r| crate::wire::reliability::sigma_to_ppm_value(r.sigma)
.map(|v| serde_json::json!({ "value": v }))
);
wire_export!(
uanalytics_ppm_to_sigma,
PpmRequest,
|r| crate::wire::reliability::ppm_to_sigma_value(r.ppm)
.map(|v| serde_json::json!({ "value": v }))
);
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct TwoReqAB {
a: Vec<f64>,
b: Vec<f64>,
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct TwoReqXY {
x: Vec<f64>,
y: Vec<f64>,
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct DataReq {
data: Vec<f64>,
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct OneSampleReq {
data: Vec<f64>,
mu0: f64,
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct GroupsReq {
groups: Vec<Vec<f64>>,
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct GofReq {
observed: Vec<f64>,
expected: Vec<f64>,
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct TableReq {
table: Vec<Vec<f64>>,
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct FisherReq {
table: serde_json::Value,
}
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct PValuesReq {
p_values: Vec<f64>,
}
wire_export!(
uanalytics_one_sample_t_test,
OneSampleReq,
|r| crate::wire::hypothesis::one_sample_t_dto(&r.data, r.mu0)
);
wire_export!(
uanalytics_two_sample_t_test,
TwoReqAB,
|r| crate::wire::hypothesis::two_sample_t_dto(&r.a, &r.b)
);
wire_export!(
uanalytics_mann_whitney_u_test,
TwoReqAB,
|r| crate::wire::hypothesis::mann_whitney_dto(&r.a, &r.b)
);
wire_export!(
uanalytics_paired_t_test,
TwoReqXY,
|r| crate::wire::hypothesis::paired_t_dto(&r.x, &r.y)
);
wire_export!(
uanalytics_wilcoxon_signed_rank_test,
TwoReqXY,
|r| crate::wire::hypothesis::wilcoxon_dto(&r.x, &r.y)
);
wire_export!(
uanalytics_jarque_bera_test,
DataReq,
|r| crate::wire::hypothesis::jarque_bera_dto(&r.data)
);
wire_export!(
uanalytics_shapiro_wilk_test,
DataReq,
|r| crate::wire::hypothesis::shapiro_wilk_dto(&r.data)
);
wire_export!(
uanalytics_anderson_darling_test,
DataReq,
|r| crate::wire::hypothesis::anderson_darling_dto(&r.data)
);
wire_export!(
uanalytics_adf_test,
crate::wire::hypothesis::AdfInputDto,
|r| crate::wire::hypothesis::adf_dto(r)
);
wire_export!(
uanalytics_mann_kendall_test,
DataReq,
|r| crate::wire::hypothesis::mann_kendall_dto(&r.data)
);
wire_export!(
uanalytics_one_way_anova,
GroupsReq,
|r| crate::wire::hypothesis::one_way_anova_dto(&r.groups)
);
wire_export!(
uanalytics_kruskal_wallis_test,
GroupsReq,
|r| crate::wire::hypothesis::groups_test_dto("kruskal_wallis_test", &r.groups)
);
wire_export!(
uanalytics_levene_test,
GroupsReq,
|r| crate::wire::hypothesis::groups_test_dto("levene_test", &r.groups)
);
wire_export!(
uanalytics_bartlett_test,
GroupsReq,
|r| crate::wire::hypothesis::groups_test_dto("bartlett_test", &r.groups)
);
wire_export!(
uanalytics_chi_squared_goodness_of_fit,
GofReq,
|r| crate::wire::hypothesis::chi_squared_gof_dto(&r.observed, &r.expected)
);
wire_export!(
uanalytics_chi_squared_independence,
TableReq,
|r| crate::wire::hypothesis::chi_squared_independence_dto(&r.table)
);
wire_export!(
uanalytics_fisher_exact_test,
FisherReq,
|r| crate::wire::hypothesis::fisher_exact_dto(&r.table)
);
wire_export!(
uanalytics_bonferroni_correction,
PValuesReq,
|r| crate::wire::hypothesis::p_adjust_dto("bonferroni_correction", &r.p_values).map(|v| serde_json::json!({ "values": v }))
);
wire_export!(
uanalytics_benjamini_hochberg,
PValuesReq,
|r| crate::wire::hypothesis::p_adjust_dto("benjamini_hochberg", &r.p_values).map(|v| serde_json::json!({ "values": v }))
);
#[cfg(feature = "ffi")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct PointProcessRequest {
times: Vec<f64>,
observation: serde_json::Value,
}
#[cfg(feature = "ffi")]
fn point_process_request(
json: &str,
result_ptr: *mut *mut libc::c_char,
) -> Result<(Vec<f64>, crate::wire::point_process::ObservationDto), i32> {
use crate::wire::point_process::check_truncation;
let req: PointProcessRequest = parse_request(json, result_ptr)?;
let name = req
.observation
.get("truncation")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_string();
if let Err(e) = check_truncation(&name) {
return Err(write_error(result_ptr, ERR_COMPUTE, e));
}
let observation = serde_json::from_value(req.observation).map_err(|e| {
write_error(
result_ptr,
ERR_PARSE,
crate::wire::WireError::new(
crate::wire::code::MALFORMED_INPUT,
None,
format!("observation: {e}"),
)
.about("observation"),
)
})?;
Ok((req.times, observation))
}
macro_rules! point_process_export {
($(#[$doc:meta])* $name:ident => $dto:path) => {
$(#[$doc])*
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn $name(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let (times, observation) = match point_process_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
match $dto(×, observation) {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, e),
}
})
}
};
}
point_process_export!(
uanalytics_laplace_trend_test => crate::wire::point_process::laplace_dto
);
point_process_export!(
uanalytics_mil_hdbk_189_test => crate::wire::point_process::mil_hdbk_189_dto
);
point_process_export!(
uanalytics_power_law_process_fit => crate::wire::point_process::power_law_dto
);
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_detect_changepoints(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: crate::wire::PeltInputDto = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
match crate::wire::pelt_dto(req) {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
})
}
wire_export!(
uanalytics_detect_changepoints_multi,
crate::wire::MultiPeltInputDto,
|r| crate::wire::multi_pelt_dto(r)
);
wire_export!(
uanalytics_cusum,
crate::wire::CusumInputDto,
|r| crate::wire::cusum_dto(r)
);
wire_export!(
uanalytics_ewma,
crate::wire::EwmaInputDto,
|r| crate::wire::ewma_dto(r)
);
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_estimate_period(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: crate::wire::SeasonalityInputDto = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
match crate::wire::seasonality_dto(req) {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
})
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_spectral_residual(
request_json: *const libc::c_char,
result_ptr: *mut *mut libc::c_char,
) -> i32 {
ffi_catch(result_ptr, || {
let json = match unsafe { read_json(request_json) } {
Ok(j) => j,
Err(e) => return e,
};
let req: crate::wire::SpectralResidualInputDto = match parse_request(&json, result_ptr) {
Ok(r) => r,
Err(status) => return status,
};
match crate::wire::spectral_residual_dto(req) {
Ok(dto) => write_json(result_ptr, &dto),
Err(e) => write_error(result_ptr, ERR_COMPUTE, &e),
}
})
}
wire_export!(
uanalytics_correlation_matrix,
crate::wire::CorrelationInputDto,
|r| crate::wire::correlation_matrix_dto(r)
);
wire_export!(
uanalytics_simple_regression,
crate::wire::RegressionInputDto,
|r| crate::wire::simple_regression_dto(r)
);
wire_export!(
uanalytics_fit_best,
DataReq,
|r| crate::wire::fit_best_dto(&r.data)
);
#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn uanalytics_free_string(ptr: *mut libc::c_char) {
if !ptr.is_null() {
unsafe { drop(CString::from_raw(ptr)) };
}
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub extern "C" fn uanalytics_codes() -> *mut libc::c_char {
let json = serde_json::to_string(&crate::wire::code_catalog()).expect("codes serialize");
CString::new(json)
.expect("JSON has no interior NUL")
.into_raw()
}
#[cfg(feature = "ffi")]
#[no_mangle]
pub extern "C" fn uanalytics_version() -> *mut libc::c_char {
let version = env!("CARGO_PKG_VERSION");
CString::new(version)
.expect("version string has no interior NUL")
.into_raw()
}
#[cfg(all(test, feature = "ffi"))]
mod tests {
use super::*;
type Entry = unsafe extern "C" fn(*const libc::c_char, *mut *mut libc::c_char) -> i32;
#[test]
fn the_codes_are_the_wire_catalog() {
let ptr = uanalytics_codes();
let json = unsafe { CStr::from_ptr(ptr) }
.to_str()
.expect("UTF-8")
.to_owned();
unsafe { uanalytics_free_string(ptr) };
let codes: crate::wire::Codes = serde_json::from_str(&json).expect("a catalog");
assert_eq!(codes, crate::wire::code_catalog());
assert!(codes.refusals.iter().any(|r| r.code == "value_not_finite"));
assert!(codes.violations.is_empty());
}
fn call(entry: Entry, request: &str) -> (i32, serde_json::Value) {
let request = CString::new(request).expect("test request has no interior NUL");
let mut out: *mut libc::c_char = std::ptr::null_mut();
let code = unsafe { entry(request.as_ptr(), &mut out) };
assert!(!out.is_null(), "every status must come with a JSON body");
let body = unsafe { CStr::from_ptr(out) }
.to_str()
.expect("body is UTF-8")
.to_owned();
unsafe { uanalytics_free_string(out) };
let value = serde_json::from_str(&body).expect("body is JSON");
(code, value)
}
#[test]
fn hypothesis_tests_round_trip_with_the_wasm_codes() {
let (code, b) = call(
uanalytics_two_sample_t_test,
r#"{"a": [5.1, 4.9, 5.3, 5.0], "b": [5.6, 5.8, 5.5, 5.9, 5.7]}"#,
);
assert_eq!(code, 0, "{b}");
assert!(b["p_value"].as_f64().unwrap() < 0.05);
let (code, b) = call(
uanalytics_one_way_anova,
r#"{"groups": [[1, 2, 3], [2, 3, 4], [5, 6, 7]]}"#,
);
assert_eq!((code, b["df_between"].as_u64()), (0, Some(2)), "{b}");
let (_, b) = call(
uanalytics_fisher_exact_test,
r#"{"table": [[8, 2], [1, 5]]}"#,
);
assert!(b["p_value"].as_f64().unwrap() < 0.05, "{b}");
let (_, b) = call(
uanalytics_benjamini_hochberg,
r#"{"p_values": [0.01, 0.04, 0.03, 0.2]}"#,
);
let v: Vec<f64> = serde_json::from_value(b["values"].clone()).unwrap();
assert!((v[1] - 0.04 / 3.0 * 4.0).abs() < 1e-12 && (v[3] - 0.2).abs() < 1e-12);
let (code, b) = call(uanalytics_paired_t_test, r#"{"x": [1, 2, 3], "y": [1, 2]}"#);
assert_eq!(
(code, b["code"].as_str()),
(-3, Some("dimension_mismatch")),
"{b}"
);
let (code, b) = call(uanalytics_mann_kendall_test, r#"{"data": [1, 2]}"#);
assert_eq!(
(code, b["code"].as_str(), b["min"].as_f64()),
(-3, Some("insufficient_data"), Some(4.0))
);
let (code, b) = call(
uanalytics_bonferroni_correction,
r#"{"p_values": [0.5, 1.5]}"#,
);
assert_eq!(
(code, b["code"].as_str(), b["index"].as_u64()),
(-3, Some("parameter_out_of_range"), Some(1))
);
}
#[test]
fn reliability_surface_round_trips() {
let (code, b) = call(
uanalytics_weibull_mrr,
r#"{"failure_times": [150, 200, 250, 300, 350, 400]}"#,
);
assert_eq!(code, 0, "{b}");
assert!(b["r_squared"].as_f64().unwrap() > 0.9);
let (code, b) = call(
uanalytics_weibull_reliability,
r#"{"shape": 2, "scale": 100, "times": [100], "fractions_failed": [0.1]}"#,
);
assert_eq!(code, 0, "{b}");
assert!((b["reliability"][0].as_f64().unwrap() - (-1.0f64).exp()).abs() < 1e-12);
let (_, b) = call(uanalytics_ppm_to_sigma, r#"{"ppm": 3.4}"#);
assert!((b["value"].as_f64().unwrap() - 6.0).abs() < 1e-2);
let (code, b) = call(uanalytics_ppm_to_sigma, r#"{"ppm": 0}"#);
assert_eq!(
(
code,
b["code"].as_str(),
b["exclusive_min"].as_f64(),
b["exclusive_max"].as_f64()
),
(-3, Some("parameter_out_of_range"), Some(0.0), Some(1e6))
);
assert!(b["min"].is_null() && b["max"].is_null(), "{b}");
let (code, b) = call(
uanalytics_boxcox_capability,
r#"{"data": [1, 2, 0, 3], "usl": 9}"#,
);
assert_eq!(
(code, b["code"].as_str(), b["index"].as_u64()),
(-3, Some("non_positive_data"), Some(2))
);
let (code, b) = call(uanalytics_weibull_mle, r#"{"failure_times": [10]}"#);
assert_eq!(
(
code,
b["code"].as_str(),
b["min"].as_f64(),
b["got"].as_f64()
),
(-3, Some("insufficient_data"), Some(2.0), Some(1.0))
);
}
#[test]
fn shift_detection_and_ad_normality_round_trip_with_the_wasm_codes() {
let mut data = vec![10.0; 10];
data.extend(vec![12.0; 10]);
let req = serde_json::json!({ "data": data, "target": 10.0, "sigma": 1.0 });
let (code, b) = call(uanalytics_cusum, &req.to_string());
assert_eq!(code, 0, "{b}");
let wire = crate::wire::cusum_dto(serde_json::from_value(req.clone()).unwrap()).unwrap();
assert_eq!(b, serde_json::to_value(&wire).unwrap());
assert_eq!(
(b["h"].as_f64(), b["in_control"].as_bool()),
(Some(5.0), Some(false))
);
let (code, b) = call(uanalytics_ewma, &req.to_string());
assert_eq!(code, 0, "{b}");
let wire = crate::wire::ewma_dto(serde_json::from_value(req).unwrap()).unwrap();
assert_eq!(b, serde_json::to_value(&wire).unwrap());
let (code, b) = call(
uanalytics_ewma,
r#"{"data": [1, 2], "target": 1, "sigma": 1, "lambda": 1.5}"#,
);
assert_eq!(
(
code,
b["code"].as_str(),
b["parameter"].as_str(),
b["max"].as_f64()
),
(
-3,
Some("parameter_out_of_range"),
Some("lambda"),
Some(1.0)
),
"{b}"
);
let (code, b) = call(uanalytics_cusum, r#"{"data": [], "target": 1, "sigma": 1}"#);
assert_eq!(
(code, b["code"].as_str(), b["parameter"].as_str()),
(-3, Some("empty_input"), Some("data"))
);
let multi = serde_json::json!({
"signals": [[0, 0, 0, 0, 5, 5, 5, 5], [1, 1, 1, 1, 4, 4, 4, 4]]
});
let (code, b) = call(uanalytics_detect_changepoints_multi, &multi.to_string());
assert_eq!(
(code, b["changepoints"].clone()),
(0, serde_json::json!([4])),
"{b}"
);
let (code, b) = call(
uanalytics_detect_changepoints_multi,
r#"{"signals": [[0, 0, 5, 5], [0, 5]]}"#,
);
assert_eq!(
(
code,
b["code"].as_str(),
b["index"].as_u64(),
b["got"].as_f64()
),
(-3, Some("dimension_mismatch"), Some(1), Some(2.0))
);
let (code, b) = call(
uanalytics_anderson_darling_test,
r#"{"data": [2.1, 1.9, 2.0, 2.2, 1.8, 2.05, 1.95, 2.15]}"#,
);
assert_eq!(code, 0, "{b}");
assert!(b["p_value"].as_f64().unwrap() > 0.05, "{b}");
let (code, b) = call(
uanalytics_anderson_darling_test,
r#"{"data": [3, 3, 3, 3, 3, 3, 3, 3]}"#,
);
assert_eq!(
(code, b["code"].as_str(), b["parameter"].as_str()),
(-3, Some("invalid_input"), Some("data"))
);
let (code, b) = call(uanalytics_anderson_darling_test, r#"{"data": [1, 2, 3]}"#);
assert_eq!(
(
code,
b["code"].as_str(),
b["min"].as_f64(),
b["got"].as_f64()
),
(-3, Some("insufficient_data"), Some(8.0), Some(3.0))
);
}
#[test]
fn correlation_regression_and_fitting_round_trip_and_name_their_refusals() {
let req = serde_json::json!({
"variables": [[1, 2, 3, 4, 5], [2, 4, 5, 4, 5], [5, 4, 3, 2, 1]],
"method": "spearman"
});
let (code, b) = call(uanalytics_correlation_matrix, &req.to_string());
assert_eq!(code, 0, "{b}");
let wire =
crate::wire::correlation_matrix_dto(serde_json::from_value(req).unwrap()).unwrap();
assert_eq!(b, serde_json::to_value(&wire).unwrap());
assert!(
(b["matrix"][0][2].as_f64().unwrap() + 1.0).abs() < 1e-12,
"{b}"
);
assert_eq!(b["matrix"][1][1].as_f64(), Some(1.0), "{b}");
let (code, b) = call(
uanalytics_correlation_matrix,
r#"{"variables": [[1, 2, 3], [4, 4, 4]]}"#,
);
assert_eq!(
(code, b["code"].as_str(), b["index"].as_u64()),
(-3, Some("invalid_input"), Some(1)),
"{b}"
);
let (_, b) = call(
uanalytics_correlation_matrix,
r#"{"variables": [[1, 2, 3], [4, 5]]}"#,
);
assert_eq!(
(b["code"].as_str(), b["index"].as_u64()),
(Some("dimension_mismatch"), Some(1))
);
let (_, b) = call(
uanalytics_correlation_matrix,
r#"{"variables": [[1, 2, 3], [3, 2, 1]], "method": "rank"}"#,
);
assert_eq!(b["code"], "unknown_option");
let (code, b) = call(
uanalytics_simple_regression,
r#"{"x": [1, 2, 3, 4, 5], "y": [2.1, 3.9, 6.2, 7.8, 10.1]}"#,
);
assert_eq!(code, 0, "{b}");
assert!((b["slope"].as_f64().unwrap() - 1.99).abs() < 1e-9, "{b}");
assert!(b["slope_p"].as_f64().unwrap() < 1e-3, "{b}");
assert_eq!(b["residuals"].as_array().unwrap().len(), 5);
let (_, b) = call(
uanalytics_simple_regression,
r#"{"x": [1, 2, 3], "y": [2, 4, 6]}"#,
);
assert!(b["slope_t"].is_null() && b["f_statistic"].is_null(), "{b}");
let (_, b) = call(
uanalytics_simple_regression,
r#"{"x": [1, 2, 3], "y": [2, 4]}"#,
);
assert_eq!(
(
b["code"].as_str(),
b["parameter"].as_str(),
b["expected"].as_f64()
),
(Some("dimension_mismatch"), Some("y"), Some(3.0))
);
let (_, b) = call(
uanalytics_simple_regression,
r#"{"x": [2, 2, 2], "y": [1, 2, 3]}"#,
);
assert_eq!(
(b["code"].as_str(), b["parameter"].as_str()),
(Some("invalid_input"), Some("x"))
);
let (code, b) = call(
uanalytics_fit_best,
r#"{"data": [12, 25, 31, 44, 52, 60, 71, 85]}"#,
);
assert_eq!(code, 0, "{b}");
let names: Vec<&str> = b
.as_array()
.unwrap()
.iter()
.map(|f| f["distribution"].as_str().unwrap())
.collect();
assert!(
names.contains(&"Weibull") && !names.contains(&"Poisson"),
"{names:?}"
);
let weibull = b
.as_array()
.unwrap()
.iter()
.find(|f| f["distribution"] == "Weibull")
.unwrap();
assert!(
weibull["parameters"]["shape"].as_f64().unwrap() > 1.0,
"{weibull}"
);
let (_, b) = call(uanalytics_fit_best, r#"{"data": [3, 3, 3]}"#);
assert_eq!(
(b["code"].as_str(), b["parameter"].as_str()),
(Some("invalid_input"), Some("data"))
);
}
#[test]
fn adf_round_trips_and_names_what_it_refuses() {
let mut state: u64 = 0x2545_f491_4f6c_dd1d;
let mut shock = move || {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(state >> 11) as f64 / (1u64 << 53) as f64 - 0.5
};
let mut data = vec![0.0_f64; 60];
for i in 1..data.len() {
data[i] = 0.3 * data[i - 1] + shock();
}
let req = serde_json::json!({ "data": data });
let (code, b) = call(uanalytics_adf_test, &req.to_string());
assert_eq!(code, 0, "{b}");
let wire = crate::wire::hypothesis::adf_dto(serde_json::from_value(req).unwrap()).unwrap();
assert_eq!(b, serde_json::to_value(&wire).unwrap());
let levels: Vec<f64> = b["levels"]
.as_array()
.unwrap()
.iter()
.map(|l| l["level"].as_f64().unwrap())
.collect();
assert_eq!(levels, vec![0.01, 0.05, 0.10]);
assert_eq!(b["levels"][1]["rejected"], true, "{b}");
let mut walk = vec![0.0_f64; 60];
for i in 1..walk.len() {
walk[i] = walk[i - 1] + shock();
}
let (_, b) = call(
uanalytics_adf_test,
&serde_json::json!({ "data": walk, "model": "constant_trend" }).to_string(),
);
assert_eq!(b["levels"][1]["rejected"], false, "{b}");
let ten = r#"[1, 3, 2, 5, 4, 6, 5, 8, 7, 9]"#;
let (code, b) = call(
uanalytics_adf_test,
&format!(r#"{{"data": {ten}, "lags": 2}}"#),
);
assert_eq!((code, b["n_lags"].as_u64()), (0, Some(2)), "{b}");
let (code, b) = call(
uanalytics_adf_test,
&format!(r#"{{"data": {ten}, "model": "none", "max_lags": 3}}"#),
);
assert_eq!(code, 0, "{b}");
assert!(b["n_lags"].as_u64().is_some_and(|p| p <= 3), "{b}");
let (_, b) = call(
uanalytics_adf_test,
&format!(r#"{{"data": {ten}, "lags": 1, "max_lags": 3}}"#),
);
assert_eq!(
(b["code"].as_str(), b["parameter"].as_str()),
(Some("invalid_option"), Some("lags")),
"{b}"
);
let (code, b) = call(
uanalytics_adf_test,
&format!(r#"{{"data": {ten}, "max_lags": 4}}"#),
);
assert_eq!(
(
code,
b["code"].as_str(),
b["parameter"].as_str(),
b["max"].as_f64()
),
(
-3,
Some("parameter_out_of_range"),
Some("max_lags"),
Some(3.0)
)
);
let (_, b) = call(
uanalytics_adf_test,
&format!(r#"{{"data": {ten}, "model": "trend"}}"#),
);
assert_eq!(
(b["code"].as_str(), b["got"].as_str()),
(Some("unknown_option"), Some("trend"))
);
let (_, b) = call(uanalytics_adf_test, r#"{"data": [1, 2, 3]}"#);
assert_eq!(
(b["code"].as_str(), b["min"].as_f64()),
(Some("insufficient_data"), Some(10.0))
);
}
#[test]
fn a_negative_count_is_out_of_range_in_the_c_body() {
let ten = r#"[1, 3, 2, 5, 4, 6, 5, 8, 7, 9]"#;
let (code, b) = call(
uanalytics_adf_test,
&format!(r#"{{"data": {ten}, "max_lags": -1}}"#),
);
assert_eq!(code, -3, "{b}");
assert_eq!(
b,
serde_json::json!({
"error": b["error"],
"code": "parameter_out_of_range",
"index": null,
"parameter": "max_lags",
"min": 0.0,
"max": 3.0,
"got": -1.0,
})
);
for got in [-1.0, 1.0] {
let (code, b) = call(
uanalytics_detect_changepoints,
&format!(r#"{{"data": [0, 0, 5, 5], "min_segment_len": {got}}}"#),
);
assert_eq!(code, -3, "{b}");
assert_eq!(
b,
serde_json::json!({
"error": b["error"],
"code": "parameter_out_of_range",
"index": null,
"parameter": "min_segment_len",
"min": 2.0,
"max": null,
"got": got,
})
);
}
let (code, b) = call(
uanalytics_spectral_residual,
&serde_json::json!({ "data": vec![1.0; 20], "judgement_window": -1 }).to_string(),
);
assert_eq!(
(
code,
b["code"].as_str(),
b["parameter"].as_str(),
b["min"].as_f64(),
b["got"].as_f64()
),
(
-3,
Some("parameter_out_of_range"),
Some("judgement_window"),
Some(1.0),
Some(-1.0)
),
"{b}"
);
for lags in ["2", "2.0"] {
let (code, b) = call(
uanalytics_adf_test,
&format!(r#"{{"data": {ten}, "lags": {lags}}}"#),
);
assert_eq!((code, b["n_lags"].as_u64()), (0, Some(2)), "{b}");
}
}
#[test]
fn a_fractional_count_is_malformed_at_its_path_in_the_c_body() {
let (code, b) = call(
uanalytics_adf_test,
r#"{"data": [1, 3, 2, 5, 4, 6, 5, 8, 7, 9], "max_lags": 2.5}"#,
);
assert_eq!(code, -2, "{b}");
assert_eq!(
(b["code"].as_str(), b["parameter"].as_str(), &b["index"]),
(
Some("malformed_input"),
Some("max_lags"),
&serde_json::Value::Null
),
"{b}"
);
assert!(
b["error"]
.as_str()
.is_some_and(|m| m.contains("expected a whole number")),
"{b}"
);
let (_, b) = call(
uanalytics_detect_changepoints_multi,
r#"{"signals": [[1, 2], [3, "4"]]}"#,
);
assert_eq!(
(
b["code"].as_str(),
b["parameter"].as_str(),
b["index"].as_u64()
),
(Some("malformed_input"), Some("signals[1]"), Some(1)),
"{b}"
);
let (code, b) = call(uanalytics_adf_test, "{");
assert_eq!((code, &b["parameter"]), (-2, &serde_json::Value::Null));
let (_, b) = call(uanalytics_adf_test, "{}");
assert_eq!(b["parameter"], serde_json::Value::Null, "{b}");
}
#[test]
fn refusal_details_reach_the_c_body() {
let (code, b) = call(
uanalytics_detect_changepoints,
r#"{"data": [0, 0, 5, 5], "penalty": -1}"#,
);
assert_eq!(
(code, b["code"].as_str(), b["parameter"].as_str()),
(-3, Some("parameter_out_of_range"), Some("penalty"))
);
assert_eq!(
b,
serde_json::json!({
"error": b["error"],
"code": "parameter_out_of_range",
"index": null,
"parameter": "penalty",
"min": null,
"exclusive_min": 0.0,
"max": null,
"got": -1.0,
})
);
let (code, b) = call(
uanalytics_ewma,
r#"{"data": [1, 2], "target": 1, "sigma": 1, "lambda": 0}"#,
);
assert_eq!((code, b["parameter"].as_str()), (-3, Some("lambda")), "{b}");
assert_eq!(
(
b["min"].is_null(),
b["exclusive_min"].as_f64(),
b["max"].as_f64(),
b.get("exclusive_max")
),
(true, Some(0.0), Some(1.0), None),
"{b}"
);
let (code, b) = call(
uanalytics_process_capability,
r#"{"data": [1, 2, 3], "usl": 1, "lsl": 2}"#,
);
assert_eq!((code, b["parameter"].as_str()), (-3, Some("usl")), "{b}");
assert_eq!(
(
b["min"].is_null(),
b["exclusive_min"].as_f64(),
b["got"].as_f64()
),
(true, Some(2.0), Some(1.0)),
"{b}"
);
let (_, b) = call(
uanalytics_detect_changepoints,
r#"{"data": [0, 0, 5, 5], "cost": "l1"}"#,
);
assert_eq!(b["code"], "unknown_option");
assert_eq!(b["expected"], serde_json::json!(["l2", "normal"]));
assert_eq!(b["got"], "l1");
}
#[test]
fn point_process_tests_round_trip_and_refuse_with_codes() {
let req = r#"{"times": [12, 15, 27, 34, 44, 53], "observation": {"truncation": "time", "end": 60}}"#;
let (code, b) = call(uanalytics_mil_hdbk_189_test, req);
assert_eq!(code, 0, "{b}");
assert!((b["statistic"].as_f64().unwrap() - 9.593).abs() < 1e-3);
assert_eq!(
(b["df"].as_f64(), b["direction"].as_str()),
(Some(12.0), Some("increasing"))
);
let (_, b) = call(uanalytics_laplace_trend_test, req);
assert!((b["p_value"].as_f64().unwrap() - 0.906).abs() < 5e-4);
assert!(b["df"].is_null());
let (_, b) = call(uanalytics_power_law_process_fit, req);
assert!((b["beta"].as_f64().unwrap() - 1.25093).abs() < 5e-6);
let (code, b) = call(
uanalytics_laplace_trend_test,
r#"{"times": [1, 2], "observation": {"truncation": "timed", "end": 3}}"#,
);
assert_eq!(
(code, b["code"].as_str(), b["parameter"].as_str()),
(-3, Some("unknown_option"), Some("observation.truncation"))
);
assert_eq!(b["got"], "timed", "{b}");
assert_eq!(b["expected"], serde_json::json!(["time", "failure"]), "{b}");
let (code, b) = call(
uanalytics_laplace_trend_test,
r#"{"times": [2, 1], "observation": {"truncation": "failure"}}"#,
);
assert_eq!(
(code, b["code"].as_str(), b["index"].as_u64()),
(-3, Some("events_unordered"), Some(1))
);
let (code, b) = call(
uanalytics_laplace_trend_test,
r#"{"times": [1, 0, 2], "observation": {"truncation": "failure"}}"#,
);
assert_eq!(
(
code,
b["code"].as_str(),
b["index"].as_u64(),
b["parameter"].as_str()
),
(-3, Some("parameter_out_of_range"), Some(1), Some("times")),
"{b}"
);
assert_eq!(
(
b["min"].is_null(),
b["exclusive_min"].as_f64(),
b["got"].as_f64()
),
(true, Some(0.0), Some(0.0)),
"{b}"
);
let (code, b) = call(
uanalytics_laplace_trend_test,
r#"{"times": [1], "observation": {"truncation": "time"}}"#,
);
assert_eq!(
(code, b["code"].as_str(), b["parameter"].as_str()),
(-2, Some("malformed_input"), Some("observation"))
);
}
fn subgroups(n: usize, count: usize) -> String {
let groups: Vec<Vec<f64>> = (0..count)
.map(|g| {
(0..n)
.map(|i| 10.0 + 0.1 * ((g * 7 + i * 3) % 5) as f64)
.collect()
})
.collect();
serde_json::json!({ "subgroups": groups }).to_string()
}
#[test]
fn a_rejected_request_reports_a_failure_status() {
let (code, body) = call(uanalytics_xbar_r_chart, r#"{"subgroups": []}"#);
assert_eq!(code, -3);
assert!(body["error"].is_string());
}
#[test]
fn malformed_json_reports_the_parse_status() {
let (code, body) = call(uanalytics_xbar_r_chart, "{not json");
assert_eq!(code, -2);
assert!(body["error"].is_string());
}
#[test]
fn a_chart_refuses_the_laney_factor_as_an_unknown_key() {
for (entry, request) in [
(
uanalytics_p_chart as Entry,
r#"{"samples": [[3, 50], [5, 50]], "phi": 1.2}"#,
),
(
uanalytics_u_chart,
r#"{"samples": [[3, 1.5], [5, 2]], "phi": 1.2}"#,
),
] {
let (code, body) = call(entry, request);
assert_eq!(code, -2, "{request}: {body}");
assert_eq!(body["code"], "malformed_input", "{body}");
assert!(
body["error"]
.as_str()
.unwrap()
.contains("unknown field `phi`"),
"{body}"
);
}
}
#[test]
fn every_attributes_chart_the_wasm_binding_has_is_reachable_from_c() {
let cases: [(Entry, &str, &str); 6] = [
(
uanalytics_np_chart,
r#"{"defectives": [2, 3, 1, 4], "sample_size": 50}"#,
"cl",
),
(uanalytics_c_chart, r#"{"defects": [3, 5, 2, 4]}"#, "cl"),
(
uanalytics_u_chart,
r#"{"samples": [[3, 1.5], [5, 2], [2, 1]]}"#,
"u_bar",
),
(
uanalytics_laney_u_chart,
r#"{"samples": [[3, 1.5], [5, 2], [2, 1], [6, 2.5]]}"#,
"phi",
),
(uanalytics_g_chart, r#"{"gaps": [10, 20, 30, 15]}"#, "g_bar"),
(
uanalytics_t_chart,
r#"{"times": [1.5, 2.0, 0.5, 3.0]}"#,
"t_bar",
),
];
for (entry, request, field) in cases {
let (code, body) = call(entry, request);
assert_eq!(code, 0, "{request}: {body}");
assert!(body[field].is_number(), "{request}: {body}");
assert!(
body["points"].as_array().is_some_and(|p| !p.is_empty()),
"{body}"
);
}
}
#[test]
fn attributes_refusals_carry_their_row() {
let (code, body) = call(uanalytics_c_chart, r#"{"defects": [3, 2.5, 4]}"#);
assert_eq!(code, -3, "{body}");
assert_eq!(body["code"], "count_not_whole", "{body}");
assert_eq!(body["index"], 1, "{body}");
let (code, body) = call(
uanalytics_u_chart,
r#"{"samples": [[3, 1.5]], "u_bar": -1}"#,
);
assert_eq!(code, -3, "{body}");
assert_eq!(body["code"], "parameter_out_of_range", "{body}");
assert_eq!(body["parameter"], "u_bar", "{body}");
assert_eq!(
(
body["min"].is_null(),
body["exclusive_min"].as_f64(),
body["got"].as_f64()
),
(true, Some(0.0), Some(-1.0)),
"{body}"
);
let (code, body) = call(uanalytics_t_chart, r#"{"times": [1, 0, 2]}"#);
assert_eq!(code, -3, "{body}");
assert_eq!(body["code"], "parameter_out_of_range", "{body}");
assert_eq!(body["index"], 1, "{body}");
assert_eq!(
(
body["min"].is_null(),
body["exclusive_min"].as_f64(),
body["max"].is_null(),
body["got"].as_f64()
),
(true, Some(0.0), true, Some(0.0)),
"{body}"
);
}
#[test]
fn a_known_u_bar_fixes_the_centre_line() {
let (code, body) = call(
uanalytics_u_chart,
r#"{"samples": [[3, 1.5], [5, 2], [2, 1]], "u_bar": 2.0}"#,
);
assert_eq!(code, 0, "{body}");
assert_eq!(body["u_bar"], 2.0, "{body}");
assert!(body["points"][0]["z"].is_number(), "{body}");
}
#[test]
fn xbar_r_accepts_every_subgroup_size_the_crate_supports() {
use crate::spc::{MAX_SUBGROUP_SIZE, MIN_SUBGROUP_SIZE};
for n in MIN_SUBGROUP_SIZE..=MAX_SUBGROUP_SIZE {
let (code, body) = call(uanalytics_xbar_r_chart, &subgroups(n, 5));
assert_eq!(code, 0, "n={n}: {body}");
}
let (code, _) = call(
uanalytics_xbar_r_chart,
&subgroups(MAX_SUBGROUP_SIZE + 1, 5),
);
assert_eq!(code, -3);
}
#[test]
fn xbar_r_limits_are_the_crate_chart_limits() {
use crate::spc::{ControlChart, XBarRChart};
let request = subgroups(12, 6);
let (code, body) = call(uanalytics_xbar_r_chart, &request);
assert_eq!(code, 0, "{body}");
let parsed: SpcChartRequest = serde_json::from_str(&request).expect("request parses");
let mut chart = XBarRChart::new(12).expect("12 is in range");
for g in &parsed.subgroups {
chart.add_sample(g).unwrap();
}
let x = chart.control_limits().expect("limits");
let r = chart.r_limits().expect("limits");
assert_eq!(body["xbar_ucl"].as_f64(), Some(x.ucl));
assert_eq!(body["xbar_lcl"].as_f64(), Some(x.lcl));
assert_eq!(body["r_ucl"].as_f64(), Some(r.ucl));
assert_eq!(body["r_lcl"].as_f64(), Some(r.lcl));
assert_eq!(body["sigma_hat"].as_f64(), chart.sigma_hat());
assert!(
body.get("x_bar_ucl").is_none(),
"the old spelling must be gone"
);
assert_eq!(body["xbar_points"].as_array().map(Vec::len), Some(6));
assert!(body["xbar_points"][0]["violations"].is_array());
assert!(body["in_control"].is_boolean());
}
#[test]
fn xbar_r_rejects_ragged_or_non_finite_subgroups() {
let (code, _) = call(
uanalytics_xbar_r_chart,
r#"{"subgroups": [[1.0, 2.0, 3.0], [1.0, 2.0]]}"#,
);
assert_eq!(code, -3);
let (code, _) = call(
uanalytics_xbar_r_chart,
r#"{"subgroups": [[1.0, 2.0, 3.0], [1.0, 1e400, 2.0]]}"#,
);
assert_ne!(code, 0);
}
#[test]
fn p_chart_matches_the_crate_chart() {
use crate::spc::PChart;
let (code, body) = call(
uanalytics_p_chart,
r#"{"samples": [[3, 100], [5, 120], [2, 80], [4, 100]]}"#,
);
assert_eq!(code, 0, "{body}");
let mut chart = PChart::new();
for (d, n) in [(3, 100), (5, 120), (2, 80), (4, 100)] {
chart.add_sample(d, n).unwrap();
}
assert_eq!(body["p_bar"].as_f64(), chart.p_bar());
let ucls: Vec<f64> = chart.points().iter().map(|p| p.ucl).collect();
let got: Vec<f64> = body["points"]
.as_array()
.unwrap()
.iter()
.map(|p| p["ucl"].as_f64().unwrap())
.collect();
assert_eq!(got, ucls);
}
#[test]
fn p_chart_pair_order_is_the_crate_order_and_the_reverse_is_refused() {
let (code, body) = call(uanalytics_p_chart, r#"{"samples": [[100, 3], [120, 5]]}"#);
assert_eq!(code, -3, "{body}");
assert!(
body["error"].as_str().unwrap().contains("samples[0]"),
"{body}"
);
let (code, body) = call(uanalytics_p_chart, r#"{"samples": [[3, 100], [5, 120]]}"#);
assert_eq!(code, 0, "{body}");
let p0 = body["points"][0]["value"].as_f64().unwrap();
assert!((p0 - 0.03).abs() < 1e-12, "3 of 100 is 0.03, got {p0}");
}
#[test]
fn p_chart_rejects_an_impossible_sample() {
let (code, body) = call(uanalytics_p_chart, r#"{"samples": [[100, 3], [10, 12]]}"#);
assert_eq!(code, -3, "{body}");
let (code, body) = call(uanalytics_p_chart, r#"{"samples": [[3, 100], [0, 0]]}"#);
assert_eq!(code, -3, "{body}");
assert_eq!(body["code"], "sample_size_not_whole", "{body}");
assert_eq!(body["index"], 1, "{body}");
for bad in ["-3", "10.5"] {
let req = format!(r#"{{"samples": [[3, 100], [1, {bad}]]}}"#);
let (code, body) = call(uanalytics_p_chart, &req);
assert_eq!(code, -3, "{body}");
assert_eq!(body["code"], "sample_size_not_whole", "{body}");
assert_eq!(body["index"], 1, "{body}");
}
}
#[test]
fn error_bodies_carry_a_code_and_the_row() {
let (code, body) = call(uanalytics_p_chart, r#"{"samples": [[3, 100], [1.5, 100]]}"#);
assert_eq!(code, -3, "{body}");
assert_eq!(body["code"], "count_not_whole", "{body}");
assert_eq!(body["index"], 1, "{body}");
assert!(
body["error"].as_str().unwrap().contains("samples[1]"),
"{body}"
);
let (code, body) = call(uanalytics_laney_p_chart, r#"{"samples": [[3, 100]]}"#);
assert_eq!(code, -3, "{body}");
assert_eq!(body["code"], "insufficient_data", "{body}");
assert!(body["index"].is_null(), "{body}");
let (code, body) = call(uanalytics_p_chart, "{");
assert_eq!(code, -2, "{body}");
assert_eq!(body["code"], "malformed_input", "{body}");
}
#[test]
fn laney_p_chart_reads_pairs_in_the_order_the_p_chart_does() {
use crate::spc::laney_p_chart;
let pairs = [(3, 100), (9, 120), (2, 80), (7, 100), (4, 110)];
let request = serde_json::json!({ "samples": pairs });
let (code, body) = call(uanalytics_laney_p_chart, &request.to_string());
assert_eq!(code, 0, "{body}");
let expected = laney_p_chart(&pairs, None).expect("valid samples");
assert_eq!(body["p_bar"].as_f64(), Some(expected.p_bar));
assert_eq!(body["phi"].as_f64(), Some(expected.phi));
let (code, body) = call(
uanalytics_laney_p_chart,
r#"{"samples": [[3, 100], [12, 10], [2, 90]]}"#,
);
assert_eq!(code, -3, "{body}");
}
#[test]
fn ffi_bodies_are_the_wire_contract_the_wasm_binding_emits() {
use crate::spc::RuleSet;
let groups: Vec<Vec<f64>> = (0..6)
.map(|g| {
(0..5)
.map(|i| 10.0 + 0.1 * ((g * 7 + i * 3) % 5) as f64)
.collect()
})
.collect();
let (code, body) = call(
uanalytics_xbar_r_chart,
&serde_json::json!({ "subgroups": groups }).to_string(),
);
assert_eq!(code, 0, "{body}");
let wire = crate::wire::xbar_r_dto(groups, RuleSet::nelson()).expect("chart");
assert_eq!(body, serde_json::to_value(&wire).unwrap());
let samples = [[3_u64, 100], [5, 120], [2, 80], [4, 100]];
let (code, body) = call(
uanalytics_p_chart,
&serde_json::json!({ "samples": samples }).to_string(),
);
assert_eq!(code, 0, "{body}");
let pairs: Vec<(u64, u64)> = samples.iter().map(|&[d, n]| (d, n)).collect();
let wire = crate::wire::p_chart_dto(&pairs, &Default::default()).expect("chart");
assert_eq!(body, serde_json::to_value(&wire).unwrap());
let (code, body) = call(
uanalytics_laney_p_chart,
&serde_json::json!({ "samples": samples }).to_string(),
);
assert_eq!(code, 0, "{body}");
let wire = crate::wire::laney_p_dto(&pairs, &Default::default()).expect("chart");
assert_eq!(body, serde_json::to_value(&wire).unwrap());
let cap = serde_json::json!({
"data": [10.1, 9.8, 10.3, 10.0, 9.7, 10.2, 10.1, 9.9],
"usl": 11.0, "lsl": 9.0, "sigma_within": 0.2, "target": 10.0
});
let (code, body) = call(uanalytics_process_capability, &cap.to_string());
assert_eq!(code, 0, "{body}");
let wire = crate::wire::capability_dto(serde_json::from_value(cap).unwrap()).unwrap();
assert_eq!(body, serde_json::to_value(&wire).unwrap());
let pct = serde_json::json!({
"data": (0..40).map(|i| 10.0 + (i as f64 * 0.37).sin()).collect::<Vec<_>>(),
"usl": 11.5, "lsl": 8.5
});
let (code, body) = call(uanalytics_percentile_capability, &pct.to_string());
assert_eq!(code, 0, "{body}");
let wire =
crate::wire::percentile_capability_dto(serde_json::from_value(pct).unwrap()).unwrap();
assert_eq!(body, serde_json::to_value(&wire).unwrap());
let grr = serde_json::json!({
"measurements": (0..5).map(|p| (0..3).map(|o| (0..3).map(|t|
10.0 + p as f64 + 0.1 * o as f64 + 0.03 * ((p * 7 + o * 3 + t * 5) % 4) as f64
).collect::<Vec<_>>()).collect::<Vec<_>>()).collect::<Vec<_>>(),
"tolerance": 6.0
});
let (code, body) = call(uanalytics_gage_rr_xbar_r, &grr.to_string());
assert_eq!(code, 0, "{body}");
let wire =
crate::wire::gage_rr_xbar_r_dto(serde_json::from_value(grr.clone()).unwrap()).unwrap();
assert_eq!(body, serde_json::to_value(&wire).unwrap());
let (code, body) = call(uanalytics_gage_rr_anova, &grr.to_string());
assert_eq!(code, 0, "{body}");
let wire = crate::wire::gage_rr_anova_dto(serde_json::from_value(grr).unwrap()).unwrap();
assert_eq!(body, serde_json::to_value(&wire).unwrap());
let pelt = serde_json::json!({ "data": [1.0, 1.1, 0.9, 1.0, 5.0, 5.1, 4.9, 5.0] });
let (code, body) = call(uanalytics_detect_changepoints, &pelt.to_string());
assert_eq!(code, 0, "{body}");
let wire = crate::wire::pelt_dto(serde_json::from_value(pelt).unwrap()).unwrap();
assert_eq!(body, serde_json::to_value(&wire).unwrap());
let saw =
serde_json::json!({ "data": (0..40).map(|i| (i % 7) as f64).collect::<Vec<_>>() });
let (code, body) = call(uanalytics_estimate_period, &saw.to_string());
assert_eq!(code, 0, "{body}");
assert_eq!(body["period"], 7, "{body}");
let wire = crate::wire::seasonality_dto(serde_json::from_value(saw).unwrap()).unwrap();
assert_eq!(body, serde_json::to_value(&wire).unwrap());
let line = serde_json::json!({ "data": (0..40).map(|i| i as f64).collect::<Vec<_>>() });
let (code, body) = call(uanalytics_estimate_period, &line.to_string());
assert_eq!(code, 0, "{body}");
assert!(body["period"].is_null(), "a line has no period: {body}");
let short = serde_json::json!({ "data": [1.0, 2.0, 3.0] });
let (code, body) = call(uanalytics_estimate_period, &short.to_string());
assert_eq!(code, ERR_COMPUTE, "{body}");
let mut spiked = vec![1.0; 40];
spiked[25] = 9.0;
let sr = serde_json::json!({ "data": spiked, "sensitivity": 90 });
let (code, body) = call(uanalytics_spectral_residual, &sr.to_string());
assert_eq!(code, 0, "{body}");
assert_eq!(body["anomalies"], serde_json::json!([25]), "{body}");
assert_eq!(body["points"].as_array().unwrap().len(), 40);
let wire = crate::wire::spectral_residual_dto(serde_json::from_value(sr).unwrap()).unwrap();
assert_eq!(body, serde_json::to_value(&wire).unwrap());
let bad = serde_json::json!({ "data": vec![1.0; 40], "threshold": 0.0 });
let (code, body) = call(uanalytics_spectral_residual, &bad.to_string());
assert_eq!(code, ERR_COMPUTE, "{body}");
assert_eq!(
(
body["parameter"].as_str(),
body["min"].is_null(),
body["exclusive_min"].as_f64(),
body["max"].is_null(),
body["got"].as_f64()
),
(Some("threshold"), true, Some(0.0), true, Some(0.0)),
"{body}"
);
let bad = serde_json::json!({ "data": vec![1.0; 40], "sensitivity": 100 });
let (_, body) = call(uanalytics_spectral_residual, &bad.to_string());
assert_eq!(
(
body["parameter"].as_str(),
body["exclusive_min"].as_f64(),
body["exclusive_max"].as_f64(),
body["got"].as_f64()
),
(Some("sensitivity"), Some(0.0), Some(100.0), Some(100.0)),
"{body}"
);
let groups: Vec<Vec<f64>> = (0..6)
.map(|g| {
(0..12)
.map(|i| 10.0 + 0.1 * ((g * 7 + i * 3) % 5) as f64)
.collect()
})
.collect();
let (code, body) = call(
uanalytics_xbar_s_chart,
&serde_json::json!({ "subgroups": groups }).to_string(),
);
assert_eq!(code, 0, "{body}");
let wire = crate::wire::xbar_s_dto(groups, RuleSet::shewhart()).unwrap();
assert_eq!(body, serde_json::to_value(&wire).unwrap());
let values: Vec<f64> = (0..20).map(|i| 10.0 + 0.3 * ((i * 7) % 5) as f64).collect();
let (code, body) = call(
uanalytics_imr_chart,
&serde_json::json!({ "values": values, "rules": "nelson" }).to_string(),
);
assert_eq!(code, 0, "{body}");
let wire = crate::wire::imr_dto(values.clone(), RuleSet::nelson()).unwrap();
assert_eq!(body, serde_json::to_value(&wire).unwrap());
let limits = serde_json::json!({ "ucl": 11.0, "cl": 10.5, "lcl": 10.0 });
let (code, body) = call(
uanalytics_run_rules,
&serde_json::json!({ "values": values, "limits": limits, "rules": "western_electric" })
.to_string(),
);
assert_eq!(code, 0, "{body}");
let wire = crate::wire::run_rules_dto(
values,
serde_json::from_value(limits).unwrap(),
RuleSet::western_electric(),
)
.unwrap();
assert_eq!(body, serde_json::to_value(&wire).unwrap());
}
#[test]
fn imr_sigma_hat_feeds_capability_the_number_the_old_default_produced() {
use crate::spc::{ControlChart, IndividualMRChart};
let data = [10.1, 9.8, 10.3, 10.0, 9.7, 10.2, 10.1, 9.9];
let (code, body) = call(
uanalytics_imr_chart,
&serde_json::json!({ "values": data }).to_string(),
);
assert_eq!(code, 0, "{body}");
let sigma_hat = body["sigma_hat"]
.as_f64()
.expect("eight values give a sigma");
let mut imr = IndividualMRChart::new();
for x in data {
imr.add_sample(&[x]).unwrap();
}
assert_eq!(Some(sigma_hat), imr.sigma_hat());
let (code, body) = call(
uanalytics_process_capability,
&serde_json::json!({ "data": data, "usl": 11.0, "lsl": 9.0, "sigma_within": sigma_hat })
.to_string(),
);
assert_eq!(code, 0, "{body}");
assert_eq!(body["sigma_source"], "within");
assert!(body["cp"].is_f64(), "{body}");
}
#[test]
fn xbar_r_honours_the_rules_option_like_the_wasm_binding() {
let groups: Vec<Vec<f64>> = (0..12)
.map(|g| {
let base = 10.0 + 0.05 * g as f64;
vec![base - 0.3, base + 0.2, base - 0.1, base + 0.4]
})
.collect();
let with_nelson = serde_json::json!({ "subgroups": groups, "rules": "nelson" });
let limits_only = serde_json::json!({ "subgroups": groups, "rules": [] });
let (code, body) = call(uanalytics_xbar_r_chart, &with_nelson.to_string());
assert_eq!(code, 0, "{body}");
let fired: Vec<String> = body["xbar_points"]
.as_array()
.unwrap()
.iter()
.flat_map(|p| p["violations"].as_array().unwrap().clone())
.map(|v| v.as_str().unwrap().to_owned())
.collect();
assert!(
fired.iter().any(|v| v == "SixTrend"),
"drift should trip the trend test: {fired:?}"
);
let (code, body) = call(uanalytics_xbar_r_chart, &limits_only.to_string());
assert_eq!(code, 0, "{body}");
for p in body["xbar_points"].as_array().unwrap() {
for v in p["violations"].as_array().unwrap() {
assert_eq!(v, "BeyondLimits", "only the limit check may fire: {p}");
}
}
let (code, body) = call(
uanalytics_xbar_r_chart,
&serde_json::json!({ "subgroups": [[1.0, 2.0]], "rules": ["NoSuchRule"] }).to_string(),
);
assert_eq!(code, -3, "{body}");
}
#[test]
fn capability_reports_no_short_term_indices_without_a_within_sigma() {
use crate::spc::{ControlChart, IndividualMRChart};
let data = [10.1, 9.8, 10.3, 10.0, 9.7, 10.2, 10.1, 9.9];
let request = serde_json::json!({ "data": data, "usl": 11.0, "lsl": 9.0 });
let (code, body) = call(uanalytics_process_capability, &request.to_string());
assert_eq!(code, 0, "{body}");
assert_eq!(body["sigma_source"], "overall");
assert!(body["cp"].is_null() && body["cpk"].is_null(), "{body}");
assert!(body["std_dev_within"].is_null(), "{body}");
assert!(body["pp"].is_f64() && body["ppk"].is_f64(), "{body}");
let mut imr = IndividualMRChart::new();
for x in data {
imr.add_sample(&[x]).unwrap();
}
let sigma_hat = imr.sigma_hat().expect("eight values");
let request = serde_json::json!({
"data": data, "usl": 11.0, "lsl": 9.0, "sigma_within": sigma_hat
});
let (code, body) = call(uanalytics_process_capability, &request.to_string());
assert_eq!(code, 0, "{body}");
assert_eq!(body["sigma_source"], "within");
assert_eq!(body["std_dev_within"].as_f64(), Some(sigma_hat));
assert!(body["cp"].is_f64(), "{body}");
}
#[test]
fn capability_rejects_a_non_positive_sigma_within() {
let request = r#"{"data": [1.0, 2.0, 3.0], "usl": 5.0, "sigma_within": 0.0}"#;
let (code, _) = call(uanalytics_process_capability, request);
assert_eq!(code, -3);
}
#[test]
fn changepoints_takes_the_wire_penalty_and_cost_like_the_wasm_binding() {
let data = [1.0, 1.0, 1.0, 5.0, 5.0, 5.0];
for penalty in [
serde_json::json!("AIC"),
serde_json::json!("MBIC"),
serde_json::json!("3.5"),
] {
let request = serde_json::json!({ "data": data, "penalty": penalty });
let (code, body) = call(uanalytics_detect_changepoints, &request.to_string());
assert_eq!(code, -3, "{penalty}: {body}");
}
for penalty in [serde_json::json!("bic"), serde_json::json!(3.5)] {
let request = serde_json::json!({ "data": data, "penalty": penalty });
let (code, body) = call(uanalytics_detect_changepoints, &request.to_string());
assert_eq!(code, 0, "{penalty}: {body}");
assert!(body["n_segments"].is_u64(), "{body}");
}
let (code, body) = call(
uanalytics_detect_changepoints,
&serde_json::json!({ "data": data, "cost": "normal" }).to_string(),
);
assert_eq!(code, 0, "{body}");
let (code, body) = call(
uanalytics_detect_changepoints,
&serde_json::json!({ "data": data, "cost": "l1" }).to_string(),
);
assert_eq!(code, -3, "{body}");
}
#[test]
fn ffi_rejects_unknown_request_fields_like_the_wasm_binding_does() {
let (code, body) = call(
uanalytics_process_capability,
r#"{"data": [1.0, 2.0, 3.0], "usl": 5.0, "sigmaWithin": 0.5}"#,
);
assert_eq!(code, -2, "{body}");
assert!(
body["error"].as_str().unwrap().contains("sigmaWithin"),
"{body}"
);
let (code, body) = call(
uanalytics_simple_regression,
r#"{"x": [1.0, 2.0, 3.0], "y": [2.0, 4.0, 6.1], "weights": [1.0, 1.0, 1.0]}"#,
);
assert_eq!(code, -2, "{body}");
assert!(
body["error"].as_str().unwrap().contains("weights"),
"{body}"
);
}
}
#[cfg(all(test, feature = "ffi"))]
mod request_keys;