mapnik/test/visual/runner.cpp
2024-07-22 10:20:47 +01:00

381 lines
12 KiB
C++

/*****************************************************************************
*
* This file is part of Mapnik (c++ mapping toolkit)
*
* Copyright (C) 2024 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 <algorithm>
#include <future>
#include <atomic>
#include <mapnik/load_map.hpp>
#include "runner.hpp"
#include "parse_map_sizes.hpp"
namespace visual_tests {
struct renderer_name_visitor
{
template<typename Renderer>
std::string operator()(Renderer const&) const
{
return Renderer::renderer_type::name;
}
};
class renderer_visitor
{
public:
renderer_visitor(std::string const& name,
mapnik::Map& map,
map_size const& tiles,
double scale_factor,
result_list& results,
report_type& report,
std::size_t iterations,
bool is_fail_limit,
std::atomic<std::size_t>& fail_count)
: name_(name)
, map_(map)
, tiles_(tiles)
, scale_factor_(scale_factor)
, results_(results)
, report_(report)
, iterations_(iterations)
, is_fail_limit_(is_fail_limit)
, fail_count_(fail_count)
{}
template<typename T, typename std::enable_if<T::renderer_type::support_tiles>::type* = nullptr>
void operator()(T const& renderer) const
{
test(renderer);
}
template<typename T, typename std::enable_if<!T::renderer_type::support_tiles>::type* = nullptr>
void operator()(T const& renderer) const
{
if (tiles_.width == 1 && tiles_.height == 1)
{
test(renderer);
}
}
private:
template<typename T>
void test(T const& renderer) const
{
map_size size{map_.width(), map_.height()};
std::chrono::high_resolution_clock::time_point start(std::chrono::high_resolution_clock::now());
for (std::size_t i = iterations_; i > 0; i--)
{
typename T::image_type image(render(renderer));
if (i == 1)
{
std::chrono::high_resolution_clock::time_point end(std::chrono::high_resolution_clock::now());
result r(renderer.report(image, name_, size, tiles_, scale_factor_));
r.duration = end - start;
mapnik::util::apply_visitor(report_visitor(r), report_);
results_.push_back(std::move(r));
if (is_fail_limit_ && r.state == STATE_FAIL)
{
++fail_count_;
}
}
}
}
template<typename T, typename std::enable_if<T::renderer_type::support_tiles>::type* = nullptr>
typename T::image_type render(T const& renderer) const
{
if (tiles_.width == 1 && tiles_.height == 1)
{
return renderer.render(map_, scale_factor_);
}
else
{
return renderer.render(map_, scale_factor_, tiles_);
}
}
template<typename T, typename std::enable_if<!T::renderer_type::support_tiles>::type* = nullptr>
typename T::image_type render(T const& renderer) const
{
return renderer.render(map_, scale_factor_);
}
std::string const& name_;
mapnik::Map& map_;
map_size const& tiles_;
double scale_factor_;
result_list& results_;
report_type& report_;
std::size_t iterations_;
bool is_fail_limit_;
std::atomic<std::size_t>& fail_count_;
};
runner::runner(runner::path_type const& styles_dir,
config const& defaults,
std::size_t iterations,
std::size_t fail_limit,
std::size_t jobs,
runner::renderer_container const& renderers)
: styles_dir_(styles_dir)
, defaults_(defaults)
, jobs_(jobs)
, iterations_(iterations)
, fail_limit_(fail_limit)
, renderers_(renderers)
{}
result_list runner::test_all(report_type& report) const
{
mapnik::fs::directory_iterator begin(styles_dir_);
mapnik::fs::directory_iterator end;
std::vector<runner::path_type> files(begin, end);
return test_parallel(files, report, jobs_);
}
result_list runner::test(std::vector<std::string> const& style_names, report_type& report) const
{
std::vector<runner::path_type> files;
files.reserve(style_names.size());
std::transform(style_names.begin(),
style_names.end(),
std::back_inserter(files),
[&](runner::path_type const& name) {
return (name.extension() == ".xml") ? name : (styles_dir_ / (name.string() + ".xml"));
});
return test_parallel(files, report, jobs_);
}
result_list
runner::test_parallel(std::vector<runner::path_type> 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<std::future<result_list>> futures(jobs);
std::atomic<std::size_t> fail_count(0);
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), std::ref(fail_count));
}
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_type> report,
std::reference_wrapper<std::atomic<std::size_t>> fail_count) const
{
result_list results;
for (runner::files_iterator i = begin; i != end; i++)
{
runner::path_type const& file = *i;
if (file.extension() == ".xml")
{
try
{
result_list r = test_one(file, report, fail_count.get());
std::move(r.begin(), r.end(), std::back_inserter(results));
}
catch (std::exception const& ex)
{
result r;
r.state = STATE_ERROR;
r.name = file.string();
r.error_message = ex.what();
r.duration = std::chrono::high_resolution_clock::duration::zero();
results.emplace_back(r);
mapnik::util::apply_visitor(report_visitor(r), report.get());
++fail_count.get();
}
}
if (fail_limit_ && fail_count.get() >= fail_limit_)
{
break;
}
}
return results;
}
void runner::parse_params(mapnik::parameters const& params, config& cfg) const
{
cfg.status = *params.get<mapnik::value_bool>("status", cfg.status);
std::optional<std::string> sizes = params.get<std::string>("sizes");
if (sizes)
{
cfg.sizes.clear();
parse_map_sizes(*sizes, cfg.sizes);
}
std::optional<std::string> tiles = params.get<std::string>("tiles");
if (tiles)
{
cfg.tiles.clear();
parse_map_sizes(*tiles, cfg.tiles);
}
std::optional<std::string> bbox_string = params.get<std::string>("bbox");
if (bbox_string)
{
cfg.bbox.from_string(*bbox_string);
}
for (auto const& renderer : renderers_)
{
std::string renderer_name = mapnik::util::apply_visitor(renderer_name_visitor(), renderer);
std::optional<mapnik::value_bool> enabled = params.get<mapnik::value_bool>(renderer_name);
if (enabled && !*enabled)
{
cfg.ignored_renderers.insert(renderer_name);
}
}
}
result_list
runner::test_one(runner::path_type const& style_path, report_type& report, std::atomic<std::size_t>& fail_count) const
{
config cfg(defaults_);
mapnik::Map map(cfg.sizes.front().width, cfg.sizes.front().height);
result_list results;
try
{
mapnik::load_map(map, style_path.string(), 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;
}
parse_params(map.get_extra_parameters(), cfg);
if (!cfg.status)
{
return results;
}
std::string name(style_path.stem().string());
for (auto const& size : cfg.sizes)
{
for (auto const& scale_factor : cfg.scales)
{
for (auto const& tiles_count : cfg.tiles)
{
if (!tiles_count.width || !tiles_count.height)
{
throw std::runtime_error("Cannot render zero tiles.");
}
if (size.width % tiles_count.width || size.height % tiles_count.height)
{
throw std::runtime_error("Tile size is not an integer.");
}
for (auto const& ren : renderers_)
{
std::string renderer_name = mapnik::util::apply_visitor(renderer_name_visitor(), ren);
if (cfg.ignored_renderers.count(renderer_name))
{
continue;
}
map.resize(size.width, size.height);
if (cfg.bbox.valid())
{
map.zoom_to_box(cfg.bbox);
}
else
{
map.zoom_all();
}
mapnik::util::apply_visitor(renderer_visitor(name,
map,
tiles_count,
scale_factor,
results,
report,
iterations_,
fail_limit_,
fail_count),
ren);
if (fail_limit_ && fail_count >= fail_limit_)
{
return results;
}
}
}
}
}
return results;
}
} // namespace visual_tests