mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 11:28:13 +08:00
Build a gathered subject's copy plan on collapse and reject an empty carry
This commit is contained in:
@@ -112,6 +112,7 @@ impl<'e> Frames<'e> {
|
|||||||
frame,
|
frame,
|
||||||
free: self.reborrow(),
|
free: self.reborrow(),
|
||||||
filled_fields: false,
|
filled_fields: false,
|
||||||
|
carried_empty: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,4 +183,48 @@ mod tests {
|
|||||||
}
|
}
|
||||||
assert!(addresses.windows(2).all(|pair| pair[0] == pair[1]), "each claim reuses the same region");
|
assert!(addresses.windows(2).all(|pair| pair[0] == pair[1]), "each claim reuses the same region");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "carried an empty plan and filled nothing")]
|
||||||
|
fn carrying_an_empty_plan_and_filling_nothing_refuses_to_close() {
|
||||||
|
// The shape that served uninitialized bytes before this guard existed: a
|
||||||
|
// layout that DECLARES a field, a carry whose plan turned out empty, and
|
||||||
|
// nothing else to fill it. The field would otherwise be the prior frame's
|
||||||
|
// bytes, which for a reference field is an uninitialized read.
|
||||||
|
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")]);
|
||||||
|
let mut frame_arena = FrameArena::new();
|
||||||
|
frame_arena.reserve(1 << 10);
|
||||||
|
let frames = frame_arena.frames();
|
||||||
|
let scope = frames.scope();
|
||||||
|
let mut claim = scope.claim(&layout);
|
||||||
|
let value = crate::record::access::RecordValue::zeroed();
|
||||||
|
let src = layout.rec(&value);
|
||||||
|
// SAFETY: an empty plan reads nothing from `src`; closing afterwards is
|
||||||
|
// what the guard refuses.
|
||||||
|
unsafe { claim.carry(src, &[]) };
|
||||||
|
// SAFETY: the assertion fires before anything reads the unfilled field.
|
||||||
|
let _ = unsafe { claim.finish() };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_empty_carry_whose_fields_are_written_closes_normally() {
|
||||||
|
// The control that pins what the guard actually tracks: the same empty
|
||||||
|
// carry, but the declared field is then written. Closing must succeed, so
|
||||||
|
// the guard is about a field left unfilled rather than about the carry.
|
||||||
|
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")]);
|
||||||
|
let offset = layout.fields.first().expect("the fixture declares one field").offset;
|
||||||
|
let mut frame_arena = FrameArena::new();
|
||||||
|
frame_arena.reserve(1 << 10);
|
||||||
|
let frames = frame_arena.frames();
|
||||||
|
let scope = frames.scope();
|
||||||
|
let mut claim = scope.claim(&layout);
|
||||||
|
let value = crate::record::access::RecordValue::zeroed();
|
||||||
|
let src = layout.rec(&value);
|
||||||
|
// SAFETY: an empty plan reads nothing from `src`.
|
||||||
|
unsafe { claim.carry(src, &[]) };
|
||||||
|
// SAFETY: `offset` is this layout's own resolved offset for an f64 field.
|
||||||
|
unsafe { claim.attr_at(offset, 0.5_f64) };
|
||||||
|
// SAFETY: the write above filled the declared field.
|
||||||
|
let _ = unsafe { claim.finish() };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -478,6 +478,18 @@ pub struct LayoutMeta {
|
|||||||
/// The materialized subject a reducer folds, as `(input, levels)`. The fold
|
/// The materialized subject a reducer folds, as `(input, levels)`. The fold
|
||||||
/// consumes the whole subject input, so only the node's own levels remain.
|
/// consumes the whole subject input, so only the node's own levels remain.
|
||||||
pub folded: Option<(u8, u8)>,
|
pub folded: Option<(u8, u8)>,
|
||||||
|
/// Whether [`sources`](Self::sources)`[0]` is a GATHERED subject, whose
|
||||||
|
/// per-lane layout is this output's base.
|
||||||
|
///
|
||||||
|
/// Carried explicitly because it cannot be inferred from `sources` and
|
||||||
|
/// `level_delta`. A gathered subject that also collapses the level
|
||||||
|
/// (`level_delta < 0`) still has a well-defined copy plan: the gathered
|
||||||
|
/// lane's layout IS the base being copied into. A LAZY folding subject looks
|
||||||
|
/// identical from the outside - un-materialized, so it also sits in
|
||||||
|
/// `sources` with a negative delta - but copying its fields down is exactly
|
||||||
|
/// what the plan must not do. Keying on "sources is non-empty" would
|
||||||
|
/// conflate the two and reintroduce the defect in the other direction.
|
||||||
|
pub gathered: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The attributes a node reads from one input, recorded on [`LayoutMeta`] for
|
/// The attributes a node reads from one input, recorded on [`LayoutMeta`] for
|
||||||
@@ -513,6 +525,8 @@ impl LayoutMeta {
|
|||||||
removes: Vec::new(),
|
removes: Vec::new(),
|
||||||
level_delta: 0,
|
level_delta: 0,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
// A retype keeps input 0's level, so its plan is already unconditional.
|
||||||
|
gathered: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,7 +559,9 @@ impl LayoutMeta {
|
|||||||
let layout = self.fold(inputs);
|
let layout = self.fold(inputs);
|
||||||
let frame_bytes = layout.frame_bytes();
|
let frame_bytes = layout.frame_bytes();
|
||||||
let plan = match self.sources.first() {
|
let plan = match self.sources.first() {
|
||||||
Some(&source) if self.level_delta >= 0 => {
|
// A gathered subject copies from the lane it gathered, whose layout is
|
||||||
|
// this output's base, so its plan holds however the level moves.
|
||||||
|
Some(&source) if self.level_delta >= 0 || self.gathered => {
|
||||||
let from = inputs[source as usize].expect("layout resolve source input has no layout");
|
let from = inputs[source as usize].expect("layout resolve source input has no layout");
|
||||||
let carry_element = matches!(self.element, ElementSpec::Carried);
|
let carry_element = matches!(self.element, ElementSpec::Carried);
|
||||||
let removes: Vec<(&str, u8)> = self.removes.clone();
|
let removes: Vec<(&str, u8)> = self.removes.clone();
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ impl<'a> SlotRun<'a> {
|
|||||||
frame: (self.layout.frame_bytes() != 0).then_some(frame),
|
frame: (self.layout.frame_bytes() != 0).then_some(frame),
|
||||||
free: frames.reborrow(),
|
free: frames.reborrow(),
|
||||||
filled_fields: false,
|
filled_fields: false,
|
||||||
|
carried_empty: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +87,11 @@ pub struct FrameClaim<'e, 'l> {
|
|||||||
/// Set by the writes that fill the declared fields, so the safe closers can
|
/// Set by the writes that fill the declared fields, so the safe closers can
|
||||||
/// refuse a field-bearing frame that was never filled.
|
/// refuse a field-bearing frame that was never filled.
|
||||||
pub(in crate::record) filled_fields: bool,
|
pub(in crate::record) filled_fields: bool,
|
||||||
|
/// Set where a carry ran against an EMPTY plan, which fills nothing. Kept
|
||||||
|
/// apart from `filled_fields` because "carried nothing" and "never carried"
|
||||||
|
/// are different mistakes: the first is a wiring defect the closers can
|
||||||
|
/// prove, the second is the ordinary shape of a fresh record.
|
||||||
|
pub(in crate::record) carried_empty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'e, 'l> FrameClaim<'e, 'l> {
|
impl<'e, 'l> FrameClaim<'e, 'l> {
|
||||||
@@ -124,7 +130,16 @@ impl<'e, 'l> FrameClaim<'e, 'l> {
|
|||||||
/// serving the source through [`Self::frames`] establishes it.
|
/// serving the source through [`Self::frames`] establishes it.
|
||||||
pub unsafe fn carry(&mut self, src: Rec<'_>, plan: &[(usize, usize, usize)]) {
|
pub unsafe fn carry(&mut self, src: Rec<'_>, plan: &[(usize, usize, usize)]) {
|
||||||
unsafe { apply_plan(src, self.dst(), plan) };
|
unsafe { apply_plan(src, self.dst(), plan) };
|
||||||
self.filled_fields = true;
|
// An empty plan copied nothing, so it must not satisfy the closers'
|
||||||
|
// "carried or wrote its fields" guard: the frame still holds whatever
|
||||||
|
// bytes the previous claim left, and a declared field read out of them
|
||||||
|
// is an uninitialized read rather than a missing value. Recorded rather
|
||||||
|
// than ignored so the closers can tell "carried nothing" apart from
|
||||||
|
// "never carried", which are not the same mistake.
|
||||||
|
match plan.is_empty() {
|
||||||
|
true => self.carried_empty = true,
|
||||||
|
false => self.filled_fields = true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copies the source record's element bytes into the frame, for a gathered
|
/// Copies the source record's element bytes into the frame, for a gathered
|
||||||
@@ -207,6 +222,17 @@ impl<'e, 'l> FrameClaim<'e, 'l> {
|
|||||||
/// The frame must hold a complete record of the layout, written through
|
/// The frame must hold a complete record of the layout, written through
|
||||||
/// the carry, element, and field writes.
|
/// the carry, element, and field writes.
|
||||||
pub unsafe fn finish(mut self) -> RecordValue<'e> {
|
pub unsafe fn finish(mut self) -> RecordValue<'e> {
|
||||||
|
// A frame that CARRIED, but through an empty plan, and then filled
|
||||||
|
// nothing else: its declared fields hold the previous claim's bytes, and
|
||||||
|
// a reference field read out of them is an uninitialized read. This is
|
||||||
|
// narrower than [`Self::lift`]'s guard on purpose - a frame that never
|
||||||
|
// carried at all may legitimately serve fields the census staged as
|
||||||
|
// declared defaults, so only an empty carry is evidence of the defect.
|
||||||
|
assert!(
|
||||||
|
!(self.carried_empty && !self.filled_fields && !self.layout.fields.is_empty()),
|
||||||
|
"a layout with {} fields carried an empty plan and filled nothing: its fields would be the prior frame's bytes",
|
||||||
|
self.layout.fields.len()
|
||||||
|
);
|
||||||
match self.frame {
|
match self.frame {
|
||||||
Some(frame) => RecordValue::spilled(unsafe { Rec::new(frame.cast_const()) }),
|
Some(frame) => RecordValue::spilled(unsafe { Rec::new(frame.cast_const()) }),
|
||||||
// SAFETY: the inline record is the value's own bytes.
|
// SAFETY: the inline record is the value's own bytes.
|
||||||
|
|||||||
@@ -295,6 +295,8 @@ pub(crate) fn layout_meta_tokens(node: &Node, element_spec: TokenStream2, core_t
|
|||||||
Some((index, levels)) => quote!(::core::option::Option::Some((#index, #levels))),
|
Some((index, levels)) => quote!(::core::option::Option::Some((#index, #levels))),
|
||||||
None => quote!(::core::option::Option::None),
|
None => quote!(::core::option::Option::None),
|
||||||
};
|
};
|
||||||
|
// A gathered subject's plan holds however the level moves; see `LayoutMeta::gathered`.
|
||||||
|
let gathered = gathered_subject(node).is_some();
|
||||||
quote! {
|
quote! {
|
||||||
#core_types::record::LayoutMeta {
|
#core_types::record::LayoutMeta {
|
||||||
sources: ::std::vec![#(#sources),*],
|
sources: ::std::vec![#(#sources),*],
|
||||||
@@ -308,6 +310,7 @@ pub(crate) fn layout_meta_tokens(node: &Node, element_spec: TokenStream2, core_t
|
|||||||
removes: ::std::vec![#(#removes),*],
|
removes: ::std::vec![#(#removes),*],
|
||||||
level_delta: #level_delta,
|
level_delta: #level_delta,
|
||||||
folded: #folded,
|
folded: #folded,
|
||||||
|
gathered: #gathered,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,6 +319,19 @@ fn reverse_lanes_extent(content: ListIn<'_, f64>, _opacity: ValueIn<'_, f64>, le
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gather-carrier kernel that COLLAPSES its level: one value for the whole
|
||||||
|
/// subject, gathered from a named lane. Before a gathered subject built its copy
|
||||||
|
/// plan regardless of the level change, this shape declared the lane's columns
|
||||||
|
/// and copied none of them, serving the previous frame's bytes instead.
|
||||||
|
#[node_macro::node(category("Test"))]
|
||||||
|
fn collapse_to_lane<'e>(_ctx: impl Ctx + core_types::context::ExtractArena<'e> + Copy, content: IList<f64>, opacity: f64) -> Result<(Lane<f64>, Attr<Opacity>), Interrupt> {
|
||||||
|
if content.is_empty() {
|
||||||
|
return Err(GraphError::past_end().into());
|
||||||
|
}
|
||||||
|
let total: f64 = (0..content.len()).map(|lane| content.get(lane)).sum();
|
||||||
|
Ok((content.lane(0).map_element(total), Attr(opacity)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Gather-carrier kernel with a substituted subject: the lane's record carries
|
/// Gather-carrier kernel with a substituted subject: the lane's record carries
|
||||||
/// exactly as it does for [`reverse_lanes`], but `map_element` replaces the
|
/// exactly as it does for [`reverse_lanes`], but `map_element` replaces the
|
||||||
/// element, so a node that rewrites what it produces still keeps every column
|
/// element, so a node that rewrites what it produces still keeps every column
|
||||||
@@ -839,6 +852,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 1,
|
level_delta: 1,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
let node = install(
|
let node = install(
|
||||||
RepeatNode::new(RecordSource::new(bare_source(&base, 7.), &base, &base), count_edge, reverse_edge, &base, &count_layout, &reverse_layout),
|
RepeatNode::new(RecordSource::new(bare_source(&base, 7.), &base, &base), count_edge, reverse_edge, &base, &count_layout, &reverse_layout),
|
||||||
@@ -879,6 +893,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 1,
|
level_delta: 1,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
let repeat = install(
|
let repeat = install(
|
||||||
RepeatNode::new(
|
RepeatNode::new(
|
||||||
@@ -927,6 +942,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 1,
|
level_delta: 1,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
let repeat = install(
|
let repeat = install(
|
||||||
RepeatNode::new(
|
RepeatNode::new(
|
||||||
@@ -981,6 +997,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 1,
|
level_delta: 1,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
let repeat = install(
|
let repeat = install(
|
||||||
RepeatNode::new(
|
RepeatNode::new(
|
||||||
@@ -1083,6 +1100,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 0,
|
level_delta: 0,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
let node = install(
|
let node = install(
|
||||||
ExtendNode::new(RecordSource::new(base, &base_layout, &union), RecordSource::new(new, &new_layout, &union), &union),
|
ExtendNode::new(RecordSource::new(base, &base_layout, &union), RecordSource::new(new, &new_layout, &union), &union),
|
||||||
@@ -1155,6 +1173,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 0,
|
level_delta: 0,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let left_inner = install(
|
let left_inner = install(
|
||||||
@@ -1233,6 +1252,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 0,
|
level_delta: 0,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
let extend = install(
|
let extend = install(
|
||||||
ExtendNode::new(RecordSource::new(base, &base_layout, &union), RecordSource::new(new, &new_layout, &union), &union),
|
ExtendNode::new(RecordSource::new(base, &base_layout, &union), RecordSource::new(new, &new_layout, &union), &union),
|
||||||
@@ -1281,6 +1301,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 1,
|
level_delta: 1,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
let extend_meta = || core_types::record::LayoutMeta {
|
let extend_meta = || core_types::record::LayoutMeta {
|
||||||
named_writes: Vec::new(),
|
named_writes: Vec::new(),
|
||||||
@@ -1294,6 +1315,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 0,
|
level_delta: 0,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
let (base_count, base_count_layout) = lifted_value(2u32);
|
let (base_count, base_count_layout) = lifted_value(2u32);
|
||||||
let (base_reverse, base_reverse_layout) = lifted_value(false);
|
let (base_reverse, base_reverse_layout) = lifted_value(false);
|
||||||
@@ -1373,6 +1395,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 0,
|
level_delta: 0,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
let build = |index: f64| {
|
let build = |index: f64| {
|
||||||
let content = LeveledSourceNode {
|
let content = LeveledSourceNode {
|
||||||
@@ -1430,6 +1453,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 0,
|
level_delta: 0,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
install(
|
install(
|
||||||
IndexElementsNode::new(RecordSource::new(content, &layout, &layout), index_edge, &layout, &index_layout),
|
IndexElementsNode::new(RecordSource::new(content, &layout, &layout), index_edge, &layout, &index_layout),
|
||||||
@@ -1566,6 +1590,52 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_gathered_collapse_carries_its_named_lanes_columns() {
|
||||||
|
let arena = Arena::new(1 << 16).unwrap();
|
||||||
|
let generations = [];
|
||||||
|
let scope = scope_fixture(&generations, &arena);
|
||||||
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
|
// Lanes carry DISTINCT transforms, so "carried lane 0's column" is
|
||||||
|
// distinguishable from "carried some other lane's" and from stale bytes.
|
||||||
|
let layout = Layout::default().with_writes(
|
||||||
|
1,
|
||||||
|
core_types::record::element_write::<f64>(),
|
||||||
|
&[core_types::record::FieldWrite::of::<Transform>(0), core_types::record::FieldWrite::of::<Opacity>(0)],
|
||||||
|
);
|
||||||
|
let frames = frames_for(&[&layout]);
|
||||||
|
let rows = [(1., 10., 0.1), (2., 30., 0.2), (3., 20., 0.3)];
|
||||||
|
let content = LeveledCarriedSource {
|
||||||
|
layout: layout.clone(),
|
||||||
|
rows: rows
|
||||||
|
.iter()
|
||||||
|
.map(|&(element, x, opacity)| (element, DAffine2::from_translation(glam::DVec2::new(x, 0.)), opacity))
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
let node = install(
|
||||||
|
CollapseToLaneNode::new(RecordSource::new(content, &layout, &layout), ValueSource::new(0.25)),
|
||||||
|
collapse_to_lane_layout_meta(),
|
||||||
|
&[Some(&layout)],
|
||||||
|
);
|
||||||
|
|
||||||
|
let out = Node::<ContextImpl>::layout(&node).clone();
|
||||||
|
assert_eq!(out.depth, 0, "the collapse serves one value rather than a level");
|
||||||
|
assert!(
|
||||||
|
out.offset_of(<Transform as AttributeMarker>::NAME, 0).is_some(),
|
||||||
|
"the gathered lane's undeclared column is declared on the output"
|
||||||
|
);
|
||||||
|
|
||||||
|
let GPoll::Final(served) = core_types::record::capture(&node, &ctx, &frames) else {
|
||||||
|
panic!("expected a final record");
|
||||||
|
};
|
||||||
|
assert_eq!(served.element::<f64>(), 6., "the collapsed element is the kernel's own value");
|
||||||
|
let transform: DAffine2 = served.attr::<Transform>();
|
||||||
|
assert_eq!(transform.translation.x, 10., "lane 0's transform is carried - not lane 1's or 2's, and not stale frame bytes");
|
||||||
|
let opacity: f64 = served.attr::<Opacity>();
|
||||||
|
assert_eq!(opacity, 0.25, "the declared write still overrides the carried opacity");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_substituted_subject_keeps_the_lanes_other_columns() {
|
fn a_substituted_subject_keeps_the_lanes_other_columns() {
|
||||||
let arena = Arena::new(1 << 16).unwrap();
|
let arena = Arena::new(1 << 16).unwrap();
|
||||||
@@ -1781,6 +1851,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 1,
|
level_delta: 1,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
let nested = install(
|
let nested = install(
|
||||||
RepeatNode::new(
|
RepeatNode::new(
|
||||||
@@ -1838,6 +1909,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 1,
|
level_delta: 1,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
let nested = install(
|
let nested = install(
|
||||||
RepeatNode::new(
|
RepeatNode::new(
|
||||||
@@ -1952,6 +2024,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 1,
|
level_delta: 1,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
let repeat = install(
|
let repeat = install(
|
||||||
RepeatNode::new(
|
RepeatNode::new(
|
||||||
@@ -2082,6 +2155,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta: 1,
|
level_delta: 1,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
};
|
};
|
||||||
let repeat = install(
|
let repeat = install(
|
||||||
RepeatNode::new(
|
RepeatNode::new(
|
||||||
|
|||||||
@@ -437,6 +437,7 @@ mod tests {
|
|||||||
removes: vec![],
|
removes: vec![],
|
||||||
level_delta,
|
level_delta,
|
||||||
folded: None,
|
folded: None,
|
||||||
|
gathered: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user