2 Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
4 This file is part of DCP-o-matic.
6 DCP-o-matic is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 DCP-o-matic is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
22 #include "lib/config.h"
23 #include "lib/content.h"
24 #include "lib/dcp_content.h"
25 #include "lib/content_factory.h"
27 #include "lib/map_cli.h"
28 #include "lib/text_content.h"
33 #include <dcp/reel_picture_asset.h>
34 #include <dcp/reel_sound_asset.h>
35 #include <boost/algorithm/string.hpp>
36 #include <boost/filesystem.hpp>
37 #include <boost/optional.hpp>
38 #include <boost/test/unit_test.hpp>
41 using std::dynamic_pointer_cast;
42 using std::make_shared;
43 using std::shared_ptr;
46 using boost::optional;
51 run(vector<string> const& args, vector<string>& output)
53 vector<char*> argv(args.size() + 1);
54 for (auto i = 0U; i < args.size(); ++i) {
55 argv[i] = const_cast<char*>(args[i].c_str());
57 argv[args.size()] = nullptr;
59 auto error = map_cli(args.size(), argv.data(), [&output](string s) { output.push_back(s); });
61 std::cout << *error << "\n";
69 boost::filesystem::path
70 find_prefix(boost::filesystem::path dir, string prefix)
72 auto iter = std::find_if(boost::filesystem::directory_iterator(dir), boost::filesystem::directory_iterator(), [prefix](boost::filesystem::path const& p) {
73 return boost::starts_with(p.filename().string(), prefix);
76 BOOST_REQUIRE(iter != boost::filesystem::directory_iterator());
82 boost::filesystem::path
83 find_cpl(boost::filesystem::path dir)
85 return find_prefix(dir, "cpl_");
89 /** Map a single DCP into a new DCP */
90 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy)
92 string const name = "map_simple_dcp_copy";
93 string const out = String::compose("build/test/%1_out", name);
95 auto content = content_factory("test/data/flat_red.png");
96 auto film = new_test_film2(name + "_in", content);
97 make_and_verify_dcp(film);
99 vector<string> const args = {
102 "-d", film->dir(film->dcp_name()).string(),
103 find_cpl(film->dir(film->dcp_name())).string()
106 boost::filesystem::remove_all(out);
108 vector<string> output_messages;
109 auto error = run(args, output_messages);
114 BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "j2c_")));
115 BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "pcm_")));
119 /** Map a single DCP into a new DCP, referring to the CPL by ID */
120 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_by_id)
122 string const name = "map_simple_dcp_copy_by_id";
123 string const out = String::compose("build/test/%1_out", name);
125 auto content = content_factory("test/data/flat_red.png");
126 auto film = new_test_film2(name + "_in", content);
127 make_and_verify_dcp(film);
129 dcp::CPL cpl(find_cpl(film->dir(film->dcp_name())));
131 vector<string> const args = {
134 "-d", film->dir(film->dcp_name()).string(),
138 boost::filesystem::remove_all(out);
140 vector<string> output_messages;
141 auto error = run(args, output_messages);
146 BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "j2c_")));
147 BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "pcm_")));
151 /** Map a single DCP into a new DCP using the symlink option */
152 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_symlinks)
154 string const name = "map_simple_dcp_copy_with_symlinks";
155 string const out = String::compose("build/test/%1_out", name);
157 auto content = content_factory("test/data/flat_red.png");
158 auto film = new_test_film2(name + "_in", content);
159 make_and_verify_dcp(film);
161 vector<string> const args = {
164 "-d", film->dir(film->dcp_name()).string(),
166 find_cpl(film->dir(film->dcp_name())).string()
169 boost::filesystem::remove_all(out);
171 vector<string> output_messages;
172 auto error = run(args, output_messages);
175 /* We can't verify this DCP because the symlinks will make it fail
176 * (as it should be, I think).
179 BOOST_CHECK(boost::filesystem::is_symlink(find_prefix(out, "j2c_")));
180 BOOST_CHECK(boost::filesystem::is_symlink(find_prefix(out, "pcm_")));
184 /** Map a single DCP into a new DCP using the hardlink option */
185 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_hardlinks)
187 string const name = "map_simple_dcp_copy_with_hardlinks";
188 string const out = String::compose("build/test/%1_out", name);
190 auto content = content_factory("test/data/flat_red.png");
191 auto film = new_test_film2(name + "_in", content);
192 make_and_verify_dcp(film);
194 vector<string> const args = {
197 "-d", film->dir(film->dcp_name()).string(),
199 find_cpl(film->dir(film->dcp_name())).string()
202 boost::filesystem::remove_all(out);
204 vector<string> output_messages;
205 auto error = run(args, output_messages);
210 BOOST_CHECK_EQUAL(boost::filesystem::hard_link_count(find_prefix(out, "j2c_")), 2U);
211 BOOST_CHECK_EQUAL(boost::filesystem::hard_link_count(find_prefix(out, "pcm_")), 2U);
215 /** Map a single Interop DCP with subs into a new DCP */
216 BOOST_AUTO_TEST_CASE(map_simple_interop_dcp_with_subs)
218 string const name = "map_simple_interop_dcp_with_subs";
219 string const out = String::compose("build/test/%1_out", name);
221 auto picture = content_factory("test/data/flat_red.png").front();
222 auto subs = content_factory("test/data/15s.srt").front();
223 auto film = new_test_film2(name + "_in", { picture, subs });
224 film->set_interop(true);
225 subs->only_text()->set_language(dcp::LanguageTag("de"));
226 make_and_verify_dcp(film, {dcp::VerificationNote::Code::INVALID_STANDARD});
228 vector<string> const args = {
231 "-d", film->dir(film->dcp_name()).string(),
232 find_cpl(film->dir(film->dcp_name())).string()
235 boost::filesystem::remove_all(out);
237 vector<string> output_messages;
238 auto error = run(args, output_messages);
241 verify_dcp(out, {dcp::VerificationNote::Code::INVALID_STANDARD});
247 test_map_ov_vf_copy(vector<string> extra_args = {})
249 string const name = "map_ov_vf_copy";
250 string const out = String::compose("build/test/%1_out", name);
252 auto ov_content = content_factory("test/data/flat_red.png");
253 auto ov_film = new_test_film2(name + "_ov", ov_content);
254 make_and_verify_dcp(ov_film);
256 auto const ov_dir = ov_film->dir(ov_film->dcp_name());
257 auto vf_ov = make_shared<DCPContent>(ov_dir);
258 auto vf_sound = content_factory("test/data/sine_440.wav").front();
259 auto vf_film = new_test_film2(name + "_vf", { vf_ov, vf_sound });
260 vf_ov->set_reference_video(true);
261 make_and_verify_dcp(vf_film, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
263 auto const vf_dir = vf_film->dir(vf_film->dcp_name());
265 vector<string> args = {
268 "-d", ov_dir.string(),
269 "-d", vf_dir.string(),
270 find_cpl(vf_dir).string()
273 args.insert(std::end(args), std::begin(extra_args), std::end(extra_args));
275 boost::filesystem::remove_all(out);
277 vector<string> output_messages;
278 auto error = run(args, output_messages);
283 check_file(find_file(out, "cpl_"), find_file(vf_dir, "cpl_"));
284 check_file(find_file(out, "j2c_"), find_file(ov_dir, "j2c_"));
285 check_file(find_file(out, "pcm_"), find_file(vf_dir, "pcm_"));
289 /** Map an OV and a VF into a single DCP */
290 BOOST_AUTO_TEST_CASE(map_ov_vf_copy)
292 test_map_ov_vf_copy();
293 test_map_ov_vf_copy({"-l"});
297 /** Map an OV and VF into a single DCP, where the VF refers to the OV's assets multiple times */
298 BOOST_AUTO_TEST_CASE(map_ov_vf_copy_multiple_reference)
300 string const name = "map_ov_vf_copy_multiple_reference";
301 string const out = String::compose("build/test/%1_out", name);
303 auto ov_content = content_factory("test/data/flat_red.png");
304 auto ov_film = new_test_film2(name + "_ov", ov_content);
305 make_and_verify_dcp(ov_film);
307 auto const ov_dir = ov_film->dir(ov_film->dcp_name());
309 auto vf_ov1 = make_shared<DCPContent>(ov_dir);
310 auto vf_ov2 = make_shared<DCPContent>(ov_dir);
311 auto vf_sound = content_factory("test/data/sine_440.wav").front();
312 auto vf_film = new_test_film2(name + "_vf", { vf_ov1, vf_ov2, vf_sound });
313 vf_film->set_reel_type(ReelType::BY_VIDEO_CONTENT);
314 vf_ov2->set_position(vf_film, vf_ov1->end(vf_film));
315 vf_ov1->set_reference_video(true);
316 vf_ov2->set_reference_video(true);
317 make_and_verify_dcp(vf_film, {dcp::VerificationNote::Code::EXTERNAL_ASSET}, false);
319 auto const vf_dir = vf_film->dir(vf_film->dcp_name());
321 vector<string> const args = {
324 "-d", ov_dir.string(),
325 "-d", vf_dir.string(),
327 find_cpl(vf_dir).string()
330 boost::filesystem::remove_all(out);
332 vector<string> output_messages;
333 auto error = run(args, output_messages);
338 check_file(find_file(out, "cpl_"), find_file(vf_dir, "cpl_"));
339 check_file(find_file(out, "j2c_"), find_file(ov_dir, "j2c_"));
343 /** Map a single DCP into a new DCP using the rename option */
344 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_rename)
347 Config::instance()->set_dcp_asset_filename_format(dcp::NameFormat("hello%c"));
348 string const name = "map_simple_dcp_copy_with_rename";
349 string const out = String::compose("build/test/%1_out", name);
351 auto content = content_factory("test/data/flat_red.png");
352 auto film = new_test_film2(name + "_in", content);
353 make_and_verify_dcp(film);
355 vector<string> const args = {
358 "-d", film->dir(film->dcp_name()).string(),
360 find_cpl(film->dir(film->dcp_name())).string()
363 boost::filesystem::remove_all(out);
365 vector<string> output_messages;
366 auto error = run(args, output_messages);
371 dcp::DCP out_dcp(out);
374 BOOST_REQUIRE_EQUAL(out_dcp.cpls().size(), 1U);
375 auto const cpl = out_dcp.cpls()[0];
376 BOOST_REQUIRE_EQUAL(cpl->reels().size(), 1U);
377 auto const reel = cpl->reels()[0];
378 BOOST_REQUIRE(reel->main_picture());
379 BOOST_REQUIRE(reel->main_sound());
380 auto const picture = reel->main_picture()->asset();
381 BOOST_REQUIRE(picture);
382 auto const sound = reel->main_sound()->asset();
383 BOOST_REQUIRE(sound);
385 BOOST_REQUIRE(picture->file());
386 BOOST_CHECK(picture->file().get().filename() == picture->id() + ".mxf");
388 BOOST_REQUIRE(sound->file());
389 BOOST_CHECK(sound->file().get().filename() == sound->id() + ".mxf");
395 test_two_cpls_each_with_subs(string name, bool interop)
397 string const out = String::compose("build/test/%1_out", name);
399 vector<dcp::VerificationNote::Code> acceptable_errors;
401 acceptable_errors.push_back(dcp::VerificationNote::Code::INVALID_STANDARD);
403 acceptable_errors.push_back(dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE);
404 acceptable_errors.push_back(dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME);
407 shared_ptr<Film> films[2];
408 for (auto i = 0; i < 2; ++i) {
409 auto picture = content_factory("test/data/flat_red.png").front();
410 auto subs = content_factory("test/data/15s.srt").front();
411 films[i] = new_test_film2(String::compose("%1_%2_in", name, i), { picture, subs });
412 films[i]->set_interop(interop);
413 subs->only_text()->set_language(dcp::LanguageTag("de"));
414 make_and_verify_dcp(films[i], acceptable_errors);
417 vector<string> const args = {
420 "-d", films[0]->dir(films[0]->dcp_name()).string(),
421 "-d", films[1]->dir(films[1]->dcp_name()).string(),
422 find_cpl(films[0]->dir(films[0]->dcp_name())).string(),
423 find_cpl(films[1]->dir(films[1]->dcp_name())).string()
426 boost::filesystem::remove_all(out);
428 vector<string> output_messages;
429 auto error = run(args, output_messages);
432 verify_dcp(out, acceptable_errors);
436 BOOST_AUTO_TEST_CASE(map_two_interop_cpls_each_with_subs)
438 test_two_cpls_each_with_subs("map_two_interop_cpls_each_with_subs", true);
442 BOOST_AUTO_TEST_CASE(map_two_smpte_cpls_each_with_subs)
444 test_two_cpls_each_with_subs("map_two_smpte_cpls_each_with_subs", false);
448 BOOST_AUTO_TEST_CASE(map_with_given_config)
452 string const name = "map_with_given_config";
453 string const out = String::compose("build/test/%1_out", name);
455 auto content = content_factory("test/data/flat_red.png");
456 auto film = new_test_film2(name + "_in", content);
457 make_and_verify_dcp(film);
459 vector<string> const args = {
462 "-d", film->dir(film->dcp_name()).string(),
463 "--config", "test/data/map_with_given_config",
464 find_cpl(film->dir(film->dcp_name())).string()
467 boost::filesystem::remove_all(out);
468 boost::filesystem::remove_all("test/data/map_with_given_config/2.18");
470 Config::instance()->drop();
471 vector<string> output_messages;
472 auto error = run(args, output_messages);
475 /* It should be signed by the key in test/data/map_with_given_config, not the one in test/data/signer_key */
476 BOOST_CHECK(dcp::file_to_string(find_file(out, "cpl_")).find("dnQualifier=\\+uOcNN2lPuxpxgd/5vNkkBER0GE=,CN=CS.dcpomatic.smpte-430-2.LEAF,OU=dcpomatic.com,O=dcpomatic.com") != std::string::npos);
480 BOOST_AUTO_TEST_CASE(map_multireel_interop_ov_and_vf_adding_ccaps)
482 string const name = "map_multireel_interop_ov_and_vf_adding_ccaps";
483 string const out = String::compose("build/test/%1_out", name);
485 vector<shared_ptr<Content>> video = {
486 content_factory("test/data/flat_red.png")[0],
487 content_factory("test/data/flat_red.png")[0],
488 content_factory("test/data/flat_red.png")[0]
491 auto ov = new_test_film2(name + "_ov", { video[0], video[1], video[2] });
492 ov->set_reel_type(ReelType::BY_VIDEO_CONTENT);
493 ov->set_interop(true);
494 make_and_verify_dcp(ov, { dcp::VerificationNote::Code::INVALID_STANDARD });
496 auto ov_dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
498 vector<shared_ptr<Content>> ccap = {
499 content_factory("test/data/short.srt")[0],
500 content_factory("test/data/short.srt")[0],
501 content_factory("test/data/short.srt")[0]
504 auto vf = new_test_film2(name + "_vf", { ov_dcp, ccap[0], ccap[1], ccap[2] });
505 vf->set_interop(true);
506 vf->set_reel_type(ReelType::BY_VIDEO_CONTENT);
507 ov_dcp->set_reference_video(true);
508 ov_dcp->set_reference_audio(true);
509 for (auto i = 0; i < 3; ++i) {
510 ccap[i]->text[0]->set_use(true);
511 ccap[i]->text[0]->set_type(TextType::CLOSED_CAPTION);
516 dcp::VerificationNote::Code::INVALID_STANDARD,
517 dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
518 dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
519 dcp::VerificationNote::Code::EXTERNAL_ASSET
522 vector<string> const args = {
525 "-d", ov->dir(ov->dcp_name()).string(),
526 "-d", vf->dir(vf->dcp_name()).string(),
527 find_cpl(vf->dir(vf->dcp_name())).string()
530 boost::filesystem::remove_all(out);
532 vector<string> output_messages;
533 auto error = run(args, output_messages);
536 verify_dcp(out, { dcp::VerificationNote::Code::INVALID_STANDARD });
540 BOOST_AUTO_TEST_CASE(map_uses_config_for_issuer_and_creator)
544 Config::instance()->set_dcp_issuer("ostrabagalous");
545 Config::instance()->set_dcp_creator("Fred");
547 string const name = "map_uses_config_for_issuer_and_creator";
548 string const out = String::compose("build/test/%1_out", name);
550 auto content = content_factory("test/data/flat_red.png");
551 auto film = new_test_film2(name + "_in", content);
552 make_and_verify_dcp(film);
554 vector<string> const args = {
557 "-d", film->dir(film->dcp_name()).string(),
558 find_cpl(film->dir(film->dcp_name())).string()
561 boost::filesystem::remove_all(out);
563 vector<string> output_messages;
564 auto error = run(args, output_messages);
567 cxml::Document assetmap("AssetMap");
568 assetmap.read_file(film->dir(film->dcp_name()) / "ASSETMAP.xml");
569 BOOST_CHECK(assetmap.string_child("Issuer") == "ostrabagalous");
570 BOOST_CHECK(assetmap.string_child("Creator") == "Fred");
572 cxml::Document pkl("PackingList");
573 pkl.read_file(find_prefix(out, "pkl_"));
574 BOOST_CHECK(pkl.string_child("Issuer") == "ostrabagalous");
575 BOOST_CHECK(pkl.string_child("Creator") == "Fred");
579 BOOST_AUTO_TEST_CASE(map_handles_interop_png_subs)
581 string const name = "map_handles_interop_png_subs";
582 auto arrietty = content_factory(TestPaths::private_data() / "arrietty_JP-EN.mkv")[0];
583 auto film = new_test_film2(name + "_input", { arrietty });
584 film->set_interop(true);
585 arrietty->set_trim_end(dcpomatic::ContentTime::from_seconds(110));
586 arrietty->text[0]->set_use(true);
587 arrietty->text[0]->set_language(dcp::LanguageTag("de"));
591 dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
592 dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
593 dcp::VerificationNote::Code::INVALID_STANDARD
596 auto const out = boost::filesystem::path("build") / "test" / (name + "_output");
598 vector<string> const args = {
601 "-d", film->dir(film->dcp_name()).string(),
602 find_cpl(film->dir(film->dcp_name())).string()
605 boost::filesystem::remove_all(out);
607 vector<string> output_messages;
608 auto error = run(args, output_messages);
611 verify_dcp(out, { dcp::VerificationNote::Code::INVALID_STANDARD });