From 688f76a260baeab9982fa76c0b9a4647745dd456 Mon Sep 17 00:00:00 2001 From: Jiri Drbalek Date: Tue, 17 Feb 2015 16:28:49 +0000 Subject: [PATCH] new visual test runner --- test/build.py | 15 ++ test/run | 8 +- test/visual/compare_images.hpp | 53 +++++++ test/visual/config.cpp | 40 +++++ test/visual/config.hpp | 82 ++++++++++ test/visual/map_sizes_grammar.hpp | 62 ++++++++ test/visual/renderer.hpp | 170 ++++++++++++++++++++ test/visual/report.cpp | 170 ++++++++++++++++++++ test/visual/report.hpp | 124 +++++++++++++++ test/visual/run.cpp | 103 +++++++++++++ test/visual/runner.cpp | 248 ++++++++++++++++++++++++++++++ test/visual/runner.hpp | 70 +++++++++ 12 files changed, 1143 insertions(+), 2 deletions(-) create mode 100644 test/visual/compare_images.hpp create mode 100644 test/visual/config.cpp create mode 100644 test/visual/config.hpp create mode 100644 test/visual/map_sizes_grammar.hpp create mode 100644 test/visual/renderer.hpp create mode 100644 test/visual/report.cpp create mode 100644 test/visual/report.hpp create mode 100644 test/visual/run.cpp create mode 100644 test/visual/runner.cpp create mode 100644 test/visual/runner.hpp diff --git a/test/build.py b/test/build.py index ec6dce5b3..91e5ee418 100644 --- a/test/build.py +++ b/test/build.py @@ -23,6 +23,9 @@ else: test_env.PrependUnique(CPPPATH=test_env['CAIRO_CPPPATHS']) test_env.Append(CPPDEFINES = '-DHAVE_CAIRO') test_env.PrependUnique(CPPPATH=['./']) + if test_env['PLATFORM'] == 'Linux': + test_env['LINKFLAGS'].append('-pthread') + test_env.AppendUnique(LIBS='boost_program_options%s' % env['BOOST_APPEND']) test_env_local = test_env.Clone() @@ -39,6 +42,18 @@ else: test_program = test_env_local.Program(standalone.replace('.cpp','-bin'), source=standalone) Depends(test_program, env.subst('../src/%s' % env['MAPNIK_LIB_NAME'])) + # visual tests + source = Split( + """ + visual/config.cpp + visual/report.cpp + visual/runner.cpp + visual/run.cpp + """ + ) + test_program = test_env_local.Program('visual/run', source=source) + Depends(test_program, env.subst('../src/%s' % env['MAPNIK_LIB_NAME'])) + # build locally if installing if 'install' in COMMAND_LINE_TARGETS: env.Alias('install',test_program) diff --git a/test/run b/test/run index a7606a761..f0d4a8c47 100755 --- a/test/run +++ b/test/run @@ -23,7 +23,7 @@ if [[ -f mapnik-settings.env ]]; then fi run_substep "Running C++ Unit tests..." -./test/unit/run +./test/unit/run failures=$((failures+$?)) run_substep "Running standalone C++ tests..." @@ -34,4 +34,8 @@ if [ -n "$(find test/standalone/ -maxdepth 1 -name '*-bin' -print -quit)" ]; the done fi -exit $failures \ No newline at end of file +run_substep "Running visual tests..." +./test/visual/run +failures=$((failures+$?)) + +exit $failures diff --git a/test/visual/compare_images.hpp b/test/visual/compare_images.hpp new file mode 100644 index 000000000..d227d7864 --- /dev/null +++ b/test/visual/compare_images.hpp @@ -0,0 +1,53 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 Artem Pavlenko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + *****************************************************************************/ + +#ifndef COMPARE_IMAGES_HPP +#define COMPARE_IMAGES_HPP + +// stl +#include + +// mapnik +#include +#include + +namespace visual_tests +{ + +template +unsigned compare_images(Image const & actual, std::string const & reference) +{ + std::unique_ptr reader(mapnik::get_image_reader(reference, "png")); + if (!reader.get()) + { + throw mapnik::image_reader_exception("Failed to load: " + reference); + } + + mapnik::image_any ref_image_any = reader->read(0, 0, reader->width(), reader->height()); + Image const & reference_image = mapnik::util::get(ref_image_any); + + return mapnik::compare(actual, reference_image, 0, true); +} + +} + +#endif diff --git a/test/visual/config.cpp b/test/visual/config.cpp new file mode 100644 index 000000000..83cad7d43 --- /dev/null +++ b/test/visual/config.cpp @@ -0,0 +1,40 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 Artem Pavlenko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + *****************************************************************************/ + +// mapnik +#include + +#include "config.hpp" + +namespace visual_tests +{ + +bool ensure_dir(boost::filesystem::path const & dir) +{ + if (!mapnik::util::exists(dir.string())) + { + return boost::filesystem::create_directories(dir); + } + return true; +} + +} diff --git a/test/visual/config.hpp b/test/visual/config.hpp new file mode 100644 index 000000000..ff9f7c065 --- /dev/null +++ b/test/visual/config.hpp @@ -0,0 +1,82 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 Artem Pavlenko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + *****************************************************************************/ + +#ifndef CONFIG_HPP +#define CONFIG_HPP + +// stl +#include +#include + +// boost +#include + +namespace visual_tests +{ + +struct map_size +{ + map_size(int width, int height) : width(width), height(height) { } + map_size() { } + unsigned width; + unsigned height; +}; + +struct config +{ + config() : status(true), + scales({ 1.0, 2.0 }), + sizes({ { 500, 100 } }) { } + + bool status; + std::vector scales; + std::vector sizes; +}; + +enum result_state : std::uint8_t +{ + STATE_OK, + STATE_FAIL, + STATE_ERROR, + STATE_OVERWRITE +}; + +struct result +{ + std::string name; + result_state state; + char const * renderer_name; + map_size size; + double scale_factor; + boost::filesystem::path actual_image_path; + boost::filesystem::path reference_image_path; + std::string error_message; + unsigned diff; +}; + +using result_list = std::vector; + +bool ensure_dir(boost::filesystem::path const & dir); + +} + +#endif diff --git a/test/visual/map_sizes_grammar.hpp b/test/visual/map_sizes_grammar.hpp new file mode 100644 index 000000000..29464223d --- /dev/null +++ b/test/visual/map_sizes_grammar.hpp @@ -0,0 +1,62 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 Artem Pavlenko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + *****************************************************************************/ + +#ifndef MAP_SIZES_GRAMMAR_HPP +#define MAP_SIZES_GRAMMAR_HPP + +// boost +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-local-typedef" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#include +#include +#pragma GCC diagnostic pop + +namespace visual_tests +{ + +namespace qi = boost::spirit::qi; +namespace ascii = boost::spirit::ascii; + +template +struct map_sizes_grammar : qi::grammar(), ascii::space_type> +{ + map_sizes_grammar() : map_sizes_grammar::base_type(start) + { + using namespace boost::spirit::qi; + using namespace boost::phoenix; + + int_type int_; + _1_type _1; + _2_type _2; + _val_type _val; + + start = (int_ >> ',' >> int_)[push_back(_val, construct(_1, _2))] % ';'; + } + + qi::rule(), ascii::space_type> start; +}; + +} + +#endif diff --git a/test/visual/renderer.hpp b/test/visual/renderer.hpp new file mode 100644 index 000000000..6c8d234e2 --- /dev/null +++ b/test/visual/renderer.hpp @@ -0,0 +1,170 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 Artem Pavlenko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + *****************************************************************************/ + +#ifndef RENDERER_HPP +#define RENDERER_HPP + +#include "compare_images.hpp" + +// stl +#include +#include + +// mapnik +#include +#include +#if defined(HAVE_CAIRO) +#include +#include +#endif + +// boost +#include + +namespace visual_tests +{ + +template +struct renderer_base +{ + using image_type = ImageType; + + unsigned compare(image_type const & actual, boost::filesystem::path const& reference) const + { + return compare_images(actual, reference.string()); + } + + void save(image_type const & image, boost::filesystem::path const& path) const + { + mapnik::save_to_file(image, path.string(), "png32"); + } +}; + +struct agg_renderer : renderer_base +{ + static constexpr const char * name = "agg"; + + image_type render(mapnik::Map const & map, double scale_factor) const + { + image_type image(map.width(), map.height()); + mapnik::agg_renderer ren(map, image, scale_factor); + ren.apply(); + return image; + } +}; + +#if defined(HAVE_CAIRO) +struct cairo_renderer : renderer_base +{ + static constexpr const char * name = "cairo"; + + image_type render(mapnik::Map const & map, double scale_factor) const + { + mapnik::cairo_surface_ptr image_surface( + cairo_image_surface_create(CAIRO_FORMAT_ARGB32, map.width(), map.height()), + mapnik::cairo_surface_closer()); + mapnik::cairo_ptr image_context(mapnik::create_context(image_surface)); + mapnik::cairo_renderer ren(map, image_context, scale_factor); + ren.apply(); + image_type image(map.width(), map.height()); + mapnik::cairo_image_to_rgba8(image, image_surface); + return image; + } +}; +#endif + +struct grid_renderer : renderer_base +{ + static constexpr const char * name = "grid"; + + image_type render(mapnik::Map const & map, double scale_factor) const + { + image_type image(map.width(), map.height()); + // TODO: Render grid here. + return image; + } +}; + +template +class renderer +{ +public: + renderer(boost::filesystem::path const & output_dir, boost::filesystem::path const & reference_dir, bool overwrite) + : output_dir(output_dir), reference_dir(reference_dir), overwrite(overwrite) + { + } + + result test(std::string const & name, mapnik::Map const & map, double scale_factor) const + { + typename Renderer::image_type image(ren.render(map, scale_factor)); + boost::filesystem::path reference = reference_dir / image_file_name(name, map.width(), map.height(), scale_factor, true); + result res; + + res.state = STATE_OK; + res.name = name; + res.renderer_name = Renderer::name; + res.scale_factor = scale_factor; + res.size = map_size(map.width(), map.height()); + res.reference_image_path = reference; + res.diff = ren.compare(image, reference); + + if (res.diff) + { + ensure_dir(output_dir); + boost::filesystem::path path = output_dir / image_file_name(name, map.width(), map.height(), scale_factor); + res.actual_image_path = path; + res.state = STATE_FAIL; + ren.save(image, path); + + if (overwrite) + { + ren.save(image, reference); + res.state = STATE_OVERWRITE; + } + } + + return res; + } + +private: + std::string image_file_name(std::string const & test_name, + double width, + double height, + double scale_factor, + bool reference=false) const + { + std::stringstream s; + s << test_name << '-' << width << '-' << height << '-' + << std::fixed << std::setprecision(1) << scale_factor + << '-' << Renderer::name << (reference ? "-reference" : "") << ".png"; + return s.str(); + } + + Renderer ren; + boost::filesystem::path const & output_dir; + boost::filesystem::path const & reference_dir; + const bool overwrite; +}; + +} + +#endif diff --git a/test/visual/report.cpp b/test/visual/report.cpp new file mode 100644 index 000000000..7f6b7c355 --- /dev/null +++ b/test/visual/report.cpp @@ -0,0 +1,170 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 Artem Pavlenko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + *****************************************************************************/ + +// stl +#include +#include +#include + +#include "report.hpp" + +namespace visual_tests +{ + +void console_report::report(result const & r) +{ + s << '"' << r.name << '-' << r.size.width << '-' << r.size.height << '-' << std::fixed << + std::setprecision(1) << r.scale_factor << "\" with " << r.renderer_name << "... "; + + switch (r.state) + { + case STATE_OK: + s << "OK"; + break; + case STATE_FAIL: + s << "FAILED (" << r.diff << " different pixels)"; + break; + case STATE_OVERWRITE: + s << "OVERWRITTEN (" << r.diff << " different pixels)"; + break; + case STATE_ERROR: + s << "ERROR (" << r.error_message << ")"; + break; + } + + s << std::endl; +} + +unsigned console_report::summary(result_list const & results) +{ + unsigned ok = 0; + unsigned error = 0; + unsigned fail = 0; + unsigned overwrite = 0; + + for (auto const & r : results) + { + switch (r.state) + { + case STATE_OK: ok++; break; + case STATE_FAIL: fail++; break; + case STATE_ERROR: error++; break; + case STATE_OVERWRITE: overwrite++; break; + } + } + + s << std::endl; + s << "Visual rendering: " << fail << " failed / " << ok << " passed / " + << overwrite << " overwritten / " << error << " errors" << std::endl; + + return fail + error; +} + +void console_short_report::report(result const & r) +{ + s << '.'; +} + +void html_report::report(result const & r, boost::filesystem::path const & output_dir) +{ + using namespace boost::filesystem; + + path reference = output_dir / r.reference_image_path.filename(); + path actual = output_dir / r.actual_image_path.filename(); + + copy_file(r.reference_image_path, reference, copy_option::overwrite_if_exists); + copy_file(r.actual_image_path, actual, copy_option::overwrite_if_exists); + + s << "
\n" + " \n" + " \n" + " \n" + "
\n" + "
" << r.diff << "
\n" + "
\n" + " \n" + " \n" + " \n" + "
\n"; +} + +constexpr const char * html_header = R"template( + + + + + + +
+
expected
+
% difference
+
actual
+)template"; + +constexpr const char * html_footer = R"template(
+ +)template"; + +void html_report::summary(result_list const & results, boost::filesystem::path const & output_dir) +{ + s << html_header; + + for (auto const & r : results) + { + if (r.state != STATE_OK) + { + report(r, output_dir); + } + } + + s << html_footer; +} + +void html_summary(result_list const & results, boost::filesystem::path output_dir) +{ + boost::filesystem::path html_root = output_dir / "visual-test-results"; + ensure_dir(html_root); + boost::filesystem::path html_report_path = html_root / "comparison.html"; + std::ofstream output_file(html_report_path.string()); + html_report report(output_file); + report.summary(results, html_root); +} + +} diff --git a/test/visual/report.hpp b/test/visual/report.hpp new file mode 100644 index 000000000..cdccad2aa --- /dev/null +++ b/test/visual/report.hpp @@ -0,0 +1,124 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 Artem Pavlenko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + *****************************************************************************/ + +#ifndef CONSOLE_REPORT_HPP +#define CONSOLE_REPORT_HPP + +// stl +#include + +#include + +#include "config.hpp" + +namespace visual_tests +{ + +class console_report +{ +public: + console_report() : s(std::clog) + { + } + + console_report(std::ostream & s) : s(s) + { + } + + void report(result const & r); + unsigned summary(result_list const & results); + +protected: + std::ostream & s; +}; + +class console_short_report : public console_report +{ +public: + console_short_report() : console_report() + { + } + + console_short_report(std::ostream & s) : console_report(s) + { + } + + void report(result const & r); +}; + +class html_report +{ +public: + html_report(std::ostream & s) : s(s) + { + } + + void report(result const & r, boost::filesystem::path const & output_dir); + void summary(result_list const & results, boost::filesystem::path const & output_dir); + +protected: + std::ostream & s; +}; + +using report_type = mapnik::util::variant; + +class report_visitor +{ +public: + report_visitor(result const & r) + : result_(r) + { + } + + template + void operator()(T & report) const + { + return report.report(result_); + } + +private: + result const & result_; +}; + +class summary_visitor +{ +public: + summary_visitor(result_list const & r) + : result_(r) + { + } + + template + unsigned operator()(T & report) const + { + return report.summary(result_); + } + +private: + result_list const & result_; +}; + +void html_summary(result_list const & results, boost::filesystem::path output_dir); + +} + +#endif diff --git a/test/visual/run.cpp b/test/visual/run.cpp new file mode 100644 index 000000000..b1a469a20 --- /dev/null +++ b/test/visual/run.cpp @@ -0,0 +1,103 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 Artem Pavlenko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + *****************************************************************************/ + +#include "runner.hpp" +#include "config.hpp" + +#include +#include + +// boost +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + using namespace visual_tests; + namespace po = boost::program_options; + + po::options_description desc("visual test runner"); + desc.add_options() + ("help,h", "produce usage message") + ("verbose,v", "verbose output") + ("overwrite,o", "overwrite reference image") + ("jobs,j", po::value()->default_value(1), "number of paralel threads") + ("styles-dir", po::value()->default_value("test/data-visual/styles"), "directory with styles") + ("images-dir", po::value()->default_value("test/data-visual/images"), "directory with reference images") + ("output-dir", po::value()->default_value("/tmp/mapnik-visual-images"), "directory for output files") + ("uuid-subdir,u", "write output files to subdirectory with unique name") + ("styles", po::value>(), "selected styles to test") + ("fonts", po::value()->default_value("fonts"), "font search path") + ("plugins", po::value()->default_value("plugins/input"), "input plugins search path") + ; + + po::positional_options_description p; + p.add("styles", -1); + po::variables_map vm; + po::store(po::command_line_parser(argc, argv).options(desc).positional(p).run(), vm); + po::notify(vm); + + if (vm.count("help")) + { + std::clog << desc << std::endl; + return 1; + } + + mapnik::freetype_engine::register_fonts(vm["fonts"].as(), true); + mapnik::datasource_cache::instance().register_datasources(vm["plugins"].as()); + + boost::filesystem::path output_dir(vm["output-dir"].as()); + + if (vm.count("uuid-subdir")) + { + boost::uuids::uuid uuid = boost::uuids::random_generator()(); + output_dir /= boost::lexical_cast(uuid); + } + + runner run(vm["styles-dir"].as(), + output_dir, + vm["images-dir"].as(), + vm.count("overwrite"), + vm["jobs"].as()); + report_type report = vm.count("verbose") ? report_type((console_report())) : report_type((console_short_report())); + result_list results; + + if (vm.count("styles")) + { + results = run.test(vm["styles"].as>(), report); + } + else + { + results = run.test_all(report); + } + + unsigned failed_count = mapnik::util::apply_visitor(summary_visitor(results), report); + + if (failed_count) + { + html_summary(results, output_dir); + } + + return failed_count; +} diff --git a/test/visual/runner.cpp b/test/visual/runner.cpp new file mode 100644 index 000000000..76c038377 --- /dev/null +++ b/test/visual/runner.cpp @@ -0,0 +1,248 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 Artem Pavlenko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + *****************************************************************************/ + +// stl +#include +#include + +#include +#include + +#include "runner.hpp" + +namespace visual_tests +{ + +class renderer_visitor +{ +public: + renderer_visitor(std::string const & name, mapnik::Map const & map, double scale_factor) + : name_(name), map_(map), scale_factor_(scale_factor) + { + } + + template + result operator()(T const & renderer) const + { + return renderer.test(name_, map_, scale_factor_); + } + +private: + std::string const & name_; + mapnik::Map const & map_; + double scale_factor_; +}; + +runner::runner(boost::filesystem::path const & styles_dir, + boost::filesystem::path const & output_dir, + boost::filesystem::path const & reference_dir, + bool overwrite, + std::size_t jobs) + : styles_dir_(styles_dir), + output_dir_(output_dir), + reference_dir_(reference_dir), + jobs_(jobs), + renderers_{ renderer(output_dir_, reference_dir_, overwrite), + renderer(output_dir_, reference_dir_, overwrite)/*, + renderer(output_dir_, reference_dir_, overwrite)*/ } +{ +} + +result_list runner::test_all(report_type & report) const +{ + std::vector files = mapnik::util::list_directory(styles_dir_.string()); + return test_paralel(files, report, jobs_); +} + +result_list runner::test(std::vector const & style_names, report_type & report) const +{ + std::vector files(style_names.size()); + std::transform(style_names.begin(), style_names.end(), std::back_inserter(files), + [&](std::string const & name) + { + return (styles_dir_ / (name + ".xml")).string(); + }); + return test_paralel(files, report, jobs_); +} + +result_list runner::test_paralel(std::vector const & files, report_type & report, std::size_t jobs) const +{ + result_list results; + + if (files.empty()) + { + return results; + } + + if (jobs == 0) + { + jobs = 1; + } + + std::size_t chunk_size = files.size() / jobs; + + if (chunk_size == 0) + { + chunk_size = files.size(); + jobs = 1; + } + + std::launch launch(jobs == 1 ? std::launch::deferred : std::launch::async); + std::vector> futures(jobs); + + for (std::size_t i = 0; i < jobs; i++) + { + files_iterator begin(files.begin() + i * chunk_size); + files_iterator end(files.begin() + (i + 1) * chunk_size); + + // Handle remainder of files.size() / jobs + if (i == jobs - 1) + { + end = files.end(); + } + + futures[i] = std::async(launch, &runner::test_range, this, begin, end, std::ref(report)); + } + + for (auto & f : futures) + { + result_list r = f.get(); + std::move(r.begin(), r.end(), std::back_inserter(results)); + } + + return results; +} + +result_list runner::test_range(files_iterator begin, files_iterator end, std::reference_wrapper report) const +{ + config defaults; + result_list results; + + for (std::vector::const_iterator i = begin; i != end; i++) + { + std::string const & file = *i; + if (file.size() >= 3 && file.substr(file.size() - 3) == "xml") + { + try + { + result_list r = test_one(file, defaults, report); + std::move(r.begin(), r.end(), std::back_inserter(results)); + } + catch (std::exception const& ex) + { + result r; + r.state = STATE_ERROR; + r.name = file; + r.error_message = ex.what(); + results.emplace_back(r); + mapnik::util::apply_visitor(report_visitor(r), report.get()); + } + } + } + + return results; +} + +result_list runner::test_one(std::string const& style_path, config cfg, report_type & report) const +{ + mapnik::Map m(cfg.sizes.front().width, cfg.sizes.front().height); + result_list results; + + try + { + mapnik::load_map(m, style_path, true); + } + catch (std::exception const& ex) + { + std::string what = ex.what(); + if (what.find("Could not create datasource") != std::string::npos || + what.find("Postgis Plugin: could not connect to server") != std::string::npos) + { + return results; + } + throw; + } + + mapnik::parameters const & params = m.get_extra_parameters(); + + boost::optional status = params.get("status", cfg.status); + + if (!*status) + { + return results; + } + + boost::optional sizes = params.get("sizes"); + + if (sizes) + { + cfg.sizes.clear(); + parse_map_sizes(*sizes, cfg.sizes); + } + + boost::filesystem::path p(style_path); + std::string name(p.stem().string()); + + for (map_size const & size : cfg.sizes) + { + m.resize(size.width, size.height); + + boost::optional bbox_string = params.get("bbox"); + + if (bbox_string) + { + mapnik::box2d bbox; + bbox.from_string(*bbox_string); + m.zoom_to_box(bbox); + } + else + { + m.zoom_all(); + } + + for (double const & scale_factor : cfg.scales) + { + for(auto const& ren : renderers_) + { + result r = mapnik::util::apply_visitor(renderer_visitor(name, m, scale_factor), ren); + results.emplace_back(r); + mapnik::util::apply_visitor(report_visitor(r), report); + } + } + } + + return results; +} + +void runner::parse_map_sizes(std::string const & str, std::vector & sizes) const +{ + boost::spirit::ascii::space_type space; + std::string::const_iterator iter = str.begin(); + std::string::const_iterator end = str.end(); + if (!boost::spirit::qi::phrase_parse(iter, end, map_sizes_parser_, space, sizes)) + { + throw std::runtime_error("Failed to parse list of sizes: '" + str + "'"); + } +} + +} + diff --git a/test/visual/runner.hpp b/test/visual/runner.hpp new file mode 100644 index 000000000..2b1c7a301 --- /dev/null +++ b/test/visual/runner.hpp @@ -0,0 +1,70 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 Artem Pavlenko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + *****************************************************************************/ + +#ifndef VISUAL_TEST_RUNNER_HPP +#define VISUAL_TEST_RUNNER_HPP + +#include + +#include "config.hpp" +#include "report.hpp" +#include "renderer.hpp" +#include "map_sizes_grammar.hpp" + +namespace visual_tests +{ + +class runner +{ + using renderer_type = mapnik::util::variant, + renderer/*, + renderer*/>; + +public: + runner(boost::filesystem::path const & styles_dir, + boost::filesystem::path const & output_dir, + boost::filesystem::path const & reference_dir, + bool overwrite, + std::size_t jobs); + + result_list test_all(report_type & report) const; + result_list test(std::vector const & style_names, report_type & report) const; + +private: + using files_iterator = std::vector::const_iterator; + + result_list test_paralel(std::vector const & files, report_type & report, std::size_t jobs) const; + result_list test_range(files_iterator begin, files_iterator end, std::reference_wrapper report) const; + result_list test_one(std::string const & style_path, config cfg, report_type & report) const; + void parse_map_sizes(std::string const & str, std::vector & sizes) const; + + const map_sizes_grammar map_sizes_parser_; + const boost::filesystem::path styles_dir_; + const boost::filesystem::path output_dir_; + const boost::filesystem::path reference_dir_; + const std::size_t jobs_; + const renderer_type renderers_[boost::mpl::size::value]; +}; + +} + +#endif