Fix argument parsing on Ubuntu 16.04.
[libdcp.git] / test / test.cc
1 /*
2     Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
6     libdcp 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     libdcp 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 libdcp.  If not, see <http://www.gnu.org/licenses/>.
18
19     In addition, as a special exception, the copyright holders give
20     permission to link the code of portions of this program with the
21     OpenSSL library under certain conditions as described in each
22     individual source file, and distribute linked combinations
23     including the two.
24
25     You must obey the GNU General Public License in all respects
26     for all of the code used other than OpenSSL.  If you modify
27     file(s) with this exception, you may extend this exception to your
28     version of the file(s), but you are not obligated to do so.  If you
29     do not wish to do so, delete this exception statement from your
30     version.  If you delete this exception statement from all source
31     files in the program, then also delete it here.
32 */
33
34 #define BOOST_TEST_DYN_LINK
35 #define BOOST_TEST_MODULE libdcp_test
36 #include "compose.hpp"
37 #include "cpl.h"
38 #include "dcp.h"
39 #include "interop_subtitle_asset.h"
40 #include "file.h"
41 #include "j2k_transcode.h"
42 #include "mono_picture_asset.h"
43 #include "mono_picture_asset.h"
44 #include "openjpeg_image.h"
45 #include "picture_asset_writer.h"
46 #include "picture_asset_writer.h"
47 #include "reel.h"
48 #include "reel_asset.h"
49 #include "reel_interop_closed_caption_asset.h"
50 #include "reel_interop_subtitle_asset.h"
51 #include "reel_markers_asset.h"
52 #include "reel_mono_picture_asset.h"
53 #include "reel_mono_picture_asset.h"
54 #include "reel_smpte_closed_caption_asset.h"
55 #include "reel_smpte_subtitle_asset.h"
56 #include "reel_sound_asset.h"
57 #include "smpte_subtitle_asset.h"
58 #include "sound_asset.h"
59 #include "sound_asset_writer.h"
60 #include "test.h"
61 #include "util.h"
62 #include "warnings.h"
63 LIBDCP_DISABLE_WARNINGS
64 #include <asdcp/KM_util.h>
65 #include <asdcp/KM_prng.h>
66 LIBDCP_ENABLE_WARNINGS
67 #include <sndfile.h>
68 LIBDCP_DISABLE_WARNINGS
69 #include <libxml++/libxml++.h>
70 LIBDCP_ENABLE_WARNINGS
71 #include <boost/algorithm/string.hpp>
72 #include <boost/test/unit_test.hpp>
73 #include <cstdio>
74 #include <iostream>
75
76
77 using std::string;
78 using std::min;
79 using std::vector;
80 using std::shared_ptr;
81 using std::make_shared;
82 using boost::optional;
83
84
85 boost::filesystem::path private_test;
86 boost::filesystem::path xsd_test = "build/test/xsd with spaces";
87
88
89 struct TestConfig
90 {
91         TestConfig()
92         {
93                 dcp::init ();
94                 auto const argc = boost::unit_test::framework::master_test_suite().argc;
95                 auto const argv = boost::unit_test::framework::master_test_suite().argv;
96                 if (argc >= 3 && strcmp(argv[1], "--") == 0) {
97                         /* For some reason on Ubuntu 16.04 the -- we pass in ends up in argv[1] so we
98                          * need to get the private_test from argv[2]...
99                          */
100                         private_test = argv[2];
101                 } else if (argc >= 2) {
102                         /* ... but everywhere else it's removed, so we can just get the private_test
103                          * value from argv[1].
104                          */
105                         private_test = argv[1];
106                 }
107
108                 using namespace boost::filesystem;
109                 boost::system::error_code ec;
110                 remove_all (xsd_test, ec);
111                 boost::filesystem::create_directory (xsd_test);
112                 for (directory_iterator i = directory_iterator("xsd"); i != directory_iterator(); ++i) {
113                         copy_file (*i, xsd_test / i->path().filename());
114                 }
115         }
116 };
117
118
119 void
120 check_xml (xmlpp::Element* ref, xmlpp::Element* test, vector<string> ignore_tags, bool ignore_whitespace)
121 {
122         BOOST_CHECK_EQUAL (ref->get_name (), test->get_name ());
123         BOOST_CHECK_EQUAL (ref->get_namespace_prefix (), test->get_namespace_prefix ());
124
125         if (find(ignore_tags.begin(), ignore_tags.end(), ref->get_name()) != ignore_tags.end()) {
126                 return;
127         }
128
129         auto whitespace_content = [](xmlpp::Node* node) {
130                 auto content = dynamic_cast<xmlpp::ContentNode*>(node);
131                 return content && content->get_content().find_first_not_of(" \t\r\n") == string::npos;
132         };
133
134         auto ref_children = ref->get_children ();
135         auto test_children = test->get_children ();
136
137         auto k = ref_children.begin ();
138         auto l = test_children.begin ();
139         while (k != ref_children.end() && l != test_children.end()) {
140
141                 if (dynamic_cast<xmlpp::CommentNode*>(*k)) {
142                         ++k;
143                         continue;
144                 }
145
146                 if (dynamic_cast<xmlpp::CommentNode*>(*l)) {
147                         ++l;
148                         continue;
149                 }
150
151                 if (whitespace_content(*k) && ignore_whitespace) {
152                         ++k;
153                         continue;
154                 }
155
156                 if (whitespace_content(*l) && ignore_whitespace) {
157                         ++l;
158                         continue;
159                 }
160
161                 /* XXX: should be doing xmlpp::EntityReference, xmlpp::XIncludeEnd, xmlpp::XIncludeStart */
162
163                 auto ref_el = dynamic_cast<xmlpp::Element*> (*k);
164                 auto test_el = dynamic_cast<xmlpp::Element*> (*l);
165                 BOOST_CHECK ((ref_el && test_el) || (!ref_el && !test_el));
166                 if (ref_el && test_el) {
167                         check_xml (ref_el, test_el, ignore_tags, ignore_whitespace);
168                 }
169
170                 auto ref_cn = dynamic_cast<xmlpp::ContentNode*> (*k);
171                 auto test_cn = dynamic_cast<xmlpp::ContentNode*> (*l);
172                 BOOST_CHECK ((ref_cn && test_cn) || (!ref_cn && !test_cn));
173                 if (ref_cn && test_cn) {
174                         BOOST_CHECK_EQUAL (ref_cn->get_content(), test_cn->get_content());
175                 }
176
177                 ++k;
178                 ++l;
179         }
180
181         while (k != ref_children.end() && ignore_whitespace && whitespace_content(*k)) {
182                 ++k;
183         }
184
185         while (l != test_children.end() && ignore_whitespace && whitespace_content(*l)) {
186                 ++l;
187         }
188
189         BOOST_REQUIRE (k == ref_children.end());
190         BOOST_REQUIRE (l == test_children.end());
191
192         auto ref_attributes = ref->get_attributes ();
193         auto test_attributes = test->get_attributes ();
194         BOOST_CHECK_EQUAL (ref_attributes.size(), test_attributes.size ());
195
196         auto m = ref_attributes.begin();
197         auto n = test_attributes.begin();
198         while (m != ref_attributes.end ()) {
199                 BOOST_CHECK_EQUAL ((*m)->get_name(), (*n)->get_name());
200                 BOOST_CHECK_EQUAL ((*m)->get_value(), (*n)->get_value());
201
202                 ++m;
203                 ++n;
204         }
205 }
206
207 void
208 check_xml (string ref, string test, vector<string> ignore, bool ignore_whitespace)
209 {
210         auto ref_parser = new xmlpp::DomParser ();
211         ref_parser->parse_memory (ref);
212         auto ref_root = ref_parser->get_document()->get_root_node ();
213         auto test_parser = new xmlpp::DomParser ();
214         test_parser->parse_memory (test);
215         auto test_root = test_parser->get_document()->get_root_node ();
216
217         check_xml (ref_root, test_root, ignore, ignore_whitespace);
218 }
219
220 void
221 check_file (boost::filesystem::path ref, boost::filesystem::path check)
222 {
223         uintmax_t size = boost::filesystem::file_size (ref);
224         BOOST_CHECK_EQUAL (size, boost::filesystem::file_size(check));
225         dcp::File ref_file(ref, "rb");
226         BOOST_REQUIRE (ref_file);
227         dcp::File check_file(check, "rb");
228         BOOST_REQUIRE (check_file);
229
230         int const buffer_size = 65536;
231         std::vector<uint8_t> ref_buffer(buffer_size);
232         std::vector<uint8_t> check_buffer(buffer_size);
233
234         uintmax_t pos = 0;
235
236         while (pos < size) {
237                 uintmax_t this_time = min (uintmax_t(buffer_size), size - pos);
238                 size_t r = ref_file.read(ref_buffer.data(), 1, this_time);
239                 BOOST_CHECK_EQUAL (r, this_time);
240                 r = check_file.read(check_buffer.data(), 1, this_time);
241                 BOOST_CHECK_EQUAL (r, this_time);
242
243                 if (memcmp(ref_buffer.data(), check_buffer.data(), this_time) != 0) {
244                         for (int i = 0; i < buffer_size; ++i) {
245                                 if (ref_buffer[i] != check_buffer[i]) {
246                                         BOOST_CHECK_MESSAGE (
247                                                 false,
248                                                 dcp::String::compose("File %1 differs from reference %2 at offset %3", check, ref, pos + i)
249                                                 );
250                                         break;
251                                 }
252                         }
253                         break;
254                 }
255
256                 pos += this_time;
257         }
258 }
259
260
261 RNGFixer::RNGFixer ()
262 {
263         Kumu::cth_test = true;
264         Kumu::FortunaRNG().Reset();
265 }
266
267
268 RNGFixer::~RNGFixer ()
269 {
270         Kumu::cth_test = false;
271 }
272
273
274 shared_ptr<dcp::MonoPictureAsset>
275 simple_picture (boost::filesystem::path path, string suffix, int frames, optional<dcp::Key> key)
276 {
277         dcp::MXFMetadata mxf_meta;
278         mxf_meta.company_name = "OpenDCP";
279         mxf_meta.product_name = "OpenDCP";
280         mxf_meta.product_version = "0.0.25";
281
282         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
283         mp->set_metadata (mxf_meta);
284         if (key) {
285                 mp->set_key (*key);
286         }
287         auto picture_writer = mp->start_write(path / dcp::String::compose("video%1.mxf", suffix), dcp::PictureAsset::Behaviour::MAKE_NEW);
288
289         dcp::Size const size (1998, 1080);
290         auto image = make_shared<dcp::OpenJPEGImage>(size);
291         for (int i = 0; i < 3; ++i) {
292                 memset (image->data(i), 0, 2 * size.width * size.height);
293         }
294         auto j2c = dcp::compress_j2k (image, 100000000, 24, false, false);
295
296         for (int i = 0; i < frames; ++i) {
297                 picture_writer->write (j2c.data(), j2c.size());
298         }
299         picture_writer->finalize ();
300
301         return mp;
302 }
303
304
305 shared_ptr<dcp::SoundAsset>
306 simple_sound(boost::filesystem::path path, string suffix, dcp::MXFMetadata mxf_meta, string language, int frames, int sample_rate, optional<dcp::Key> key, int channels)
307 {
308         /* Set a valid language, then overwrite it, so that the language parameter can be badly formed */
309         auto ms = make_shared<dcp::SoundAsset>(dcp::Fraction(24, 1), sample_rate, channels, dcp::LanguageTag("en-US"), dcp::Standard::SMPTE);
310         if (key) {
311                 ms->set_key (*key);
312         }
313         ms->_language = language;
314         ms->set_metadata (mxf_meta);
315         auto sound_writer = ms->start_write(path / dcp::String::compose("audio%1.mxf", suffix), {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
316
317         int const samples_per_frame = sample_rate / 24;
318
319         float* silence[channels];
320         for (auto i = 0; i < channels; ++i) {
321                 silence[i] = new float[samples_per_frame];
322                 memset (silence[i], 0, samples_per_frame * sizeof(float));
323         }
324
325         for (auto i = 0; i < frames; ++i) {
326                 sound_writer->write(silence, channels, samples_per_frame);
327         }
328
329         sound_writer->finalize ();
330
331         for (auto i = 0; i < channels; ++i) {
332                 delete[] silence[i];
333         }
334
335         return ms;
336 }
337
338
339 shared_ptr<dcp::DCP>
340 make_simple (boost::filesystem::path path, int reels, int frames, dcp::Standard standard, optional<dcp::Key> key)
341 {
342         /* Some known metadata */
343         dcp::MXFMetadata mxf_meta;
344         mxf_meta.company_name = "OpenDCP";
345         mxf_meta.product_name = "OpenDCP";
346         mxf_meta.product_version = "0.0.25";
347
348         auto constexpr sample_rate = 48000;
349
350         boost::filesystem::remove_all (path);
351         boost::filesystem::create_directories (path);
352         auto d = make_shared<dcp::DCP>(path);
353         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, standard);
354         cpl->set_annotation_text ("A Test DCP");
355         cpl->set_issuer ("OpenDCP 0.0.25");
356         cpl->set_creator ("OpenDCP 0.0.25");
357         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
358         cpl->set_content_version (
359                 dcp::ContentVersion("urn:uuid:75ac29aa-42ac-1234-ecae-49251abefd11", "content-version-label-text")
360                 );
361         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,R,C,LFE,Ls,Rs"));
362         cpl->set_main_sound_sample_rate(sample_rate);
363         cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
364         cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
365         cpl->set_version_number(1);
366
367         for (int i = 0; i < reels; ++i) {
368                 string suffix = reels == 1 ? "" : dcp::String::compose("%1", i);
369
370                 auto mp = simple_picture (path, suffix, frames, key);
371                 auto ms = simple_sound (path, suffix, mxf_meta, "en-US", frames, sample_rate, key);
372
373                 auto reel = make_shared<dcp::Reel>(
374                         shared_ptr<dcp::ReelMonoPictureAsset>(new dcp::ReelMonoPictureAsset(mp, 0)),
375                         shared_ptr<dcp::ReelSoundAsset>(new dcp::ReelSoundAsset(ms, 0))
376                         );
377
378                 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
379                 if (i == 0) {
380                         markers->set (dcp::Marker::FFOC, dcp::Time(0, 0, 0, 1, 24));
381                 }
382                 if (i == reels - 1) {
383                         markers->set (dcp::Marker::LFOC, dcp::Time(0, 0, 0, frames - 1, 24));
384                 }
385                 reel->add (markers);
386
387                 cpl->add (reel);
388         }
389
390         d->set_annotation_text("A Test DCP");
391         d->add (cpl);
392         return d;
393 }
394
395
396 shared_ptr<dcp::Subtitle>
397 simple_subtitle ()
398 {
399         return std::make_shared<dcp::SubtitleString>(
400                 optional<string>(),
401                 false,
402                 false,
403                 false,
404                 dcp::Colour(255, 255, 255),
405                 42,
406                 1,
407                 dcp::Time(0, 0, 4, 0, 24),
408                 dcp::Time(0, 0, 8, 0, 24),
409                 0.5,
410                 dcp::HAlign::CENTER,
411                 0.8,
412                 dcp::VAlign::TOP,
413                 0,
414                 dcp::Direction::LTR,
415                 "Hello world",
416                 dcp::Effect::NONE,
417                 dcp::Colour(255, 255, 255),
418                 dcp::Time(),
419                 dcp::Time(),
420                 0
421                 );
422 }
423
424
425 shared_ptr<dcp::ReelMarkersAsset>
426 simple_markers (int frames)
427 {
428         auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
429         markers->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
430         markers->set (dcp::Marker::LFOC, dcp::Time(frames - 1, 24, 24));
431         return markers;
432 }
433
434
435 shared_ptr<dcp::DCP>
436 make_simple_with_interop_subs (boost::filesystem::path path)
437 {
438         auto dcp = make_simple (path, 1, 24, dcp::Standard::INTEROP);
439
440         auto subs = make_shared<dcp::InteropSubtitleAsset>();
441         subs->add (simple_subtitle());
442
443         boost::filesystem::create_directory (path / "subs");
444         dcp::ArrayData data(4096);
445         memset(data.data(), 0, data.size());
446         subs->add_font ("afont", data);
447         subs->write (path / "subs" / "subs.xml");
448
449         auto reel_subs = make_shared<dcp::ReelInteropSubtitleAsset>(subs, dcp::Fraction(24, 1), 240, 0);
450         dcp->cpls().front()->reels().front()->add (reel_subs);
451
452         return dcp;
453 }
454
455
456 shared_ptr<dcp::DCP>
457 make_simple_with_smpte_subs (boost::filesystem::path path)
458 {
459         auto dcp = make_simple (path, 1, 192);
460
461         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
462         subs->set_language (dcp::LanguageTag("de-DE"));
463         subs->set_start_time (dcp::Time());
464         subs->add (simple_subtitle());
465         dcp::ArrayData fake_font(1024);
466         subs->add_font("font", fake_font);
467
468         subs->write (path / "subs.mxf");
469
470         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 192, 0);
471         dcp->cpls().front()->reels().front()->add (reel_subs);
472
473         return dcp;
474 }
475
476
477 shared_ptr<dcp::DCP>
478 make_simple_with_interop_ccaps (boost::filesystem::path path)
479 {
480         auto dcp = make_simple (path, 1, 24, dcp::Standard::INTEROP);
481
482         auto subs = make_shared<dcp::InteropSubtitleAsset>();
483         subs->add (simple_subtitle());
484         subs->write (path / "ccap.xml");
485
486         auto reel_caps = make_shared<dcp::ReelInteropClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 240, 0);
487         dcp->cpls()[0]->reels()[0]->add (reel_caps);
488
489         return dcp;
490 }
491
492
493 shared_ptr<dcp::DCP>
494 make_simple_with_smpte_ccaps (boost::filesystem::path path)
495 {
496         auto dcp = make_simple (path, 1, 192);
497
498         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
499         subs->set_language (dcp::LanguageTag("de-DE"));
500         subs->set_start_time (dcp::Time());
501         subs->add (simple_subtitle());
502         dcp::ArrayData fake_font(1024);
503         subs->add_font("font", fake_font);
504         subs->write (path / "ccap.mxf");
505
506         auto reel_caps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 192, 0);
507         dcp->cpls()[0]->reels()[0]->add(reel_caps);
508
509         return dcp;
510 }
511
512
513 shared_ptr<dcp::OpenJPEGImage>
514 black_image (dcp::Size size)
515 {
516         auto image = make_shared<dcp::OpenJPEGImage>(size);
517         int const pixels = size.width * size.height;
518         for (int i = 0; i < 3; ++i) {
519                 memset (image->data(i), 0, pixels * sizeof(int));
520         }
521         return image;
522 }
523
524
525 shared_ptr<dcp::ReelAsset>
526 black_picture_asset (boost::filesystem::path dir, int frames)
527 {
528         auto image = black_image ();
529         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
530         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
531
532         auto asset = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
533         asset->set_metadata (dcp::MXFMetadata("libdcp", "libdcp", "1.6.4devel"));
534         boost::filesystem::create_directories (dir);
535         auto writer = asset->start_write(dir / "pic.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
536         for (int i = 0; i < frames; ++i) {
537                 writer->write (frame.data(), frame.size());
538         }
539         writer->finalize ();
540
541         return make_shared<dcp::ReelMonoPictureAsset>(asset, 0);
542 }
543
544
545 boost::filesystem::path
546 find_file (boost::filesystem::path dir, string filename_part)
547 {
548         boost::optional<boost::filesystem::path> found;
549         for (auto i: boost::filesystem::directory_iterator(dir)) {
550                 if (i.path().filename().string().find(filename_part) != string::npos) {
551                         BOOST_REQUIRE (!found);
552                         found = i;
553                 }
554         }
555         BOOST_REQUIRE (found);
556         return *found;
557 }
558
559
560 Editor::Editor(boost::filesystem::path path)
561         : _path(path)
562 {
563         _content = dcp::file_to_string (_path);
564 }
565
566
567 Editor::~Editor()
568 {
569         auto f = fopen(_path.string().c_str(), "w");
570         BOOST_REQUIRE (f);
571         fwrite (_content.c_str(), _content.length(), 1, f);
572         fclose (f);
573 }
574
575
576 void
577 Editor::replace(string a, string b)
578 {
579         ChangeChecker cc(this);
580         boost::algorithm::replace_all (_content, a, b);
581 }
582
583
584 void
585 Editor::delete_first_line_containing(string s)
586 {
587         ChangeChecker cc(this);
588         auto lines = as_lines();
589         _content = "";
590         bool done = false;
591         for (auto i: lines) {
592                 if (i.find(s) == string::npos || done) {
593                         _content += i + "\n";
594                 } else {
595                         done = true;
596                 }
597         }
598 }
599
600 void
601 Editor::delete_lines(string from, string to)
602 {
603         ChangeChecker cc(this);
604         auto lines = as_lines();
605         bool deleting = false;
606         _content = "";
607         for (auto i: lines) {
608                 if (i.find(from) != string::npos) {
609                         deleting = true;
610                 }
611                 if (!deleting) {
612                         _content += i + "\n";
613                 }
614                 if (deleting && i.find(to) != string::npos) {
615                         deleting = false;
616                 }
617         }
618 }
619
620
621 void
622 Editor::insert(string after, string line)
623 {
624         ChangeChecker cc(this);
625         auto lines = as_lines();
626         _content = "";
627         bool replaced = false;
628         for (auto i: lines) {
629                 _content += i + "\n";
630                 if (!replaced && i.find(after) != string::npos) {
631                         _content += line + "\n";
632                         replaced = true;
633                 }
634         }
635 }
636
637
638 void
639 Editor::delete_lines_after(string after, int lines_to_delete)
640 {
641         ChangeChecker cc(this);
642         auto lines = as_lines();
643         _content = "";
644         auto iter = std::find_if(lines.begin(), lines.end(), [after](string const& line) {
645                 return line.find(after) != string::npos;
646         });
647         int to_delete = 0;
648         for (auto i = lines.begin(); i != lines.end(); ++i) {
649                 if (i == iter) {
650                         to_delete = lines_to_delete;
651                         _content += *i + "\n";
652                 } else if (to_delete == 0) {
653                         _content += *i + "\n";
654                 } else {
655                         --to_delete;
656                 }
657         }
658 }
659
660
661 vector<string>
662 Editor::as_lines() const
663 {
664         vector<string> lines;
665         boost::algorithm::split(lines, _content, boost::is_any_of("\r\n"), boost::token_compress_on);
666         return lines;
667 }
668
669
670 BOOST_GLOBAL_FIXTURE (TestConfig);