1/* sane - Scanner Access Now Easy. 2 3 Copyright (C) 2019 Povilas Kanapickas <povilas@radix.lt> 4 5 This file is part of the SANE package. 6 7 This program is free software; you can redistribute it and/or 8 modify it under the terms of the GNU General Public License as 9 published by the Free Software Foundation; either version 2 of the 10 License, or (at your option) any later version. 11 12 This program is distributed in the hope that it will be useful, but 13 WITHOUT ANY WARRANTY; without even the implied warranty of 14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 General Public License for more details. 16 17 You should have received a copy of the GNU General Public License 18 along with this program. If not, see <https://www.gnu.org/licenses/>. 19*/ 20 21#define DEBUG_DECLARE_ONLY 22 23#include "../../../backend/genesys/device.h" 24#include "../../../backend/genesys/enums.h" 25#include "../../../backend/genesys/error.h" 26#include "../../../backend/genesys/low.h" 27#include "../../../backend/genesys/genesys.h" 28#include "../../../backend/genesys/test_settings.h" 29#include "../../../backend/genesys/test_scanner_interface.h" 30#include "../../../backend/genesys/utilities.h" 31#include "../../../include/sane/saneopts.h" 32#include "sys/stat.h" 33#include <cstdio> 34#include <cstring> 35#include <fstream> 36#include <sstream> 37#include <string> 38#include <unordered_set> 39 40#define XSTR(s) STR(s) 41#define STR(s) #s 42#define CURR_SRCDIR XSTR(TESTSUITE_BACKEND_GENESYS_SRCDIR) 43 44struct TestConfig 45{ 46 std::uint16_t vendor_id = 0; 47 std::uint16_t product_id = 0; 48 std::uint16_t bcd_device = 0; 49 std::string model_name; 50 genesys::ScanMethod method = genesys::ScanMethod::FLATBED; 51 genesys::ScanColorMode color_mode = genesys::ScanColorMode::COLOR_SINGLE_PASS; 52 unsigned depth = 0; 53 unsigned resolution = 0; 54 55 std::string name() const 56 { 57 std::stringstream out; 58 out << "capture_" << model_name 59 << '_' << method 60 << '_' << color_mode 61 << "_depth" << depth 62 << "_dpi" << resolution; 63 return out.str(); 64 } 65 66}; 67 68class SaneOptions 69{ 70public: 71 void fetch(SANE_Handle handle) 72 { 73 handle_ = handle; 74 options_.resize(1); 75 options_[0] = fetch_option(0); 76 77 if (std::strcmp(options_[0].name, SANE_NAME_NUM_OPTIONS) != 0 || 78 options_[0].type != SANE_TYPE_INT) 79 { 80 throw std::runtime_error("Expected option number option"); 81 } 82 int option_count = 0; 83 TIE(sane_control_option(handle, 0, SANE_ACTION_GET_VALUE, &option_count, nullptr)); 84 85 options_.resize(option_count); 86 for (int i = 0; i < option_count; ++i) { 87 options_[i] = fetch_option(i); 88 } 89 } 90 91 void close() 92 { 93 handle_ = nullptr; 94 } 95 96 bool get_value_bool(const std::string& name) const 97 { 98 auto i = find_option(name, SANE_TYPE_BOOL); 99 int value = 0; 100 TIE(sane_control_option(handle_, i, SANE_ACTION_GET_VALUE, &value, nullptr)); 101 return value; 102 } 103 104 void set_value_bool(const std::string& name, bool value) 105 { 106 auto i = find_option(name, SANE_TYPE_BOOL); 107 int value_int = value; 108 TIE(sane_control_option(handle_, i, SANE_ACTION_SET_VALUE, &value_int, nullptr)); 109 } 110 111 bool get_value_button(const std::string& name) const 112 { 113 auto i = find_option(name, SANE_TYPE_BUTTON); 114 int value = 0; 115 TIE(sane_control_option(handle_, i, SANE_ACTION_GET_VALUE, &value, nullptr)); 116 return value; 117 } 118 119 void set_value_button(const std::string& name, bool value) 120 { 121 auto i = find_option(name, SANE_TYPE_BUTTON); 122 int value_int = value; 123 TIE(sane_control_option(handle_, i, SANE_ACTION_SET_VALUE, &value_int, nullptr)); 124 } 125 126 int get_value_int(const std::string& name) const 127 { 128 auto i = find_option(name, SANE_TYPE_INT); 129 int value = 0; 130 TIE(sane_control_option(handle_, i, SANE_ACTION_GET_VALUE, &value, nullptr)); 131 return value; 132 } 133 134 void set_value_int(const std::string& name, int value) 135 { 136 auto i = find_option(name, SANE_TYPE_INT); 137 TIE(sane_control_option(handle_, i, SANE_ACTION_SET_VALUE, &value, nullptr)); 138 } 139 140 float get_value_float(const std::string& name) const 141 { 142 auto i = find_option(name, SANE_TYPE_FIXED); 143 int value = 0; 144 TIE(sane_control_option(handle_, i, SANE_ACTION_GET_VALUE, &value, nullptr)); 145 return genesys::fixed_to_float(value); 146 } 147 148 void set_value_float(const std::string& name, float value) 149 { 150 auto i = find_option(name, SANE_TYPE_FIXED); 151 int value_int = SANE_FIX(value); 152 TIE(sane_control_option(handle_, i, SANE_ACTION_SET_VALUE, &value_int, nullptr)); 153 } 154 155 std::string get_value_string(const std::string& name) const 156 { 157 auto i = find_option(name, SANE_TYPE_STRING); 158 std::string value; 159 value.resize(options_[i].size + 1); 160 TIE(sane_control_option(handle_, i, SANE_ACTION_GET_VALUE, &value.front(), nullptr)); 161 value.resize(std::strlen(&value.front())); 162 return value; 163 } 164 165 void set_value_string(const std::string& name, const std::string& value) 166 { 167 auto i = find_option(name, SANE_TYPE_STRING); 168 TIE(sane_control_option(handle_, i, SANE_ACTION_SET_VALUE, 169 const_cast<char*>(&value.front()), nullptr)); 170 } 171 172private: 173 SANE_Option_Descriptor fetch_option(int index) 174 { 175 const auto* option = sane_get_option_descriptor(handle_, index); 176 if (option == nullptr) { 177 throw std::runtime_error("Got nullptr option"); 178 } 179 return *option; 180 } 181 182 std::size_t find_option(const std::string& name, SANE_Value_Type type) const 183 { 184 for (std::size_t i = 0; i < options_.size(); ++i) { 185 if (options_[i].name == name) { 186 if (options_[i].type != type) { 187 throw std::runtime_error("Option has incorrect type"); 188 } 189 return i; 190 } 191 } 192 throw std::runtime_error("Could not find option"); 193 } 194 195 SANE_Handle handle_; 196 std::vector<SANE_Option_Descriptor> options_; 197}; 198 199 200void print_params(const SANE_Parameters& params, std::stringstream& out) 201{ 202 out << "\n\n================\n" 203 << "Scan params:\n" 204 << "format: " << params.format << "\n" 205 << "last_frame: " << params.last_frame << "\n" 206 << "bytes_per_line: " << params.bytes_per_line << "\n" 207 << "pixels_per_line: " << params.pixels_per_line << "\n" 208 << "lines: " << params.lines << "\n" 209 << "depth: " << params.depth << "\n"; 210} 211 212void print_checkpoint(const genesys::Genesys_Device& dev, 213 genesys::TestScannerInterface& iface, 214 const std::string& checkpoint_name, 215 std::stringstream& out) 216{ 217 out << "\n\n================\n" 218 << "Checkpoint: " << checkpoint_name << "\n" 219 << "================\n\n" 220 << "dev: " << genesys::format_indent_braced_list(4, dev) << "\n\n" 221 << "iface.cached_regs: " 222 << genesys::format_indent_braced_list(4, iface.cached_regs()) << "\n\n" 223 << "iface.cached_fe_regs: " 224 << genesys::format_indent_braced_list(4, iface.cached_fe_regs()) << "\n\n" 225 << "iface.last_progress_message: " << iface.last_progress_message() << "\n\n"; 226 out << "iface.slope_tables: {\n"; 227 for (const auto& kv : iface.recorded_slope_tables()) { 228 out << " " << kv.first << ": {"; 229 for (unsigned i = 0; i < kv.second.size(); ++i) { 230 if (i % 10 == 0) { 231 out << "\n "; 232 } 233 out << ' ' << kv.second[i]; 234 } 235 out << "\n }\n"; 236 } 237 out << "}\n"; 238 if (iface.recorded_key_values().empty()) { 239 out << "iface.recorded_key_values: []\n"; 240 } else { 241 out << "iface.recorded_key_values: {\n"; 242 for (const auto& kv : iface.recorded_key_values()) { 243 out << " " << kv.first << " : " << kv.second << '\n'; 244 } 245 out << "}\n"; 246 } 247 iface.recorded_key_values().clear(); 248 out << "\n"; 249} 250 251void run_single_test_scan(const TestConfig& config, std::stringstream& out) 252{ 253 auto print_checkpoint_wrapper = [&](const genesys::Genesys_Device& dev, 254 genesys::TestScannerInterface& iface, 255 const std::string& checkpoint_name) 256 { 257 print_checkpoint(dev, iface, checkpoint_name, out); 258 }; 259 260 genesys::enable_testing_mode(config.vendor_id, config.product_id, config.bcd_device, 261 print_checkpoint_wrapper); 262 263 SANE_Handle handle; 264 265 TIE(sane_init(nullptr, nullptr)); 266 TIE(sane_open(genesys::get_testing_device_name().c_str(), &handle)); 267 268 SaneOptions options; 269 options.fetch(handle); 270 271 options.set_value_button("force-calibration", true); 272 options.set_value_string(SANE_NAME_SCAN_SOURCE, 273 genesys::scan_method_to_option_string(config.method)); 274 options.set_value_string(SANE_NAME_SCAN_MODE, 275 genesys::scan_color_mode_to_option_string(config.color_mode)); 276 if (config.color_mode != genesys::ScanColorMode::LINEART) { 277 options.set_value_int(SANE_NAME_BIT_DEPTH, config.depth); 278 } 279 options.set_value_int(SANE_NAME_SCAN_RESOLUTION, config.resolution); 280 options.close(); 281 282 TIE(sane_start(handle)); 283 284 SANE_Parameters params; 285 TIE(sane_get_parameters(handle, ¶ms)); 286 287 print_params(params, out); 288 289 int buffer_size = 1024 * 1024; 290 std::vector<std::uint8_t> buffer; 291 buffer.resize(buffer_size); 292 293 std::uint64_t total_data_size = std::uint64_t(params.bytes_per_line) * params.lines; 294 std::uint64_t total_got_data = 0; 295 296 while (total_got_data < total_data_size) { 297 int ask_len = std::min<std::size_t>(buffer_size, total_data_size - total_got_data); 298 299 int got_data = 0; 300 auto status = sane_read(handle, buffer.data(), ask_len, &got_data); 301 total_got_data += got_data; 302 if (status == SANE_STATUS_EOF) { 303 break; 304 } 305 TIE(status); 306 } 307 308 sane_cancel(handle); 309 sane_close(handle); 310 sane_exit(); 311 312 genesys::disable_testing_mode(); 313} 314 315std::string read_file_to_string(const std::string& path) 316{ 317 std::ifstream in; 318 in.open(path); 319 if (!in.is_open()) { 320 return ""; 321 } 322 std::stringstream in_str; 323 in_str << in.rdbuf(); 324 return in_str.str(); 325} 326 327void write_string_to_file(const std::string& path, const std::string& contents) 328{ 329 std::ofstream out; 330 out.open(path); 331 if (!out.is_open()) { 332 throw std::runtime_error("Could not open output file: " + path); 333 } 334 out << contents; 335 out.close(); 336} 337 338struct TestResult 339{ 340 bool success = true; 341 TestConfig config; 342 std::string failure_message; 343}; 344 345TestResult perform_single_test(const TestConfig& config, const std::string& check_directory, 346 const std::string& output_directory) 347{ 348 TestResult test_result; 349 test_result.config = config; 350 351 std::stringstream result_output_stream; 352 std::string exception_output; 353 try { 354 run_single_test_scan(config, result_output_stream); 355 } catch (const std::exception& exc) { 356 exception_output = std::string("got exception: ") + typeid(exc).name() + 357 " with message\n" + exc.what() + "\n"; 358 test_result.success = false; 359 test_result.failure_message += exception_output; 360 } catch (...) { 361 exception_output = "got unknown exception\n"; 362 test_result.success = false; 363 test_result.failure_message += exception_output; 364 } 365 auto result_output = result_output_stream.str(); 366 if (!exception_output.empty()) { 367 result_output += "\n\n" + exception_output; 368 } 369 370 auto test_filename = config.name() + ".txt"; 371 auto expected_session_path = check_directory + "/" + test_filename; 372 auto current_session_path = output_directory + "/" + test_filename; 373 374 auto expected_output = read_file_to_string(expected_session_path); 375 376 bool has_output = !output_directory.empty(); 377 378 if (has_output) { 379 mkdir(output_directory.c_str(), 0777); 380 // note that check_directory and output_directory may be the same, so make sure removal 381 // happens after the expected output has already been read. 382 std::remove(current_session_path.c_str()); 383 } 384 385 if (expected_output.empty()) { 386 test_result.failure_message += "the expected data file does not exist\n"; 387 test_result.success = false; 388 } else if (expected_output != result_output) { 389 test_result.failure_message += "expected and current output are not equal\n"; 390 if (has_output) { 391 test_result.failure_message += "To examine, run:\ndiff -u \"" + current_session_path + 392 "\" \"" + expected_session_path + "\"\n"; 393 } 394 test_result.success = false; 395 } 396 397 if (has_output) { 398 write_string_to_file(current_session_path, result_output); 399 } 400 return test_result; 401} 402 403std::vector<TestConfig> get_all_test_configs() 404{ 405 genesys::genesys_init_usb_device_tables(); 406 genesys::genesys_init_sensor_tables(); 407 genesys::verify_usb_device_tables(); 408 genesys::verify_sensor_tables(); 409 410 std::vector<TestConfig> configs; 411 std::unordered_set<std::string> model_names; 412 413 for (const auto& usb_dev : *genesys::s_usb_devices) { 414 415 const auto& model = usb_dev.model(); 416 417 if (genesys::has_flag(model.flags, genesys::ModelFlag::UNTESTED)) { 418 continue; 419 } 420 if (model_names.find(model.name) != model_names.end()) { 421 continue; 422 } 423 model_names.insert(model.name); 424 425 for (auto scan_mode : { genesys::ScanColorMode::GRAY, 426 genesys::ScanColorMode::COLOR_SINGLE_PASS }) { 427 428 auto depth_values = model.bpp_gray_values; 429 if (scan_mode == genesys::ScanColorMode::COLOR_SINGLE_PASS) { 430 depth_values = model.bpp_color_values; 431 } 432 for (unsigned depth : depth_values) { 433 for (auto method_resolutions : model.resolutions) { 434 for (auto method : method_resolutions.methods) { 435 for (unsigned resolution : method_resolutions.get_resolutions()) { 436 TestConfig config; 437 config.vendor_id = usb_dev.vendor_id(); 438 config.product_id = usb_dev.product_id(); 439 config.bcd_device = usb_dev.bcd_device(); 440 config.model_name = model.name; 441 config.method = method; 442 config.depth = depth; 443 config.resolution = resolution; 444 config.color_mode = scan_mode; 445 configs.push_back(config); 446 } 447 } 448 } 449 } 450 } 451 } 452 return configs; 453} 454 455void print_help() 456{ 457 std::cerr << "Usage:\n" 458 << "session_config_test [--test={test_name}] {check_directory} [{output_directory}]\n" 459 << "session_config_test --help\n" 460 << "session_config_test --print_test_names\n"; 461} 462 463int main(int argc, const char* argv[]) 464{ 465 std::string check_directory; 466 std::string output_directory; 467 std::string test_name_filter; 468 bool print_test_names = false; 469 470 for (int argi = 1; argi < argc; ++argi) { 471 std::string arg = argv[argi]; 472 if (arg.rfind("--test=", 0) == 0) { 473 test_name_filter = arg.substr(7); 474 } else if (arg == "-h" || arg == "--help") { 475 print_help(); 476 return 0; 477 } else if (arg == "--print_test_names") { 478 print_test_names = true; 479 } else if (check_directory.empty()) { 480 check_directory = arg; 481 } else if (output_directory.empty()) { 482 output_directory = arg; 483 } 484 } 485 486 auto configs = get_all_test_configs(); 487 488 if (print_test_names) { 489 for (const auto& config : configs) { 490 std::cout << config.name() << "\n"; 491 } 492 return 0; 493 } 494 495 if (check_directory.empty()) { 496 print_help(); 497 return 1; 498 } 499 500 bool test_success = true; 501 for (unsigned i = 0; i < configs.size(); ++i) { 502 const auto& config = configs[i]; 503 504 if (!test_name_filter.empty() && config.name() != test_name_filter) { 505 continue; 506 } 507 508 auto result = perform_single_test(config, check_directory, output_directory); 509 std::cerr << "(" << i << "/" << configs.size() << "): " 510 << (result.success ? "SUCCESS: " : "FAIL: ") 511 << result.config.name() << "\n"; 512 if (!result.success) { 513 std::cerr << result.failure_message; 514 } 515 516 test_success &= result.success; 517 } 518 519 if (!test_success) { 520 return 1; 521 } 522 return 0; 523} 524