From a1aefb0cb25dd7d579975a4d8ff23029d4920512 Mon Sep 17 00:00:00 2001 From: Gabriel Ozouf Date: Wed, 5 Aug 2020 17:50:57 +0200 Subject: [PATCH] [apps/graph] Added tests on graph ranges Change-Id: I6c080f40c934cd31083be3d19aa0a4d0bb33c5cc --- apps/graph/Makefile | 4 +- apps/graph/test/caching.cpp | 45 ++++----- apps/graph/test/helper.cpp | 16 ++++ apps/graph/test/helper.h | 19 ++++ apps/graph/test/range.cpp | 182 ++++++++++++++++++++++++++++++++++++ 5 files changed, 237 insertions(+), 29 deletions(-) create mode 100644 apps/graph/test/helper.cpp create mode 100644 apps/graph/test/helper.h create mode 100644 apps/graph/test/range.cpp diff --git a/apps/graph/Makefile b/apps/graph/Makefile index 1a3fbddcc..de03db3d6 100644 --- a/apps/graph/Makefile +++ b/apps/graph/Makefile @@ -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)) diff --git a/apps/graph/test/caching.cpp b/apps/graph/test/caching.cpp index 70ee68631..059c7cef3 100644 --- a/apps/graph/test/caching.cpp +++ b/apps/graph/test/caching.cpp @@ -1,5 +1,5 @@ #include -#include "../app.h" +#include "helper.h" #include using namespace Poincare; @@ -13,17 +13,6 @@ bool floatEquals(float a, float b, float tolerance = 1.f/static_cast(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(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 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); } } diff --git a/apps/graph/test/helper.cpp b/apps/graph/test/helper.cpp new file mode 100644 index 000000000..6c4f4ac3c --- /dev/null +++ b/apps/graph/test/helper.cpp @@ -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(store->modelForRecord(record).operator->()); + f->setPlotType(type, Poincare::Preferences::AngleUnit::Degree, context); + f->setContent(definition, context); + return f; +} + +} diff --git a/apps/graph/test/helper.h b/apps/graph/test/helper.h new file mode 100644 index 000000000..2f5e9fb58 --- /dev/null +++ b/apps/graph/test/helper.h @@ -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 diff --git a/apps/graph/test/range.cpp b/apps/graph/test/range.cpp new file mode 100644 index 000000000..defda15e7 --- /dev/null +++ b/apps/graph/test/range.cpp @@ -0,0 +1,182 @@ +#include +#include "helper.h" +#include + +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 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(θ)"); +} + +}