Skip to content
Merged
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
104 changes: 102 additions & 2 deletions crates/gf-api/tests/bdd/tck_steps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
//! steps. Tiers are added (and `@skip-rust` removed feature-by-feature) as more
//! step vocabulary and result-value types are supported (#598–#601).

use arrow::array::{Array, BooleanArray, Float64Array, Int64Array, StringArray};
use arrow::array::{
Array, BooleanArray, Float64Array, Int64Array, ListArray, StringArray, StructArray,
};
use arrow::datatypes::DataType;
use cucumber::gherkin::Step;
use cucumber::{given, then, when};
Expand All @@ -30,6 +32,26 @@ async fn given_any_graph(world: &mut GraphForgeWorld) {
world.last_exec = None;
}

/// `And having executed:` followed by a `"""…"""` setup query (typically a
/// `CREATE`). Runs it against the forge and asserts it succeeded — the graph
/// state it builds is what the scenario's main query then reads. (`Given an
/// empty graph` is provided by `api_steps`; reused here to avoid a duplicate.)
#[given("having executed:")]
async fn given_having_executed(world: &mut GraphForgeWorld, step: &Step) {
let query = step
.docstring
.as_deref()
.expect("`And having executed:` requires a doc-string query")
.trim();
let forge = world
.forge
.as_ref()
.expect("a forge must exist (set up by a Given step)");
forge
.execute(query)
.unwrap_or_else(|e| panic!("setup `having executed` failed for {query:?}: {e}"));
}

// ---------------------------------------------------------------------------
// WHEN
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -165,9 +187,87 @@ fn render_cell(array: &dyn Array, row: usize) -> String {
.expect("Utf8 array");
format!("'{}'", a.value(row))
}
// A whole node value (#785/#889): `Struct{node_uuid, labels, <props>}`.
DataType::Struct(fields) if fields.iter().any(|f| f.name() == "labels") => {
let s = array
.as_any()
.downcast_ref::<StructArray>()
.expect("Struct array");
render_node_struct(s, row)
}
// A list value (`collect(...)`, `nodes(p)`): render each element.
DataType::List(_) => {
let l = array
.as_any()
.downcast_ref::<ListArray>()
.expect("List array");
let elems = l.value(row);
let parts: Vec<String> = (0..elems.len())
.map(|i| render_cell(elems.as_ref(), i))
.collect();
format!("[{}]", parts.join(", "))
}
other => panic!(
"TCK result rendering is not implemented for Arrow type {other:?} \
— extend render_cell() as new tiers are un-skipped"
— extend render_cell() as new tiers are un-skipped (relationship / \
path / map values are #889 slice 3)"
),
}
}

/// Render a node-value `Struct` (#785/#889) as the openCypher TCK node literal
/// `(:Label {key: value, …})`.
///
/// - `node_uuid` is omitted: a non-deterministic identity the TCK never
/// references in expected results.
/// - Labels come from the `labels` `List<Utf8>` field (`:L1:L2`); a node with no
/// labels renders none.
/// - Property keys are emitted in **sorted** order so an actual node compares
/// equal regardless of the struct's physical field order. (Features are only
/// un-skipped when their expected node literals are written sorted / have ≤1
/// property, until a structural comparison lands.)
fn render_node_struct(s: &StructArray, row: usize) -> String {
use std::collections::BTreeMap;

let mut labels = String::new();
if let Some(labels_arr) = s.column_by_name("labels") {
let list = labels_arr
.as_any()
.downcast_ref::<ListArray>()
.expect("labels is a List");
if !list.is_null(row) {
let vals = list.value(row);
let strs = vals
.as_any()
.downcast_ref::<StringArray>()
.expect("labels are Utf8");
for i in 0..strs.len() {
if !strs.is_null(i) {
labels.push(':');
labels.push_str(strs.value(i));
}
}
}
}

let mut props: BTreeMap<String, String> = BTreeMap::new();
for (i, field) in s.fields().iter().enumerate() {
let name = field.name();
if name == "node_uuid" || name == "labels" {
continue;
}
props.insert(name.clone(), render_cell(s.column(i).as_ref(), row));
}

let props_str = if props.is_empty() {
String::new()
} else {
let body = props
.iter()
.map(|(k, v)| format!("{k}: {v}"))
.collect::<Vec<_>>()
.join(", ");
format!(" {{{body}}}")
};
format!("({labels}{props_str})")
}
Loading
Loading