f946c5dcfed90da0b10a3a796cc4178157b03aa3
[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 "j2k_transcode.h"
41 #include "mono_picture_asset.h"
42 #include "mono_picture_asset.h"
43 #include "openjpeg_image.h"
44 #include "picture_asset_writer.h"
45 #include "picture_asset_writer.h"
46 #include "reel.h"
47 #include "reel_asset.h"
48 #include "reel_interop_closed_caption_asset.h"
49 #include "reel_interop_subtitle_asset.h"
50 #include "reel_markers_asset.h"
51 #include "reel_mono_picture_asset.h"
52 #include "reel_mono_picture_asset.h"
53 #include "reel_smpte_closed_caption_asset.h"
54 #include "reel_smpte_subtitle_asset.h"
55 #include "reel_sound_asset.h"
56 #include "smpte_subtitle_asset.h"
57 #include "sound_asset.h"
58 #include "sound_asset_writer.h"
59 #include "test.h"
60 #include "util.h"
61 #include "warnings.h"
62 LIBDCP_DISABLE_WARNINGS
63 #include <asdcp/KM_util.h>
64 #include <asdcp/KM_prng.h>
65 LIBDCP_ENABLE_WARNINGS
66 #include <sndfile.h>
67 LIBDCP_DISABLE_WARNINGS
68 #include <libxml++/libxml++.h>
69 LIBDCP_ENABLE_WARNINGS
70 #include <boost/test/unit_test.hpp>
71 #include <cstdio>
72 #include <iostream>
73
74
75 using std::string;
76 using std::min;
77 using std::vector;
78 using std::shared_ptr;
79 using std::make_shared;
80 using boost::optional;
81
82
83 boost::filesystem::path private_test;
84 boost::filesystem::path xsd_test = "build/test/xsd with spaces";
85
86
87 struct TestConfig
88 {
89         TestConfig()
90         {
91                 dcp::init ();
92                 if (boost::unit_test::framework::master_test_suite().argc >= 2) {
93                         private_test = boost::unit_test::framework::master_test_suite().argv[1];
94                 }
95
96                 using namespace boost::filesystem;
97                 boost::system::error_code ec;
98                 remove_all (xsd_test, ec);
99                 boost::filesystem::create_directory (xsd_test);
100                 for (directory_iterator i = directory_iterator("xsd"); i != directory_iterator(); ++i) {
101                         copy_file (*i, xsd_test / i->path().filename());
102                 }
103         }
104 };
105
106
107 void
108 check_xml (xmlpp::Element* ref, xmlpp::Element* test, vector<string> ignore_tags, bool ignore_whitespace)
109 {
110         BOOST_CHECK_EQUAL (ref->get_name (), test->get_name ());
111         BOOST_CHECK_EQUAL (ref->get_namespace_prefix (), test->get_namespace_prefix ());
112
113         if (find(ignore_tags.begin(), ignore_tags.end(), ref->get_name()) != ignore_tags.end()) {
114                 return;
115         }
116
117         auto whitespace_content = [](xmlpp::Node* node) {
118                 auto content = dynamic_cast<xmlpp::ContentNode*>(node);
119                 return content && content->get_content().find_first_not_of(" \t\r\n") == string::npos;
120         };
121
122         auto ref_children = ref->get_children ();
123         auto test_children = test->get_children ();
124
125         auto k = ref_children.begin ();
126         auto l = test_children.begin ();
127         while (k != ref_children.end() && l != test_children.end()) {
128
129                 if (dynamic_cast<xmlpp::CommentNode*>(*k)) {
130                         ++k;
131                         continue;
132                 }
133
134                 if (dynamic_cast<xmlpp::CommentNode*>(*l)) {
135                         ++l;
136                         continue;
137                 }
138
139                 if (whitespace_content(*k) && ignore_whitespace) {
140                         ++k;
141                         continue;
142                 }
143
144                 if (whitespace_content(*l) && ignore_whitespace) {
145                         ++l;
146                         continue;
147                 }
148
149                 /* XXX: should be doing xmlpp::EntityReference, xmlpp::XIncludeEnd, xmlpp::XIncludeStart */
150
151                 auto ref_el = dynamic_cast<xmlpp::Element*> (*k);
152                 auto test_el = dynamic_cast<xmlpp::Element*> (*l);
153                 BOOST_CHECK ((ref_el && test_el) || (!ref_el && !test_el));
154                 if (ref_el && test_el) {
155                         check_xml (ref_el, test_el, ignore_tags, ignore_whitespace);
156                 }
157
158                 auto ref_cn = dynamic_cast<xmlpp::ContentNode*> (*k);
159                 auto test_cn = dynamic_cast<xmlpp::ContentNode*> (*l);
160                 BOOST_CHECK ((ref_cn && test_cn) || (!ref_cn && !test_cn));
161                 if (ref_cn && test_cn) {
162                         BOOST_CHECK_EQUAL (ref_cn->get_content(), test_cn->get_content());
163                 }
164
165                 ++k;
166                 ++l;
167         }
168
169         while (k != ref_children.end() && ignore_whitespace && whitespace_content(*k)) {
170                 ++k;
171         }
172
173         while (l != test_children.end() && ignore_whitespace && whitespace_content(*l)) {
174                 ++l;
175         }
176
177         BOOST_REQUIRE (k == ref_children.end());
178         BOOST_REQUIRE (l == test_children.end());
179
180         auto ref_attributes = ref->get_attributes ();
181         auto test_attributes = test->get_attributes ();
182         BOOST_CHECK_EQUAL (ref_attributes.size(), test_attributes.size ());
183
184         auto m = ref_attributes.begin();
185         auto n = test_attributes.begin();
186         while (m != ref_attributes.end ()) {
187                 BOOST_CHECK_EQUAL ((*m)->get_name(), (*n)->get_name());
188                 BOOST_CHECK_EQUAL ((*m)->get_value(), (*n)->get_value());
189
190                 ++m;
191                 ++n;
192         }
193 }
194
195 void
196 check_xml (string ref, string test, vector<string> ignore, bool ignore_whitespace)
197 {
198         auto ref_parser = new xmlpp::DomParser ();
199         ref_parser->parse_memory (ref);
200         auto ref_root = ref_parser->get_document()->get_root_node ();
201         auto test_parser = new xmlpp::DomParser ();
202         test_parser->parse_memory (test);
203         auto test_root = test_parser->get_document()->get_root_node ();
204
205         check_xml (ref_root, test_root, ignore, ignore_whitespace);
206 }
207
208 void
209 check_file (boost::filesystem::path ref, boost::filesystem::path check)
210 {
211         uintmax_t size = boost::filesystem::file_size (ref);
212         BOOST_CHECK_EQUAL (size, boost::filesystem::file_size(check));
213         auto ref_file = dcp::fopen_boost (ref, "rb");
214         BOOST_REQUIRE (ref_file);
215         auto check_file = dcp::fopen_boost (check, "rb");
216         BOOST_REQUIRE (check_file);
217
218         int const buffer_size = 65536;
219         auto ref_buffer = new uint8_t[buffer_size];
220         auto check_buffer = new uint8_t[buffer_size];
221
222         uintmax_t pos = 0;
223
224         while (pos < size) {
225                 uintmax_t this_time = min (uintmax_t(buffer_size), size - pos);
226                 size_t r = fread (ref_buffer, 1, this_time, ref_file);
227                 BOOST_CHECK_EQUAL (r, this_time);
228                 r = fread (check_buffer, 1, this_time, check_file);
229                 BOOST_CHECK_EQUAL (r, this_time);
230
231                 if (memcmp(ref_buffer, check_buffer, this_time) != 0) {
232                         for (int i = 0; i < buffer_size; ++i) {
233                                 if (ref_buffer[i] != check_buffer[i]) {
234                                         BOOST_CHECK_MESSAGE (
235                                                 false,
236                                                 dcp::String::compose("File %1 differs from reference %2 at offset %3", check, ref, pos + i)
237                                                 );
238                                         break;
239                                 }
240                         }
241                         break;
242                 }
243
244                 pos += this_time;
245         }
246
247         delete[] ref_buffer;
248         delete[] check_buffer;
249
250         fclose (ref_file);
251         fclose (check_file);
252 }
253
254
255 RNGFixer::RNGFixer ()
256 {
257         Kumu::cth_test = true;
258         Kumu::FortunaRNG().Reset();
259 }
260
261
262 RNGFixer::~RNGFixer ()
263 {
264         Kumu::cth_test = false;
265 }
266
267
268 shared_ptr<dcp::MonoPictureAsset>
269 simple_picture (boost::filesystem::path path, string suffix, int frames, optional<dcp::Key> key)
270 {
271         dcp::MXFMetadata mxf_meta;
272         mxf_meta.company_name = "OpenDCP";
273         mxf_meta.product_name = "OpenDCP";
274         mxf_meta.product_version = "0.0.25";
275
276         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
277         mp->set_metadata (mxf_meta);
278         if (key) {
279                 mp->set_key (*key);
280         }
281         auto picture_writer = mp->start_write (path / dcp::String::compose("video%1.mxf", suffix), false);
282
283         dcp::Size const size (1998, 1080);
284         auto image = make_shared<dcp::OpenJPEGImage>(size);
285         for (int i = 0; i < 3; ++i) {
286                 memset (image->data(i), 0, 2 * size.width * size.height);
287         }
288         auto j2c = dcp::compress_j2k (image, 100000000, 24, false, false);
289
290         for (int i = 0; i < frames; ++i) {
291                 picture_writer->write (j2c.data(), j2c.size());
292         }
293         picture_writer->finalize ();
294
295         return mp;
296 }
297
298
299 shared_ptr<dcp::SoundAsset>
300 simple_sound (boost::filesystem::path path, string suffix, dcp::MXFMetadata mxf_meta, string language, int frames, int sample_rate, optional<dcp::Key> key)
301 {
302         int const channels = 6;
303
304         /* Set a valid language, then overwrite it, so that the language parameter can be badly formed */
305         auto ms = make_shared<dcp::SoundAsset>(dcp::Fraction(24, 1), sample_rate, channels, dcp::LanguageTag("en-US"), dcp::Standard::SMPTE);
306         if (key) {
307                 ms->set_key (*key);
308         }
309         ms->_language = language;
310         ms->set_metadata (mxf_meta);
311         shared_ptr<dcp::SoundAssetWriter> sound_writer = ms->start_write (path / dcp::String::compose("audio%1.mxf", suffix));
312
313         int const samples_per_frame = sample_rate / 24;
314
315         float* silence[channels];
316         for (auto i = 0; i < channels; ++i) {
317                 silence[i] = new float[samples_per_frame];
318                 memset (silence[i], 0, samples_per_frame * sizeof(float));
319         }
320
321         for (auto i = 0; i < frames; ++i) {
322                 sound_writer->write (silence, samples_per_frame);
323         }
324
325         sound_writer->finalize ();
326
327         for (auto i = 0; i < channels; ++i) {
328                 delete[] silence[i];
329         }
330
331         return ms;
332 }
333
334
335 shared_ptr<dcp::DCP>
336 make_simple (boost::filesystem::path path, int reels, int frames, dcp::Standard standard, optional<dcp::Key> key)
337 {
338         /* Some known metadata */
339         dcp::MXFMetadata mxf_meta;
340         mxf_meta.company_name = "OpenDCP";
341         mxf_meta.product_name = "OpenDCP";
342         mxf_meta.product_version = "0.0.25";
343
344         auto constexpr sample_rate = 48000;
345
346         boost::filesystem::remove_all (path);
347         boost::filesystem::create_directories (path);
348         auto d = make_shared<dcp::DCP>(path);
349         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, standard);
350         cpl->set_annotation_text ("A Test DCP");
351         cpl->set_issuer ("OpenDCP 0.0.25");
352         cpl->set_creator ("OpenDCP 0.0.25");
353         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
354         cpl->set_content_version (
355                 dcp::ContentVersion("urn:uuid:75ac29aa-42ac-1234-ecae-49251abefd11", "content-version-label-text")
356                 );
357         cpl->set_main_sound_configuration("51/L,R,C,LFE,Ls,Rs");
358         cpl->set_main_sound_sample_rate(sample_rate);
359         cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
360         cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
361         cpl->set_version_number(1);
362
363         for (int i = 0; i < reels; ++i) {
364                 string suffix = reels == 1 ? "" : dcp::String::compose("%1", i);
365
366                 auto mp = simple_picture (path, suffix, frames, key);
367                 auto ms = simple_sound (path, suffix, mxf_meta, "en-US", frames, sample_rate, key);
368
369                 auto reel = make_shared<dcp::Reel>(
370                         shared_ptr<dcp::ReelMonoPictureAsset>(new dcp::ReelMonoPictureAsset(mp, 0)),
371                         shared_ptr<dcp::ReelSoundAsset>(new dcp::ReelSoundAsset(ms, 0))
372                         );
373
374                 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames, 0);
375                 if (i == 0) {
376                         markers->set (dcp::Marker::FFOC, dcp::Time(0, 0, 0, 1, 24));
377                 }
378                 if (i == reels - 1) {
379                         markers->set (dcp::Marker::LFOC, dcp::Time(0, 0, 0, frames - 1, 24));
380                 }
381                 reel->add (markers);
382
383                 cpl->add (reel);
384         }
385
386         d->add (cpl);
387         return d;
388 }
389
390
391 shared_ptr<dcp::Subtitle>
392 simple_subtitle ()
393 {
394         return make_shared<dcp::SubtitleString>(
395                 optional<string>(),
396                 false,
397                 false,
398                 false,
399                 dcp::Colour(255, 255, 255),
400                 42,
401                 1,
402                 dcp::Time(0, 0, 4, 0, 24),
403                 dcp::Time(0, 0, 8, 0, 24),
404                 0.5,
405                 dcp::HAlign::CENTER,
406                 0.8,
407                 dcp::VAlign::TOP,
408                 dcp::Direction::LTR,
409                 "Hello world",
410                 dcp::Effect::NONE,
411                 dcp::Colour(255, 255, 255),
412                 dcp::Time(),
413                 dcp::Time()
414                 );
415 }
416
417
418 shared_ptr<dcp::ReelMarkersAsset>
419 simple_markers (int frames)
420 {
421         auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames, 0);
422         markers->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
423         markers->set (dcp::Marker::LFOC, dcp::Time(frames - 1, 24, 24));
424         return markers;
425 }
426
427
428 shared_ptr<dcp::DCP>
429 make_simple_with_interop_subs (boost::filesystem::path path)
430 {
431         auto dcp = make_simple (path, 1, 24, dcp::Standard::INTEROP);
432
433         auto subs = make_shared<dcp::InteropSubtitleAsset>();
434         subs->add (simple_subtitle());
435
436         boost::filesystem::create_directory (path / "subs");
437         dcp::ArrayData data(4096);
438         subs->add_font ("afont", data);
439         subs->write (path / "subs" / "subs.xml");
440
441         auto reel_subs = make_shared<dcp::ReelInteropSubtitleAsset>(subs, dcp::Fraction(24, 1), 240, 0);
442         dcp->cpls().front()->reels().front()->add (reel_subs);
443
444         return dcp;
445 }
446
447
448 shared_ptr<dcp::DCP>
449 make_simple_with_smpte_subs (boost::filesystem::path path)
450 {
451         auto dcp = make_simple (path, 1, 192);
452
453         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
454         subs->set_language (dcp::LanguageTag("de-DE"));
455         subs->set_start_time (dcp::Time());
456         subs->add (simple_subtitle());
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         subs->write (path / "ccap.mxf");
493
494         auto reel_caps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 192, 0);
495         dcp->cpls()[0]->reels()[0]->add(reel_caps);
496
497         return dcp;
498 }
499
500
501 shared_ptr<dcp::OpenJPEGImage>
502 black_image (dcp::Size size)
503 {
504         auto image = make_shared<dcp::OpenJPEGImage>(size);
505         int const pixels = size.width * size.height;
506         for (int i = 0; i < 3; ++i) {
507                 memset (image->data(i), 0, pixels * sizeof(int));
508         }
509         return image;
510 }
511
512
513 shared_ptr<dcp::ReelAsset>
514 black_picture_asset (boost::filesystem::path dir, int frames)
515 {
516         auto image = black_image ();
517         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
518         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
519
520         auto asset = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
521         asset->set_metadata (dcp::MXFMetadata("libdcp", "libdcp", "1.6.4devel"));
522         boost::filesystem::create_directories (dir);
523         auto writer = asset->start_write (dir / "pic.mxf", true);
524         for (int i = 0; i < frames; ++i) {
525                 writer->write (frame.data(), frame.size());
526         }
527         writer->finalize ();
528
529         return make_shared<dcp::ReelMonoPictureAsset>(asset, 0);
530 }
531
532
533 boost::filesystem::path
534 find_file (boost::filesystem::path dir, string filename_part)
535 {
536         boost::optional<boost::filesystem::path> found;
537         for (auto i: boost::filesystem::directory_iterator(dir)) {
538                 if (i.path().filename().string().find(filename_part) != string::npos) {
539                         BOOST_REQUIRE (!found);
540                         found = i;
541                 }
542         }
543         BOOST_REQUIRE (found);
544         return *found;
545 }
546
547
548 BOOST_GLOBAL_FIXTURE (TestConfig);