2 Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
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.
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.
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/>.
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
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.
34 #define BOOST_TEST_DYN_LINK
35 #define BOOST_TEST_MODULE libdcp_test
36 #include "compose.hpp"
39 #include "interop_subtitle_asset.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"
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"
63 LIBDCP_DISABLE_WARNINGS
64 #include <asdcp/KM_util.h>
65 #include <asdcp/KM_prng.h>
66 LIBDCP_ENABLE_WARNINGS
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>
80 using std::shared_ptr;
81 using std::make_shared;
82 using boost::optional;
85 boost::filesystem::path private_test;
86 boost::filesystem::path xsd_test = "build/test/xsd with spaces";
94 auto const argc = boost::unit_test::framework::master_test_suite().argc;
95 auto const argv = boost::unit_test::framework::master_test_suite().argv;
96 if (argc >= 3 && strcmp(argv[1], "--") == 0) {
97 /* For some reason on Ubuntu 16.04 the -- we pass in ends up in argv[1] so we
98 * need to get the private_test from argv[2]...
100 private_test = argv[2];
101 } else if (argc >= 2) {
102 /* ... but everywhere else it's removed, so we can just get the private_test
103 * value from argv[1].
105 private_test = argv[1];
108 using namespace boost::filesystem;
109 boost::system::error_code ec;
110 remove_all (xsd_test, ec);
111 boost::filesystem::create_directory (xsd_test);
112 for (directory_iterator i = directory_iterator("xsd"); i != directory_iterator(); ++i) {
113 copy_file (*i, xsd_test / i->path().filename());
120 check_xml (xmlpp::Element* ref, xmlpp::Element* test, vector<string> ignore_tags, bool ignore_whitespace)
122 BOOST_CHECK_EQUAL (ref->get_name (), test->get_name ());
123 BOOST_CHECK_EQUAL (ref->get_namespace_prefix (), test->get_namespace_prefix ());
125 if (find(ignore_tags.begin(), ignore_tags.end(), ref->get_name()) != ignore_tags.end()) {
129 auto whitespace_content = [](xmlpp::Node* node) {
130 auto content = dynamic_cast<xmlpp::ContentNode*>(node);
131 return content && content->get_content().find_first_not_of(" \t\r\n") == string::npos;
134 auto ref_children = ref->get_children ();
135 auto test_children = test->get_children ();
137 auto k = ref_children.begin ();
138 auto l = test_children.begin ();
139 while (k != ref_children.end() && l != test_children.end()) {
141 if (dynamic_cast<xmlpp::CommentNode*>(*k)) {
146 if (dynamic_cast<xmlpp::CommentNode*>(*l)) {
151 if (whitespace_content(*k) && ignore_whitespace) {
156 if (whitespace_content(*l) && ignore_whitespace) {
161 /* XXX: should be doing xmlpp::EntityReference, xmlpp::XIncludeEnd, xmlpp::XIncludeStart */
163 auto ref_el = dynamic_cast<xmlpp::Element*> (*k);
164 auto test_el = dynamic_cast<xmlpp::Element*> (*l);
165 BOOST_CHECK ((ref_el && test_el) || (!ref_el && !test_el));
166 if (ref_el && test_el) {
167 check_xml (ref_el, test_el, ignore_tags, ignore_whitespace);
170 auto ref_cn = dynamic_cast<xmlpp::ContentNode*> (*k);
171 auto test_cn = dynamic_cast<xmlpp::ContentNode*> (*l);
172 BOOST_CHECK ((ref_cn && test_cn) || (!ref_cn && !test_cn));
173 if (ref_cn && test_cn) {
174 BOOST_CHECK_EQUAL (ref_cn->get_content(), test_cn->get_content());
181 while (k != ref_children.end() && ignore_whitespace && whitespace_content(*k)) {
185 while (l != test_children.end() && ignore_whitespace && whitespace_content(*l)) {
189 BOOST_REQUIRE (k == ref_children.end());
190 BOOST_REQUIRE (l == test_children.end());
192 auto ref_attributes = ref->get_attributes ();
193 auto test_attributes = test->get_attributes ();
194 BOOST_CHECK_EQUAL (ref_attributes.size(), test_attributes.size ());
196 auto m = ref_attributes.begin();
197 auto n = test_attributes.begin();
198 while (m != ref_attributes.end ()) {
199 BOOST_CHECK_EQUAL ((*m)->get_name(), (*n)->get_name());
200 BOOST_CHECK_EQUAL ((*m)->get_value(), (*n)->get_value());
208 check_xml (string ref, string test, vector<string> ignore, bool ignore_whitespace)
210 auto ref_parser = new xmlpp::DomParser ();
211 ref_parser->parse_memory (ref);
212 auto ref_root = ref_parser->get_document()->get_root_node ();
213 auto test_parser = new xmlpp::DomParser ();
214 test_parser->parse_memory (test);
215 auto test_root = test_parser->get_document()->get_root_node ();
217 check_xml (ref_root, test_root, ignore, ignore_whitespace);
221 check_file (boost::filesystem::path ref, boost::filesystem::path check)
223 uintmax_t size = boost::filesystem::file_size (ref);
224 BOOST_CHECK_EQUAL (size, boost::filesystem::file_size(check));
225 dcp::File ref_file(ref, "rb");
226 BOOST_REQUIRE (ref_file);
227 dcp::File check_file(check, "rb");
228 BOOST_REQUIRE (check_file);
230 int const buffer_size = 65536;
231 std::vector<uint8_t> ref_buffer(buffer_size);
232 std::vector<uint8_t> check_buffer(buffer_size);
237 uintmax_t this_time = min (uintmax_t(buffer_size), size - pos);
238 size_t r = ref_file.read(ref_buffer.data(), 1, this_time);
239 BOOST_CHECK_EQUAL (r, this_time);
240 r = check_file.read(check_buffer.data(), 1, this_time);
241 BOOST_CHECK_EQUAL (r, this_time);
243 if (memcmp(ref_buffer.data(), check_buffer.data(), this_time) != 0) {
244 for (int i = 0; i < buffer_size; ++i) {
245 if (ref_buffer[i] != check_buffer[i]) {
246 BOOST_CHECK_MESSAGE (
248 dcp::String::compose("File %1 differs from reference %2 at offset %3", check, ref, pos + i)
261 RNGFixer::RNGFixer ()
263 Kumu::cth_test = true;
264 Kumu::FortunaRNG().Reset();
268 RNGFixer::~RNGFixer ()
270 Kumu::cth_test = false;
274 shared_ptr<dcp::MonoPictureAsset>
275 simple_picture (boost::filesystem::path path, string suffix, int frames, optional<dcp::Key> key)
277 dcp::MXFMetadata mxf_meta;
278 mxf_meta.company_name = "OpenDCP";
279 mxf_meta.product_name = "OpenDCP";
280 mxf_meta.product_version = "0.0.25";
282 auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
283 mp->set_metadata (mxf_meta);
287 auto picture_writer = mp->start_write(path / dcp::String::compose("video%1.mxf", suffix), dcp::PictureAsset::Behaviour::MAKE_NEW);
289 dcp::Size const size (1998, 1080);
290 auto image = make_shared<dcp::OpenJPEGImage>(size);
291 for (int i = 0; i < 3; ++i) {
292 memset (image->data(i), 0, 2 * size.width * size.height);
294 auto j2c = dcp::compress_j2k (image, 100000000, 24, false, false);
296 for (int i = 0; i < frames; ++i) {
297 picture_writer->write (j2c.data(), j2c.size());
299 picture_writer->finalize ();
305 shared_ptr<dcp::SoundAsset>
306 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)
308 /* Set a valid language, then overwrite it, so that the language parameter can be badly formed */
309 auto ms = make_shared<dcp::SoundAsset>(dcp::Fraction(24, 1), sample_rate, channels, dcp::LanguageTag("en-US"), dcp::Standard::SMPTE);
313 ms->_language = language;
314 ms->set_metadata (mxf_meta);
315 auto sound_writer = ms->start_write(path / dcp::String::compose("audio%1.mxf", suffix), {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
317 int const samples_per_frame = sample_rate / 24;
319 float* silence[channels];
320 for (auto i = 0; i < channels; ++i) {
321 silence[i] = new float[samples_per_frame];
322 memset (silence[i], 0, samples_per_frame * sizeof(float));
325 for (auto i = 0; i < frames; ++i) {
326 sound_writer->write(silence, channels, samples_per_frame);
329 sound_writer->finalize ();
331 for (auto i = 0; i < channels; ++i) {
340 make_simple (boost::filesystem::path path, int reels, int frames, dcp::Standard standard, optional<dcp::Key> key)
342 /* Some known metadata */
343 dcp::MXFMetadata mxf_meta;
344 mxf_meta.company_name = "OpenDCP";
345 mxf_meta.product_name = "OpenDCP";
346 mxf_meta.product_version = "0.0.25";
348 auto constexpr sample_rate = 48000;
350 boost::filesystem::remove_all (path);
351 boost::filesystem::create_directories (path);
352 auto d = make_shared<dcp::DCP>(path);
353 auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, standard);
354 cpl->set_annotation_text ("A Test DCP");
355 cpl->set_issuer ("OpenDCP 0.0.25");
356 cpl->set_creator ("OpenDCP 0.0.25");
357 cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
358 cpl->set_content_version (
359 dcp::ContentVersion("urn:uuid:75ac29aa-42ac-1234-ecae-49251abefd11", "content-version-label-text")
361 cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,R,C,LFE,Ls,Rs"));
362 cpl->set_main_sound_sample_rate(sample_rate);
363 cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
364 cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
365 cpl->set_version_number(1);
367 for (int i = 0; i < reels; ++i) {
368 string suffix = reels == 1 ? "" : dcp::String::compose("%1", i);
370 auto mp = simple_picture (path, suffix, frames, key);
371 auto ms = simple_sound (path, suffix, mxf_meta, "en-US", frames, sample_rate, key);
373 auto reel = make_shared<dcp::Reel>(
374 shared_ptr<dcp::ReelMonoPictureAsset>(new dcp::ReelMonoPictureAsset(mp, 0)),
375 shared_ptr<dcp::ReelSoundAsset>(new dcp::ReelSoundAsset(ms, 0))
378 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
380 markers->set (dcp::Marker::FFOC, dcp::Time(0, 0, 0, 1, 24));
382 if (i == reels - 1) {
383 markers->set (dcp::Marker::LFOC, dcp::Time(0, 0, 0, frames - 1, 24));
390 d->set_annotation_text("A Test DCP");
396 shared_ptr<dcp::Subtitle>
399 return std::make_shared<dcp::SubtitleString>(
404 dcp::Colour(255, 255, 255),
407 dcp::Time(0, 0, 4, 0, 24),
408 dcp::Time(0, 0, 8, 0, 24),
417 dcp::Colour(255, 255, 255),
421 std::vector<dcp::Ruby>()
426 shared_ptr<dcp::ReelMarkersAsset>
427 simple_markers (int frames)
429 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
430 markers->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
431 markers->set (dcp::Marker::LFOC, dcp::Time(frames - 1, 24, 24));
437 make_simple_with_interop_subs (boost::filesystem::path path)
439 auto dcp = make_simple (path, 1, 24, dcp::Standard::INTEROP);
441 auto subs = make_shared<dcp::InteropSubtitleAsset>();
442 subs->add (simple_subtitle());
444 boost::filesystem::create_directory (path / "subs");
445 dcp::ArrayData data(4096);
446 memset(data.data(), 0, data.size());
447 subs->add_font ("afont", data);
448 subs->write (path / "subs" / "subs.xml");
450 auto reel_subs = make_shared<dcp::ReelInteropSubtitleAsset>(subs, dcp::Fraction(24, 1), 240, 0);
451 dcp->cpls().front()->reels().front()->add (reel_subs);
458 make_simple_with_smpte_subs (boost::filesystem::path path)
460 auto dcp = make_simple (path, 1, 192);
462 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
463 subs->set_language (dcp::LanguageTag("de-DE"));
464 subs->set_start_time (dcp::Time());
465 subs->add (simple_subtitle());
466 dcp::ArrayData fake_font(1024);
467 subs->add_font("font", fake_font);
469 subs->write (path / "subs.mxf");
471 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 192, 0);
472 dcp->cpls().front()->reels().front()->add (reel_subs);
479 make_simple_with_interop_ccaps (boost::filesystem::path path)
481 auto dcp = make_simple (path, 1, 24, dcp::Standard::INTEROP);
483 auto subs = make_shared<dcp::InteropSubtitleAsset>();
484 subs->add (simple_subtitle());
485 subs->write (path / "ccap.xml");
487 auto reel_caps = make_shared<dcp::ReelInteropClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 240, 0);
488 dcp->cpls()[0]->reels()[0]->add (reel_caps);
495 make_simple_with_smpte_ccaps (boost::filesystem::path path)
497 auto dcp = make_simple (path, 1, 192);
499 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
500 subs->set_language (dcp::LanguageTag("de-DE"));
501 subs->set_start_time (dcp::Time());
502 subs->add (simple_subtitle());
503 dcp::ArrayData fake_font(1024);
504 subs->add_font("font", fake_font);
505 subs->write (path / "ccap.mxf");
507 auto reel_caps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 192, 0);
508 dcp->cpls()[0]->reels()[0]->add(reel_caps);
514 shared_ptr<dcp::OpenJPEGImage>
515 black_image (dcp::Size size)
517 auto image = make_shared<dcp::OpenJPEGImage>(size);
518 int const pixels = size.width * size.height;
519 for (int i = 0; i < 3; ++i) {
520 memset (image->data(i), 0, pixels * sizeof(int));
526 shared_ptr<dcp::ReelAsset>
527 black_picture_asset (boost::filesystem::path dir, int frames)
529 auto image = black_image ();
530 auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
531 BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
533 auto asset = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
534 asset->set_metadata (dcp::MXFMetadata("libdcp", "libdcp", "1.6.4devel"));
535 boost::filesystem::create_directories (dir);
536 auto writer = asset->start_write(dir / "pic.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
537 for (int i = 0; i < frames; ++i) {
538 writer->write (frame.data(), frame.size());
542 return make_shared<dcp::ReelMonoPictureAsset>(asset, 0);
546 boost::filesystem::path
547 find_file (boost::filesystem::path dir, string filename_part)
549 boost::optional<boost::filesystem::path> found;
550 for (auto i: boost::filesystem::directory_iterator(dir)) {
551 if (i.path().filename().string().find(filename_part) != string::npos) {
552 BOOST_REQUIRE (!found);
556 BOOST_REQUIRE (found);
561 Editor::Editor(boost::filesystem::path path)
564 _content = dcp::file_to_string (_path);
570 auto f = fopen(_path.string().c_str(), "w");
572 fwrite (_content.c_str(), _content.length(), 1, f);
578 Editor::replace(string a, string b)
580 ChangeChecker cc(this);
581 boost::algorithm::replace_all (_content, a, b);
586 Editor::delete_first_line_containing(string s)
588 ChangeChecker cc(this);
589 auto lines = as_lines();
592 for (auto i: lines) {
593 if (i.find(s) == string::npos || done) {
594 _content += i + "\n";
602 Editor::delete_lines(string from, string to)
604 ChangeChecker cc(this);
605 auto lines = as_lines();
606 bool deleting = false;
608 for (auto i: lines) {
609 if (i.find(from) != string::npos) {
613 _content += i + "\n";
615 if (deleting && i.find(to) != string::npos) {
623 Editor::insert(string after, string line)
625 ChangeChecker cc(this);
626 auto lines = as_lines();
628 bool replaced = false;
629 for (auto i: lines) {
630 _content += i + "\n";
631 if (!replaced && i.find(after) != string::npos) {
632 _content += line + "\n";
640 Editor::delete_lines_after(string after, int lines_to_delete)
642 ChangeChecker cc(this);
643 auto lines = as_lines();
645 auto iter = std::find_if(lines.begin(), lines.end(), [after](string const& line) {
646 return line.find(after) != string::npos;
649 for (auto i = lines.begin(); i != lines.end(); ++i) {
651 to_delete = lines_to_delete;
652 _content += *i + "\n";
653 } else if (to_delete == 0) {
654 _content += *i + "\n";
663 Editor::as_lines() const
665 vector<string> lines;
666 boost::algorithm::split(lines, _content, boost::is_any_of("\r\n"), boost::token_compress_on);
671 BOOST_GLOBAL_FIXTURE (TestConfig);