diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 10c595c63..0c4f9c23f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -85,6 +85,7 @@ qt5_add_resources(Mapper_RESOURCES_RCC ${Mapper_RESOURCES} OPTIONS -no-compress) # (To be used by Mapper executable and by system tests.) set(Mapper_Common_SRCS + cli.cpp global.cpp mapper_resource.cpp settings.cpp diff --git a/src/cli.cpp b/src/cli.cpp new file mode 100644 index 000000000..03789cf37 --- /dev/null +++ b/src/cli.cpp @@ -0,0 +1,385 @@ +/* + * Copyright 2026 Michael Behrens + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + +#include "cli.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "core/map.h" +#include "core/map_printer.h" +#include "fileformats/file_format.h" +#include "fileformats/file_format_registry.h" +#include "fileformats/file_import_export.h" + +#ifdef QT_PRINTSUPPORT_LIB +#include +#include +#include +#include "core/map_printer.h" +#endif + + +namespace OpenOrienteering { + +namespace { + + +bool exportViaFileFormat(const QString& output_path, const Map& map, const QString& format_id) +{ + const FileFormat* format = nullptr; + if (!format_id.isEmpty()) + format = FileFormats.findFormat(format_id.toLatin1().constData()); + else + format = FileFormats.findFormatForFilename(output_path, &FileFormat::supportsWriting); + + if (!format) + { + return false; + } + + auto exporter = format->makeExporter(output_path, &map, nullptr); + if (!exporter) + return false; + + return exporter->doExport(); +} + + +#ifdef QT_PRINTSUPPORT_LIB + +bool exportViaPdf(const QString& output_path, Map& map, const QRectF& print_area, int dpi = 300) +{ + if (print_area.isEmpty()) + { + fprintf(stderr, "Error: map has no extent\n"); + return false; + } + + MapPrinter printer(map, nullptr); + printer.setTarget(MapPrinter::pdfTarget()); + printer.setPrintArea(print_area); + printer.setCustomPageSize(print_area.size()); + printer.setResolution(dpi); + + auto qprinter = printer.makePrinter(); + if (!qprinter) + { + fprintf(stderr, "Error: failed to create PDF printer.\n"); + return false; + } + + qprinter->setOutputFileName(output_path); + + // setOutputFileName may reset the page size, re-apply it + qprinter->setFullPage(true); + qprinter->setPageMargins(QMarginsF(0, 0, 0, 0), QPageLayout::Millimeter); + qprinter->setPaperSize(printer.getPrintAreaPaperSize(), QPrinter::Millimeter); + + QPainter painter(qprinter.get()); + printer.drawPage(&painter, printer.getPrintArea()); + painter.end(); + return true; +} + + +bool exportViaImage(const QString& output_path, Map& map, const QRectF& print_area, int dpi = 300, const char* image_format = nullptr) +{ + if (print_area.isEmpty()) + { + fprintf(stderr, "Error: map has no extent\n"); + return false; + } + + MapPrinter printer(map, nullptr); + printer.setTarget(MapPrinter::imageTarget()); + printer.setPrintArea(print_area); + printer.setResolution(dpi); + + auto const& options = printer.getOptions(); + qreal pixel_per_mm = options.resolution / 25.4; + QSizeF mm_size = printer.getPrintAreaPaperSize(); + int w = qMax(1, qRound(mm_size.width() * pixel_per_mm)); + int h = qMax(1, qRound(mm_size.height() * pixel_per_mm)); + + QImage image(w, h, QImage::Format_ARGB32_Premultiplied); + if (image.isNull()) + { + fprintf(stderr, "Error: not enough memory for image of size %dx%d\n", w, h); + return false; + } + image.fill(Qt::white); + image.setDotsPerMeterX(qRound(pixel_per_mm * 1000)); + image.setDotsPerMeterY(qRound(pixel_per_mm * 1000)); + + QPainter p(&image); + printer.drawPage(&p, printer.getPrintArea(), &image); + p.end(); + + if (!image.save(output_path, image_format)) + { + fprintf(stderr, "Error: failed to save image to '%s'\n", qPrintable(output_path)); + return false; + } + return true; +} + +#endif + + +int runExport(const QStringList& sub_args) +{ + QCommandLineParser parser; + parser.setApplicationDescription(QStringLiteral("Export map to printable and GIS formats")); + + QCommandLineOption input_option( + QStringList{QStringLiteral("i"), QStringLiteral("input")}, + QStringLiteral("Input map file."), + QStringLiteral("path")); + parser.addOption(input_option); + + QCommandLineOption output_option( + QStringList{QStringLiteral("o"), QStringLiteral("output")}, + QStringLiteral("Output file path."), + QStringLiteral("path")); + parser.addOption(output_option); + + QCommandLineOption format_option( + QStringLiteral("output-format"), + QStringLiteral("Output format ID (e.g. pdf, png, jpg)."), + QStringLiteral("id")); + parser.addOption(format_option); + + QCommandLineOption full_map_option(QStringLiteral("full-map"), QStringLiteral("Export the full map extent instead of the saved print area.")); + parser.addOption(full_map_option); + + QCommandLineOption dpi_option( + QStringLiteral("dpi"), + QStringLiteral("Output resolution in DPI (default: 300)."), + QStringLiteral("dpi"), + QStringLiteral("300")); + parser.addOption(dpi_option); + + parser.addHelpOption(); + + // QCommandLineParser requires the program name as first argument + QStringList cli_args = {QStringLiteral("mapper")}; + cli_args.append(sub_args); + + if (!parser.parse(cli_args)) + { + fprintf(stderr, "Error: %s\n", qPrintable(parser.errorText())); + return 1; + } + + if (parser.isSet(QStringLiteral("help"))) + { + fprintf(stderr, "%s", qPrintable(parser.helpText())); + return 0; + } + + if (!parser.isSet(input_option)) + { + fprintf(stderr, "Error: --input / -i is required.\n"); + return 1; + } + QString input_path = parser.value(input_option); + + if (!parser.isSet(output_option)) + { + fprintf(stderr, "Error: --output / -o is required.\n"); + return 1; + } + QString output_path = parser.value(output_option); + + QString format_id = parser.value(format_option); + + int dpi = parser.value(dpi_option).toInt(); + + Map map; + if (!map.loadFrom(input_path)) + { + fprintf(stderr, "Error: failed to load map from '%s'\n", qPrintable(input_path)); + return 1; + } + + QRectF print_area = parser.isSet(full_map_option) + ? map.calculateExtent() + : map.printerConfig().print_area; + if (print_area.isEmpty()) + { + fprintf(stderr, "Error: map has no content in the selected print area.\n"); + return 1; + } + + // Determine the format key: use format_id if set, otherwise the file extension + QString format_key = format_id.isEmpty() ? QFileInfo(output_path).suffix().toLower() : format_id.toLower(); + + // Try MapPrinter for PDF and image formats (always, regardless of format_id) +#ifdef QT_PRINTSUPPORT_LIB + if (format_key == QStringLiteral("pdf")) + { + if (exportViaPdf(output_path, map, print_area, dpi)) + return 0; + fprintf(stderr, "Error: PDF export failed.\n"); + return 1; + } + + if (QImageWriter::supportedImageFormats().contains(format_key.toLatin1())) + { + // Pass explicit format when format_id is set, so QImage::save + // uses the requested format regardless of file extension. + QByteArray format_key_latin; + const char* img_fmt = nullptr; + if (!format_id.isEmpty()) + { + format_key_latin = format_key.toLatin1(); + img_fmt = format_key_latin.constData(); + } + if (exportViaImage(output_path, map, print_area, dpi, img_fmt)) + return 0; + fprintf(stderr, "Error: image export failed.\n"); + return 1; + } +#endif + + fprintf(stderr, "Error: no exporter found for '%s'.\n", qPrintable(output_path)); + return 1; +} + +int runConvert(const QStringList& sub_args) +{ + QCommandLineParser parser; + parser.setApplicationDescription(QStringLiteral("Convert between orienteering map formats")); + + QCommandLineOption input_option( + QStringList{QStringLiteral("i"), QStringLiteral("input")}, + QStringLiteral("Input map file."), + QStringLiteral("path")); + parser.addOption(input_option); + + QCommandLineOption output_option( + QStringList{QStringLiteral("o"), QStringLiteral("output")}, + QStringLiteral("Output file path."), + QStringLiteral("path")); + parser.addOption(output_option); + + QCommandLineOption format_option( + QStringLiteral("output-format"), + QStringLiteral("Output format ID (e.g. XML, OCD, OCD12)."), + QStringLiteral("id"), + QString{}); + parser.addOption(format_option); + + parser.addHelpOption(); + + QStringList cli_args = {QStringLiteral("mapper")}; + cli_args.append(sub_args); + + if (!parser.parse(cli_args)) + { + fprintf(stderr, "Error: %s\n", qPrintable(parser.errorText())); + return 1; + } + + if (parser.isSet(QStringLiteral("help"))) + { + fprintf(stderr, "%s", qPrintable(parser.helpText())); + return 0; + } + + if (!parser.isSet(input_option)) + { + fprintf(stderr, "Error: --input / -i is required.\n"); + return 1; + } + QString input_path = parser.value(input_option); + + if (!parser.isSet(output_option)) + { + fprintf(stderr, "Error: --output / -o is required.\n"); + return 1; + } + QString output_path = parser.value(output_option); + + QString format_id = parser.value(format_option); + + Map map; + if (!map.loadFrom(input_path)) + { + fprintf(stderr, "Error: failed to load map from '%s'\n", qPrintable(input_path)); + return 1; + } + + if (exportViaFileFormat(output_path, map, format_id)) + return 0; + + fprintf(stderr, "Error: no exporter found for '%s'.\n", qPrintable(output_path)); + return 1; +} + + +} // namespace + + +int execCli(int argc, char** argv) +{ + if (argc < 3) + { + fprintf(stderr, "Error: no subcommand specified.\n"); + return 1; + } + + QString subcommand = QString::fromLocal8Bit(argv[2]); + + QStringList sub_args; + for (int i = 3; i < argc; ++i) + sub_args << QString::fromLocal8Bit(argv[i]); + + if (subcommand == QLatin1String("export")) + return runExport(sub_args); + + if (subcommand == QLatin1String("convert")) + return runConvert(sub_args); + + if (subcommand == QLatin1String("help") || subcommand == QLatin1String("--help")) + { + fprintf(stderr, + "Usage: %s --cli [options]\n\n" + "Available subcommands:\n" + " export Export map to printable and image formats\n" + " convert Convert between orienteering map formats\n" + " help Show this help\n", + argv[0]); + return 0; + } + + fprintf(stderr, "Error: unknown subcommand '%s'. Available: export, convert\n", qPrintable(subcommand)); + return 1; +} + + +} // namespace OpenOrienteering diff --git a/src/cli.h b/src/cli.h new file mode 100644 index 000000000..f939a4d87 --- /dev/null +++ b/src/cli.h @@ -0,0 +1,29 @@ +/* + * Copyright 2026 Michael Behrens + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + +#ifndef OPENORIENTEERING_CLI_H +#define OPENORIENTEERING_CLI_H + +namespace OpenOrienteering { + +int execCli(int argc, char** argv); + +} // namespace OpenOrienteering + +#endif diff --git a/src/main.cpp b/src/main.cpp index 19221c5d7..03266979d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,6 +20,7 @@ #include +#include #include #include // IWYU pragma: no_include @@ -51,6 +52,7 @@ #include "global.h" #include "mapper_config.h" #include "mapper_resource.h" +#include "cli.h" #include "gui/home_screen_controller.h" #include "gui/main_window.h" #include "gui/widgets/mapper_proxystyle.h" @@ -125,6 +127,32 @@ void resetActivationWindow(QtSingleApplication& app) int main(int argc, char** argv) { + // Detect cli arg early, before creating any QApplication, + // so that cli works even when oom is already running. + int cli_pos = -1; + for (int i = 1; i < argc; ++i) + { + if (std::strcmp(argv[i], "--cli") == 0) + { + cli_pos = i; + break; + } + } + + if (cli_pos > 0) + { + doStaticInitializations(); + // On Linux, the cli may run without a display server (CI, headless). + // Default to offscreen rendering when no platform is explicitly set. + // A QGuiApplication is needed for pdf/image export. +#if defined(Q_OS_LINUX) + if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) + qputenv("QT_QPA_PLATFORM", "offscreen"); +#endif + QGuiApplication cli_app(argc, argv); + return OpenOrienteering::execCli(argc, argv); + } + #ifdef MAPPER_USE_QTSINGLEAPPLICATION // Create single-instance application. // Use "oo-mapper" instead of the executable as identifier, in case we launch from different paths. diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b1471a2f1..6b33c60af 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -190,6 +190,9 @@ add_unit_test(util_t ../src/util/util add_system_test(coord_xml_t MANUAL) # System tests +if(NOT ANDROID) + add_system_test(cli_t) +endif() add_system_test(file_format_t) add_system_test(duplicate_equals_t) add_system_test(map_t) diff --git a/test/cli_t.cpp b/test/cli_t.cpp new file mode 100644 index 000000000..803ec2041 --- /dev/null +++ b/test/cli_t.cpp @@ -0,0 +1,396 @@ +/* + * Copyright 2026 Michael Behrens + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + + +#include "cli_t.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cli.h" +#include "global.h" +#include "test_config.h" + +using namespace OpenOrienteering; + + +namespace { + +/** + * Builds a mutable argv array from string literals and calls execCli. + * + * The caller must ensure the arguments remain valid for the duration of + * the call (they do when using string literals). + */ +int execCliFrom(std::initializer_list args) +{ + std::vector argv; + argv.reserve(args.size()); + for (auto a : args) + argv.push_back(const_cast(a)); + return execCli(int(argv.size()), argv.data()); +} + +} // namespace + + +void CliTest::initTestCase() +{ + doStaticInitializations(); + + // Register a Qt search path for test data + QDir::addSearchPath(QStringLiteral("testdata"), + QDir(QString::fromUtf8(MAPPER_TEST_SOURCE_DIR)) + .absoluteFilePath(QStringLiteral("data"))); +} + + +void CliTest::testNoSubcommand() +{ + QCOMPARE(execCliFrom({"mapper", "--cli"}), 1); +} + + +void CliTest::testNonexistingSubcommand() +{ + QCOMPARE(execCliFrom({"mapper", "--cli", "nonexistingsubcommand"}), 1); +} + + +void CliTest::testExportMissingInput() +{ + QCOMPARE(execCliFrom({"mapper", "--cli", "export", "-o", "out.pdf"}), 1); +} + + +void CliTest::testExportMissingOutput() +{ + QCOMPARE(execCliFrom({"mapper", "--cli", "export", "-i", "in.omap"}), 1); +} + + +void CliTest::testExportInvalidInput() +{ + QCOMPARE(execCliFrom({"mapper", "--cli", "export", "-i", "nonexistent.omap", "-o", "out.pdf"}), 1); +} + + +void CliTest::testExportPdf() +{ + QTemporaryDir temp_dir; + QVERIFY(temp_dir.isValid()); + + auto input = QString::fromUtf8("testdata:/barrier.omap"); + QVERIFY(QFileInfo::exists(input)); + + QString output = temp_dir.path() + QStringLiteral("/output.pdf"); + auto input_bytes = input.toLocal8Bit(); + auto output_bytes = output.toLocal8Bit(); + + int result = execCliFrom({ + "mapper", "--cli", "export", + "-i", input_bytes.constData(), + "-o", output_bytes.constData(), + "--full-map" + }); + QCOMPARE(result, 0); + QVERIFY(QFile::exists(output)); + QVERIFY(QFileInfo(output).size() > 0); +} + + +void CliTest::testExportPng() +{ + QTemporaryDir temp_dir; + QVERIFY(temp_dir.isValid()); + + auto input = QString::fromUtf8("testdata:/barrier.omap"); + QVERIFY(QFileInfo::exists(input)); + + QString output = temp_dir.path() + QStringLiteral("/output.png"); + auto input_bytes = input.toLocal8Bit(); + auto output_bytes = output.toLocal8Bit(); + + int result = execCliFrom({ + "mapper", "--cli", "export", + "-i", input_bytes.constData(), + "-o", output_bytes.constData(), + "--full-map" + }); + QCOMPARE(result, 0); + QVERIFY(QFile::exists(output)); + QVERIFY(QFileInfo(output).size() > 0); +} + + +void CliTest::testExportFullMapFlag() +{ + QTemporaryDir temp_dir; + QVERIFY(temp_dir.isValid()); + + auto input = QString::fromUtf8("testdata:/barrier.omap"); + QString output = temp_dir.path() + QStringLiteral("/output.pdf"); + auto input_bytes = input.toLocal8Bit(); + auto output_bytes = output.toLocal8Bit(); + + int result = execCliFrom({ + "mapper", "--cli", "export", + "-i", input_bytes.constData(), + "-o", output_bytes.constData(), + "--full-map" + }); + QCOMPARE(result, 0); + QVERIFY(QFile::exists(output)); + QVERIFY(QFileInfo(output).size() > 0); +} + + +void CliTest::testConvertMissingInput() +{ + QCOMPARE(execCliFrom({"mapper", "--cli", "convert", "-o", "out.omap"}), 1); +} + + +void CliTest::testConvertMissingOutput() +{ + QCOMPARE(execCliFrom({"mapper", "--cli", "convert", "-i", "in.omap"}), 1); +} + + +void CliTest::testConvertInvalidInput() +{ + QCOMPARE(execCliFrom({"mapper", "--cli", "convert", "-i", "nonexistent.omap", "-o", "out.omap"}), 1); +} + + +void CliTest::testConvertOmapToXmap() +{ + QTemporaryDir temp_dir; + QVERIFY(temp_dir.isValid()); + + auto input = QString::fromUtf8("testdata:/barrier.omap"); + QVERIFY(QFileInfo::exists(input)); + + QString output = temp_dir.path() + QStringLiteral("/output.xmap"); + auto input_bytes = input.toLocal8Bit(); + auto output_bytes = output.toLocal8Bit(); + + int result = execCliFrom({ + "mapper", "--cli", "convert", + "-i", input_bytes.constData(), + "-o", output_bytes.constData() + }); + QCOMPARE(result, 0); + QVERIFY(QFile::exists(output)); + QVERIFY(QFileInfo(output).size() > 0); +} + + +void CliTest::testConvertXmapToOmap() +{ + QTemporaryDir temp_dir; + QVERIFY(temp_dir.isValid()); + + auto input = QString::fromUtf8("testdata:/barrier.omap"); + QVERIFY(QFileInfo::exists(input)); + + QString xmap = temp_dir.path() + QStringLiteral("/intermediate.xmap"); + QString omap = temp_dir.path() + QStringLiteral("/output.omap"); + auto input_bytes = input.toLocal8Bit(); + auto xmap_bytes = xmap.toLocal8Bit(); + auto omap_bytes = omap.toLocal8Bit(); + + int r1 = execCliFrom({ + "mapper", "--cli", "convert", + "-i", input_bytes.constData(), + "-o", xmap_bytes.constData() + }); + QCOMPARE(r1, 0); + QVERIFY(QFile::exists(xmap)); + + int r2 = execCliFrom({ + "mapper", "--cli", "convert", + "-i", xmap_bytes.constData(), + "-o", omap_bytes.constData() + }); + QCOMPARE(r2, 0); + QVERIFY(QFile::exists(omap)); + QVERIFY(QFileInfo(omap).size() > 0); +} + + +void CliTest::testConvertOmapToOcd() +{ + QTemporaryDir temp_dir; + QVERIFY(temp_dir.isValid()); + + auto input = QString::fromUtf8("testdata:/barrier.omap"); + QVERIFY(QFileInfo::exists(input)); + + QString output = temp_dir.path() + QStringLiteral("/output.ocd"); + auto input_bytes = input.toLocal8Bit(); + auto output_bytes = output.toLocal8Bit(); + + int result = execCliFrom({ + "mapper", "--cli", "convert", + "-i", input_bytes.constData(), + "-o", output_bytes.constData() + }); + QCOMPARE(result, 0); + QVERIFY(QFile::exists(output)); + QVERIFY(QFileInfo(output).size() > 0); +} + + +void CliTest::testConvertWithFormatId() +{ + QTemporaryDir temp_dir; + QVERIFY(temp_dir.isValid()); + + auto input = QString::fromUtf8("testdata:/barrier.omap"); + QVERIFY(QFileInfo::exists(input)); + + QString output = temp_dir.path() + QStringLiteral("/output.xmap"); + auto input_bytes = input.toLocal8Bit(); + auto output_bytes = output.toLocal8Bit(); + + int result = execCliFrom({ + "mapper", "--cli", "convert", + "-i", input_bytes.constData(), + "-o", output_bytes.constData(), + "--output-format", "XML" + }); + QCOMPARE(result, 0); + QVERIFY(QFile::exists(output)); + QVERIFY(QFileInfo(output).size() > 0); +} + + +void CliTest::testExportFormatIdOverridesExtension() +{ + QTemporaryDir temp_dir; + QVERIFY(temp_dir.isValid()); + + auto input = QString::fromUtf8("testdata:/barrier.omap"); + QVERIFY(QFileInfo::exists(input)); + + // Write a PNG to a file named .pdf + QString output = temp_dir.path() + QStringLiteral("/output.pdf"); + auto input_bytes = input.toLocal8Bit(); + auto output_bytes = output.toLocal8Bit(); + + int result = execCliFrom({ + "mapper", "--cli", "export", + "-i", input_bytes.constData(), + "-o", output_bytes.constData(), + "--output-format", "png", + "--full-map" + }); + QCOMPARE(result, 0); + QVERIFY(QFile::exists(output)); + QVERIFY(QFileInfo(output).size() > 0); +} + + +void CliTest::testConvertFormatIdOverridesExtension() +{ + QTemporaryDir temp_dir; + QVERIFY(temp_dir.isValid()); + + auto input = QString::fromUtf8("testdata:/barrier.omap"); + QVERIFY(QFileInfo::exists(input)); + + // Write XML format to a non-native file extension + QString output = temp_dir.path() + QStringLiteral("/output.foo"); + auto input_bytes = input.toLocal8Bit(); + auto output_bytes = output.toLocal8Bit(); + + int result = execCliFrom({ + "mapper", "--cli", "convert", + "-i", input_bytes.constData(), + "-o", output_bytes.constData(), + "--output-format", "XML" + }); + QCOMPARE(result, 0); + QVERIFY(QFile::exists(output)); + QVERIFY(QFileInfo(output).size() > 0); +} + + +void CliTest::testExportRejectsNativeFormatId() +{ + QTemporaryDir temp_dir; + QVERIFY(temp_dir.isValid()); + + auto input = QString::fromUtf8("testdata:/barrier.omap"); + QVERIFY(QFileInfo::exists(input)); + + QString output = temp_dir.path() + QStringLiteral("/output.foo"); + auto input_bytes = input.toLocal8Bit(); + auto output_bytes = output.toLocal8Bit(); + + int result = execCliFrom({ + "mapper", "--cli", "export", + "-i", input_bytes.constData(), + "-o", output_bytes.constData(), + "--output-format", "XML" + }); + QCOMPARE(result, 1); +} + + +void CliTest::testConvertRejectsUnknownFormatId() +{ + QTemporaryDir temp_dir; + QVERIFY(temp_dir.isValid()); + + auto input = QString::fromUtf8("testdata:/barrier.omap"); + QVERIFY(QFileInfo::exists(input)); + + QString output = temp_dir.path() + QStringLiteral("/output.omap"); + auto input_bytes = input.toLocal8Bit(); + auto output_bytes = output.toLocal8Bit(); + + int result = execCliFrom({ + "mapper", "--cli", "convert", + "-i", input_bytes.constData(), + "-o", output_bytes.constData(), + "--output-format", "nonexistingformat" + }); + QCOMPARE(result, 1); +} +int main(int argc, char** argv) +{ +#ifdef Q_OS_LINUX + // Offscreen avoids failing when no display server is available + qputenv("QT_QPA_PLATFORM", "offscreen"); +#endif + QApplication app(argc, argv); + CliTest tc; + return QTest::qExec(&tc, argc, argv); +} diff --git a/test/cli_t.h b/test/cli_t.h new file mode 100644 index 000000000..e191d7997 --- /dev/null +++ b/test/cli_t.h @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Michael Behrens + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + +#ifndef OPENORIENTEERING_CLI_T_H +#define OPENORIENTEERING_CLI_T_H + +#include + + +class CliTest : public QObject +{ +Q_OBJECT + +private slots: + void initTestCase(); + + void testNoSubcommand(); + void testNonexistingSubcommand(); + void testExportMissingInput(); + void testExportMissingOutput(); + void testExportInvalidInput(); + void testExportPdf(); + void testExportPng(); + void testExportFullMapFlag(); + + void testConvertMissingInput(); + void testConvertMissingOutput(); + void testConvertInvalidInput(); + void testConvertOmapToXmap(); + void testConvertXmapToOmap(); + void testConvertOmapToOcd(); + void testConvertWithFormatId(); + void testExportFormatIdOverridesExtension(); + void testConvertFormatIdOverridesExtension(); + void testExportRejectsNativeFormatId(); + void testConvertRejectsUnknownFormatId(); +}; + +#endif