test(plot): pin the crosshair's lookup, readout, placement, sync and clip
This commit is contained in:
1 parent
fd0c471430
commit
8e27e22ebe
6 files changed
+798
-17
No files matched your search
@@ -1809,3 +1809,384 @@ PLOT_TEST(non_finite_region_bounds_are_refused) {
|
||||
PLOT_CHECK(p.region_count() == before + 1u);
|
||||
PLOT_CHECK_NEAR(p.region(before).x0, 30.0, 1e-12);
|
||||
}
|
||||
|
||||
// ------------------------------------------------- the crosshair (spec §15)
|
||||
|
||||
namespace {
|
||||
|
||||
/// A series of hand-computable values -- `y[i] == 10 * i` -- so every interpolation the cases
|
||||
/// below assert can be written down as a number rather than recomputed by a formula the
|
||||
/// implementation might share with the test.
|
||||
std::vector<f32> tens(u64 n) {
|
||||
std::vector<f32> y(n);
|
||||
for (u64 i = 0; i < n; ++i) y[i] = f32(10.0 * static_cast<f64>(i));
|
||||
return y;
|
||||
}
|
||||
|
||||
/// The crosshair's own scene: four `tens` samples on x = 0..3, a [0, 30] y axis, a text
|
||||
/// backend to measure the readout's rows with, and a viewport with room to place a panel in.
|
||||
struct CrosshairScene {
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
plot::data::ArraySeries series;
|
||||
plot::Plot plot;
|
||||
u32 ax = 0;
|
||||
u32 ay = 0;
|
||||
|
||||
CrosshairScene() : series(tens(4), 0.0, 1.0) {
|
||||
plot.set_text_backend(&font);
|
||||
plot.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
ax = plot.add_x_axis({"t", {0.0, 3.0}, false, plot::AxisPlace::Bottom, false});
|
||||
ay = plot.add_y_axis({"v", {0.0, 30.0}, false, plot::AxisPlace::Left, false});
|
||||
}
|
||||
};
|
||||
|
||||
/// A series that counts what a reader asked it for.
|
||||
///
|
||||
/// The crosshair's lookup must be O(1) -- index arithmetic and at most two sample reads -- and
|
||||
/// "it does not scan" is only observable through the one path a raw sample can be read from:
|
||||
/// `Series::copy_raw()`, which `gather()` reaches. This is a `Series` the test owns, so the
|
||||
/// count is the test's and not a counter the implementation maintains about itself: long
|
||||
/// enough (a quarter of a million samples) that a walk over the data would be unmistakable in
|
||||
/// it, whatever path the walk took.
|
||||
class CountingSeries final : public plot::data::Series {
|
||||
public:
|
||||
CountingSeries(u64 n, f64 x0, f64 dx) : samples_(n), x0_(x0), dx_(dx) {
|
||||
for (u64 i = 0; i < n; ++i) samples_[i] = f32(i % 100u);
|
||||
pyramid_.reserve(n);
|
||||
for (const f32 v : samples_) pyramid_.append(v);
|
||||
}
|
||||
|
||||
u64 size() const override { return samples_.size(); }
|
||||
f64 x0() const override { return x0_; }
|
||||
f64 dx() const override { return dx_; }
|
||||
plot::Range x_bounds() const override {
|
||||
return {x0_, x0_ + static_cast<f64>(samples_.size() - 1u) * dx_};
|
||||
}
|
||||
plot::Range y_bounds() const override { return {0.0, 99.0}; }
|
||||
const plot::data::MinMaxPyramid& pyramid() const override { return pyramid_; }
|
||||
void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out) const override {
|
||||
++gather_calls_;
|
||||
samples_read_ += end - begin;
|
||||
gather_impl(level, begin, end, out);
|
||||
}
|
||||
|
||||
u64 gather_calls() const noexcept { return gather_calls_; }
|
||||
u64 samples_read() const noexcept { return samples_read_; }
|
||||
|
||||
protected:
|
||||
void copy_raw(u64 begin, u64 end, f32* out) const override {
|
||||
for (u64 i = begin; i < end && i < samples_.size(); ++i) out[i - begin] = samples_[i];
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<f32> samples_;
|
||||
f64 x0_ = 0.0;
|
||||
f64 dx_ = 1.0;
|
||||
plot::data::MinMaxPyramid pyramid_;
|
||||
mutable u64 gather_calls_ = 0;
|
||||
mutable u64 samples_read_ = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
/// Spec §15, the lookup: a readout's y is **linear interpolation between the two samples
|
||||
/// adjacent to the crosshair's x**, and hand-computed here rather than checked against the
|
||||
/// implementation's own arithmetic. The four corners of the rule are all asserted: a query
|
||||
/// between two samples, the two exact-sample ends of the window (where the span collapses onto
|
||||
/// one sample), and a one-sample series, whose window is that sample alone.
|
||||
PLOT_TEST(crosshair_value_is_interpolated_between_neighbours) {
|
||||
CrosshairScene scene;
|
||||
const u32 trace = scene.plot.add_trace(
|
||||
{&scene.series, plot::Color::hex(0x40C0FF), 1.5f, scene.ax, scene.ay});
|
||||
|
||||
// The span itself, for a query halfway between sample 1 (x = 1, y = 10) and sample 2
|
||||
// (x = 2, y = 20): the two neighbours, and the fraction between them. Asserting the span and
|
||||
// not only the value is what pins the convention -- a nearest-sample lookup would report the
|
||||
// span's `t` as 0 or 1, and an average of a *range* would not report a span at all.
|
||||
const plot::CrosshairSpan half = plot::crosshair_span(scene.series, 1.5);
|
||||
PLOT_CHECK(half.valid);
|
||||
PLOT_CHECK(half.begin == 1u);
|
||||
PLOT_CHECK(half.end == 2u);
|
||||
PLOT_CHECK_NEAR(half.t, 0.5, 1e-12);
|
||||
|
||||
f64 y = 0.0;
|
||||
PLOT_CHECK(scene.plot.crosshair_value(trace, 1.5, y));
|
||||
PLOT_CHECK_NEAR(y, 15.0, 1e-9);
|
||||
// A quarter of the way through another pair: 20 + (30 - 20) * 0.25.
|
||||
PLOT_CHECK(scene.plot.crosshair_value(trace, 2.25, y));
|
||||
PLOT_CHECK_NEAR(y, 22.5, 1e-9);
|
||||
|
||||
// The ends of the window are samples too, so a query exactly on one reads that sample --
|
||||
// and reads it *as* a span of one, with nothing to its left or right to interpolate with.
|
||||
const plot::CrosshairSpan first = plot::crosshair_span(scene.series, 0.0);
|
||||
PLOT_CHECK(first.valid);
|
||||
PLOT_CHECK(first.begin == 0u);
|
||||
PLOT_CHECK(first.end == 0u);
|
||||
PLOT_CHECK(first.t == 0.0);
|
||||
PLOT_CHECK(scene.plot.crosshair_value(trace, 0.0, y));
|
||||
PLOT_CHECK_NEAR(y, 0.0, 1e-9);
|
||||
const plot::CrosshairSpan last = plot::crosshair_span(scene.series, 3.0);
|
||||
PLOT_CHECK(last.valid);
|
||||
PLOT_CHECK(last.begin == 3u);
|
||||
PLOT_CHECK(last.end == 3u);
|
||||
PLOT_CHECK(scene.plot.crosshair_value(trace, 3.0, y));
|
||||
PLOT_CHECK_NEAR(y, 30.0, 1e-9);
|
||||
|
||||
// Outside the window there is no value at all -- not the nearest sample's, not the end of
|
||||
// the window's, which are the clamped lies the header refuses -- and a series with no
|
||||
// samples has no span at any x.
|
||||
PLOT_CHECK(!plot::crosshair_span(scene.series, -1e-9).valid);
|
||||
PLOT_CHECK(!plot::crosshair_span(scene.series, 3.0 + 1e-9).valid);
|
||||
PLOT_CHECK(!scene.plot.crosshair_value(trace, 4.0, y));
|
||||
PLOT_CHECK(!scene.plot.crosshair_value(trace, -1.0, y));
|
||||
plot::data::ArraySeries empty(std::vector<f32>{}, 0.0, 1.0);
|
||||
PLOT_CHECK(!plot::crosshair_span(empty, 0.0).valid);
|
||||
const u32 empty_trace = scene.plot.add_trace(
|
||||
{&empty, plot::Color::hex(0x00E070), 1.5f, scene.ax, scene.ay, "quiet"});
|
||||
PLOT_CHECK(!scene.plot.crosshair_value(empty_trace, 1.0, y));
|
||||
PLOT_CHECK(!scene.plot.crosshair_value(/*trace=*/99u, 1.0, y)); // an id that names nothing
|
||||
|
||||
// A one-sample series: the sample itself is the only query it answers, and it answers it
|
||||
// without a neighbour to interpolate against.
|
||||
plot::data::ArraySeries single(std::vector<f32>{7.0f}, 5.0, 1.0);
|
||||
plot::Plot one;
|
||||
one.add_x_axis({"t", {0.0, 10.0}, false, plot::AxisPlace::Bottom, false});
|
||||
one.add_y_axis({"v", {0.0, 10.0}, false, plot::AxisPlace::Left, false});
|
||||
const u32 single_trace =
|
||||
one.add_trace({&single, plot::Color::hex(0x40C0FF), 1.5f, 0u, 0u});
|
||||
const plot::CrosshairSpan only = plot::crosshair_span(single, 5.0);
|
||||
PLOT_CHECK(only.valid);
|
||||
PLOT_CHECK(only.begin == 0u);
|
||||
PLOT_CHECK(only.end == 0u);
|
||||
PLOT_CHECK(one.crosshair_value(single_trace, 5.0, y));
|
||||
PLOT_CHECK_NEAR(y, 7.0, 1e-9);
|
||||
PLOT_CHECK(!one.crosshair_value(single_trace, 5.5, y));
|
||||
|
||||
// The readout's row is the same lookup: the number a user reads off the panel is the value
|
||||
// this test computed by hand, not a second derivation of it.
|
||||
scene.plot.set_crosshair(1.5, true);
|
||||
const plot::CrosshairLayout& crosshair = scene.plot.layout().crosshair;
|
||||
PLOT_CHECK(crosshair.active);
|
||||
// One row: the sampled trace. The named-but-empty series added above is *not* a row -- a trace
|
||||
// with no samples is not a signal under the pointer, so the readout lists nothing for it
|
||||
// (a series that has samples but not *at this x* is the other case: a row with no value, which
|
||||
// the figure's case pins).
|
||||
PLOT_CHECK(crosshair.rows.size() == 1u);
|
||||
PLOT_CHECK(crosshair.rows[0].valid);
|
||||
PLOT_CHECK_NEAR(crosshair.rows[0].y, 15.0, 1e-9);
|
||||
PLOT_CHECK(crosshair.rows[0].value == "15");
|
||||
}
|
||||
|
||||
/// Spec §15, the readout's membership: **every visible trace gets a row, whether or not it is
|
||||
/// named** -- the readout is about what is under the pointer, so the legend's rule of skipping
|
||||
/// unnamed traces does not apply -- while a trace with no series or no samples is not under the
|
||||
/// pointer and is not listed. An unnamed trace's row carries a positional label, so the rows
|
||||
/// stay identifiable without a name being invented for it.
|
||||
PLOT_TEST(crosshair_readout_lists_every_visible_trace_including_unnamed_ones) {
|
||||
CrosshairScene scene;
|
||||
plot::data::ArraySeries empty(std::vector<f32>{}, 0.0, 1.0);
|
||||
scene.plot.add_trace({&scene.series, plot::Color::hex(0x40C0FF), 1.5f, scene.ax, scene.ay,
|
||||
"sine"});
|
||||
scene.plot.add_trace({&scene.series, plot::Color::hex(0x00E070), 1.5f, scene.ax, scene.ay, ""});
|
||||
scene.plot.add_trace({nullptr, plot::Color::hex(0xFF00FF), 1.5f, scene.ax, scene.ay, "pending"});
|
||||
scene.plot.add_trace({&empty, plot::Color::hex(0xFFB020), 1.5f, scene.ax, scene.ay, "quiet"});
|
||||
scene.plot.set_crosshair(1.5, true);
|
||||
|
||||
const plot::CrosshairLayout& crosshair = scene.plot.layout().crosshair;
|
||||
PLOT_CHECK(crosshair.active);
|
||||
PLOT_CHECK(crosshair.rows.size() == 2u);
|
||||
// In trace order, with the ids the plot handed out: the named trace and the unnamed one.
|
||||
PLOT_CHECK(crosshair.rows[0].trace == 0u);
|
||||
PLOT_CHECK(crosshair.rows[0].label == "sine");
|
||||
PLOT_CHECK(crosshair.rows[1].trace == 1u);
|
||||
// The placeholder is positional and derived from the id alone, so the same trace keeps the
|
||||
// same caption frame after frame -- and it is not the legend's behaviour of disappearing.
|
||||
PLOT_CHECK(crosshair.rows[1].label == plot::crosshair_unnamed_label(1u));
|
||||
PLOT_CHECK(crosshair.rows[1].label == "#1");
|
||||
|
||||
// Both are listed with the value their own series has at x = 1.5, the swatch, the label's box
|
||||
// and the value's box -- all placed by the plot, all inside the panel it measured.
|
||||
PLOT_CHECK(crosshair.panel.w > 0.0f && crosshair.panel.h > 0.0f);
|
||||
for (const plot::CrosshairRow& row : crosshair.rows) {
|
||||
PLOT_CHECK(row.valid);
|
||||
PLOT_CHECK(row.listed);
|
||||
PLOT_CHECK_NEAR(row.y, 15.0, 1e-9);
|
||||
PLOT_CHECK(row.value == "15");
|
||||
PLOT_CHECK(row.swatch.x >= crosshair.panel.x);
|
||||
PLOT_CHECK(row.swatch.right() <= crosshair.panel.right());
|
||||
PLOT_CHECK(row.swatch.y >= crosshair.panel.y);
|
||||
PLOT_CHECK(row.swatch.bottom() <= crosshair.panel.bottom());
|
||||
PLOT_CHECK(row.label_box.x >= crosshair.panel.x + row.swatch.w);
|
||||
PLOT_CHECK(row.label_box.right() <= crosshair.panel.right());
|
||||
PLOT_CHECK(row.value_box.w > 0.0f);
|
||||
PLOT_CHECK(row.value_box.right() <= crosshair.panel.right());
|
||||
PLOT_CHECK(row.value_box.x >= row.label_box.right());
|
||||
}
|
||||
// The two rows' swatches are their own traces' colours, so a row can be told apart from its
|
||||
// neighbour -- and the panel is wide enough for the widest row's label, gutter and value.
|
||||
PLOT_CHECK(crosshair.rows[1].swatch.w == crosshair.rows[0].swatch.w);
|
||||
PLOT_CHECK(crosshair.rows[1].swatch.y > crosshair.rows[0].swatch.y);
|
||||
PLOT_CHECK(crosshair.rows[1].label_box.x == crosshair.rows[0].label_box.x);
|
||||
PLOT_CHECK(crosshair.rows[1].value_box.right() == crosshair.rows[0].value_box.right());
|
||||
|
||||
// Clearing the crosshair empties the whole geometry -- nothing of it is left for a renderer
|
||||
// to stage -- and re-placing it at another x moves the panel with the row it lists.
|
||||
scene.plot.clear_crosshair();
|
||||
const plot::CrosshairLayout& cleared = scene.plot.layout().crosshair;
|
||||
PLOT_CHECK(!cleared.active);
|
||||
PLOT_CHECK(cleared.rows.empty());
|
||||
PLOT_CHECK(cleared.panel.w == 0.0f && cleared.panel.h == 0.0f);
|
||||
PLOT_CHECK(cleared.vertical.w == 0.0f && cleared.horizontal.h == 0.0f);
|
||||
|
||||
// A crosshair with the readout switched off still draws its lines -- and lists nothing.
|
||||
scene.plot.set_crosshair(2.0, true);
|
||||
plot::Crosshair no_readout = scene.plot.crosshair();
|
||||
PLOT_CHECK(no_readout.active);
|
||||
PLOT_CHECK(!no_readout.horizontal); // the one-coordinate form draws no row
|
||||
PLOT_CHECK(no_readout.show_readout);
|
||||
}
|
||||
|
||||
/// Spec §15, the panel's placement: near the pointer, flipped over it where it would otherwise
|
||||
/// cross an edge, and clamped -- so it can never leave `axes`. A panel that did leave would be
|
||||
/// drawn over a label strip, which is the defect Task 1 fixed, so all four corners are walked
|
||||
/// and every piece of the panel's geometry is asserted against the rect it was measured for.
|
||||
PLOT_TEST(crosshair_panel_stays_inside_the_axes_at_all_four_edges) {
|
||||
CrosshairScene scene;
|
||||
scene.plot.add_trace({&scene.series, plot::Color::hex(0x40C0FF), 1.5f, scene.ax, scene.ay,
|
||||
"a long signal name"});
|
||||
scene.plot.add_trace({&scene.series, plot::Color::hex(0x00E070), 1.5f, scene.ax, scene.ay, ""});
|
||||
scene.plot.add_trace({&scene.series, plot::Color::hex(0xFF00FF), 1.5f, scene.ax, scene.ay,
|
||||
"third"});
|
||||
|
||||
const plot::Rect axes = scene.plot.layout().axes;
|
||||
const plot::Transform tx = scene.plot.x_transform(scene.ax);
|
||||
const plot::Transform ty = scene.plot.y_transform(scene.ay);
|
||||
|
||||
// A pointer one pixel inside each corner of the data area, as its *data* coordinates -- the
|
||||
// form the crosshair takes -- so the four cases are the four edges a panel could fall off.
|
||||
const struct {
|
||||
const char* name;
|
||||
f32 px;
|
||||
f32 py;
|
||||
} corners[4] = {
|
||||
{"top left", axes.x + 1.0f, axes.y + 1.0f},
|
||||
{"top right", axes.right() - 1.0f, axes.y + 1.0f},
|
||||
{"bottom left", axes.x + 1.0f, axes.bottom() - 1.0f},
|
||||
{"bottom right", axes.right() - 1.0f, axes.bottom() - 1.0f},
|
||||
};
|
||||
|
||||
for (const auto& corner : corners) {
|
||||
scene.plot.set_crosshair(tx.to_data_x(corner.px), ty.to_data_y(corner.py), true);
|
||||
const plot::CrosshairLayout& crosshair = scene.plot.layout().crosshair;
|
||||
std::printf("[crosshair] %s: panel %.1f,%.1f %.1fx%.1f in axes %.1f,%.1f %.1fx%.1f\n",
|
||||
corner.name, static_cast<double>(crosshair.panel.x),
|
||||
static_cast<double>(crosshair.panel.y), static_cast<double>(crosshair.panel.w),
|
||||
static_cast<double>(crosshair.panel.h), static_cast<double>(axes.x),
|
||||
static_cast<double>(axes.y), static_cast<double>(axes.w),
|
||||
static_cast<double>(axes.h));
|
||||
// There is a real panel to place, with all three rows listed: the corner is not a case the
|
||||
// fit dropped out of.
|
||||
PLOT_CHECK(crosshair.panel.w > 0.0f);
|
||||
PLOT_CHECK(crosshair.panel.h > 0.0f);
|
||||
u32 listed = 0;
|
||||
for (const plot::CrosshairRow& row : crosshair.rows) {
|
||||
if (row.listed) ++listed;
|
||||
}
|
||||
PLOT_CHECK(listed == 3u);
|
||||
|
||||
// Inside the axes, half-open on the far edges: the panel's own rect and every box in it.
|
||||
PLOT_CHECK(crosshair.panel.x >= axes.x);
|
||||
PLOT_CHECK(crosshair.panel.y >= axes.y);
|
||||
PLOT_CHECK(crosshair.panel.right() <= axes.right());
|
||||
PLOT_CHECK(crosshair.panel.bottom() <= axes.bottom());
|
||||
for (const plot::CrosshairRow& row : crosshair.rows) {
|
||||
PLOT_CHECK(row.swatch.x >= axes.x && row.swatch.right() <= axes.right());
|
||||
PLOT_CHECK(row.swatch.y >= axes.y && row.swatch.bottom() <= axes.bottom());
|
||||
PLOT_CHECK(row.label_box.x >= axes.x && row.label_box.right() <= axes.right());
|
||||
PLOT_CHECK(row.value_box.x >= axes.x && row.value_box.right() <= axes.right());
|
||||
}
|
||||
// The two lines are inside it as well, stroke and all -- which is what leaves the clip
|
||||
// nothing to trim but the sub-pixel a fractional edge rasterises outside the rect.
|
||||
PLOT_CHECK(crosshair.vertical.x >= axes.x);
|
||||
PLOT_CHECK(crosshair.vertical.right() <= axes.right());
|
||||
PLOT_CHECK(crosshair.vertical.y >= axes.y);
|
||||
PLOT_CHECK(crosshair.vertical.bottom() <= axes.bottom());
|
||||
PLOT_CHECK(crosshair.horizontal.x >= axes.x);
|
||||
PLOT_CHECK(crosshair.horizontal.right() <= axes.right());
|
||||
PLOT_CHECK(crosshair.horizontal.y >= axes.y);
|
||||
PLOT_CHECK(crosshair.horizontal.bottom() <= axes.bottom());
|
||||
|
||||
// Near the pointer, on the side the flip chose: down and to the right of it where there is
|
||||
// room, and *over* it to the left or up where there is not -- so the panel is beside the
|
||||
// crosshair rather than pinned to a corner whenever the crosshair is in that half.
|
||||
const bool flip_x = corner.px > axes.x + 0.5f * axes.w;
|
||||
const bool flip_y = corner.py > axes.y + 0.5f * axes.h;
|
||||
if (flip_x) {
|
||||
PLOT_CHECK(crosshair.panel.right() <= corner.px);
|
||||
} else {
|
||||
PLOT_CHECK(crosshair.panel.x >= corner.px);
|
||||
}
|
||||
if (flip_y) {
|
||||
PLOT_CHECK(crosshair.panel.bottom() <= corner.py);
|
||||
} else {
|
||||
PLOT_CHECK(crosshair.panel.y >= corner.py);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spec §15's cost requirement: the lookup runs on **every pointer move**, so it is index
|
||||
/// arithmetic plus at most two sample reads and never a scan. The count is taken from the one
|
||||
/// path a raw sample can be read through, over a series long enough that a walk would be
|
||||
/// unmistakable -- and the values are asserted too, so a lookup that read nothing and invented
|
||||
/// a number cannot pass by being cheap.
|
||||
PLOT_TEST(crosshair_lookup_does_not_scan) {
|
||||
constexpr u64 kSamples = 250'000u;
|
||||
CountingSeries series(kSamples, 0.0, 1.0);
|
||||
plot::Plot p;
|
||||
p.add_x_axis({"t", {0.0, static_cast<f64>(kSamples - 1u)}, false, plot::AxisPlace::Bottom,
|
||||
false});
|
||||
p.add_y_axis({"v", {0.0, 99.0}, false, plot::AxisPlace::Left, false});
|
||||
const u32 trace = p.add_trace({&series, plot::Color::hex(0x40C0FF), 1.5f, 0u, 0u});
|
||||
|
||||
// A query between two samples: 1234 % 100 = 34, the next is 35, halfway between them.
|
||||
f64 y = 0.0;
|
||||
const u64 before_reads = series.samples_read();
|
||||
PLOT_CHECK(p.crosshair_value(trace, 1234.5, y));
|
||||
PLOT_CHECK_NEAR(y, 34.5, 1e-9);
|
||||
PLOT_CHECK(series.samples_read() - before_reads <= 2u);
|
||||
PLOT_CHECK(series.gather_calls() <= 1u);
|
||||
|
||||
// The same bound at a query on a sample, at the far end of the series, and at both ends of
|
||||
// the window: what varies is the index, never the amount of data touched. A lookup that
|
||||
// scanned would read `kSamples` samples here, four orders of magnitude past the bound.
|
||||
const f64 queries[6] = {0.0, 1.0, 99'999.5, static_cast<f64>(kSamples - 1u),
|
||||
static_cast<f64>(kSamples) - 1.5, 987'654.321};
|
||||
for (const f64 x : queries) {
|
||||
const u64 reads = series.samples_read();
|
||||
const u64 calls = series.gather_calls();
|
||||
const bool found = p.crosshair_value(trace, x, y);
|
||||
if (x <= static_cast<f64>(kSamples - 1u) + 1e-9) {
|
||||
PLOT_CHECK(found);
|
||||
} else {
|
||||
PLOT_CHECK(!found); // past the newest sample: no value, and nothing read for one
|
||||
}
|
||||
PLOT_CHECK(series.samples_read() - reads <= 2u);
|
||||
PLOT_CHECK(series.gather_calls() - calls <= 1u);
|
||||
}
|
||||
|
||||
// A whole readout is a lookup per *visible trace*, not per sample: the process's readout for
|
||||
// this one-trace plot stays inside the same bound, which is what keeps a frame's cost
|
||||
// proportional to the number of signals rather than to their length.
|
||||
p.set_crosshair(1234.5, true);
|
||||
const u64 reads = series.samples_read();
|
||||
const u64 calls = series.gather_calls();
|
||||
const plot::CrosshairLayout& crosshair = p.layout().crosshair;
|
||||
PLOT_CHECK(crosshair.rows.size() == 1u);
|
||||
PLOT_CHECK(crosshair.rows[0].valid);
|
||||
PLOT_CHECK_NEAR(crosshair.rows[0].y, 34.5, 1e-9);
|
||||
PLOT_CHECK(series.samples_read() - reads <= 2u);
|
||||
PLOT_CHECK(series.gather_calls() - calls <= 1u);
|
||||
// And the readout is not vacuous: the series it read two samples of really does hold a
|
||||
// quarter of a million of them.
|
||||
PLOT_CHECK(series.size() == kSamples);
|
||||
}
|
||||
Reference in new issue
Block a user