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