Add verifier check for the actual asset file's ID not being the same as the one in...
[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         std::vector<uint8_t> ref_buffer(buffer_size);
221         std::vector<uint8_t> check_buffer(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.data(), 1, this_time);
228                 BOOST_CHECK_EQUAL (r, this_time);
229                 r = check_file.read(check_buffer.data(), 1, this_time);
230                 BOOST_CHECK_EQUAL (r, this_time);
231
232                 if (memcmp(ref_buffer.data(), check_buffer.data(), 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
249
250 RNGFixer::RNGFixer ()
251 {
252         Kumu::cth_test = true;
253         Kumu::FortunaRNG().Reset();
254 }
255
256
257 RNGFixer::~RNGFixer ()
258 {
259         Kumu::cth_test = false;
260 }
261
262
263 shared_ptr<dcp::MonoPictureAsset>
264 simple_picture (boost::filesystem::path path, string suffix, int frames, optional<dcp::Key> key)
265 {
266         dcp::MXFMetadata mxf_meta;
267         mxf_meta.company_name = "OpenDCP";
268         mxf_meta.product_name = "OpenDCP";
269         mxf_meta.product_version = "0.0.25";
270
271         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
272         mp->set_metadata (mxf_meta);
273         if (key) {
274                 mp->set_key (*key);
275         }
276         auto picture_writer = mp->start_write(path / dcp::String::compose("video%1.mxf", suffix), dcp::PictureAsset::Behaviour::MAKE_NEW);
277
278         dcp::Size const size (1998, 1080);
279         auto image = make_shared<dcp::OpenJPEGImage>(size);
280         for (int i = 0; i < 3; ++i) {
281                 memset (image->data(i), 0, 2 * size.width * size.height);
282         }
283         auto j2c = dcp::compress_j2k (image, 100000000, 24, false, false);
284
285         for (int i = 0; i < frames; ++i) {
286                 picture_writer->write (j2c.data(), j2c.size());
287         }
288         picture_writer->finalize ();
289
290         return mp;
291 }
292
293
294 shared_ptr<dcp::SoundAsset>
295 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)
296 {
297         /* Set a valid language, then overwrite it, so that the language parameter can be badly formed */
298         auto ms = make_shared<dcp::SoundAsset>(dcp::Fraction(24, 1), sample_rate, channels, dcp::LanguageTag("en-US"), dcp::Standard::SMPTE);
299         if (key) {
300                 ms->set_key (*key);
301         }
302         ms->_language = language;
303         ms->set_metadata (mxf_meta);
304         auto sound_writer = ms->start_write(path / dcp::String::compose("audio%1.mxf", suffix), {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
305
306         int const samples_per_frame = sample_rate / 24;
307
308         float* silence[channels];
309         for (auto i = 0; i < channels; ++i) {
310                 silence[i] = new float[samples_per_frame];
311                 memset (silence[i], 0, samples_per_frame * sizeof(float));
312         }
313
314         for (auto i = 0; i < frames; ++i) {
315                 sound_writer->write(silence, channels, samples_per_frame);
316         }
317
318         sound_writer->finalize ();
319
320         for (auto i = 0; i < channels; ++i) {
321                 delete[] silence[i];
322         }
323
324         return ms;
325 }
326
327
328 shared_ptr<dcp::DCP>
329 make_simple (boost::filesystem::path path, int reels, int frames, dcp::Standard standard, optional<dcp::Key> key)
330 {
331         /* Some known metadata */
332         dcp::MXFMetadata mxf_meta;
333         mxf_meta.company_name = "OpenDCP";
334         mxf_meta.product_name = "OpenDCP";
335         mxf_meta.product_version = "0.0.25";
336
337         auto constexpr sample_rate = 48000;
338
339         boost::filesystem::remove_all (path);
340         boost::filesystem::create_directories (path);
341         auto d = make_shared<dcp::DCP>(path);
342         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, standard);
343         cpl->set_annotation_text ("A Test DCP");
344         cpl->set_issuer ("OpenDCP 0.0.25");
345         cpl->set_creator ("OpenDCP 0.0.25");
346         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
347         cpl->set_content_version (
348                 dcp::ContentVersion("urn:uuid:75ac29aa-42ac-1234-ecae-49251abefd11", "content-version-label-text")
349                 );
350         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,R,C,LFE,Ls,Rs"));
351         cpl->set_main_sound_sample_rate(sample_rate);
352         cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
353         cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
354         cpl->set_version_number(1);
355
356         for (int i = 0; i < reels; ++i) {
357                 string suffix = reels == 1 ? "" : dcp::String::compose("%1", i);
358
359                 auto mp = simple_picture (path, suffix, frames, key);
360                 auto ms = simple_sound (path, suffix, mxf_meta, "en-US", frames, sample_rate, key);
361
362                 auto reel = make_shared<dcp::Reel>(
363                         shared_ptr<dcp::ReelMonoPictureAsset>(new dcp::ReelMonoPictureAsset(mp, 0)),
364                         shared_ptr<dcp::ReelSoundAsset>(new dcp::ReelSoundAsset(ms, 0))
365                         );
366
367                 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
368                 if (i == 0) {
369                         markers->set (dcp::Marker::FFOC, dcp::Time(0, 0, 0, 1, 24));
370                 }
371                 if (i == reels - 1) {
372                         markers->set (dcp::Marker::LFOC, dcp::Time(0, 0, 0, frames - 1, 24));
373                 }
374                 reel->add (markers);
375
376                 cpl->add (reel);
377         }
378
379         d->set_annotation_text("A Test DCP");
380         d->add (cpl);
381         return d;
382 }
383
384
385 shared_ptr<dcp::Subtitle>
386 simple_subtitle ()
387 {
388         return std::make_shared<dcp::SubtitleString>(
389                 optional<string>(),
390                 false,
391                 false,
392                 false,
393                 dcp::Colour(255, 255, 255),
394                 42,
395                 1,
396                 dcp::Time(0, 0, 4, 0, 24),
397                 dcp::Time(0, 0, 8, 0, 24),
398                 0.5,
399                 dcp::HAlign::CENTER,
400                 0.8,
401                 dcp::VAlign::TOP,
402                 0,
403                 dcp::Direction::LTR,
404                 "Hello world",
405                 dcp::Effect::NONE,
406                 dcp::Colour(255, 255, 255),
407                 dcp::Time(),
408                 dcp::Time(),
409                 0
410                 );
411 }
412
413
414 shared_ptr<dcp::ReelMarkersAsset>
415 simple_markers (int frames)
416 {
417         auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
418         markers->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
419         markers->set (dcp::Marker::LFOC, dcp::Time(frames - 1, 24, 24));
420         return markers;
421 }
422
423
424 shared_ptr<dcp::DCP>
425 make_simple_with_interop_subs (boost::filesystem::path path)
426 {
427         auto dcp = make_simple (path, 1, 24, dcp::Standard::INTEROP);
428
429         auto subs = make_shared<dcp::InteropSubtitleAsset>();
430         subs->add (simple_subtitle());
431
432         boost::filesystem::create_directory (path / "subs");
433         dcp::ArrayData data(4096);
434         memset(data.data(), 0, data.size());
435         subs->add_font ("afont", data);
436         subs->write (path / "subs" / "subs.xml");
437
438         auto reel_subs = make_shared<dcp::ReelInteropSubtitleAsset>(subs, dcp::Fraction(24, 1), 240, 0);
439         dcp->cpls().front()->reels().front()->add (reel_subs);
440
441         return dcp;
442 }
443
444
445 shared_ptr<dcp::DCP>
446 make_simple_with_smpte_subs (boost::filesystem::path path)
447 {
448         auto dcp = make_simple (path, 1, 192);
449
450         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
451         subs->set_language (dcp::LanguageTag("de-DE"));
452         subs->set_start_time (dcp::Time());
453         subs->add (simple_subtitle());
454         dcp::ArrayData fake_font(1024);
455         subs->add_font("font", fake_font);
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         dcp::ArrayData fake_font(1024);
492         subs->add_font("font", fake_font);
493         subs->write (path / "ccap.mxf");
494
495         auto reel_caps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 192, 0);
496         dcp->cpls()[0]->reels()[0]->add(reel_caps);
497
498         return dcp;
499 }
500
501
502 shared_ptr<dcp::OpenJPEGImage>
503 black_image (dcp::Size size)
504 {
505         auto image = make_shared<dcp::OpenJPEGImage>(size);
506         int const pixels = size.width * size.height;
507         for (int i = 0; i < 3; ++i) {
508                 memset (image->data(i), 0, pixels * sizeof(int));
509         }
510         return image;
511 }
512
513
514 shared_ptr<dcp::ReelAsset>
515 black_picture_asset (boost::filesystem::path dir, int frames)
516 {
517         auto image = black_image ();
518         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
519         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
520
521         auto asset = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
522         asset->set_metadata (dcp::MXFMetadata("libdcp", "libdcp", "1.6.4devel"));
523         boost::filesystem::create_directories (dir);
524         auto writer = asset->start_write(dir / "pic.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
525         for (int i = 0; i < frames; ++i) {
526                 writer->write (frame.data(), frame.size());
527         }
528         writer->finalize ();
529
530         return make_shared<dcp::ReelMonoPictureAsset>(asset, 0);
531 }
532
533
534 boost::filesystem::path
535 find_file (boost::filesystem::path dir, string filename_part)
536 {
537         boost::optional<boost::filesystem::path> found;
538         for (auto i: boost::filesystem::directory_iterator(dir)) {
539                 if (i.path().filename().string().find(filename_part) != string::npos) {
540                         BOOST_REQUIRE (!found);
541                         found = i;
542                 }
543         }
544         BOOST_REQUIRE (found);
545         return *found;
546 }
547
548
549 BOOST_GLOBAL_FIXTURE (TestConfig);