Task: 2026/10/08/share-a-console-lock-reading-that-sees-a-lock-taken-before-launch
805 lines
22 KiB
C++
805 lines
22 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#ifdef _DEBUG
|
|
|
|
#include "test/test_animation_clock.h"
|
|
|
|
#include "base/unique_qptr.h"
|
|
#include "core/application.h"
|
|
#include "test/test_capture.h"
|
|
#include "test/test_console_lock.h"
|
|
#include "test/test_ink.h"
|
|
#include "test/test_log.h"
|
|
#include "test/test_runner.h"
|
|
#include "test/test_widgets.h"
|
|
#include "ui/effects/animation_value.h"
|
|
#include "ui/effects/animations.h"
|
|
#include "ui/style/style_core_scale.h"
|
|
#include "ui/rp_widget.h"
|
|
#include "window/window_controller.h"
|
|
|
|
#include <QtCore/QPointer>
|
|
#include <QtGui/QPainter>
|
|
#include <QtGui/QWindow>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <thread>
|
|
|
|
#include "styles/style_widgets.h"
|
|
|
|
namespace Test {
|
|
namespace {
|
|
|
|
// The probe's own record: how many times the manager called it, and the
|
|
// |now| of its last call.
|
|
struct Probe {
|
|
int calls = 0;
|
|
crl::time now = -1;
|
|
};
|
|
|
|
[[nodiscard]] QString ElapsedText(
|
|
const std::vector<crl::time> &elapsed,
|
|
NumberFormat format) {
|
|
auto parts = QStringList();
|
|
for (const auto value : elapsed) {
|
|
parts.push_back(HelperNumber(value, format));
|
|
}
|
|
return parts.isEmpty() ? u"none"_q : parts.join(u',');
|
|
}
|
|
|
|
// One requested time: gate, wait, drain, tick, prove, bound, gate, render.
|
|
void TakeFrame(
|
|
const ClockedRequest &request,
|
|
const ClockedRun &run,
|
|
const Probe &probe,
|
|
not_null<ClockedFrame*> frame,
|
|
NumberFormat format) {
|
|
const auto end = request.animationEnd;
|
|
if (end > 0 && frame->requested >= end) {
|
|
frame->gate = ClockedEndGateText(frame->requested, end, format);
|
|
return;
|
|
}
|
|
const auto deadline = run.actionStarted + frame->requested;
|
|
const auto waitFrom = crl::now();
|
|
while (crl::now() < deadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
frame->waited = crl::now() - waitFrom;
|
|
|
|
SettlePostponedCalls();
|
|
const auto beforeTick = probe.calls;
|
|
Core::App().animationManager().update();
|
|
frame->probeTicks = probe.calls - beforeTick;
|
|
if (!frame->probeTicks) {
|
|
frame->refusal = ClockedNoTickText(
|
|
beforeTick,
|
|
crl::now() - run.actionStarted,
|
|
frame->waited,
|
|
format);
|
|
return;
|
|
}
|
|
frame->tick = probe.now;
|
|
frame->reachedLo = frame->tick - run.actionFinished;
|
|
frame->reachedHi = frame->tick - run.actionStarted;
|
|
if (end > 0 && frame->reachedHi >= end) {
|
|
frame->gate = ClockedReachedGateText(
|
|
frame->reachedLo,
|
|
frame->reachedHi,
|
|
end,
|
|
format);
|
|
return;
|
|
}
|
|
|
|
const auto beforeRender = probe.calls;
|
|
request.render(*frame);
|
|
frame->renderTicks = probe.calls - beforeRender;
|
|
if (frame->renderTicks) {
|
|
frame->refusal = ClockedRenderTickText(frame->renderTicks, format);
|
|
}
|
|
}
|
|
|
|
void ReportFrame(
|
|
const QString &name,
|
|
const ClockedFrame &frame,
|
|
NumberFormat format) {
|
|
const auto what = ClockedFrameSubject(name, frame, format);
|
|
if (!frame.gate.isEmpty()) {
|
|
Skipped(what, frame.gate);
|
|
} else if (!frame.refusal.isEmpty()) {
|
|
Fail(u"fixture gate: "_q + what, frame.refusal);
|
|
} else {
|
|
Note(name + u": "_q + ClockedFrameText(frame, format));
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool ClockedFrame::taken() const {
|
|
return gate.isEmpty() && refusal.isEmpty() && (tick >= 0);
|
|
}
|
|
|
|
int ClockedRun::taken() const {
|
|
return int(ranges::count_if(frames, &ClockedFrame::taken));
|
|
}
|
|
|
|
ClockedRun RunClockedFrames(const ClockedRequest &request) {
|
|
const auto format = HelperNumberFormat();
|
|
auto result = ClockedRun{ .animationEnd = request.animationEnd };
|
|
result.refusal = ClockedRequestRefusal(request, format);
|
|
if (!result.refusal.isEmpty()) {
|
|
const auto name = request.name.isEmpty()
|
|
? u"(unnamed)"_q
|
|
: request.name;
|
|
Fail(
|
|
u"fixture gate: %1: clocked frames refused"_q.arg(name),
|
|
result.refusal);
|
|
return result;
|
|
}
|
|
|
|
auto probe = Probe();
|
|
auto probeAnimation = Ui::Animations::Basic([&](crl::time now) {
|
|
++probe.calls;
|
|
probe.now = now;
|
|
return true;
|
|
});
|
|
probeAnimation.start();
|
|
|
|
Settle([&] {
|
|
result.actionStarted = crl::now();
|
|
if (request.handler) {
|
|
request.handler->onClick(request.context);
|
|
} else {
|
|
request.action();
|
|
}
|
|
});
|
|
result.actionFinished = crl::now();
|
|
|
|
const auto count = int(request.elapsed.size());
|
|
result.frames.reserve(count);
|
|
for (auto index = 0; index != count; ++index) {
|
|
auto frame = ClockedFrame{
|
|
.index = index,
|
|
.requested = request.elapsed[index],
|
|
};
|
|
TakeFrame(request, result, probe, &frame, format);
|
|
ReportFrame(request.name, frame, format);
|
|
result.frames.push_back(std::move(frame));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QString ClockedFrameText(const ClockedFrame &frame) {
|
|
return ClockedFrameText(frame, HelperNumberFormat());
|
|
}
|
|
|
|
QString ClockedFrameText(const ClockedFrame &frame, NumberFormat format) {
|
|
const auto ticked = (frame.tick >= 0);
|
|
auto result = u"kind=frame index=%1 requested=%2 reached=%3 tick=%4 "
|
|
"waited=%5 probeTicks=%6 renderTicks=%7"_q
|
|
.arg(HelperNumber(frame.index + 1, format))
|
|
.arg(HelperNumber(frame.requested, format))
|
|
.arg(ticked
|
|
? u"[%1,%2]"_q
|
|
.arg(HelperNumber(frame.reachedLo, format))
|
|
.arg(HelperNumber(frame.reachedHi, format))
|
|
: u"none"_q)
|
|
.arg(ticked ? HelperNumber(frame.tick, format) : u"none"_q)
|
|
.arg(HelperNumber(frame.waited, format))
|
|
.arg(HelperNumber(frame.probeTicks, format))
|
|
.arg(HelperNumber(frame.renderTicks, format));
|
|
if (!frame.gate.isEmpty()) {
|
|
result += u" gate="_q + frame.gate;
|
|
}
|
|
if (!frame.refusal.isEmpty()) {
|
|
result += u" refused="_q + frame.refusal;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QString ClockedFrameSubject(
|
|
const QString &name,
|
|
const ClockedFrame &frame,
|
|
NumberFormat format) {
|
|
auto text = u"%1: frame %2 at %3 ms"_q.arg(name);
|
|
text = ArgNumber(text, frame.index + 1, format);
|
|
return ArgNumber(text, frame.requested, format);
|
|
}
|
|
|
|
QString ClockedEndGateText(
|
|
crl::time requested,
|
|
crl::time end,
|
|
NumberFormat format) {
|
|
return u"requested %1 ms is at or after the declared "
|
|
"animation end %2 ms: no in-progress frame exists to judge, so "
|
|
"nothing was waited for, ticked or rendered"_q
|
|
.arg(HelperNumber(requested, format))
|
|
.arg(HelperNumber(end, format));
|
|
}
|
|
|
|
QString ClockedReachedGateText(
|
|
crl::time reachedLo,
|
|
crl::time reachedHi,
|
|
crl::time end,
|
|
NumberFormat format) {
|
|
return u"the tick reached [%1,%2] ms, at or after the "
|
|
"declared animation end %3 ms: the animation may have "
|
|
"finished, so no in-progress frame exists to judge; the frame "
|
|
"was not rendered"_q
|
|
.arg(HelperNumber(reachedLo, format))
|
|
.arg(HelperNumber(reachedHi, format))
|
|
.arg(HelperNumber(end, format));
|
|
}
|
|
|
|
QString ClockedNoTickText(
|
|
int probeCalls,
|
|
crl::time sinceAction,
|
|
crl::time waited,
|
|
NumberFormat format) {
|
|
return u"manager-did-not-tick: "
|
|
"Core::App().animationManager().update() did not call the "
|
|
"probe animation (probe calls %1 before and after it, %2 ms "
|
|
"after the action started, waited %3 ms): the manager skipped "
|
|
"the tick - a schedule callback still pending (_scheduled; "
|
|
"under Manager::SetScheduleWithInvokeQueued(true) it is an "
|
|
"InvokeQueued call the postponed-call drain does not run) or "
|
|
"an update in progress (_updating); the frame was not "
|
|
"rendered"_q
|
|
.arg(HelperNumber(probeCalls, format))
|
|
.arg(HelperNumber(sinceAction, format))
|
|
.arg(HelperNumber(waited, format));
|
|
}
|
|
|
|
QString ClockedRenderTickText(int renderTicks, NumberFormat format) {
|
|
return u"render-advanced-the-manager: the probe "
|
|
"animation was called %1 time(s) during the render, so the "
|
|
"frame may show a value past its reached elapsed bounds; "
|
|
"render synchronously (Test::GrabWidget / GrabRect), never "
|
|
"through repaint() or processEvents()"_q
|
|
.arg(HelperNumber(renderTicks, format));
|
|
}
|
|
|
|
QString ClockedRequestRefusal(
|
|
const ClockedRequest &request,
|
|
NumberFormat format) {
|
|
if (request.name.isEmpty()) {
|
|
return u"the request has no name"_q;
|
|
} else if (bool(request.action) == bool(request.handler)) {
|
|
return u"exactly one of action and handler must be set "
|
|
"(action=%1 handler=%2)"_q
|
|
.arg(HelperNumber(request.action ? 1 : 0, format))
|
|
.arg(HelperNumber(request.handler ? 1 : 0, format));
|
|
} else if (!request.render) {
|
|
return u"no render was supplied"_q;
|
|
} else if (request.elapsed.empty()) {
|
|
return u"no elapsed time was requested"_q;
|
|
}
|
|
auto previous = crl::time(-1);
|
|
for (const auto value : request.elapsed) {
|
|
if (value <= previous) {
|
|
return u"elapsed times must be non-negative and strictly "
|
|
"increasing, read %1"_q.arg(
|
|
ElapsedText(request.elapsed, format));
|
|
}
|
|
previous = value;
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
namespace {
|
|
|
|
constexpr auto kDuration = crl::time(200);
|
|
constexpr auto kWidthFactor = 8;
|
|
constexpr auto kBarTolerance = 8;
|
|
constexpr auto kMaxOtherPixels = 2;
|
|
constexpr auto kInProgressFrames = 3;
|
|
constexpr auto kSheetZoom = 2;
|
|
|
|
// Owned by the bar's lifetime(), so the Simple dies with the bar, before
|
|
// the animation manager (see the header). |rendering| is set by the test's
|
|
// own render around its grab, so the paint counts tell grab paints from any
|
|
// other paint without trusting the helper.
|
|
struct BarFixture {
|
|
Ui::Animations::Simple animation;
|
|
crl::time handlerAt = 0;
|
|
int handlerCalls = 0;
|
|
int paints = 0;
|
|
int renderPaints = 0;
|
|
bool rendering = false;
|
|
bool clicked = false;
|
|
};
|
|
|
|
// The bar's own visibility and its top-level window's state, printed beside
|
|
// the frames.
|
|
struct WindowReading {
|
|
QString window;
|
|
bool bar = false;
|
|
bool barHidden = false;
|
|
bool barVisible = false;
|
|
bool minimized = false;
|
|
bool windowHidden = false;
|
|
bool exposed = false;
|
|
bool screenLocked = false;
|
|
};
|
|
|
|
// The middle device row of one rendered frame: |filled| bar pixels and
|
|
// |other| pixels matching neither colour, of |width|.
|
|
struct BarReading {
|
|
int filled = 0;
|
|
int width = 0;
|
|
int other = 0;
|
|
double value = 0.;
|
|
QString refusal;
|
|
};
|
|
|
|
// What the test's render kept of one frame, keyed by the frame's |index|.
|
|
// |simple| is the Simple's own value at the render, context only.
|
|
struct FrameShot {
|
|
int index = 0;
|
|
QImage image;
|
|
BarReading bar;
|
|
double simple = 0.;
|
|
};
|
|
|
|
struct State {
|
|
base::unique_qptr<Ui::RpWidget> widget;
|
|
BarFixture *fixture = nullptr;
|
|
ClickHandlerPtr handler;
|
|
QString fixtureGate;
|
|
crl::time duration = 0;
|
|
};
|
|
|
|
[[nodiscard]] QColor BarColor() {
|
|
return QColor(0x20, 0x60, 0xd0);
|
|
}
|
|
|
|
[[nodiscard]] QColor TrackColor() {
|
|
return QColor(0xff, 0xff, 0xff);
|
|
}
|
|
|
|
[[nodiscard]] QString ColorHex(QColor color) {
|
|
return u"#%1%2%3"_q
|
|
.arg(color.red(), 2, 16, QChar('0'))
|
|
.arg(color.green(), 2, 16, QChar('0'))
|
|
.arg(color.blue(), 2, 16, QChar('0'));
|
|
}
|
|
|
|
[[nodiscard]] QString GateText() {
|
|
return u"animation clock self-test: fixture gate: a never-shown "
|
|
"harness-owned bar widget in the primary window, with animations "
|
|
"enabled"_q;
|
|
}
|
|
|
|
[[nodiscard]] double Linear(crl::time elapsed, crl::time duration) {
|
|
return std::clamp(double(elapsed) / duration, 0., 1.);
|
|
}
|
|
|
|
[[nodiscard]] WindowReading ReadBarWindow(QWidget *bar) {
|
|
auto result = WindowReading();
|
|
result.screenLocked = ReadConsoleLock().locked();
|
|
if (!bar) {
|
|
return result;
|
|
}
|
|
const auto window = bar->window();
|
|
const auto handle = window->windowHandle();
|
|
result.bar = true;
|
|
result.barHidden = bar->isHidden();
|
|
result.barVisible = bar->isVisible();
|
|
result.minimized = window->isMinimized();
|
|
result.windowHidden = window->isHidden();
|
|
result.exposed = handle && handle->isExposed();
|
|
result.window = WidgetDescription(window);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] QString WindowReadingText(const WindowReading &reading) {
|
|
if (!reading.bar) {
|
|
return u"bar=none screenLocked=%1"_q
|
|
.arg(reading.screenLocked ? 1 : 0);
|
|
}
|
|
return u"bar{hidden=%1 visible=%2} window{minimized=%3 hidden=%4 "
|
|
"exposed=%5 screenLocked=%6 %7}"_q
|
|
.arg(reading.barHidden ? 1 : 0)
|
|
.arg(reading.barVisible ? 1 : 0)
|
|
.arg(reading.minimized ? 1 : 0)
|
|
.arg(reading.windowHidden ? 1 : 0)
|
|
.arg(reading.exposed ? 1 : 0)
|
|
.arg(reading.screenLocked ? 1 : 0)
|
|
.arg(reading.window);
|
|
}
|
|
|
|
[[nodiscard]] bool HiddenBar(const WindowReading &reading) {
|
|
return reading.bar && reading.barHidden && !reading.barVisible;
|
|
}
|
|
|
|
[[nodiscard]] BarReading ReadBar(const QImage &image) {
|
|
auto result = BarReading();
|
|
if (image.isNull()) {
|
|
result.refusal = u"unreadable-frame: the render returned a null "
|
|
"image"_q;
|
|
return result;
|
|
}
|
|
result.width = image.width();
|
|
const auto y = image.height() / 2;
|
|
for (auto x = 0; x != result.width; ++x) {
|
|
const auto color = image.pixelColor(x, y);
|
|
if (ChannelDelta(color, BarColor()) <= kBarTolerance) {
|
|
++result.filled;
|
|
} else if (ChannelDelta(color, TrackColor()) > kBarTolerance) {
|
|
++result.other;
|
|
}
|
|
}
|
|
result.value = double(result.filled) / result.width;
|
|
if (result.other > kMaxOtherPixels) {
|
|
result.refusal = u"unreadable-frame: %1 of %2 pixels of the middle "
|
|
"row match neither the bar %3 nor the track %4 (at most %5)"_q
|
|
.arg(result.other)
|
|
.arg(result.width)
|
|
.arg(ColorHex(BarColor()))
|
|
.arg(ColorHex(TrackColor()))
|
|
.arg(kMaxOtherPixels);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] QString BarReadingText(const BarReading &reading) {
|
|
if (!reading.refusal.isEmpty()) {
|
|
return u"painted=none (%1)"_q.arg(reading.refusal);
|
|
}
|
|
return u"painted=%1 (%2/%3, other %4)"_q
|
|
.arg(reading.value, 0, 'f', 4)
|
|
.arg(reading.filled)
|
|
.arg(reading.width)
|
|
.arg(reading.other);
|
|
}
|
|
|
|
[[nodiscard]] const ClockedFrame *FindFrame(
|
|
const ClockedRun &run,
|
|
int index) {
|
|
const auto i = ranges::find(run.frames, index, &ClockedFrame::index);
|
|
return (i != end(run.frames)) ? &*i : nullptr;
|
|
}
|
|
|
|
[[nodiscard]] const FrameShot *FindShot(
|
|
const std::vector<FrameShot> &shots,
|
|
int index) {
|
|
const auto i = ranges::find(shots, index, &FrameShot::index);
|
|
return (i != end(shots)) ? &*i : nullptr;
|
|
}
|
|
|
|
// The painted value of a taken frame with a readable bar, or nullptr.
|
|
[[nodiscard]] const BarReading *TakenBar(
|
|
const ClockedFrame *frame,
|
|
const FrameShot *shot) {
|
|
return (frame && frame->taken() && shot && shot->bar.refusal.isEmpty())
|
|
? &shot->bar
|
|
: nullptr;
|
|
}
|
|
|
|
void PaintBar(
|
|
not_null<BarFixture*> fixture,
|
|
not_null<Ui::RpWidget*> widget,
|
|
QPainter &p) {
|
|
++fixture->paints;
|
|
if (fixture->rendering) {
|
|
++fixture->renderPaints;
|
|
}
|
|
const auto value = fixture->animation.value(fixture->clicked ? 1. : 0.);
|
|
p.fillRect(widget->rect(), TrackColor());
|
|
p.fillRect(
|
|
QRectF(0., 0., value * widget->width(), widget->height()),
|
|
BarColor());
|
|
}
|
|
|
|
void BuildBar(
|
|
not_null<State*> state,
|
|
not_null<Window::Controller*> controller) {
|
|
const auto row = st::defaultActiveButton.height;
|
|
state->widget = base::make_unique_q<Ui::RpWidget>(
|
|
controller->widget().get());
|
|
const auto raw = state->widget.get();
|
|
const auto fixture = raw->lifetime().make_state<BarFixture>();
|
|
state->fixture = fixture;
|
|
raw->setAttribute(Qt::WA_OpaquePaintEvent);
|
|
raw->setGeometry(0, 0, row * kWidthFactor, row);
|
|
raw->paintOn([=](QPainter &p) {
|
|
PaintBar(fixture, raw, p);
|
|
});
|
|
|
|
const auto weak = QPointer<QWidget>(raw);
|
|
state->handler = std::make_shared<LambdaClickHandler>([=] {
|
|
if (!weak) {
|
|
return;
|
|
}
|
|
++fixture->handlerCalls;
|
|
fixture->handlerAt = crl::now();
|
|
fixture->clicked = true;
|
|
fixture->animation.start([=] {
|
|
if (weak) {
|
|
weak->update();
|
|
}
|
|
}, 0., 1., kDuration, anim::linear);
|
|
});
|
|
}
|
|
|
|
void PrepareFixture(not_null<State*> state) {
|
|
state->duration = kDuration * anim::SlowMultiplier();
|
|
const auto controller = Core::App().activePrimaryWindow();
|
|
if (controller) {
|
|
BuildBar(state, controller);
|
|
}
|
|
const auto widget = state->widget.get();
|
|
const auto reading = ReadBarWindow(widget);
|
|
const auto disabled = anim::Disabled();
|
|
const auto ok = controller && HiddenBar(reading) && !disabled;
|
|
Check(
|
|
ok,
|
|
GateText(),
|
|
u"controller=%1 %2 dpr=%3 slowMultiplier=%4 duration=%5 "
|
|
"animationsDisabled=%6 bar=%7 track=%8 size=%9"_q
|
|
.arg(controller ? 1 : 0)
|
|
.arg(WindowReadingText(reading))
|
|
.arg(style::DevicePixelRatio())
|
|
.arg(anim::SlowMultiplier())
|
|
.arg(state->duration)
|
|
.arg(disabled ? 1 : 0)
|
|
.arg(ColorHex(BarColor()))
|
|
.arg(ColorHex(TrackColor()))
|
|
.arg(widget
|
|
? u"%1x%2"_q.arg(widget->width()).arg(widget->height())
|
|
: u"none"_q));
|
|
if (!ok) {
|
|
state->fixtureGate = GateText();
|
|
state->widget = nullptr;
|
|
state->fixture = nullptr;
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] QString ActionText(
|
|
const ClockedRun &run,
|
|
not_null<const BarFixture*> fixture) {
|
|
return u"handlerCalls=%1 handlerAt=%2 action=[%3,%4]%5"_q
|
|
.arg(fixture->handlerCalls)
|
|
.arg(fixture->handlerAt)
|
|
.arg(run.actionStarted)
|
|
.arg(run.actionFinished)
|
|
.arg(run.refusal.isEmpty()
|
|
? QString()
|
|
: (u" refused="_q + run.refusal));
|
|
}
|
|
|
|
void CheckHandler(
|
|
const ClockedRun &run,
|
|
not_null<const BarFixture*> fixture) {
|
|
Check(
|
|
(fixture->handlerCalls == 1)
|
|
&& (run.actionStarted <= fixture->handlerAt)
|
|
&& (fixture->handlerAt <= run.actionFinished),
|
|
u"animation clock self-test: the click handler ran inside the "
|
|
"synchronous action"_q,
|
|
ActionText(run, fixture));
|
|
}
|
|
|
|
void CheckFrameValue(
|
|
const ClockedRun &run,
|
|
const std::vector<FrameShot> &shots,
|
|
int index,
|
|
crl::time duration,
|
|
const WindowReading &window) {
|
|
const auto frame = FindFrame(run, index);
|
|
const auto shot = FindShot(shots, index);
|
|
const auto bar = TakenBar(frame, shot);
|
|
const auto what = u"animation clock self-test: frame at %1 % of the "
|
|
"%2 ms linear animation: the painted value lies within the "
|
|
"transition at its reached elapsed bounds"_q
|
|
.arg((index + 1) * 25)
|
|
.arg(duration);
|
|
const auto frameText = frame
|
|
? ClockedFrameText(*frame)
|
|
: u"frame=none"_q;
|
|
const auto shotText = shot
|
|
? u"%1 simple=%2"_q
|
|
.arg(BarReadingText(shot->bar))
|
|
.arg(shot->simple, 0, 'f', 4)
|
|
: u"painted=none (no image)"_q;
|
|
if (!bar) {
|
|
Check(
|
|
false,
|
|
what,
|
|
u"%1 %2 %3"_q
|
|
.arg(frameText)
|
|
.arg(shotText)
|
|
.arg(WindowReadingText(window)));
|
|
return;
|
|
}
|
|
const auto tolerance = 1.5 / bar->width;
|
|
const auto lo = Linear(frame->reachedLo, duration);
|
|
const auto hi = Linear(frame->reachedHi, duration);
|
|
Check(
|
|
(lo - tolerance <= bar->value) && (bar->value <= hi + tolerance),
|
|
what,
|
|
u"%1 %2 bounds=[%3,%4] tol=%5 %6"_q
|
|
.arg(frameText)
|
|
.arg(shotText)
|
|
.arg(lo, 0, 'f', 4)
|
|
.arg(hi, 0, 'f', 4)
|
|
.arg(tolerance, 0, 'f', 4)
|
|
.arg(WindowReadingText(window)));
|
|
}
|
|
|
|
void CheckIncreasing(
|
|
const ClockedRun &run,
|
|
const std::vector<FrameShot> &shots) {
|
|
auto parts = QStringList();
|
|
auto values = std::vector<double>();
|
|
for (auto index = 0; index != kInProgressFrames; ++index) {
|
|
const auto bar = TakenBar(
|
|
FindFrame(run, index),
|
|
FindShot(shots, index));
|
|
if (bar) {
|
|
values.push_back(bar->value);
|
|
parts.push_back(u"%1 (%2/%3)"_q
|
|
.arg(bar->value, 0, 'f', 4)
|
|
.arg(bar->filled)
|
|
.arg(bar->width));
|
|
} else {
|
|
parts.push_back(u"none"_q);
|
|
}
|
|
}
|
|
const auto increasing = (int(values.size()) == kInProgressFrames)
|
|
&& (ranges::adjacent_find(values, std::greater_equal<>())
|
|
== end(values));
|
|
Check(
|
|
increasing,
|
|
u"animation clock self-test: the painted values strictly "
|
|
"increase"_q,
|
|
u"values=%1"_q.arg(parts.join(u", "_q)));
|
|
}
|
|
|
|
void CheckPastEnd(
|
|
const ClockedRun &run,
|
|
const std::vector<FrameShot> &shots,
|
|
int requested) {
|
|
const auto index = kInProgressFrames;
|
|
const auto frame = FindFrame(run, index);
|
|
const auto shot = FindShot(shots, index);
|
|
Check(
|
|
frame
|
|
&& !frame->gate.isEmpty()
|
|
&& !shot
|
|
&& (int(run.frames.size()) == requested),
|
|
u"animation clock self-test: the requested time past the "
|
|
"animation end is an N/A gate, not a frame"_q,
|
|
u"frames=%1 (requested %2) %3 image=%4"_q
|
|
.arg(int(run.frames.size()))
|
|
.arg(requested)
|
|
.arg(frame ? ClockedFrameText(*frame) : u"frame=none"_q)
|
|
.arg(shot ? u"present"_q : u"none"_q));
|
|
}
|
|
|
|
void CheckNoCompositorPaint(
|
|
const ClockedRun &run,
|
|
const WindowReading &before,
|
|
const WindowReading &after,
|
|
int outsidePaints,
|
|
int renderPaints) {
|
|
const auto taken = run.taken();
|
|
Check(
|
|
HiddenBar(before)
|
|
&& HiddenBar(after)
|
|
&& (outsidePaints == 0)
|
|
&& (renderPaints == taken)
|
|
&& (taken == kInProgressFrames),
|
|
u"animation clock self-test: the frames advanced on a widget no "
|
|
"compositor paint reached"_q,
|
|
u"before{%1} after{%2} paintsOutsideRenders=%3 renderPaints=%4 "
|
|
"framesTaken=%5 (need %6)"_q
|
|
.arg(WindowReadingText(before))
|
|
.arg(WindowReadingText(after))
|
|
.arg(outsidePaints)
|
|
.arg(renderPaints)
|
|
.arg(taken)
|
|
.arg(kInProgressFrames));
|
|
}
|
|
|
|
void SaveFrames(const std::vector<FrameShot> &shots) {
|
|
auto zoomed = std::vector<QImage>();
|
|
for (const auto &shot : shots) {
|
|
zoomed.push_back(Zoom(shot.image, kSheetZoom));
|
|
}
|
|
const auto sheet = ContactSheet(zoomed);
|
|
if (!sheet.isNull()) {
|
|
SaveImage(sheet, u"animation_clock_frames"_q);
|
|
}
|
|
}
|
|
|
|
void CheckClockedFrames(not_null<State*> state) {
|
|
const auto widget = state->widget.get();
|
|
const auto fixture = state->fixture;
|
|
const auto duration = state->duration;
|
|
const auto before = ReadBarWindow(widget);
|
|
const auto paintsBefore = fixture->paints;
|
|
const auto renderPaintsBefore = fixture->renderPaints;
|
|
auto shots = std::vector<FrameShot>();
|
|
const auto elapsed = std::vector<crl::time>{
|
|
duration / 4,
|
|
duration / 2,
|
|
3 * duration / 4,
|
|
duration + duration / 4,
|
|
};
|
|
const auto run = RunClockedFrames({
|
|
.name = u"animation clock self-test"_q,
|
|
.handler = state->handler,
|
|
.context = ClickContext{ Qt::LeftButton },
|
|
.elapsed = elapsed,
|
|
.animationEnd = duration,
|
|
.render = [&](const ClockedFrame &frame) {
|
|
fixture->rendering = true;
|
|
auto image = GrabWidget(widget);
|
|
fixture->rendering = false;
|
|
shots.push_back({
|
|
.index = frame.index,
|
|
.image = std::move(image),
|
|
.simple = fixture->animation.value(-1.),
|
|
});
|
|
},
|
|
});
|
|
const auto after = ReadBarWindow(widget);
|
|
const auto renderPaints = fixture->renderPaints - renderPaintsBefore;
|
|
const auto outsidePaints = fixture->paints
|
|
- paintsBefore
|
|
- renderPaints;
|
|
for (auto &shot : shots) {
|
|
shot.bar = ReadBar(shot.image);
|
|
}
|
|
|
|
CheckHandler(run, fixture);
|
|
for (auto index = 0; index != kInProgressFrames; ++index) {
|
|
CheckFrameValue(run, shots, index, duration, after);
|
|
}
|
|
CheckIncreasing(run, shots);
|
|
CheckPastEnd(run, shots, int(elapsed.size()));
|
|
CheckNoCompositorPaint(run, before, after, outsidePaints, renderPaints);
|
|
|
|
SaveFrames(shots);
|
|
state->widget = nullptr;
|
|
state->fixture = nullptr;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void AppendAnimationClockSelfTest(not_null<Runner*> runner) {
|
|
const auto state = std::make_shared<State>();
|
|
|
|
runner->add({
|
|
.name = u"animation clock self-test: fixture"_q,
|
|
.run = [=] {
|
|
PrepareFixture(state.get());
|
|
},
|
|
});
|
|
runner->add({
|
|
.name = u"animation clock self-test: clocked frames from a click "
|
|
"handler"_q,
|
|
.skipReason = [=] {
|
|
if (!state->fixtureGate.isEmpty()) {
|
|
return state->fixtureGate;
|
|
} else if (!state->widget) {
|
|
return u"fixture gate: the bar widget was destroyed with "
|
|
"its window before the clocked stage"_q;
|
|
}
|
|
return QString();
|
|
},
|
|
.run = [=] {
|
|
CheckClockedFrames(state.get());
|
|
},
|
|
});
|
|
}
|
|
|
|
} // namespace Test
|
|
|
|
#endif // _DEBUG
|