Cleanup: use std::vector and some auto.
[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), false);
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)
296 {
297         int const channels = 6;
298
299         /* Set a valid language, then overwrite it, so that the language parameter can be badly formed */
300         auto ms = make_shared<dcp::SoundAsset>(dcp::Fraction(24, 1), sample_rate, channels, dcp::LanguageTag("en-US"), dcp::Standard::SMPTE);
301         if (key) {
302                 ms->set_key (*key);
303         }
304         ms->_language = language;
305         ms->set_metadata (mxf_meta);
306         shared_ptr<dcp::SoundAssetWriter> sound_writer = ms->start_write (path / dcp::String::compose("audio%1.mxf", suffix));
307
308         int const samples_per_frame = sample_rate / 24;
309
310         float* silence[channels];
311         for (auto i = 0; i < channels; ++i) {
312                 silence[i] = new float[samples_per_frame];
313                 memset (silence[i], 0, samples_per_frame * sizeof(float));
314         }
315
316         for (auto i = 0; i < frames; ++i) {
317                 sound_writer->write (silence, samples_per_frame);
318         }
319
320         sound_writer->finalize ();
321
322         for (auto i = 0; i < channels; ++i) {
323                 delete[] silence[i];
324         }
325
326         return ms;
327 }
328
329
330 shared_ptr<dcp::DCP>
331 make_simple (boost::filesystem::path path, int reels, int frames, dcp::Standard standard, optional<dcp::Key> key)
332 {
333         /* Some known metadata */
334         dcp::MXFMetadata mxf_meta;
335         mxf_meta.company_name = "OpenDCP";
336         mxf_meta.product_name = "OpenDCP";
337         mxf_meta.product_version = "0.0.25";
338
339         auto constexpr sample_rate = 48000;
340
341         boost::filesystem::remove_all (path);
342         boost::filesystem::create_directories (path);
343         auto d = make_shared<dcp::DCP>(path);
344         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, standard);
345         cpl->set_annotation_text ("A Test DCP");
346         cpl->set_issuer ("OpenDCP 0.0.25");
347         cpl->set_creator ("OpenDCP 0.0.25");
348         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
349         cpl->set_content_version (
350                 dcp::ContentVersion("urn:uuid:75ac29aa-42ac-1234-ecae-49251abefd11", "content-version-label-text")
351                 );
352         cpl->set_main_sound_configuration("51/L,R,C,LFE,Ls,Rs");
353         cpl->set_main_sound_sample_rate(sample_rate);
354         cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
355         cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
356         cpl->set_version_number(1);
357
358         for (int i = 0; i < reels; ++i) {
359                 string suffix = reels == 1 ? "" : dcp::String::compose("%1", i);
360
361                 auto mp = simple_picture (path, suffix, frames, key);
362                 auto ms = simple_sound (path, suffix, mxf_meta, "en-US", frames, sample_rate, key);
363
364                 auto reel = make_shared<dcp::Reel>(
365                         shared_ptr<dcp::ReelMonoPictureAsset>(new dcp::ReelMonoPictureAsset(mp, 0)),
366                         shared_ptr<dcp::ReelSoundAsset>(new dcp::ReelSoundAsset(ms, 0))
367                         );
368
369                 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
370                 if (i == 0) {
371                         markers->set (dcp::Marker::FFOC, dcp::Time(0, 0, 0, 1, 24));
372                 }
373                 if (i == reels - 1) {
374                         markers->set (dcp::Marker::LFOC, dcp::Time(0, 0, 0, frames - 1, 24));
375                 }
376                 reel->add (markers);
377
378                 cpl->add (reel);
379         }
380
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
456         subs->write (path / "subs.mxf");
457
458         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 192, 0);
459         dcp->cpls().front()->reels().front()->add (reel_subs);
460
461         return dcp;
462 }
463
464
465 shared_ptr<dcp::DCP>
466 make_simple_with_interop_ccaps (boost::filesystem::path path)
467 {
468         auto dcp = make_simple (path, 1, 24, dcp::Standard::INTEROP);
469
470         auto subs = make_shared<dcp::InteropSubtitleAsset>();
471         subs->add (simple_subtitle());
472         subs->write (path / "ccap.xml");
473
474         auto reel_caps = make_shared<dcp::ReelInteropClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 240, 0);
475         dcp->cpls()[0]->reels()[0]->add (reel_caps);
476
477         return dcp;
478 }
479
480
481 shared_ptr<dcp::DCP>
482 make_simple_with_smpte_ccaps (boost::filesystem::path path)
483 {
484         auto dcp = make_simple (path, 1, 192);
485
486         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
487         subs->set_language (dcp::LanguageTag("de-DE"));
488         subs->set_start_time (dcp::Time());
489         subs->add (simple_subtitle());
490         subs->write (path / "ccap.mxf");
491
492         auto reel_caps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 192, 0);
493         dcp->cpls()[0]->reels()[0]->add(reel_caps);
494
495         return dcp;
496 }
497
498
499 shared_ptr<dcp::OpenJPEGImage>
500 black_image (dcp::Size size)
501 {
502         auto image = make_shared<dcp::OpenJPEGImage>(size);
503         int const pixels = size.width * size.height;
504         for (int i = 0; i < 3; ++i) {
505                 memset (image->data(i), 0, pixels * sizeof(int));
506         }
507         return image;
508 }
509
510
511 shared_ptr<dcp::ReelAsset>
512 black_picture_asset (boost::filesystem::path dir, int frames)
513 {
514         auto image = black_image ();
515         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
516         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
517
518         auto asset = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
519         asset->set_metadata (dcp::MXFMetadata("libdcp", "libdcp", "1.6.4devel"));
520         boost::filesystem::create_directories (dir);
521         auto writer = asset->start_write (dir / "pic.mxf", true);
522         for (int i = 0; i < frames; ++i) {
523                 writer->write (frame.data(), frame.size());
524         }
525         writer->finalize ();
526
527         return make_shared<dcp::ReelMonoPictureAsset>(asset, 0);
528 }
529
530
531 boost::filesystem::path
532 find_file (boost::filesystem::path dir, string filename_part)
533 {
534         boost::optional<boost::filesystem::path> found;
535         for (auto i: boost::filesystem::directory_iterator(dir)) {
536                 if (i.path().filename().string().find(filename_part) != string::npos) {
537                         BOOST_REQUIRE (!found);
538                         found = i;
539                 }
540         }
541         BOOST_REQUIRE (found);
542         return *found;
543 }
544
545
546 BOOST_GLOBAL_FIXTURE (TestConfig);