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