ca75ed5207681a7720fa601b3162f391b5543141
[dcpomatic.git] / test / map_cli_test.cc
1 /*
2     Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
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.
10
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.
15
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/>.
18
19 */
20
21
22 #include "lib/config.h"
23 #include "lib/content.h"
24 #include "lib/dcp_content.h"
25 #include "lib/content_factory.h"
26 #include "lib/film.h"
27 #include "lib/map_cli.h"
28 #include "lib/text_content.h"
29 #include "test.h"
30 #include <dcp/cpl.h>
31 #include <dcp/dcp.h>
32 #include <dcp/reel.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>
39
40
41 using std::dynamic_pointer_cast;
42 using std::make_shared;
43 using std::shared_ptr;
44 using std::string;
45 using std::vector;
46 using boost::optional;
47
48
49 static
50 optional<string>
51 run(vector<string> const& args, vector<string>& output)
52 {
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());
56         }
57         argv[args.size()] = nullptr;
58
59         auto error = map_cli(args.size(), argv.data(), [&output](string s) { output.push_back(s); });
60         if (error) {
61                 std::cout << *error << "\n";
62         }
63
64         return error;
65 }
66
67
68 static
69 boost::filesystem::path
70 find_prefix(boost::filesystem::path dir, string prefix)
71 {
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);
74         });
75
76         BOOST_REQUIRE(iter != boost::filesystem::directory_iterator());
77         return iter->path();
78 }
79
80
81 static
82 boost::filesystem::path
83 find_cpl(boost::filesystem::path dir)
84 {
85         return find_prefix(dir, "cpl_");
86 }
87
88
89 /** Map a single DCP into a new DCP */
90 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy)
91 {
92         string const name = "map_simple_dcp_copy";
93         string const out = String::compose("build/test/%1_out", name);
94
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);
98
99         vector<string> const args = {
100                 "map_cli",
101                 "-o", out,
102                 "-d", film->dir(film->dcp_name()).string(),
103                 find_cpl(film->dir(film->dcp_name())).string()
104         };
105
106         boost::filesystem::remove_all(out);
107
108         vector<string> output_messages;
109         auto error = run(args, output_messages);
110         BOOST_CHECK(!error);
111
112         verify_dcp(out, {});
113
114         BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "j2c_")));
115         BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "pcm_")));
116 }
117
118
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)
121 {
122         string const name = "map_simple_dcp_copy_by_id";
123         string const out = String::compose("build/test/%1_out", name);
124
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);
128
129         dcp::CPL cpl(find_cpl(film->dir(film->dcp_name())));
130
131         vector<string> const args = {
132                 "map_cli",
133                 "-o", out,
134                 "-d", film->dir(film->dcp_name()).string(),
135                 cpl.id()
136         };
137
138         boost::filesystem::remove_all(out);
139
140         vector<string> output_messages;
141         auto error = run(args, output_messages);
142         BOOST_CHECK(!error);
143
144         verify_dcp(out, {});
145
146         BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "j2c_")));
147         BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "pcm_")));
148 }
149
150
151 /** Map a single DCP into a new DCP using the symlink option */
152 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_symlinks)
153 {
154         string const name = "map_simple_dcp_copy_with_symlinks";
155         string const out = String::compose("build/test/%1_out", name);
156
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);
160
161         vector<string> const args = {
162                 "map_cli",
163                 "-o", out,
164                 "-d", film->dir(film->dcp_name()).string(),
165                 "-s",
166                 find_cpl(film->dir(film->dcp_name())).string()
167         };
168
169         boost::filesystem::remove_all(out);
170
171         vector<string> output_messages;
172         auto error = run(args, output_messages);
173         BOOST_CHECK(!error);
174
175         /* We can't verify this DCP because the symlinks will make it fail
176          * (as it should be, I think).
177          */
178
179         BOOST_CHECK(boost::filesystem::is_symlink(find_prefix(out, "j2c_")));
180         BOOST_CHECK(boost::filesystem::is_symlink(find_prefix(out, "pcm_")));
181 }
182
183
184 /** Map a single DCP into a new DCP using the hardlink option */
185 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_hardlinks)
186 {
187         string const name = "map_simple_dcp_copy_with_hardlinks";
188         string const out = String::compose("build/test/%1_out", name);
189
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);
193
194         vector<string> const args = {
195                 "map_cli",
196                 "-o", out,
197                 "-d", film->dir(film->dcp_name()).string(),
198                 "-l",
199                 find_cpl(film->dir(film->dcp_name())).string()
200         };
201
202         boost::filesystem::remove_all(out);
203
204         vector<string> output_messages;
205         auto error = run(args, output_messages);
206         BOOST_CHECK(!error);
207
208         verify_dcp(out, {});
209
210         /* The video file will have 3 links because DoM also makes a link into the video directory */
211         BOOST_CHECK_EQUAL(boost::filesystem::hard_link_count(find_prefix(out, "j2c_")), 3U);
212         BOOST_CHECK_EQUAL(boost::filesystem::hard_link_count(find_prefix(out, "pcm_")), 2U);
213 }
214
215
216 /** Map a single Interop DCP with subs into a new DCP */
217 BOOST_AUTO_TEST_CASE(map_simple_interop_dcp_with_subs)
218 {
219         string const name = "map_simple_interop_dcp_with_subs";
220         string const out = String::compose("build/test/%1_out", name);
221
222         auto picture = content_factory("test/data/flat_red.png").front();
223         auto subs = content_factory("test/data/15s.srt").front();
224         auto film = new_test_film2(name + "_in", { picture, subs });
225         film->set_interop(true);
226         make_and_verify_dcp(film, {dcp::VerificationNote::Code::INVALID_STANDARD});
227
228         vector<string> const args = {
229                 "map_cli",
230                 "-o", out,
231                 "-d", film->dir(film->dcp_name()).string(),
232                 find_cpl(film->dir(film->dcp_name())).string()
233         };
234
235         boost::filesystem::remove_all(out);
236
237         vector<string> output_messages;
238         auto error = run(args, output_messages);
239         BOOST_CHECK(!error);
240
241         verify_dcp(out, {dcp::VerificationNote::Code::INVALID_STANDARD});
242 }
243
244
245 static
246 void
247 test_map_ov_vf_copy(vector<string> extra_args = {})
248 {
249         string const name = "map_ov_vf_copy";
250         string const out = String::compose("build/test/%1_out", name);
251
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);
255
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);
262
263         auto const vf_dir = vf_film->dir(vf_film->dcp_name());
264
265         vector<string> args = {
266                 "map_cli",
267                 "-o", out,
268                 "-d", ov_dir.string(),
269                 "-d", vf_dir.string(),
270                 find_cpl(vf_dir).string()
271         };
272
273         args.insert(std::end(args), std::begin(extra_args), std::end(extra_args));
274
275         boost::filesystem::remove_all(out);
276
277         vector<string> output_messages;
278         auto error = run(args, output_messages);
279         BOOST_CHECK(!error);
280
281         verify_dcp(out, {});
282
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_"));
286 }
287
288
289 /** Map an OV and a VF into a single DCP */
290 BOOST_AUTO_TEST_CASE(map_ov_vf_copy)
291 {
292         test_map_ov_vf_copy();
293         test_map_ov_vf_copy({"-l"});
294 }
295
296
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)
299 {
300         string const name = "map_ov_vf_copy_multiple_reference";
301         string const out = String::compose("build/test/%1_out", name);
302
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);
306
307         auto const ov_dir = ov_film->dir(ov_film->dcp_name());
308
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);
318
319         auto const vf_dir = vf_film->dir(vf_film->dcp_name());
320
321         vector<string> const args = {
322                 "map_cli",
323                 "-o", out,
324                 "-d", ov_dir.string(),
325                 "-d", vf_dir.string(),
326                 "-l",
327                 find_cpl(vf_dir).string()
328         };
329
330         boost::filesystem::remove_all(out);
331
332         vector<string> output_messages;
333         auto error = run(args, output_messages);
334         BOOST_CHECK(!error);
335
336         verify_dcp(out, {});
337
338         check_file(find_file(out, "cpl_"), find_file(vf_dir, "cpl_"));
339         check_file(find_file(out, "j2c_"), find_file(ov_dir, "j2c_"));
340 }
341
342
343 /** Map a single DCP into a new DCP using the rename option */
344 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_rename)
345 {
346         ConfigRestorer cr;
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);
350
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);
354
355         vector<string> const args = {
356                 "map_cli",
357                 "-o", out,
358                 "-d", film->dir(film->dcp_name()).string(),
359                 "-r",
360                 find_cpl(film->dir(film->dcp_name())).string()
361         };
362
363         boost::filesystem::remove_all(out);
364
365         vector<string> output_messages;
366         auto error = run(args, output_messages);
367         BOOST_CHECK(!error);
368
369         verify_dcp(out, {});
370
371         dcp::DCP out_dcp(out);
372         out_dcp.read();
373
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);
384
385         BOOST_REQUIRE(picture->file());
386         BOOST_CHECK(picture->file().get().filename() == picture->id() + ".mxf");
387
388         BOOST_REQUIRE(sound->file());
389         BOOST_CHECK(sound->file().get().filename() == sound->id() + ".mxf");
390 }
391
392
393 static
394 void
395 test_two_cpls_each_with_subs(string name, bool interop)
396 {
397         string const out = String::compose("build/test/%1_out", name);
398
399         vector<dcp::VerificationNote::Code> acceptable_errors;
400         if (interop) {
401                 acceptable_errors.push_back(dcp::VerificationNote::Code::INVALID_STANDARD);
402         } else {
403                 acceptable_errors.push_back(dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE);
404                 acceptable_errors.push_back(dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME);
405         }
406
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                 make_and_verify_dcp(films[i], acceptable_errors);
414         }
415
416         vector<string> const args = {
417                 "map_cli",
418                 "-o", out,
419                 "-d", films[0]->dir(films[0]->dcp_name()).string(),
420                 "-d", films[1]->dir(films[1]->dcp_name()).string(),
421                 find_cpl(films[0]->dir(films[0]->dcp_name())).string(),
422                 find_cpl(films[1]->dir(films[1]->dcp_name())).string()
423         };
424
425         boost::filesystem::remove_all(out);
426
427         vector<string> output_messages;
428         auto error = run(args, output_messages);
429         BOOST_CHECK(!error);
430
431         verify_dcp(out, acceptable_errors);
432 }
433
434
435 BOOST_AUTO_TEST_CASE(map_two_interop_cpls_each_with_subs)
436 {
437         test_two_cpls_each_with_subs("map_two_interop_cpls_each_with_subs", true);
438 }
439
440
441 BOOST_AUTO_TEST_CASE(map_two_smpte_cpls_each_with_subs)
442 {
443         test_two_cpls_each_with_subs("map_two_smpte_cpls_each_with_subs", false);
444 }
445
446
447 BOOST_AUTO_TEST_CASE(map_with_given_config)
448 {
449         ConfigRestorer cr;
450
451         string const name = "map_with_given_config";
452         string const out = String::compose("build/test/%1_out", name);
453
454         auto content = content_factory("test/data/flat_red.png");
455         auto film = new_test_film2(name + "_in", content);
456         make_and_verify_dcp(film);
457
458         vector<string> const args = {
459                 "map_cli",
460                 "-o", out,
461                 "-d", film->dir(film->dcp_name()).string(),
462                 "--config", "test/data/map_with_given_config",
463                 find_cpl(film->dir(film->dcp_name())).string()
464         };
465
466         boost::filesystem::remove_all(out);
467
468         Config::instance()->drop();
469         vector<string> output_messages;
470         auto error = run(args, output_messages);
471         BOOST_CHECK(!error);
472
473         /* It should be signed by the key in test/data/map_with_given_config, not the one in test/data/signer_key */
474         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);
475 }
476
477
478 BOOST_AUTO_TEST_CASE(map_multireel_interop_ov_and_vf_adding_ccaps)
479 {
480         string const name = "map_multireel_interop_ov_and_vf_adding_ccaps";
481         string const out = String::compose("build/test/%1_out", name);
482
483         vector<shared_ptr<Content>> video = {
484                 content_factory("test/data/flat_red.png")[0],
485                 content_factory("test/data/flat_red.png")[0],
486                 content_factory("test/data/flat_red.png")[0]
487         };
488
489         auto ov = new_test_film2(name + "_ov", { video[0], video[1], video[2] });
490         ov->set_reel_type(ReelType::BY_VIDEO_CONTENT);
491         ov->set_interop(true);
492         make_and_verify_dcp(ov, { dcp::VerificationNote::Code::INVALID_STANDARD });
493
494         auto ov_dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
495
496         vector<shared_ptr<Content>> ccap = {
497                 content_factory("test/data/short.srt")[0],
498                 content_factory("test/data/short.srt")[0],
499                 content_factory("test/data/short.srt")[0]
500         };
501
502         auto vf = new_test_film2(name + "_vf", { ov_dcp, ccap[0], ccap[1], ccap[2] });
503         vf->set_interop(true);
504         vf->set_reel_type(ReelType::BY_VIDEO_CONTENT);
505         ov_dcp->set_reference_video(true);
506         ov_dcp->set_reference_audio(true);
507         for (auto i = 0; i < 3; ++i) {
508                 ccap[i]->text[0]->set_use(true);
509                 ccap[i]->text[0]->set_type(TextType::CLOSED_CAPTION);
510         }
511         make_and_verify_dcp(
512                 vf,
513                 {
514                         dcp::VerificationNote::Code::INVALID_STANDARD,
515                         dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
516                         dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
517                         dcp::VerificationNote::Code::EXTERNAL_ASSET
518                 });
519
520         vector<string> const args = {
521                 "map_cli",
522                 "-o", out,
523                 "-d", ov->dir(ov->dcp_name()).string(),
524                 "-d", vf->dir(vf->dcp_name()).string(),
525                 find_cpl(vf->dir(vf->dcp_name())).string()
526         };
527
528         boost::filesystem::remove_all(out);
529
530         vector<string> output_messages;
531         auto error = run(args, output_messages);
532         BOOST_CHECK(!error);
533
534         verify_dcp(out, { dcp::VerificationNote::Code::INVALID_STANDARD });
535 }
536
537
538 BOOST_AUTO_TEST_CASE(map_uses_config_for_issuer_and_creator)
539 {
540         ConfigRestorer cr;
541
542         Config::instance()->set_dcp_issuer("ostrabagalous");
543         Config::instance()->set_dcp_creator("Fred");
544
545         string const name = "map_uses_config_for_issuer_and_creator";
546         string const out = String::compose("build/test/%1_out", name);
547
548         auto content = content_factory("test/data/flat_red.png");
549         auto film = new_test_film2(name + "_in", content);
550         make_and_verify_dcp(film);
551
552         vector<string> const args = {
553                 "map_cli",
554                 "-o", out,
555                 "-d", film->dir(film->dcp_name()).string(),
556                 find_cpl(film->dir(film->dcp_name())).string()
557         };
558
559         boost::filesystem::remove_all(out);
560
561         vector<string> output_messages;
562         auto error = run(args, output_messages);
563         BOOST_CHECK(!error);
564
565         cxml::Document assetmap("AssetMap");
566         assetmap.read_file(film->dir(film->dcp_name()) / "ASSETMAP.xml");
567         BOOST_CHECK(assetmap.string_child("Issuer") == "ostrabagalous");
568         BOOST_CHECK(assetmap.string_child("Creator") == "Fred");
569
570         cxml::Document pkl("PackingList");
571         pkl.read_file(find_prefix(out, "pkl_"));
572         BOOST_CHECK(pkl.string_child("Issuer") == "ostrabagalous");
573         BOOST_CHECK(pkl.string_child("Creator") == "Fred");
574 }
575
576
577 BOOST_AUTO_TEST_CASE(map_handles_interop_png_subs)
578 {
579         string const name = "map_handles_interop_png_subs";
580         auto arrietty = content_factory(TestPaths::private_data() / "arrietty_JP-EN.mkv")[0];
581         auto film = new_test_film2(name + "_input", { arrietty });
582         film->set_interop(true);
583         arrietty->set_trim_end(dcpomatic::ContentTime::from_seconds(110));
584         arrietty->text[0]->set_use(true);
585         make_and_verify_dcp(
586                 film,
587                 {
588                         dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
589                         dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
590                         dcp::VerificationNote::Code::INVALID_STANDARD
591                 });
592
593         auto const out = boost::filesystem::path("build") / "test" / (name + "_output");
594
595         vector<string> const args = {
596                 "map_cli",
597                 "-o", out.string(),
598                 "-d", film->dir(film->dcp_name()).string(),
599                 find_cpl(film->dir(film->dcp_name())).string()
600         };
601
602         boost::filesystem::remove_all(out);
603
604         vector<string> output_messages;
605         auto error = run(args, output_messages);
606         BOOST_CHECK(!error);
607
608         verify_dcp(out, { dcp::VerificationNote::Code::INVALID_STANDARD });
609 }
610