[apps/graph] Added tests on graph ranges

Change-Id: I6c080f40c934cd31083be3d19aa0a4d0bb33c5cc
This commit is contained in:
Gabriel Ozouf
2020-08-05 17:50:57 +02:00
committed by Émilie Feral
parent 17f39e5e39
commit a1aefb0cb2
5 changed files with 237 additions and 29 deletions

View File

@@ -40,7 +40,9 @@ apps_src += $(app_graph_src)
i18n_files += $(call i18n_without_universal_for,graph/base)
tests_src += $(addprefix apps/graph/test/,\
caching.cpp\
caching.cpp \
helper.cpp \
range.cpp \
)
$(eval $(call depends_on_image,apps/graph/app.cpp,apps/graph/graph_icon.png))

View File

@@ -1,5 +1,5 @@
#include <quiz.h>
#include "../app.h"
#include "helper.h"
#include <cmath>
using namespace Poincare;
@@ -13,17 +13,6 @@ bool floatEquals(float a, float b, float tolerance = 1.f/static_cast<float>(Ion:
return a == b || std::abs(a - b) <= tolerance * std::abs(a + b) / 2.f || (std::isnan(a) && std::isnan(b));
}
ContinuousFunction * addFunction(ContinuousFunctionStore * store, ContinuousFunction::PlotType type, const char * definition, Context * context) {
Ion::Storage::Record::ErrorStatus err = store->addEmptyModel();
assert(err == Ion::Storage::Record::ErrorStatus::None);
(void) err; // Silence compilation warning about unused variable.
Ion::Storage::Record record = store->recordAtIndex(store->numberOfModels() - 1);
ContinuousFunction * f = static_cast<ContinuousFunction *>(store->modelForRecord(record).operator->());
f->setPlotType(type, Poincare::Preferences::AngleUnit::Degree, context);
f->setContent(definition, context);
return f;
}
void assert_check_cartesian_cache_against_function(ContinuousFunction * function, ContinuousFunctionCache * cache, Context * context, float tMin) {
/* We set the cache to nullptr to force the evaluation (otherwise we would be
* comparing the cache against itself). */
@@ -97,11 +86,11 @@ void assert_cache_stays_valid(ContinuousFunction::PlotType type, const char * de
graphRange.setYMax(3.f);
CurveViewCursor cursor;
ContinuousFunction * function = addFunction(&functionStore, type, definition, &globalContext);
ContinuousFunction * function = addFunction(definition, type, &functionStore, &globalContext);
Coordinate2D<float> origin = function->evaluateXYAtParameter(0.f, &globalContext);
cursor.moveTo(0.f, origin.x1(), origin.x2());
if (type == ContinuousFunction::PlotType::Cartesian) {
if (type == Cartesian) {
assert_cartesian_cache_stays_valid_while_panning(function, &globalContext, &graphRange, &cursor, &functionStore, 2.f);
assert_cartesian_cache_stays_valid_while_panning(function, &globalContext, &graphRange, &cursor, &functionStore, -0.4f);
} else {
@@ -112,21 +101,21 @@ void assert_cache_stays_valid(ContinuousFunction::PlotType type, const char * de
}
QUIZ_CASE(graph_caching) {
assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "x");
assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "x^2");
assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "sin(x)");
assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "sin(x)", -1e6f, 2e8f);
assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "sin(x^2)");
assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "1/x");
assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "1/x", -5e-5f, 5e-5f);
assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "-^x");
assert_cache_stays_valid(Cartesian, "x");
assert_cache_stays_valid(Cartesian, "x^2");
assert_cache_stays_valid(Cartesian, "sin(x)");
assert_cache_stays_valid(Cartesian, "sin(x)", -1e6f, 2e8f);
assert_cache_stays_valid(Cartesian, "sin(x^2)");
assert_cache_stays_valid(Cartesian, "1/x");
assert_cache_stays_valid(Cartesian, "1/x", -5e-5f, 5e-5f);
assert_cache_stays_valid(Cartesian, "-^x");
assert_cache_stays_valid(ContinuousFunction::PlotType::Polar, "1", 0.f, 360.f);
assert_cache_stays_valid(ContinuousFunction::PlotType::Polar, "θ", 0.f, 360.f);
assert_cache_stays_valid(ContinuousFunction::PlotType::Polar, "sin(θ)", 0.f, 360.f);
assert_cache_stays_valid(ContinuousFunction::PlotType::Polar, "sin(θ)", 2e-4f, 1e-3f);
assert_cache_stays_valid(ContinuousFunction::PlotType::Polar, "cos(5θ)", 0.f, 360.f);
assert_cache_stays_valid(ContinuousFunction::PlotType::Polar, "cos(5θ)", -1e8f, 1e8f);
assert_cache_stays_valid(Polar, "1", 0.f, 360.f);
assert_cache_stays_valid(Polar, "θ", 0.f, 360.f);
assert_cache_stays_valid(Polar, "sin(θ)", 0.f, 360.f);
assert_cache_stays_valid(Polar, "sin(θ)", 2e-4f, 1e-3f);
assert_cache_stays_valid(Polar, "cos(5θ)", 0.f, 360.f);
assert_cache_stays_valid(Polar, "cos(5θ)", -1e8f, 1e8f);
}
}

View File

@@ -0,0 +1,16 @@
#include "helper.h"
namespace Graph {
ContinuousFunction * addFunction(const char * definition, ContinuousFunction::PlotType type, ContinuousFunctionStore * store, Context * context) {
Ion::Storage::Record::ErrorStatus err = store->addEmptyModel();
assert(err == Ion::Storage::Record::ErrorStatus::None);
(void) err; // Silence compilation warning about unused variable.
Ion::Storage::Record record = store->recordAtIndex(store->numberOfModels() - 1);
ContinuousFunction * f = static_cast<ContinuousFunction *>(store->modelForRecord(record).operator->());
f->setPlotType(type, Poincare::Preferences::AngleUnit::Degree, context);
f->setContent(definition, context);
return f;
}
}

19
apps/graph/test/helper.h Normal file
View File

@@ -0,0 +1,19 @@
#ifndef APPS_GRAPH_TEST_HELPER_H
#define APPS_GRAPH_TEST_HELPER_H
#include "../app.h"
using namespace Poincare;
using namespace Shared;
namespace Graph {
constexpr ContinuousFunction::PlotType Cartesian = ContinuousFunction::PlotType::Cartesian;
constexpr ContinuousFunction::PlotType Polar = ContinuousFunction::PlotType::Polar;
constexpr ContinuousFunction::PlotType Parametric = ContinuousFunction::PlotType::Parametric;
ContinuousFunction * addFunction(const char * definition, ContinuousFunction::PlotType type, ContinuousFunctionStore * store, Context * context);
}
#endif

182
apps/graph/test/range.cpp Normal file
View File

@@ -0,0 +1,182 @@
#include <quiz.h>
#include "helper.h"
#include <cmath>
using namespace Poincare;
using namespace Shared;
namespace Graph {
void assert_range_inclusion_predicate(const char * definition, ContinuousFunction::PlotType type, const float * xList, size_t length, bool testInclusion) {
/* Test that all points (x, f(x)) for x in xList are either included in the
* range if testIncluded is true, or exculded from the range if testInclusion
* is false.
* If f(x) is not finite, only the presence of x in the horizontal range is
* tested. */
GlobalContext globalContext;
ContinuousFunctionStore functionStore;
ContinuousFunction * f = addFunction(definition, type, &functionStore, &globalContext);
float xMin = FLT_MAX, xMax = - FLT_MAX, yMin = FLT_MAX, yMax = - FLT_MAX;
f->rangeForDisplay(&xMin, &xMax, &yMin, &yMax, &globalContext);
assert(xMin <= xMax && yMin <= yMax);
for (size_t i = 0; i < length; i++) {
float x = xList[i];
float y = f->evaluateXYAtParameter(x, &globalContext).x2();
assert(std::isfinite(x));
if (!testInclusion) {
quiz_assert(xMin > x || x > xMax || (std::isfinite(y) && (yMin > y || y > yMax)));
} else {
/* The program can miss the exact abscissa of an extremum, resulting in
* bounds that are close from its value but that do not encompass it. We
* thus test the inclusion of (x, f(x)) along with two neighbouring
* points. */
float dx = (xMax - xMin) / (Ion::Display::Width / 2);
float y1 = f->evaluateXYAtParameter(x - dx, &globalContext).x2(), y2 = f->evaluateXYAtParameter(x + dx, &globalContext).x2();
quiz_assert(xMin <= x
&& x <= xMax
&& (!std::isfinite(y)
|| (yMin <= y && y <= yMax)
|| (yMin <= y1 && y1 <= yMax)
|| (yMin <= y2 && y2 <= yMax)));
}
}
}
void assert_range_includes_points(const char * definition, ContinuousFunction::PlotType type, const float * xList, size_t length) { assert_range_inclusion_predicate(definition, type, xList, length, true); }
void assert_range_excludes_points(const char * definition, ContinuousFunction::PlotType type, const float * xList, size_t length) { assert_range_inclusion_predicate(definition, type, xList, length, false); }
void assert_range_includes_single_point(const char * definition, ContinuousFunction::PlotType type, float x) { assert_range_includes_points(definition, type, &x, 1); }
void assert_range_excludes_single_point(const char * definition, ContinuousFunction::PlotType type, float x) { assert_range_excludes_points(definition, type, &x, 1); }
void assert_range_includes_and_bounds_asymptotes(const char * definition, const float * asymptotesXList, size_t length) {
/* The value for delta is the step the old algorithm used to sample a
* cartesian function, causing functions such as 1/x to be evaluated too
* close to 0. */
constexpr float delta = 1.f / 32.f;
for (size_t i = 0; i < length; i++) {
float x = asymptotesXList[i];
assert_range_includes_single_point(definition, Cartesian, x);
assert_range_excludes_single_point(definition, Cartesian, x - delta);
assert_range_excludes_single_point(definition, Cartesian, x + delta);
}
}
void assert_range_shows_enough_periods(const char * definition, float period, Preferences::AngleUnit angleUnit = Preferences::AngleUnit::Degree) {
/* The graph should display at least 3 periods, but no more than 7. */
constexpr int lowNumberOfPeriods = 3, highNumberOfPeriods = 8;
GlobalContext globalContext;
ContinuousFunctionStore functionStore;
ContinuousFunction * f = addFunction(definition, Cartesian, &functionStore, &globalContext);
Preferences::sharedPreferences()->setAngleUnit(angleUnit);
if (angleUnit != Preferences::AngleUnit::Degree) {
f->setPlotType(Cartesian, angleUnit, &globalContext);
}
float xMin = FLT_MAX, xMax = - FLT_MAX, yMin = FLT_MAX, yMax = - FLT_MAX;
f->rangeForDisplay(&xMin, &xMax, &yMin, &yMax, &globalContext);
assert(xMin <= xMax && yMin <= yMax);
float numberOfPeriods = (xMax - xMin) / period;
quiz_assert(lowNumberOfPeriods <= numberOfPeriods && numberOfPeriods <= highNumberOfPeriods);
}
void assert_range_displays_entire_polar_function(const char * definition) {
GlobalContext globalContext;
ContinuousFunctionStore functionStore;
ContinuousFunction * f = addFunction(definition, Polar, &functionStore, &globalContext);
float xMin = FLT_MAX, xMax = - FLT_MAX, yMin = FLT_MAX, yMax = - FLT_MAX;
f->rangeForDisplay(&xMin, &xMax, &yMin, &yMax, &globalContext);
assert(xMin <= xMax && yMin <= yMax);
for (float t = f->tMin(); t < f->tMax(); t += f->rangeStep()) {
const Coordinate2D<float> xy = f->evaluateXYAtParameter(t, &globalContext);
if (!std::isfinite(xy.x1()) || !std::isfinite(xy.x2())) {
continue;
}
quiz_assert(xMin <= xy.x1() && xy.x1() <= xMax && yMin <= xy.x2() && xy.x2() <= yMax);
}
}
QUIZ_CASE(graph_range_polynomes) {
assert_range_includes_single_point("37", Cartesian, 0.f);
assert_range_includes_single_point("x-1", Cartesian, 1.f);
assert_range_includes_single_point("100+x", Cartesian, -100.f);
assert_range_includes_single_point("x^2-20*x+101", Cartesian, 10.f);
constexpr float array1[2] = {-2.f, 3.f};
assert_range_includes_points("(x+2)*(x-3)", Cartesian, array1, 2);
constexpr float array2[2] = {-1.f, 0.5f};
assert_range_includes_points("2*x^2+x-1", Cartesian, array2, 2);
constexpr float array3[6] = {-3.f, 3.f, -2.f, 2.f, -1.f, 1.f};
assert_range_includes_points("(x+3)(x+2)(x+1)(x-1)(x-2)(x-3)", Cartesian, array3, 6);
constexpr float array4[3] = {0.f, 4.f, 5.f};
assert_range_includes_points("x^5-5*x^4", Cartesian, array4, 3);
}
QUIZ_CASE(graph_range_exponential) {
assert_range_includes_single_point("^x", Cartesian, 0.f);
assert_range_excludes_single_point("^x", Cartesian, 4.f);
assert_range_includes_single_point("^(-x)", Cartesian, 0.f);
assert_range_excludes_single_point("^(-x)", Cartesian, -4.f);
constexpr float array1[3] = {1.f, 5.f, 1e-1f};
assert_range_includes_points("ln(x)", Cartesian, array1, 3);
assert_range_excludes_single_point("ln(x)", Cartesian, 1e-3f);
constexpr float array2[2] = {-1.f, 3.f};
assert_range_includes_points("2-^(-x)", Cartesian, array2, 2);
assert_range_excludes_single_point("2-^(-x)", Cartesian, 50.f);
assert_range_excludes_single_point("2-^(-x)", Cartesian, -10.f);
assert_range_includes_single_point("x^x", Cartesian, 0.5f);
assert_range_excludes_single_point("x^x", Cartesian, -5.f);
assert_range_includes_single_point("x^(1/x)", Cartesian, 1e-3f);
}
QUIZ_CASE(graph_range_fractions) {
constexpr float array1[1] = {0.f};
assert_range_includes_and_bounds_asymptotes("1/x", array1, 1);
assert_range_includes_and_bounds_asymptotes("1/x^2", array1, 1);
constexpr float array2[2] = {-2.f, 2.f};
assert_range_includes_and_bounds_asymptotes("1/(x^2-4)", array2, 2);
constexpr float array3[1] = {-100.f};
assert_range_includes_and_bounds_asymptotes("1/(x+100)", array3, 1);
}
QUIZ_CASE(graph_range_periodic) {
assert_range_shows_enough_periods("sin(x)", 2*M_PI, Preferences::AngleUnit::Radian);
assert_range_shows_enough_periods("sin(x)", 360.f);
assert_range_shows_enough_periods("sin(x)+10", 2*M_PI, Preferences::AngleUnit::Radian);
assert_range_shows_enough_periods("sin(x)+10", 360.f);
assert_range_shows_enough_periods("cos(x)", 2*M_PI, Preferences::AngleUnit::Radian);
assert_range_shows_enough_periods("cos(x)", 360.f);
assert_range_shows_enough_periods("sin(x)/x", 2*M_PI, Preferences::AngleUnit::Radian);
assert_range_shows_enough_periods("sin(x)/x", 360.f);
assert_range_shows_enough_periods("x*sin(x)", 2*M_PI, Preferences::AngleUnit::Radian);
assert_range_shows_enough_periods("x*sin(x)", 360.f);
assert_range_shows_enough_periods("ln(sin(x))", 2*M_PI, Preferences::AngleUnit::Radian);
assert_range_shows_enough_periods("ln(sin(x))", 360.f);
constexpr float array1[2] = {-1.f, 1.f};
assert_range_includes_points("x*sin(1/x)", Cartesian, array1, 2);
}
QUIZ_CASE(graph_range_polar) {
assert_range_displays_entire_polar_function("1");
assert_range_displays_entire_polar_function("θ");
assert_range_displays_entire_polar_function("sin(θ)");
assert_range_displays_entire_polar_function("sin(10θ)");
assert_range_displays_entire_polar_function("ln(θ)");
}
}