summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2025-03-08 00:12:29 +0100
committerCarl Hetherington <cth@carlh.net>2025-03-08 00:12:29 +0100
commit182a0b48fb456355a139c21533a7d6ca0bbe42eb (patch)
tree19722a22f8667a1fe029af2ec34efbbddea3c851
parentd4bb9572785a19c394cff3242e2b9ebab7c5d31c (diff)
parent42b92cbe4518a170b217ab54a26b51f56246a50f (diff)
Merge branch '2981-headless-grok'
This makes it possible to configure Grok for encoding from the command line.
-rw-r--r--src/lib/config.cc6
-rw-r--r--src/lib/config.h4
-rw-r--r--src/lib/encode_cli.cc105
-rw-r--r--src/lib/grok/context.h4
-rw-r--r--src/lib/grok/util.cc53
-rw-r--r--src/lib/grok/util.h27
-rw-r--r--src/lib/grok_j2k_encoder_thread.cc2
-rw-r--r--src/lib/j2k_encoder.cc10
-rw-r--r--src/lib/util.cc4
-rw-r--r--src/lib/wscript2
-rw-r--r--src/wx/grok/gpu_config_panel.h51
-rw-r--r--test/config_test.cc9
m---------test/data0
-rw-r--r--test/encode_cli_test.cc163
-rwxr-xr-xtest/gpu_lister6
-rw-r--r--test/grok_util_test.cc43
-rw-r--r--test/wscript2
17 files changed, 427 insertions, 64 deletions
diff --git a/src/lib/config.cc b/src/lib/config.cc
index 3227ea433..49c64e5b6 100644
--- a/src/lib/config.cc
+++ b/src/lib/config.cc
@@ -238,7 +238,7 @@ Config::set_defaults()
set_cover_sheet_to_default();
#ifdef DCPOMATIC_GROK
- _grok = boost::none;
+ _grok = {};
#endif
_main_divider_sash_position = {};
@@ -1151,9 +1151,7 @@ Config::write_config() const
cxml::add_text_child(root, "LayoutForShortScreen", _layout_for_short_screen ? "1" : "0");
#ifdef DCPOMATIC_GROK
- if (_grok) {
- _grok->as_xml(cxml::add_child(root, "Grok"));
- }
+ _grok.as_xml(cxml::add_child(root, "Grok"));
#endif
_export.write(cxml::add_child(root, "Export"));
diff --git a/src/lib/config.h b/src/lib/config.h
index d9a95ebfd..b2a979ffa 100644
--- a/src/lib/config.h
+++ b/src/lib/config.h
@@ -666,7 +666,7 @@ public:
std::string licence;
};
- boost::optional<Grok> grok() const {
+ Grok grok() const {
return _grok;
}
#endif
@@ -1495,7 +1495,7 @@ private:
bool _layout_for_short_screen;
#ifdef DCPOMATIC_GROK
- boost::optional<Grok> _grok;
+ Grok _grok;
#endif
ExportConfig _export;
diff --git a/src/lib/encode_cli.cc b/src/lib/encode_cli.cc
index a682fa4e3..fa304b6e0 100644
--- a/src/lib/encode_cli.cc
+++ b/src/lib/encode_cli.cc
@@ -30,6 +30,7 @@
#include "filter.h"
#ifdef DCPOMATIC_GROK
#include "grok/context.h"
+#include "grok/util.h"
#endif
#include "hints.h"
#include "job_manager.h"
@@ -43,6 +44,7 @@
#include "version.h"
#include "video_content.h"
#include <dcp/filesystem.h>
+#include <dcp/raw_convert.h>
#include <dcp/version.h>
#include <fmt/format.h>
#include <getopt.h>
@@ -66,7 +68,19 @@ using boost::optional;
static void
help(function <void (string)> out)
{
- out(fmt::format("Syntax: {} [OPTION] [<FILM>]\n", program_name));
+ out(fmt::format("Syntax: {} [OPTION] [COMMAND] [<PARAMETER>]\n", program_name));
+
+ out("\nCommands:\n\n");
+ out(" make-dcp <FILM> make DCP from the given film; default if no other command is specified\n");
+ out(variant::insert_dcpomatic(" list-servers display a list of encoding servers that %1 can use (until Ctrl-C)\n"));
+ out(" dump <FILM> show a summary of the film's settings\n");
+#ifdef DCPOMATIC_GROK
+ out(" config-params list the parameters that can be set with `config`\n");
+ out(" config <PARAMETER> <VALUE> set a DCP-o-matic configuration value\n");
+ out(" list-gpus list available GPUs\n");
+#endif
+
+ out("\nOptions:\n\n");
out(variant::insert_dcpomatic(" -v, --version show %1 version\n"));
out(" -h, --help show this help\n");
out(" -f, --flags show flags passed to C++ compiler on build\n");
@@ -77,15 +91,19 @@ help(function <void (string)> out)
out(" -k, --keep-going keep running even when the job is complete\n");
out(" -s, --servers <file> specify servers to use in a text file\n");
out(variant::insert_dcpomatic(" -l, --list-servers just display a list of encoding servers that %1 is configured to use; don't encode\n"));
+ out(" (deprecated - use the list-servers command instead)\n");
out(" -d, --dcp-path echo DCP's path to stdout on successful completion (implies -n)\n");
out(" -c, --config <dir> directory containing config.xml and cinemas.xml\n");
out(" --dump just dump a summary of the film's settings; don't encode\n");
+ out(" (deprecated - use the dump command instead)\n");
out(" --no-check don't check project's content files for changes before making the DCP\n");
out(" --export-format <format> export project to a file, rather than making a DCP: specify mov or mp4\n");
out(" --export-filename <filename> filename to export to with --export-format\n");
out(" --hints analyze film for hints before encoding and abort if any are found\n");
+ out("\ne.g.\n");
+ out(fmt::format("\n {} -t 4 make-dcp my_great_movie\n", program_name));
+ out(fmt::format("\n {} config grok-licence 12345ABCD\n", program_name));
out("\n");
- out("<FILM> is the film directory.\n");
}
@@ -270,6 +288,10 @@ encode_cli(int argc, char* argv[], function<void (string)> out, function<void ()
optional<string> export_format;
optional<boost::filesystem::path> export_filename;
bool hints = false;
+ string command = "make-dcp";
+
+ /* This makes it possible to call getopt several times in the same executable, for tests */
+ optind = 0;
int option_index = 0;
while (true) {
@@ -357,6 +379,77 @@ encode_cli(int argc, char* argv[], function<void (string)> out, function<void ()
}
}
+ vector<string> commands = {
+ "make-dcp",
+ "list-servers",
+#ifdef DCPOMATIC_GROK
+ "dump",
+ "config-params",
+ "config",
+ "list-gpus"
+#else
+ "dump"
+#endif
+ };
+
+ if (optind < argc - 1) {
+ /* Command with a film specified afterwards */
+ command = argv[optind++];
+ } else if (optind < argc) {
+ /* Look for a valid command, hoping that it's not the name of a film */
+ if (std::find(commands.begin(), commands.end(), argv[optind]) != commands.end()) {
+ command = argv[optind++];
+ }
+ }
+
+
+#ifdef DCPOMATIC_GROK
+ if (command == "config-params") {
+ out("Configurable parameters:\n\n");
+ out(" grok-licence licence string for using the Grok JPEG2000 encoder\n");
+ out(" grok-enable 1 to enable the Grok encoder, 0 to disable it\n");
+ out(" grok-binary-location directory containing Grok binaries\n");
+ out(" grok-gpu-index index of GPU to use (from 0, see list-gpus)\n");
+ return {};
+ }
+
+ if (command == "config") {
+ if (optind < argc - 1) {
+ string const parameter = argv[optind++];
+ string const value = argv[optind++];
+ auto grok = Config::instance()->grok();
+ if (parameter == "grok-licence") {
+ grok.licence = value;
+ } else if (parameter == "grok-enable") {
+ if (value == "1") {
+ grok.enable = true;
+ } else if (value == "0") {
+ grok.enable = false;
+ } else {
+ return fmt::format("Invalid value {} for grok-enable (use 1 to enable, 0 to disable)", value);
+ }
+ } else if (parameter == "grok-binary-location") {
+ grok.binary_location = value;
+ } else if (parameter == "grok-gpu-index") {
+ grok.selected = dcp::raw_convert<int>(value);
+ } else {
+ return fmt::format("Unrecognised configuration parameter `{}'", parameter);
+ }
+ Config::instance()->set_grok(grok);
+ Config::instance()->write();
+ } else {
+ return fmt::format("Missing configuration parameter: use {} config <parameter> <value>", program_name);
+ }
+ return {};
+ } else if (command == "list-gpus") {
+ int N = 0;
+ for (auto gpu: get_gpu_names()) {
+ out(fmt::format("{}: {}\n", N++, gpu));
+ }
+ return {};
+ }
+#endif
+
if (config) {
State::override_path = *config;
}
@@ -376,7 +469,7 @@ encode_cli(int argc, char* argv[], function<void (string)> out, function<void ()
Config::instance()->set_servers(servers);
}
- if (list_servers_) {
+ if (command == "list-servers" || list_servers_) {
list_servers(out);
return {};
}
@@ -420,7 +513,7 @@ encode_cli(int argc, char* argv[], function<void (string)> out, function<void ()
return fmt::format("{}: error reading film `{}' ({})\n", program_name, film_dir.string(), e.what());
}
- if (dump) {
+ if (command == "dump" || dump) {
print_dump(out, film);
return {};
}
@@ -489,9 +582,9 @@ encode_cli(int argc, char* argv[], function<void (string)> out, function<void ()
if (progress) {
if (export_format) {
- out(fmt::format("\nExporting {}\n", film->name()));
+ out(fmt::format("Exporting {}\n", film->name()));
} else {
- out(fmt::format("\nMaking DCP for {}\n", film->name()));
+ out(fmt::format("Making DCP for {}\n", film->name()));
}
}
diff --git a/src/lib/grok/context.h b/src/lib/grok/context.h
index 602c8b13f..b31867cf6 100644
--- a/src/lib/grok/context.h
+++ b/src/lib/grok/context.h
@@ -99,7 +99,7 @@ public:
explicit GrokContext(DcpomaticContext* dcpomatic_context)
: _dcpomatic_context(dcpomatic_context)
{
- auto grok = Config::instance()->grok().get_value_or({});
+ auto grok = Config::instance()->grok();
if (!grok.enable) {
return;
}
@@ -216,7 +216,7 @@ public:
auto s = dcpv.get_size();
_dcpomatic_context->set_dimensions(s.width, s.height);
- auto grok = Config::instance()->grok().get_value_or({});
+ auto grok = Config::instance()->grok();
if (!_messenger->launch_grok(
_dcpomatic_context->location,
_dcpomatic_context->width,
diff --git a/src/lib/grok/util.cc b/src/lib/grok/util.cc
new file mode 100644
index 000000000..3cbc55678
--- /dev/null
+++ b/src/lib/grok/util.cc
@@ -0,0 +1,53 @@
+/*
+ Copyright (C) 2023 Grok Image Compression Inc.
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic 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 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "util.h"
+#include "../config.h"
+#include <boost/process.hpp>
+#include <future>
+
+
+using std::string;
+using std::vector;
+
+
+vector<string>
+get_gpu_names()
+{
+ namespace bp = boost::process;
+
+ auto binary = Config::instance()->grok().binary_location / "gpu_lister";
+
+ bp::ipstream stream;
+ bp::child child(binary, bp::std_out > stream);
+
+ string line;
+ vector<string> gpu_names;
+ while (child.running() && std::getline(stream, line) && !line.empty()) {
+ gpu_names.push_back(line);
+ }
+
+ child.wait();
+
+ return gpu_names;
+}
+
+
diff --git a/src/lib/grok/util.h b/src/lib/grok/util.h
new file mode 100644
index 000000000..a78ecabca
--- /dev/null
+++ b/src/lib/grok/util.h
@@ -0,0 +1,27 @@
+/*
+ Copyright (C) 2023 Grok Image Compression Inc.
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic 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 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include <boost/filesystem.hpp>
+#include <string>
+#include <vector>
+
+
+extern std::vector<std::string> get_gpu_names();
diff --git a/src/lib/grok_j2k_encoder_thread.cc b/src/lib/grok_j2k_encoder_thread.cc
index e6c256f11..d6825113c 100644
--- a/src/lib/grok_j2k_encoder_thread.cc
+++ b/src/lib/grok_j2k_encoder_thread.cc
@@ -62,7 +62,7 @@ try
LOG_TIMING("encoder-pop thread=%1 frame=%2 eyes=%3", thread_id(), frame.index(), static_cast<int>(frame.eyes()));
- auto grok = Config::instance()->grok().get_value_or({});
+ auto grok = Config::instance()->grok();
if (_context->launch(frame, grok.selected) && _context->scheduleCompress(frame)) {
frame_guard.cancel();
diff --git a/src/lib/j2k_encoder.cc b/src/lib/j2k_encoder.cc
index 309fce0b3..50452fbad 100644
--- a/src/lib/j2k_encoder.cc
+++ b/src/lib/j2k_encoder.cc
@@ -98,7 +98,7 @@ J2KEncoder::J2KEncoder(shared_ptr<const Film> film, Writer& writer)
#endif
{
#ifdef DCPOMATIC_GROK
- auto grok = Config::instance()->grok().get_value_or({});
+ auto grok = Config::instance()->grok();
_dcpomatic_context = new grk_plugin::DcpomaticContext(film, writer, _history, grok.binary_location);
if (grok.enable) {
_context = new grk_plugin::GrokContext(_dcpomatic_context);
@@ -135,7 +135,7 @@ J2KEncoder::servers_list_changed()
{
auto config = Config::instance();
#ifdef DCPOMATIC_GROK
- auto const grok_enable = config->grok().get_value_or({}).enable;
+ auto const grok_enable = config->grok().enable;
#else
auto const grok_enable = false;
#endif
@@ -162,7 +162,7 @@ void
J2KEncoder::pause()
{
#ifdef DCPOMATIC_GROK
- if (!Config::instance()->grok().get_value_or({}).enable) {
+ if (!Config::instance()->grok().enable) {
return;
}
return;
@@ -183,7 +183,7 @@ J2KEncoder::pause()
void J2KEncoder::resume()
{
#ifdef DCPOMATIC_GROK
- if (!Config::instance()->grok().get_value_or({}).enable) {
+ if (!Config::instance()->grok().enable) {
return;
}
@@ -226,7 +226,7 @@ J2KEncoder::end()
*/
for (auto & i: _queue) {
#ifdef DCPOMATIC_GROK
- if (Config::instance()->grok().get_value_or({}).enable) {
+ if (Config::instance()->grok().enable) {
if (!_context->scheduleCompress(i)){
LOG_GENERAL (N_("[%1] J2KEncoder thread pushes frame %2 back onto queue after failure"), thread_id(), i.index());
// handle error
diff --git a/src/lib/util.cc b/src/lib/util.cc
index df15e1abb..1e2f7d61f 100644
--- a/src/lib/util.cc
+++ b/src/lib/util.cc
@@ -1146,7 +1146,7 @@ setup_grok_library_path()
}
}
auto const grok = Config::instance()->grok();
- if (!grok || grok->binary_location.empty()) {
+ if (grok.binary_location.empty()) {
setenv("LD_LIRARY_PATH", old_path.c_str(), 1);
return;
}
@@ -1155,7 +1155,7 @@ setup_grok_library_path()
if (!new_path.empty()) {
new_path += ":";
}
- new_path += grok->binary_location.string();
+ new_path += grok.binary_location.string();
setenv("LD_LIBRARY_PATH", new_path.c_str(), 1);
}
diff --git a/src/lib/wscript b/src/lib/wscript
index 79f0d563e..cae78fc4a 100644
--- a/src/lib/wscript
+++ b/src/lib/wscript
@@ -267,7 +267,7 @@ def build(bld):
obj.uselib += ' POLKIT'
if bld.env.ENABLE_GROK:
- obj.source += ' grok_j2k_encoder_thread.cc'
+ obj.source += ' grok_j2k_encoder_thread.cc grok/util.cc'
if bld.env.TARGET_WINDOWS_64 or bld.env.TARGET_WINDOWS_32:
obj.uselib += ' WINSOCK2 DBGHELP SHLWAPI MSWSOCK BOOST_LOCALE SETUPAPI OLE32 UUID'
diff --git a/src/wx/grok/gpu_config_panel.h b/src/wx/grok/gpu_config_panel.h
index df38f9373..34bf38f12 100644
--- a/src/wx/grok/gpu_config_panel.h
+++ b/src/wx/grok/gpu_config_panel.h
@@ -22,30 +22,10 @@
#pragma once
+#include "lib/grok/util.h"
#include <wx/filepicker.h>
-static std::vector<std::string> get_gpu_names(boost::filesystem::path binary, boost::filesystem::path filename)
-{
- // Execute the GPU listing program and redirect its output to a file
- if (std::system((binary.string() + " > " + filename.string()).c_str()) < 0) {
- return {};
- }
-
- std::vector<std::string> gpu_names;
- std::ifstream file(filename.c_str());
- if (file.is_open())
- {
- std::string line;
- while (std::getline(file, line))
- gpu_names.push_back(line);
- file.close();
- }
-
- return gpu_names;
-}
-
-
class GpuList : public wxPanel
{
public:
@@ -63,16 +43,9 @@ public:
void update()
{
- auto grok = Config::instance()->grok().get_value_or({});
- auto lister_binary = grok.binary_location / "gpu_lister";
- auto lister_file = grok.binary_location / "gpus.txt";
- if (boost::filesystem::exists(lister_binary)) {
- auto gpu_names = get_gpu_names(lister_binary, lister_file);
-
- _combo_box->Clear();
- for (auto const& name: gpu_names) {
- _combo_box->Append(std_to_wx(name));
- }
+ _combo_box->Clear();
+ for (auto const& name: get_gpu_names()) {
+ _combo_box->Append(std_to_wx(name));
}
}
@@ -88,7 +61,7 @@ private:
{
auto selection = _combo_box->GetSelection();
if (selection != wxNOT_FOUND) {
- auto grok = Config::instance()->grok().get_value_or({});
+ auto grok = Config::instance()->grok();
grok.selected = selection;
Config::instance()->set_grok(grok);
}
@@ -155,7 +128,7 @@ private:
void setup_sensitivity()
{
- auto grok = Config::instance()->grok().get_value_or({});
+ auto grok = Config::instance()->grok();
_binary_location->Enable(grok.enable);
_gpu_list_control->Enable(grok.enable);
@@ -165,7 +138,7 @@ private:
void config_changed() override
{
- auto grok = Config::instance()->grok().get_value_or({});
+ auto grok = Config::instance()->grok();
checked_set(_enable_gpu, grok.enable);
_binary_location->SetPath(std_to_wx(grok.binary_location.string()));
@@ -177,7 +150,7 @@ private:
void enable_gpu_changed()
{
- auto grok = Config::instance()->grok().get_value_or({});
+ auto grok = Config::instance()->grok();
grok.enable = _enable_gpu->GetValue();
Config::instance()->set_grok(grok);
@@ -186,7 +159,7 @@ private:
void binary_location_changed()
{
- auto grok = Config::instance()->grok().get_value_or({});
+ auto grok = Config::instance()->grok();
grok.binary_location = wx_to_std(_binary_location->GetPath());
Config::instance()->set_grok(grok);
@@ -195,20 +168,20 @@ private:
void server_changed()
{
- auto grok = Config::instance()->grok().get_value_or({});
+ auto grok = Config::instance()->grok();
grok.licence_server = wx_to_std(_server->GetValue());
Config::instance()->set_grok(grok);
}
void port_changed()
{
- auto grok = Config::instance()->grok().get_value_or({});
+ auto grok = Config::instance()->grok();
Config::instance()->set_grok(grok);
}
void licence_changed()
{
- auto grok = Config::instance()->grok().get_value_or({});
+ auto grok = Config::instance()->grok();
grok.licence = _licence->get();
Config::instance()->set_grok(grok);
}
diff --git a/test/config_test.cc b/test/config_test.cc
index a9b95bedf..8fd19b693 100644
--- a/test/config_test.cc
+++ b/test/config_test.cc
@@ -182,9 +182,11 @@ BOOST_AUTO_TEST_CASE (config_upgrade_test1)
check_xml (dir / "config.xml", "test/data/2.14.config.xml", {});
check_xml (dir / "cinemas.xml", "test/data/2.14.cinemas.xml", {});
-#ifdef DCPOMATIC_WINDOWS
+#if defined(DCPOMATIC_WINDOWS)
/* This file has the windows path for dkdm_recipients.xml (with backslashes) */
check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.windows.sqlite.xml", {});
+#elif defined(DCPOMATIC_GROK)
+ check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.sqlite.grok.xml", {});
#else
check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.sqlite.xml", {});
#endif
@@ -215,10 +217,13 @@ BOOST_AUTO_TEST_CASE (config_upgrade_test2)
Config::instance()->write();
check_xml(dir / "cinemas.xml", "test/data/2.14.cinemas.xml", {});
-#ifdef DCPOMATIC_WINDOWS
+#if defined(DCPOMATIC_WINDOWS)
/* This file has the windows path for dkdm_recipients.xml (with backslashes) */
check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.windows.xml", {});
check_xml(dir / "config.xml", "test/data/2.16.config.windows.xml", {});
+#elif defined(DCPOMATIC_GROK)
+ check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.grok.xml", {});
+ check_xml(dir / "config.xml", "test/data/2.16.config.xml", {});
#else
check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.xml", {});
check_xml(dir / "config.xml", "test/data/2.16.config.xml", {});
diff --git a/test/data b/test/data
-Subproject df601f1580d852de043c2eca6acc4cfc9a2446b
+Subproject 7d2ed165d8e65d92a02a20e22c1caedd15db82b
diff --git a/test/encode_cli_test.cc b/test/encode_cli_test.cc
new file mode 100644
index 000000000..0a0a17e3a
--- /dev/null
+++ b/test/encode_cli_test.cc
@@ -0,0 +1,163 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic 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 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/content_factory.h"
+#include "lib/encode_cli.h"
+#include "lib/film.h"
+#include "test.h"
+#include <boost/optional.hpp>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+#include <string>
+#include <vector>
+
+
+using std::cout;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+static
+optional<string>
+run(vector<string> const& args, vector<string>& output)
+{
+ vector<char*> argv(args.size() + 1);
+ for (auto i = 0U; i < args.size(); ++i) {
+ argv[i] = const_cast<char*>(args[i].c_str());
+ }
+ argv[args.size()] = nullptr;
+
+ auto error = encode_cli(args.size(), argv.data(), [&output](string s) { output.push_back(s); }, []() { });
+ for (auto i: output) {
+ std::cout << "O:" << i;
+ }
+ if (error) {
+ std::cout << "E:" << *error << "\n";
+ }
+
+ return error;
+}
+
+
+static
+bool
+find_in_order(vector<string> const& output, vector<string> const& check)
+{
+ BOOST_REQUIRE(!check.empty());
+
+ auto next = check.begin();
+ for (auto line: output) {
+ if (line.find(*next) != string::npos) {
+ ++next;
+ if (next == check.end()) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+BOOST_AUTO_TEST_CASE(basic_encode_cli_test)
+{
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film("basic_encode_cli_test", content);
+ film->write_metadata();
+
+ vector<string> output;
+ run({ "cli", "build/test/basic_encode_cli_test" }, output);
+
+ BOOST_CHECK(find_in_order(output, { "Making DCP for", "Examining content", "OK", "Transcoding DCP", "OK" }));
+}
+
+
+BOOST_AUTO_TEST_CASE(encode_cli_with_explicit_encode_command_test)
+{
+ auto content = content_factory("test/data/flat_red.png");
+ auto film = new_test_film("basic_encode_cli_test", content);
+ film->write_metadata();
+
+ vector<string> output;
+ run({ "cli", "make-dcp", "build/test/basic_encode_cli_test" }, output);
+
+ BOOST_CHECK(find_in_order(output, { "Making DCP for", "Examining content", "OK", "Transcoding DCP", "OK" }));
+}
+
+
+#ifdef DCPOMATIC_GROK
+BOOST_AUTO_TEST_CASE(encode_cli_set_grok_licence)
+{
+ boost::filesystem::path config = "build/encode_cli_set_grok_licence";
+ boost::filesystem::remove_all(config);
+ boost::filesystem::create_directories(config);
+ ConfigRestorer cr(config);
+
+ vector<string> output;
+ auto error = run({ "cli", "config", "grok-licence", "12345678ABC" }, output);
+ BOOST_CHECK(output.empty());
+ BOOST_CHECK(!error);
+
+ cxml::Document check("Config");
+ check.read_file(config / "2.18" / "config.xml");
+ BOOST_CHECK_EQUAL(check.node_child("Grok")->string_child("Licence"), "12345678ABC");
+}
+
+
+BOOST_AUTO_TEST_CASE(encode_cli_enable_grok)
+{
+ boost::filesystem::path config = "build/encode_cli_enable_grok";
+ boost::filesystem::remove_all(config);
+ boost::filesystem::create_directories(config);
+ ConfigRestorer cr(config);
+
+ for (auto value: vector<string>{ "1", "0"}) {
+ vector<string> output;
+ auto error = run({ "cli", "config", "grok-enable", value }, output);
+ BOOST_CHECK(output.empty());
+ BOOST_CHECK(!error);
+
+ cxml::Document check("Config");
+ check.read_file(config / "2.18" / "config.xml");
+ BOOST_CHECK_EQUAL(check.node_child("Grok")->string_child("Enable"), value);
+ }
+}
+
+
+BOOST_AUTO_TEST_CASE(encode_cli_set_grok_binary_location)
+{
+ boost::filesystem::path config = "build/encode_cli_set_grok_binary_location";
+ boost::filesystem::remove_all(config);
+ boost::filesystem::create_directories(config);
+ ConfigRestorer cr(config);
+
+ vector<string> output;
+ auto error = run({ "cli", "config", "grok-binary-location", "foo/bar/baz" }, output);
+ BOOST_CHECK(output.empty());
+ BOOST_CHECK(!error);
+
+ cxml::Document check("Config");
+ check.read_file(config / "2.18" / "config.xml");
+ BOOST_CHECK_EQUAL(check.node_child("Grok")->string_child("BinaryLocation"), "foo/bar/baz");
+}
+#endif
+
diff --git a/test/gpu_lister b/test/gpu_lister
new file mode 100755
index 000000000..6dcb750de
--- /dev/null
+++ b/test/gpu_lister
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+echo "Foo bar baz"
+echo "Spondoolix Mega Kompute 2000"
+echo "Energy Sink-o-matic"
+
diff --git a/test/grok_util_test.cc b/test/grok_util_test.cc
new file mode 100644
index 000000000..2a84fe2a4
--- /dev/null
+++ b/test/grok_util_test.cc
@@ -0,0 +1,43 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic 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 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/config.h"
+#include "lib/grok/util.h"
+#include "test.h"
+#include <boost/test/unit_test.hpp>
+
+
+#ifdef DCPOMATIC_GROK
+BOOST_AUTO_TEST_CASE(get_gpu_names_test)
+{
+ ConfigRestorer cr;
+
+ Config::Grok grok;
+ grok.binary_location = "test";
+ Config::instance()->set_grok(grok);
+
+ auto names = get_gpu_names();
+ BOOST_REQUIRE_EQUAL(names.size(), 3U);
+ BOOST_CHECK_EQUAL(names[0], "Foo bar baz");
+ BOOST_CHECK_EQUAL(names[1], "Spondoolix Mega Kompute 2000");
+ BOOST_CHECK_EQUAL(names[2], "Energy Sink-o-matic");
+}
+#endif
diff --git a/test/wscript b/test/wscript
index 85bfccf56..7e53dfb02 100644
--- a/test/wscript
+++ b/test/wscript
@@ -85,6 +85,7 @@ def build(bld):
email_test.cc
empty_caption_test.cc
empty_test.cc
+ encode_cli_test.cc
encryption_test.cc
file_extension_test.cc
ffmpeg_audio_only_test.cc
@@ -109,6 +110,7 @@ def build(bld):
font_id_allocator_test.cc
frame_interval_checker_test.cc
frame_rate_test.cc
+ grok_util_test.cc
guess_crop_test.cc
hints_test.cc
image_content_fade_test.cc