Reduce test flakyness a bit.
[libdcp.git] / test / verify_test.cc
1 /*
2     Copyright (C) 2018-2021 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 #include "compose.hpp"
35 #include "cpl.h"
36 #include "dcp.h"
37 #include "interop_subtitle_asset.h"
38 #include "j2k_transcode.h"
39 #include "mono_picture_asset.h"
40 #include "mono_picture_asset_writer.h"
41 #include "openjpeg_image.h"
42 #include "raw_convert.h"
43 #include "reel.h"
44 #include "reel_interop_closed_caption_asset.h"
45 #include "reel_interop_subtitle_asset.h"
46 #include "reel_markers_asset.h"
47 #include "reel_mono_picture_asset.h"
48 #include "reel_sound_asset.h"
49 #include "reel_stereo_picture_asset.h"
50 #include "reel_smpte_closed_caption_asset.h"
51 #include "reel_smpte_subtitle_asset.h"
52 #include "smpte_subtitle_asset.h"
53 #include "stereo_picture_asset.h"
54 #include "stream_operators.h"
55 #include "test.h"
56 #include "util.h"
57 #include "verify.h"
58 #include "verify_j2k.h"
59 #include <boost/test/unit_test.hpp>
60 #include <boost/algorithm/string.hpp>
61 #include <cstdio>
62 #include <iostream>
63
64
65 using std::list;
66 using std::pair;
67 using std::string;
68 using std::vector;
69 using std::make_pair;
70 using std::make_shared;
71 using boost::optional;
72 using namespace boost::filesystem;
73 using std::shared_ptr;
74
75
76 static list<pair<string, optional<path>>> stages;
77
78 static string filename_to_id(boost::filesystem::path path)
79 {
80         return path.string().substr(4, path.string().length() - 8);
81 }
82
83 static boost::filesystem::path const dcp_test1_pkl = find_file("test/ref/DCP/dcp_test1", "pkl_").filename();
84 static string const dcp_test1_pkl_id = filename_to_id(dcp_test1_pkl);
85
86 static boost::filesystem::path const dcp_test1_cpl = find_file("test/ref/DCP/dcp_test1", "cpl_").filename();
87 static string const dcp_test1_cpl_id = filename_to_id(dcp_test1_cpl);
88
89 static string const dcp_test1_asset_map_id = "5d51e8a1-b2a5-4da6-9b66-4615c3609440";
90
91 static boost::filesystem::path const encryption_test_cpl = find_file("test/ref/DCP/encryption_test", "cpl_").filename();
92 static string const encryption_test_cpl_id = filename_to_id(encryption_test_cpl);
93
94 static boost::filesystem::path const encryption_test_pkl = find_file("test/ref/DCP/encryption_test", "pkl_").filename();
95 static string const encryption_test_pkl_id = filename_to_id(encryption_test_pkl);
96
97 static void
98 stage (string s, optional<path> p)
99 {
100         stages.push_back (make_pair (s, p));
101 }
102
103 static void
104 progress (float)
105 {
106
107 }
108
109 static void
110 prepare_directory (path path)
111 {
112         using namespace boost::filesystem;
113         remove_all (path);
114         create_directories (path);
115 }
116
117
118 static path
119 setup (int reference_number, string verify_test_suffix)
120 {
121         auto const dir = dcp::String::compose("build/test/verify_test%1", verify_test_suffix);
122         prepare_directory (dir);
123         for (auto i: directory_iterator(dcp::String::compose("test/ref/DCP/dcp_test%1", reference_number))) {
124                 copy_file (i.path(), dir / i.path().filename());
125         }
126
127         return dir;
128 }
129
130
131 static
132 shared_ptr<dcp::CPL>
133 write_dcp_with_single_asset (path dir, shared_ptr<dcp::ReelAsset> reel_asset, dcp::Standard standard = dcp::Standard::SMPTE)
134 {
135         auto reel = make_shared<dcp::Reel>();
136         reel->add (reel_asset);
137         reel->add (simple_markers());
138
139         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, standard);
140         cpl->add (reel);
141         auto dcp = make_shared<dcp::DCP>(dir);
142         dcp->add (cpl);
143         dcp->write_xml (
144                 dcp::String::compose("libdcp %1", dcp::version),
145                 dcp::String::compose("libdcp %1", dcp::version),
146                 dcp::LocalTime().as_string(),
147                 "hello"
148                 );
149
150         return cpl;
151 }
152
153
154 /** Class that can alter a file by searching and replacing strings within it.
155  *  On destruction modifies the file whose name was given to the constructor.
156  */
157 class Editor
158 {
159 public:
160         Editor (path path)
161                 : _path(path)
162         {
163                 _content = dcp::file_to_string (_path);
164         }
165
166         ~Editor ()
167         {
168                 auto f = fopen(_path.string().c_str(), "w");
169                 BOOST_REQUIRE (f);
170                 fwrite (_content.c_str(), _content.length(), 1, f);
171                 fclose (f);
172         }
173
174         void replace (string a, string b)
175         {
176                 auto old_content = _content;
177                 boost::algorithm::replace_all (_content, a, b);
178                 BOOST_REQUIRE (_content != old_content);
179         }
180
181         void delete_first_line_containing (string s)
182         {
183                 vector<string> lines;
184                 boost::algorithm::split (lines, _content, boost::is_any_of("\r\n"), boost::token_compress_on);
185                 auto old_content = _content;
186                 _content = "";
187                 bool done = false;
188                 for (auto i: lines) {
189                         if (i.find(s) == string::npos || done) {
190                                 _content += i + "\n";
191                         } else {
192                                 done = true;
193                         }
194                 }
195                 BOOST_REQUIRE (_content != old_content);
196         }
197
198         void delete_lines (string from, string to)
199         {
200                 vector<string> lines;
201                 boost::algorithm::split (lines, _content, boost::is_any_of("\r\n"), boost::token_compress_on);
202                 bool deleting = false;
203                 auto old_content = _content;
204                 _content = "";
205                 for (auto i: lines) {
206                         if (i.find(from) != string::npos) {
207                                 deleting = true;
208                         }
209                         if (!deleting) {
210                                 _content += i + "\n";
211                         }
212                         if (deleting && i.find(to) != string::npos) {
213                                 deleting = false;
214                         }
215                 }
216                 BOOST_REQUIRE (_content != old_content);
217         }
218
219 private:
220         path _path;
221         std::string _content;
222 };
223
224
225 LIBDCP_DISABLE_WARNINGS
226 static
227 void
228 dump_notes (vector<dcp::VerificationNote> const & notes)
229 {
230         for (auto i: notes) {
231                 std::cout << dcp::note_to_string(i) << "\n";
232         }
233 }
234 LIBDCP_ENABLE_WARNINGS
235
236
237 static
238 void
239 check_verify_result (vector<path> dir, vector<dcp::VerificationNote> test_notes)
240 {
241         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
242         BOOST_REQUIRE_EQUAL (notes.size(), test_notes.size());
243         std::sort (notes.begin(), notes.end());
244         std::sort (test_notes.begin(), test_notes.end());
245         for (auto i = 0U; i < notes.size(); ++i) {
246                 BOOST_REQUIRE_EQUAL (notes[i], test_notes[i]);
247         }
248 }
249
250
251 static
252 void
253 check_verify_result_after_replace (string suffix, boost::function<path (string)> file, string from, string to, vector<dcp::VerificationNote::Code> codes)
254 {
255         auto dir = setup (1, suffix);
256
257         {
258                 Editor e (file(suffix));
259                 e.replace (from, to);
260         }
261
262         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
263
264         BOOST_REQUIRE_EQUAL (notes.size(), codes.size());
265         auto i = notes.begin();
266         auto j = codes.begin();
267         while (i != notes.end()) {
268                 BOOST_CHECK_EQUAL (i->code(), *j);
269                 ++i;
270                 ++j;
271         }
272 }
273
274
275 BOOST_AUTO_TEST_CASE (verify_no_error)
276 {
277         stages.clear ();
278         auto dir = setup (1, "no_error");
279         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
280
281         path const cpl_file = dir / dcp_test1_cpl;
282         path const pkl_file = dir / dcp_test1_pkl;
283         path const assetmap_file = dir / "ASSETMAP.xml";
284
285         auto st = stages.begin();
286         BOOST_CHECK_EQUAL (st->first, "Checking DCP");
287         BOOST_REQUIRE (st->second);
288         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir));
289         ++st;
290         BOOST_CHECK_EQUAL (st->first, "Checking CPL");
291         BOOST_REQUIRE (st->second);
292         BOOST_CHECK_EQUAL (st->second.get(), canonical(cpl_file));
293         ++st;
294         BOOST_CHECK_EQUAL (st->first, "Checking reel");
295         BOOST_REQUIRE (!st->second);
296         ++st;
297         BOOST_CHECK_EQUAL (st->first, "Checking picture asset hash");
298         BOOST_REQUIRE (st->second);
299         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "video.mxf"));
300         ++st;
301         BOOST_CHECK_EQUAL (st->first, "Checking picture frame sizes");
302         BOOST_REQUIRE (st->second);
303         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "video.mxf"));
304         ++st;
305         BOOST_CHECK_EQUAL (st->first, "Checking sound asset hash");
306         BOOST_REQUIRE (st->second);
307         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "audio.mxf"));
308         ++st;
309         BOOST_CHECK_EQUAL (st->first, "Checking sound asset metadata");
310         BOOST_REQUIRE (st->second);
311         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "audio.mxf"));
312         ++st;
313         BOOST_CHECK_EQUAL (st->first, "Checking PKL");
314         BOOST_REQUIRE (st->second);
315         BOOST_CHECK_EQUAL (st->second.get(), canonical(pkl_file));
316         ++st;
317         BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
318         BOOST_REQUIRE (st->second);
319         BOOST_CHECK_EQUAL (st->second.get(), canonical(assetmap_file));
320         ++st;
321         BOOST_REQUIRE (st == stages.end());
322
323         BOOST_CHECK_EQUAL (notes.size(), 0);
324 }
325
326
327 BOOST_AUTO_TEST_CASE (verify_incorrect_picture_sound_hash)
328 {
329         using namespace boost::filesystem;
330
331         auto dir = setup (1, "incorrect_picture_sound_hash");
332
333         auto video_path = path(dir / "video.mxf");
334         auto mod = fopen(video_path.string().c_str(), "r+b");
335         BOOST_REQUIRE (mod);
336         fseek (mod, 4096, SEEK_SET);
337         int x = 42;
338         fwrite (&x, sizeof(x), 1, mod);
339         fclose (mod);
340
341         auto audio_path = path(dir / "audio.mxf");
342         mod = fopen(audio_path.string().c_str(), "r+b");
343         BOOST_REQUIRE (mod);
344         BOOST_REQUIRE_EQUAL (fseek(mod, -64, SEEK_END), 0);
345         BOOST_REQUIRE (fwrite (&x, sizeof(x), 1, mod) == 1);
346         fclose (mod);
347
348         dcp::ASDCPErrorSuspender sus;
349         check_verify_result (
350                 { dir },
351                 {
352                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_PICTURE_HASH, canonical(video_path) },
353                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_SOUND_HASH, canonical(audio_path) },
354                 });
355 }
356
357
358 BOOST_AUTO_TEST_CASE (verify_mismatched_picture_sound_hashes)
359 {
360         using namespace boost::filesystem;
361
362         auto dir = setup (1, "mismatched_picture_sound_hashes");
363
364         {
365                 Editor e (dir / dcp_test1_pkl);
366                 e.replace ("<Hash>", "<Hash>x");
367         }
368
369         check_verify_result (
370                 { dir },
371                 {
372                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, dcp_test1_cpl_id, canonical(dir / dcp_test1_cpl) },
373                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_PICTURE_HASHES, canonical(dir / "video.mxf") },
374                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_SOUND_HASHES, canonical(dir / "audio.mxf") },
375                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xD9HpNAjKsECGtJEWtwV2/T5ndG8=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 26 },
376                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xgVKhC9IkWyzQbgzpFcJ1bpqbtwk=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 19 },
377                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xz/y4fI+ocWzVB7cr8pVt2OYpOHA=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 12 }
378                 });
379 }
380
381
382 BOOST_AUTO_TEST_CASE (verify_failed_read_content_kind)
383 {
384         auto dir = setup (1, "failed_read_content_kind");
385
386         {
387                 Editor e (dir / dcp_test1_cpl);
388                 e.replace ("<ContentKind>", "<ContentKind>x");
389         }
390
391         check_verify_result (
392                 { dir },
393                 {{ dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::FAILED_READ, string("Bad content kind 'xtrailer'")}}
394                 );
395 }
396
397
398 static
399 path
400 cpl (string suffix)
401 {
402         return dcp::String::compose("build/test/verify_test%1/%2", suffix, dcp_test1_cpl);
403 }
404
405
406 static
407 path
408 pkl (string suffix)
409 {
410         return dcp::String::compose("build/test/verify_test%1/%2", suffix, dcp_test1_pkl);
411 }
412
413
414 static
415 path
416 asset_map (string suffix)
417 {
418         return dcp::String::compose("build/test/verify_test%1/ASSETMAP.xml", suffix);
419 }
420
421
422 BOOST_AUTO_TEST_CASE (verify_invalid_picture_frame_rate)
423 {
424         check_verify_result_after_replace (
425                         "invalid_picture_frame_rate", &cpl,
426                         "<FrameRate>24 1", "<FrameRate>99 1",
427                         { dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES,
428                           dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE }
429                         );
430 }
431
432 BOOST_AUTO_TEST_CASE (verify_missing_asset)
433 {
434         auto dir = setup (1, "missing_asset");
435         remove (dir / "video.mxf");
436         check_verify_result (
437                 { dir },
438                 {
439                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_ASSET, canonical(dir) / "video.mxf" }
440                 });
441 }
442
443
444 BOOST_AUTO_TEST_CASE (verify_empty_asset_path)
445 {
446         check_verify_result_after_replace (
447                         "empty_asset_path", &asset_map,
448                         "<Path>video.mxf</Path>", "<Path></Path>",
449                         { dcp::VerificationNote::Code::EMPTY_ASSET_PATH }
450                         );
451 }
452
453
454 BOOST_AUTO_TEST_CASE (verify_mismatched_standard)
455 {
456         check_verify_result_after_replace (
457                         "mismatched_standard", &cpl,
458                         "http://www.smpte-ra.org/schemas/429-7/2006/CPL", "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#",
459                         { dcp::VerificationNote::Code::MISMATCHED_STANDARD,
460                           dcp::VerificationNote::Code::INVALID_XML,
461                           dcp::VerificationNote::Code::INVALID_XML,
462                           dcp::VerificationNote::Code::INVALID_XML,
463                           dcp::VerificationNote::Code::INVALID_XML,
464                           dcp::VerificationNote::Code::INVALID_XML,
465                           dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES }
466                         );
467 }
468
469
470 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_id)
471 {
472         /* There's no MISMATCHED_CPL_HASHES error here because it can't find the correct hash by ID (since the ID is wrong) */
473         check_verify_result_after_replace (
474                         "invalid_xml_cpl_id", &cpl,
475                         "<Id>urn:uuid:81fb54df-e1bf-4647-8788-ea7ba154375b", "<Id>urn:uuid:81fb54df-e1bf-4647-8788-ea7ba154375",
476                         { dcp::VerificationNote::Code::INVALID_XML }
477                         );
478 }
479
480
481 BOOST_AUTO_TEST_CASE (verify_invalid_xml_issue_date)
482 {
483         check_verify_result_after_replace (
484                         "invalid_xml_issue_date", &cpl,
485                         "<IssueDate>", "<IssueDate>x",
486                         { dcp::VerificationNote::Code::INVALID_XML,
487                           dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES }
488                         );
489 }
490
491
492 BOOST_AUTO_TEST_CASE (verify_invalid_xml_pkl_id)
493 {
494         check_verify_result_after_replace (
495                 "invalid_xml_pkl_id", &pkl,
496                 "<Id>urn:uuid:" + dcp_test1_pkl_id.substr(0, 3),
497                 "<Id>urn:uuid:x" + dcp_test1_pkl_id.substr(1, 2),
498                 { dcp::VerificationNote::Code::INVALID_XML }
499                 );
500 }
501
502
503 BOOST_AUTO_TEST_CASE (verify_invalid_xml_asset_map_id)
504 {
505         check_verify_result_after_replace (
506                 "invalix_xml_asset_map_id", &asset_map,
507                 "<Id>urn:uuid:" + dcp_test1_asset_map_id.substr(0, 3),
508                 "<Id>urn:uuid:x" + dcp_test1_asset_map_id.substr(1, 2),
509                 { dcp::VerificationNote::Code::INVALID_XML }
510                 );
511 }
512
513
514 BOOST_AUTO_TEST_CASE (verify_invalid_standard)
515 {
516         stages.clear ();
517         auto dir = setup (3, "verify_invalid_standard");
518         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
519
520         path const cpl_file = dir / "cpl_cbfd2bc0-21cf-4a8f-95d8-9cddcbe51296.xml";
521         path const pkl_file = dir / "pkl_d87a950c-bd6f-41f6-90cc-56ccd673e131.xml";
522         path const assetmap_file = dir / "ASSETMAP";
523
524         auto st = stages.begin();
525         BOOST_CHECK_EQUAL (st->first, "Checking DCP");
526         BOOST_REQUIRE (st->second);
527         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir));
528         ++st;
529         BOOST_CHECK_EQUAL (st->first, "Checking CPL");
530         BOOST_REQUIRE (st->second);
531         BOOST_CHECK_EQUAL (st->second.get(), canonical(cpl_file));
532         ++st;
533         BOOST_CHECK_EQUAL (st->first, "Checking reel");
534         BOOST_REQUIRE (!st->second);
535         ++st;
536         BOOST_CHECK_EQUAL (st->first, "Checking picture asset hash");
537         BOOST_REQUIRE (st->second);
538         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "j2c_c6035f97-b07d-4e1c-944d-603fc2ddc242.mxf"));
539         ++st;
540         BOOST_CHECK_EQUAL (st->first, "Checking picture frame sizes");
541         BOOST_REQUIRE (st->second);
542         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "j2c_c6035f97-b07d-4e1c-944d-603fc2ddc242.mxf"));
543         ++st;
544         BOOST_CHECK_EQUAL (st->first, "Checking sound asset hash");
545         BOOST_REQUIRE (st->second);
546         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "pcm_69cf9eaf-9a99-4776-b022-6902208626c3.mxf"));
547         ++st;
548         BOOST_CHECK_EQUAL (st->first, "Checking sound asset metadata");
549         BOOST_REQUIRE (st->second);
550         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "pcm_69cf9eaf-9a99-4776-b022-6902208626c3.mxf"));
551         ++st;
552         BOOST_CHECK_EQUAL (st->first, "Checking PKL");
553         BOOST_REQUIRE (st->second);
554         BOOST_CHECK_EQUAL (st->second.get(), canonical(pkl_file));
555         ++st;
556         BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
557         BOOST_REQUIRE (st->second);
558         BOOST_CHECK_EQUAL (st->second.get(), canonical(assetmap_file));
559         ++st;
560         BOOST_REQUIRE (st == stages.end());
561
562         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
563         auto i = notes.begin ();
564         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::Type::BV21_ERROR);
565         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::Code::INVALID_STANDARD);
566         ++i;
567         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::Type::BV21_ERROR);
568         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K);
569 }
570
571 /* DCP with a short asset */
572 BOOST_AUTO_TEST_CASE (verify_invalid_duration)
573 {
574         auto dir = setup (8, "invalid_duration");
575         check_verify_result (
576                 { dir },
577                 {
578                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
579                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_DURATION, string("d7576dcb-a361-4139-96b8-267f5f8d7f91") },
580                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_INTRINSIC_DURATION, string("d7576dcb-a361-4139-96b8-267f5f8d7f91") },
581                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_DURATION, string("a2a87f5d-b749-4a7e-8d0c-9d48a4abf626") },
582                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_INTRINSIC_DURATION, string("a2a87f5d-b749-4a7e-8d0c-9d48a4abf626") },
583                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K, string("2") }
584                 });
585 }
586
587
588 static
589 shared_ptr<dcp::CPL>
590 dcp_from_frame (dcp::ArrayData const& frame, path dir)
591 {
592         auto asset = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
593         create_directories (dir);
594         auto writer = asset->start_write (dir / "pic.mxf", true);
595         for (int i = 0; i < 24; ++i) {
596                 writer->write (frame.data(), frame.size());
597         }
598         writer->finalize ();
599
600         auto reel_asset = make_shared<dcp::ReelMonoPictureAsset>(asset, 0);
601         return write_dcp_with_single_asset (dir, reel_asset);
602 }
603
604
605 BOOST_AUTO_TEST_CASE (verify_invalid_picture_frame_size_in_bytes)
606 {
607         int const too_big = 1302083 * 2;
608
609         /* Compress a black image */
610         auto image = black_image ();
611         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
612         BOOST_REQUIRE (frame.size() < too_big);
613
614         /* Place it in a bigger block with some zero padding at the end */
615         dcp::ArrayData oversized_frame(too_big);
616         memcpy (oversized_frame.data(), frame.data(), frame.size());
617         memset (oversized_frame.data() + frame.size(), 0, too_big - frame.size());
618
619         path const dir("build/test/verify_invalid_picture_frame_size_in_bytes");
620         prepare_directory (dir);
621         auto cpl = dcp_from_frame (oversized_frame, dir);
622
623         check_verify_result (
624                 { dir },
625                 {
626                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_CODESTREAM, string("missing marker start byte") },
627                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, canonical(dir / "pic.mxf") },
628                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
629                 });
630 }
631
632
633 BOOST_AUTO_TEST_CASE (verify_nearly_invalid_picture_frame_size_in_bytes)
634 {
635         int const nearly_too_big = 1302083 * 0.98;
636
637         /* Compress a black image */
638         auto image = black_image ();
639         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
640         BOOST_REQUIRE (frame.size() < nearly_too_big);
641
642         /* Place it in a bigger block with some zero padding at the end */
643         dcp::ArrayData oversized_frame(nearly_too_big);
644         memcpy (oversized_frame.data(), frame.data(), frame.size());
645         memset (oversized_frame.data() + frame.size(), 0, nearly_too_big - frame.size());
646
647         path const dir("build/test/verify_nearly_invalid_picture_frame_size_in_bytes");
648         prepare_directory (dir);
649         auto cpl = dcp_from_frame (oversized_frame, dir);
650
651         check_verify_result (
652                 { dir },
653                 {
654                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_CODESTREAM, string("missing marker start byte") },
655                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, canonical(dir / "pic.mxf") },
656                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
657                 });
658 }
659
660
661 BOOST_AUTO_TEST_CASE (verify_valid_picture_frame_size_in_bytes)
662 {
663         /* Compress a black image */
664         auto image = black_image ();
665         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
666         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
667
668         path const dir("build/test/verify_valid_picture_frame_size_in_bytes");
669         prepare_directory (dir);
670         auto cpl = dcp_from_frame (frame, dir);
671
672         check_verify_result ({ dir }, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
673 }
674
675
676 BOOST_AUTO_TEST_CASE (verify_valid_interop_subtitles)
677 {
678         path const dir("build/test/verify_valid_interop_subtitles");
679         prepare_directory (dir);
680         copy_file ("test/data/subs1.xml", dir / "subs.xml");
681         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
682         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
683         write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
684
685         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD }});
686 }
687
688
689 BOOST_AUTO_TEST_CASE (verify_invalid_interop_subtitles)
690 {
691         using namespace boost::filesystem;
692
693         path const dir("build/test/verify_invalid_interop_subtitles");
694         prepare_directory (dir);
695         copy_file ("test/data/subs1.xml", dir / "subs.xml");
696         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
697         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
698         write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
699
700         {
701                 Editor e (dir / "subs.xml");
702                 e.replace ("</ReelNumber>", "</ReelNumber><Foo></Foo>");
703         }
704
705         check_verify_result (
706                 { dir },
707                 {
708                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
709                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'Foo'"), path(), 5 },
710                         {
711                                 dcp::VerificationNote::Type::ERROR,
712                                 dcp::VerificationNote::Code::INVALID_XML,
713                                 string("element 'Foo' is not allowed for content model '(SubtitleID,MovieTitle,ReelNumber,Language,LoadFont*,Font*,Subtitle*)'"),
714                                 path(),
715                                 29
716                         }
717                 });
718 }
719
720
721 BOOST_AUTO_TEST_CASE (verify_valid_smpte_subtitles)
722 {
723         path const dir("build/test/verify_valid_smpte_subtitles");
724         prepare_directory (dir);
725         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
726         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
727         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
728         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
729
730         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
731 }
732
733
734 BOOST_AUTO_TEST_CASE (verify_invalid_smpte_subtitles)
735 {
736         using namespace boost::filesystem;
737
738         path const dir("build/test/verify_invalid_smpte_subtitles");
739         prepare_directory (dir);
740         /* This broken_smpte.mxf does not use urn:uuid: for its subtitle ID, which we tolerate (rightly or wrongly) */
741         copy_file ("test/data/broken_smpte.mxf", dir / "subs.mxf");
742         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
743         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
744         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
745
746         check_verify_result (
747                 { dir },
748                 {
749                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'Foo'"), path(), 2 },
750                         {
751                                 dcp::VerificationNote::Type::ERROR,
752                                 dcp::VerificationNote::Code::INVALID_XML,
753                                 string("element 'Foo' is not allowed for content model '(Id,ContentTitleText,AnnotationText?,IssueDate,ReelNumber?,Language?,EditRate,TimeCodeRate,StartTime?,DisplayType?,LoadFont*,SubtitleList)'"),
754                                 path(),
755                                 2
756                         },
757                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
758                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
759                 });
760 }
761
762
763 BOOST_AUTO_TEST_CASE (verify_external_asset)
764 {
765         path const ov_dir("build/test/verify_external_asset");
766         prepare_directory (ov_dir);
767
768         auto image = black_image ();
769         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
770         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
771         dcp_from_frame (frame, ov_dir);
772
773         dcp::DCP ov (ov_dir);
774         ov.read ();
775
776         path const vf_dir("build/test/verify_external_asset_vf");
777         prepare_directory (vf_dir);
778
779         auto picture = ov.cpls()[0]->reels()[0]->main_picture();
780         auto cpl = write_dcp_with_single_asset (vf_dir, picture);
781
782         check_verify_result (
783                 { vf_dir },
784                 {
785                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EXTERNAL_ASSET, picture->asset()->id() },
786                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
787                 });
788 }
789
790
791 BOOST_AUTO_TEST_CASE (verify_valid_cpl_metadata)
792 {
793         path const dir("build/test/verify_valid_cpl_metadata");
794         prepare_directory (dir);
795
796         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
797         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
798         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
799
800         auto reel = make_shared<dcp::Reel>();
801         reel->add (reel_asset);
802
803         reel->add (make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", 16 * 24), 0));
804         reel->add (simple_markers(16 * 24));
805
806         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
807         cpl->add (reel);
808         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
809         cpl->set_main_sound_sample_rate (48000);
810         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
811         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
812         cpl->set_version_number (1);
813
814         dcp::DCP dcp (dir);
815         dcp.add (cpl);
816         dcp.write_xml (
817                 dcp::String::compose("libdcp %1", dcp::version),
818                 dcp::String::compose("libdcp %1", dcp::version),
819                 dcp::LocalTime().as_string(),
820                 "hello"
821                 );
822 }
823
824
825 path find_cpl (path dir)
826 {
827         for (auto i: directory_iterator(dir)) {
828                 if (boost::starts_with(i.path().filename().string(), "cpl_")) {
829                         return i.path();
830                 }
831         }
832
833         BOOST_REQUIRE (false);
834         return {};
835 }
836
837
838 /* DCP with invalid CompositionMetadataAsset */
839 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_bad_tag)
840 {
841         using namespace boost::filesystem;
842
843         path const dir("build/test/verify_invalid_cpl_metadata_bad_tag");
844         prepare_directory (dir);
845
846         auto reel = make_shared<dcp::Reel>();
847         reel->add (black_picture_asset(dir));
848         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
849         cpl->add (reel);
850         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
851         cpl->set_main_sound_sample_rate (48000);
852         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
853         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
854         cpl->set_version_number (1);
855
856         reel->add (simple_markers());
857
858         dcp::DCP dcp (dir);
859         dcp.add (cpl);
860         dcp.write_xml (
861                 dcp::String::compose("libdcp %1", dcp::version),
862                 dcp::String::compose("libdcp %1", dcp::version),
863                 dcp::LocalTime().as_string(),
864                 "hello"
865                 );
866
867         {
868                 Editor e (find_cpl(dir));
869                 e.replace ("MainSound", "MainSoundX");
870         }
871
872         check_verify_result (
873                 { dir },
874                 {
875                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXConfiguration'"), canonical(cpl->file().get()), 54 },
876                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXSampleRate'"), canonical(cpl->file().get()), 55 },
877                         {
878                                 dcp::VerificationNote::Type::ERROR,
879                                 dcp::VerificationNote::Code::INVALID_XML,
880                                 string("element 'meta:MainSoundXConfiguration' is not allowed for content model "
881                                        "'(Id,AnnotationText?,EditRate,IntrinsicDuration,EntryPoint?,Duration?,"
882                                        "FullContentTitleText,ReleaseTerritory?,VersionNumber?,Chain?,Distributor?,"
883                                        "Facility?,AlternateContentVersionList?,Luminance?,MainSoundConfiguration,"
884                                        "MainSoundSampleRate,MainPictureStoredArea,MainPictureActiveArea,MainSubtitleLanguageList?,"
885                                        "ExtensionMetadataList?,)'"),
886                                 canonical(cpl->file().get()),
887                                 75
888                         },
889                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) },
890                 });
891 }
892
893
894 /* DCP with invalid CompositionMetadataAsset */
895 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_missing_tag)
896 {
897         path const dir("build/test/verify_invalid_cpl_metadata_missing_tag");
898         prepare_directory (dir);
899
900         auto reel = make_shared<dcp::Reel>();
901         reel->add (black_picture_asset(dir));
902         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
903         cpl->add (reel);
904         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
905         cpl->set_main_sound_sample_rate (48000);
906         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
907         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
908
909         dcp::DCP dcp (dir);
910         dcp.add (cpl);
911         dcp.write_xml (
912                 dcp::String::compose("libdcp %1", dcp::version),
913                 dcp::String::compose("libdcp %1", dcp::version),
914                 dcp::LocalTime().as_string(),
915                 "hello"
916                 );
917
918         {
919                 Editor e (find_cpl(dir));
920                 e.replace ("meta:Width", "meta:WidthX");
921         }
922
923         check_verify_result (
924                 { dir },
925                 {{ dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::FAILED_READ, string("missing XML tag Width in MainPictureStoredArea") }}
926                 );
927 }
928
929
930 BOOST_AUTO_TEST_CASE (verify_invalid_language1)
931 {
932         path const dir("build/test/verify_invalid_language1");
933         prepare_directory (dir);
934         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
935         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
936         asset->_language = "wrong-andbad";
937         asset->write (dir / "subs.mxf");
938         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
939         reel_asset->_language = "badlang";
940         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
941
942         check_verify_result (
943                 { dir },
944                 {
945                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
946                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
947                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
948                 });
949 }
950
951
952 /* SMPTE DCP with invalid <Language> in the MainClosedCaption reel and also in the XML within the MXF */
953 BOOST_AUTO_TEST_CASE (verify_invalid_language2)
954 {
955         path const dir("build/test/verify_invalid_language2");
956         prepare_directory (dir);
957         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
958         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
959         asset->_language = "wrong-andbad";
960         asset->write (dir / "subs.mxf");
961         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
962         reel_asset->_language = "badlang";
963         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
964
965         check_verify_result (
966                 {dir},
967                 {
968                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
969                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
970                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
971                 });
972 }
973
974
975 /* SMPTE DCP with invalid <Language> in the MainSound reel, the CPL additional subtitles languages and
976  * the release territory.
977  */
978 BOOST_AUTO_TEST_CASE (verify_invalid_language3)
979 {
980         path const dir("build/test/verify_invalid_language3");
981         prepare_directory (dir);
982
983         auto picture = simple_picture (dir, "foo");
984         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
985         auto reel = make_shared<dcp::Reel>();
986         reel->add (reel_picture);
987         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "frobozz");
988         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
989         reel->add (reel_sound);
990         reel->add (simple_markers());
991
992         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
993         cpl->add (reel);
994         cpl->_additional_subtitle_languages.push_back("this-is-wrong");
995         cpl->_additional_subtitle_languages.push_back("andso-is-this");
996         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
997         cpl->set_main_sound_sample_rate (48000);
998         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
999         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1000         cpl->set_version_number (1);
1001         cpl->_release_territory = "fred-jim";
1002         auto dcp = make_shared<dcp::DCP>(dir);
1003         dcp->add (cpl);
1004         dcp->write_xml (
1005                 dcp::String::compose("libdcp %1", dcp::version),
1006                 dcp::String::compose("libdcp %1", dcp::version),
1007                 dcp::LocalTime().as_string(),
1008                 "hello"
1009                 );
1010
1011         check_verify_result (
1012                 { dir },
1013                 {
1014                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("this-is-wrong") },
1015                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("andso-is-this") },
1016                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("fred-jim") },
1017                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("frobozz") },
1018                 });
1019 }
1020
1021
1022 static
1023 vector<dcp::VerificationNote>
1024 check_picture_size (int width, int height, int frame_rate, bool three_d)
1025 {
1026         using namespace boost::filesystem;
1027
1028         path dcp_path = "build/test/verify_picture_test";
1029         prepare_directory (dcp_path);
1030
1031         shared_ptr<dcp::PictureAsset> mp;
1032         if (three_d) {
1033                 mp = make_shared<dcp::StereoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1034         } else {
1035                 mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1036         }
1037         auto picture_writer = mp->start_write (dcp_path / "video.mxf", false);
1038
1039         auto image = black_image (dcp::Size(width, height));
1040         auto j2c = dcp::compress_j2k (image, 100000000, frame_rate, three_d, width > 2048);
1041         int const length = three_d ? frame_rate * 2 : frame_rate;
1042         for (int i = 0; i < length; ++i) {
1043                 picture_writer->write (j2c.data(), j2c.size());
1044         }
1045         picture_writer->finalize ();
1046
1047         auto d = make_shared<dcp::DCP>(dcp_path);
1048         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1049         cpl->set_annotation_text ("A Test DCP");
1050         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
1051         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1052         cpl->set_main_sound_sample_rate (48000);
1053         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1054         cpl->set_main_picture_active_area (dcp::Size(1998, 1080));
1055         cpl->set_version_number (1);
1056
1057         auto reel = make_shared<dcp::Reel>();
1058
1059         if (three_d) {
1060                 reel->add (make_shared<dcp::ReelStereoPictureAsset>(std::dynamic_pointer_cast<dcp::StereoPictureAsset>(mp), 0));
1061         } else {
1062                 reel->add (make_shared<dcp::ReelMonoPictureAsset>(std::dynamic_pointer_cast<dcp::MonoPictureAsset>(mp), 0));
1063         }
1064
1065         reel->add (simple_markers(frame_rate));
1066
1067         cpl->add (reel);
1068
1069         d->add (cpl);
1070         d->write_xml (
1071                 dcp::String::compose("libdcp %1", dcp::version),
1072                 dcp::String::compose("libdcp %1", dcp::version),
1073                 dcp::LocalTime().as_string(),
1074                 "A Test DCP"
1075                 );
1076
1077         return dcp::verify ({dcp_path}, &stage, &progress, xsd_test);
1078 }
1079
1080
1081 static
1082 void
1083 check_picture_size_ok (int width, int height, int frame_rate, bool three_d)
1084 {
1085         auto notes = check_picture_size(width, height, frame_rate, three_d);
1086         BOOST_CHECK_EQUAL (notes.size(), 0U);
1087 }
1088
1089
1090 static
1091 void
1092 check_picture_size_bad_frame_size (int width, int height, int frame_rate, bool three_d)
1093 {
1094         auto notes = check_picture_size(width, height, frame_rate, three_d);
1095         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1096         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1097         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS);
1098 }
1099
1100
1101 static
1102 void
1103 check_picture_size_bad_2k_frame_rate (int width, int height, int frame_rate, bool three_d)
1104 {
1105         auto notes = check_picture_size(width, height, frame_rate, three_d);
1106         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
1107         BOOST_CHECK_EQUAL (notes.back().type(), dcp::VerificationNote::Type::BV21_ERROR);
1108         BOOST_CHECK_EQUAL (notes.back().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K);
1109 }
1110
1111
1112 static
1113 void
1114 check_picture_size_bad_4k_frame_rate (int width, int height, int frame_rate, bool three_d)
1115 {
1116         auto notes = check_picture_size(width, height, frame_rate, three_d);
1117         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1118         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1119         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K);
1120 }
1121
1122
1123 BOOST_AUTO_TEST_CASE (verify_picture_size)
1124 {
1125         using namespace boost::filesystem;
1126
1127         /* 2K scope */
1128         check_picture_size_ok (2048, 858, 24, false);
1129         check_picture_size_ok (2048, 858, 25, false);
1130         check_picture_size_ok (2048, 858, 48, false);
1131         check_picture_size_ok (2048, 858, 24, true);
1132         check_picture_size_ok (2048, 858, 25, true);
1133         check_picture_size_ok (2048, 858, 48, true);
1134
1135         /* 2K flat */
1136         check_picture_size_ok (1998, 1080, 24, false);
1137         check_picture_size_ok (1998, 1080, 25, false);
1138         check_picture_size_ok (1998, 1080, 48, false);
1139         check_picture_size_ok (1998, 1080, 24, true);
1140         check_picture_size_ok (1998, 1080, 25, true);
1141         check_picture_size_ok (1998, 1080, 48, true);
1142
1143         /* 4K scope */
1144         check_picture_size_ok (4096, 1716, 24, false);
1145
1146         /* 4K flat */
1147         check_picture_size_ok (3996, 2160, 24, false);
1148
1149         /* Bad frame size */
1150         check_picture_size_bad_frame_size (2050, 858, 24, false);
1151         check_picture_size_bad_frame_size (2048, 658, 25, false);
1152         check_picture_size_bad_frame_size (1920, 1080, 48, true);
1153         check_picture_size_bad_frame_size (4000, 2000, 24, true);
1154
1155         /* Bad 2K frame rate */
1156         check_picture_size_bad_2k_frame_rate (2048, 858, 26, false);
1157         check_picture_size_bad_2k_frame_rate (2048, 858, 31, false);
1158         check_picture_size_bad_2k_frame_rate (1998, 1080, 50, true);
1159
1160         /* Bad 4K frame rate */
1161         check_picture_size_bad_4k_frame_rate (3996, 2160, 25, false);
1162         check_picture_size_bad_4k_frame_rate (3996, 2160, 48, false);
1163
1164         /* No 4K 3D */
1165         auto notes = check_picture_size(3996, 2160, 24, true);
1166         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1167         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1168         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D);
1169 }
1170
1171
1172 static
1173 void
1174 add_test_subtitle (shared_ptr<dcp::SubtitleAsset> asset, int start_frame, int end_frame, float v_position = 0, string text = "Hello")
1175 {
1176         asset->add (
1177                 make_shared<dcp::SubtitleString>(
1178                         optional<string>(),
1179                         false,
1180                         false,
1181                         false,
1182                         dcp::Colour(),
1183                         42,
1184                         1,
1185                         dcp::Time(start_frame, 24, 24),
1186                         dcp::Time(end_frame, 24, 24),
1187                         0,
1188                         dcp::HAlign::CENTER,
1189                         v_position,
1190                         dcp::VAlign::CENTER,
1191                         dcp::Direction::LTR,
1192                         text,
1193                         dcp::Effect::NONE,
1194                         dcp::Colour(),
1195                         dcp::Time(),
1196                         dcp::Time()
1197                 )
1198         );
1199 }
1200
1201
1202 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_xml_size_in_bytes)
1203 {
1204         path const dir("build/test/verify_invalid_closed_caption_xml_size_in_bytes");
1205         prepare_directory (dir);
1206
1207         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1208         for (int i = 0; i < 2048; ++i) {
1209                 add_test_subtitle (asset, i * 24, i * 24 + 20);
1210         }
1211         asset->set_language (dcp::LanguageTag("de-DE"));
1212         asset->write (dir / "subs.mxf");
1213         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 49148, 0);
1214         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1215
1216         check_verify_result (
1217                 { dir },
1218                 {
1219                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1220                         {
1221                                 dcp::VerificationNote::Type::BV21_ERROR,
1222                                 dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES,
1223                                 string("413262"),
1224                                 canonical(dir / "subs.mxf")
1225                         },
1226                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1227                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1228                 });
1229 }
1230
1231
1232 static
1233 shared_ptr<dcp::SMPTESubtitleAsset>
1234 make_large_subtitle_asset (path font_file)
1235 {
1236         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1237         dcp::ArrayData big_fake_font(1024 * 1024);
1238         big_fake_font.write (font_file);
1239         for (int i = 0; i < 116; ++i) {
1240                 asset->add_font (dcp::String::compose("big%1", i), big_fake_font);
1241         }
1242         return asset;
1243 }
1244
1245
1246 template <class T>
1247 void
1248 verify_timed_text_asset_too_large (string name)
1249 {
1250         auto const dir = path("build/test") / name;
1251         prepare_directory (dir);
1252         auto asset = make_large_subtitle_asset (dir / "font.ttf");
1253         add_test_subtitle (asset, 0, 240);
1254         asset->set_language (dcp::LanguageTag("de-DE"));
1255         asset->write (dir / "subs.mxf");
1256
1257         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), 240, 0);
1258         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1259
1260         check_verify_result (
1261                 { dir },
1262                 {
1263                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, string("121696411"), canonical(dir / "subs.mxf") },
1264                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, string("121634816"), canonical(dir / "subs.mxf") },
1265                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1266                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1267                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1268                 });
1269 }
1270
1271
1272 BOOST_AUTO_TEST_CASE (verify_subtitle_asset_too_large)
1273 {
1274         verify_timed_text_asset_too_large<dcp::ReelSMPTESubtitleAsset>("verify_subtitle_asset_too_large");
1275         verify_timed_text_asset_too_large<dcp::ReelSMPTEClosedCaptionAsset>("verify_closed_caption_asset_too_large");
1276 }
1277
1278
1279 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_language)
1280 {
1281         path dir = "build/test/verify_missing_subtitle_language";
1282         prepare_directory (dir);
1283         auto dcp = make_simple (dir, 1, 106);
1284
1285         string const xml =
1286                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1287                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1288                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1289                 "<ContentTitleText>Content</ContentTitleText>"
1290                 "<AnnotationText>Annotation</AnnotationText>"
1291                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1292                 "<ReelNumber>1</ReelNumber>"
1293                 "<EditRate>24 1</EditRate>"
1294                 "<TimeCodeRate>24</TimeCodeRate>"
1295                 "<StartTime>00:00:00:00</StartTime>"
1296                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1297                 "<SubtitleList>"
1298                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1299                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1300                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1301                 "</Subtitle>"
1302                 "</Font>"
1303                 "</SubtitleList>"
1304                 "</SubtitleReel>";
1305
1306         auto xml_file = dcp::fopen_boost (dir / "subs.xml", "w");
1307         BOOST_REQUIRE (xml_file);
1308         fwrite (xml.c_str(), xml.size(), 1, xml_file);
1309         fclose (xml_file);
1310         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1311         subs->write (dir / "subs.mxf");
1312
1313         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1314         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1315         dcp->write_xml (
1316                 dcp::String::compose("libdcp %1", dcp::version),
1317                 dcp::String::compose("libdcp %1", dcp::version),
1318                 dcp::LocalTime().as_string(),
1319                 "A Test DCP"
1320                 );
1321
1322         check_verify_result (
1323                 { dir },
1324                 {
1325                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
1326                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1327                 });
1328 }
1329
1330
1331 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_languages)
1332 {
1333         path path ("build/test/verify_mismatched_subtitle_languages");
1334         auto constexpr reel_length = 192;
1335         auto dcp = make_simple (path, 2, reel_length);
1336         auto cpl = dcp->cpls()[0];
1337
1338         {
1339                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1340                 subs->set_language (dcp::LanguageTag("de-DE"));
1341                 subs->add (simple_subtitle());
1342                 subs->write (path / "subs1.mxf");
1343                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1344                 cpl->reels()[0]->add(reel_subs);
1345         }
1346
1347         {
1348                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1349                 subs->set_language (dcp::LanguageTag("en-US"));
1350                 subs->add (simple_subtitle());
1351                 subs->write (path / "subs2.mxf");
1352                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1353                 cpl->reels()[1]->add(reel_subs);
1354         }
1355
1356         dcp->write_xml (
1357                 dcp::String::compose("libdcp %1", dcp::version),
1358                 dcp::String::compose("libdcp %1", dcp::version),
1359                 dcp::LocalTime().as_string(),
1360                 "A Test DCP"
1361                 );
1362
1363         check_verify_result (
1364                 { path },
1365                 {
1366                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1367                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") },
1368                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES }
1369                 });
1370 }
1371
1372
1373 BOOST_AUTO_TEST_CASE (verify_multiple_closed_caption_languages_allowed)
1374 {
1375         path path ("build/test/verify_multiple_closed_caption_languages_allowed");
1376         auto constexpr reel_length = 192;
1377         auto dcp = make_simple (path, 2, reel_length);
1378         auto cpl = dcp->cpls()[0];
1379
1380         {
1381                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1382                 ccaps->set_language (dcp::LanguageTag("de-DE"));
1383                 ccaps->add (simple_subtitle());
1384                 ccaps->write (path / "subs1.mxf");
1385                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1386                 cpl->reels()[0]->add(reel_ccaps);
1387         }
1388
1389         {
1390                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1391                 ccaps->set_language (dcp::LanguageTag("en-US"));
1392                 ccaps->add (simple_subtitle());
1393                 ccaps->write (path / "subs2.mxf");
1394                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1395                 cpl->reels()[1]->add(reel_ccaps);
1396         }
1397
1398         dcp->write_xml (
1399                 dcp::String::compose("libdcp %1", dcp::version),
1400                 dcp::String::compose("libdcp %1", dcp::version),
1401                 dcp::LocalTime().as_string(),
1402                 "A Test DCP"
1403                 );
1404
1405         check_verify_result (
1406                 { path },
1407                 {
1408                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1409                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") }
1410                 });
1411 }
1412
1413
1414 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_start_time)
1415 {
1416         path dir = "build/test/verify_missing_subtitle_start_time";
1417         prepare_directory (dir);
1418         auto dcp = make_simple (dir, 1, 106);
1419
1420         string const xml =
1421                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1422                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1423                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1424                 "<ContentTitleText>Content</ContentTitleText>"
1425                 "<AnnotationText>Annotation</AnnotationText>"
1426                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1427                 "<ReelNumber>1</ReelNumber>"
1428                 "<Language>de-DE</Language>"
1429                 "<EditRate>24 1</EditRate>"
1430                 "<TimeCodeRate>24</TimeCodeRate>"
1431                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1432                 "<SubtitleList>"
1433                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1434                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1435                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1436                 "</Subtitle>"
1437                 "</Font>"
1438                 "</SubtitleList>"
1439                 "</SubtitleReel>";
1440
1441         auto xml_file = dcp::fopen_boost (dir / "subs.xml", "w");
1442         BOOST_REQUIRE (xml_file);
1443         fwrite (xml.c_str(), xml.size(), 1, xml_file);
1444         fclose (xml_file);
1445         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1446         subs->write (dir / "subs.mxf");
1447
1448         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1449         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1450         dcp->write_xml (
1451                 dcp::String::compose("libdcp %1", dcp::version),
1452                 dcp::String::compose("libdcp %1", dcp::version),
1453                 dcp::LocalTime().as_string(),
1454                 "A Test DCP"
1455                 );
1456
1457         check_verify_result (
1458                 { dir },
1459                 {
1460                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1461                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1462                 });
1463 }
1464
1465
1466 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_start_time)
1467 {
1468         path dir = "build/test/verify_invalid_subtitle_start_time";
1469         prepare_directory (dir);
1470         auto dcp = make_simple (dir, 1, 106);
1471
1472         string const xml =
1473                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1474                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1475                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1476                 "<ContentTitleText>Content</ContentTitleText>"
1477                 "<AnnotationText>Annotation</AnnotationText>"
1478                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1479                 "<ReelNumber>1</ReelNumber>"
1480                 "<Language>de-DE</Language>"
1481                 "<EditRate>24 1</EditRate>"
1482                 "<TimeCodeRate>24</TimeCodeRate>"
1483                 "<StartTime>00:00:02:00</StartTime>"
1484                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1485                 "<SubtitleList>"
1486                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1487                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1488                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1489                 "</Subtitle>"
1490                 "</Font>"
1491                 "</SubtitleList>"
1492                 "</SubtitleReel>";
1493
1494         auto xml_file = dcp::fopen_boost (dir / "subs.xml", "w");
1495         BOOST_REQUIRE (xml_file);
1496         fwrite (xml.c_str(), xml.size(), 1, xml_file);
1497         fclose (xml_file);
1498         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1499         subs->write (dir / "subs.mxf");
1500
1501         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1502         dcp->cpls().front()->reels().front()->add(reel_subs);
1503         dcp->write_xml (
1504                 dcp::String::compose("libdcp %1", dcp::version),
1505                 dcp::String::compose("libdcp %1", dcp::version),
1506                 dcp::LocalTime().as_string(),
1507                 "A Test DCP"
1508                 );
1509
1510         check_verify_result (
1511                 { dir },
1512                 {
1513                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1514                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1515                 });
1516 }
1517
1518
1519 class TestText
1520 {
1521 public:
1522         TestText (int in_, int out_, float v_position_ = 0, string text_ = "Hello")
1523                 : in(in_)
1524                 , out(out_)
1525                 , v_position(v_position_)
1526                 , text(text_)
1527         {}
1528
1529         int in;
1530         int out;
1531         float v_position;
1532         string text;
1533 };
1534
1535
1536 template <class T>
1537 shared_ptr<dcp::CPL>
1538 dcp_with_text (path dir, vector<TestText> subs)
1539 {
1540         prepare_directory (dir);
1541         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1542         asset->set_start_time (dcp::Time());
1543         for (auto i: subs) {
1544                 add_test_subtitle (asset, i.in, i.out, i.v_position, i.text);
1545         }
1546         asset->set_language (dcp::LanguageTag("de-DE"));
1547         asset->write (dir / "subs.mxf");
1548
1549         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1550         return write_dcp_with_single_asset (dir, reel_asset);
1551 }
1552
1553
1554 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_first_text_time)
1555 {
1556         auto const dir = path("build/test/verify_invalid_subtitle_first_text_time");
1557         /* Just too early */
1558         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24 - 1, 5 * 24 }});
1559         check_verify_result (
1560                 { dir },
1561                 {
1562                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1563                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1564                 });
1565
1566 }
1567
1568
1569 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time)
1570 {
1571         auto const dir = path("build/test/verify_valid_subtitle_first_text_time");
1572         /* Just late enough */
1573         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 5 * 24 }});
1574         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1575 }
1576
1577
1578 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time_on_second_reel)
1579 {
1580         auto const dir = path("build/test/verify_valid_subtitle_first_text_time_on_second_reel");
1581         prepare_directory (dir);
1582
1583         auto asset1 = make_shared<dcp::SMPTESubtitleAsset>();
1584         asset1->set_start_time (dcp::Time());
1585         /* Just late enough */
1586         add_test_subtitle (asset1, 4 * 24, 5 * 24);
1587         asset1->set_language (dcp::LanguageTag("de-DE"));
1588         asset1->write (dir / "subs1.mxf");
1589         auto reel_asset1 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset1, dcp::Fraction(24, 1), 5 * 24, 0);
1590         auto reel1 = make_shared<dcp::Reel>();
1591         reel1->add (reel_asset1);
1592         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 5 * 24, 0);
1593         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
1594         reel1->add (markers1);
1595
1596         auto asset2 = make_shared<dcp::SMPTESubtitleAsset>();
1597         asset2->set_start_time (dcp::Time());
1598         /* This would be too early on first reel but should be OK on the second */
1599         add_test_subtitle (asset2, 3, 4 * 24);
1600         asset2->set_language (dcp::LanguageTag("de-DE"));
1601         asset2->write (dir / "subs2.mxf");
1602         auto reel_asset2 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset2, dcp::Fraction(24, 1), 4 * 24, 0);
1603         auto reel2 = make_shared<dcp::Reel>();
1604         reel2->add (reel_asset2);
1605         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 4 * 24, 0);
1606         markers2->set (dcp::Marker::LFOC, dcp::Time(4 * 24 - 1, 24, 24));
1607         reel2->add (markers2);
1608
1609         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1610         cpl->add (reel1);
1611         cpl->add (reel2);
1612         auto dcp = make_shared<dcp::DCP>(dir);
1613         dcp->add (cpl);
1614         dcp->write_xml (
1615                 dcp::String::compose("libdcp %1", dcp::version),
1616                 dcp::String::compose("libdcp %1", dcp::version),
1617                 dcp::LocalTime().as_string(),
1618                 "hello"
1619                 );
1620
1621
1622         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1623 }
1624
1625
1626 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_spacing)
1627 {
1628         auto const dir = path("build/test/verify_invalid_subtitle_spacing");
1629         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1630                 dir,
1631                 {
1632                         { 4 * 24,     5 * 24 },
1633                         { 5 * 24 + 1, 6 * 24 },
1634                 });
1635         check_verify_result (
1636                 {dir},
1637                 {
1638                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING },
1639                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1640                 });
1641 }
1642
1643
1644 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_spacing)
1645 {
1646         auto const dir = path("build/test/verify_valid_subtitle_spacing");
1647         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1648                 dir,
1649                 {
1650                         { 4 * 24,      5 * 24 },
1651                         { 5 * 24 + 16, 8 * 24 },
1652                 });
1653         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1654 }
1655
1656
1657 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_duration)
1658 {
1659         auto const dir = path("build/test/verify_invalid_subtitle_duration");
1660         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 1 }});
1661         check_verify_result (
1662                 {dir},
1663                 {
1664                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION },
1665                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1666                 });
1667 }
1668
1669
1670 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_duration)
1671 {
1672         auto const dir = path("build/test/verify_valid_subtitle_duration");
1673         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 17 }});
1674         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1675 }
1676
1677
1678 BOOST_AUTO_TEST_CASE (verify_subtitle_overlapping_reel_boundary)
1679 {
1680         auto const dir = path("build/test/verify_subtitle_overlapping_reel_boundary");
1681         prepare_directory (dir);
1682         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1683         asset->set_start_time (dcp::Time());
1684         add_test_subtitle (asset, 0, 4 * 24);
1685         asset->set_language (dcp::LanguageTag("de-DE"));
1686         asset->write (dir / "subs.mxf");
1687
1688         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 3 * 24, 0);
1689         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1690         check_verify_result (
1691                 {dir},
1692                 {
1693                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "72 96", boost::filesystem::canonical(asset->file().get()) },
1694                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1695                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY },
1696                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1697                 });
1698
1699 }
1700
1701
1702 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count1)
1703 {
1704         auto const dir = path ("build/test/invalid_subtitle_line_count1");
1705         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1706                 dir,
1707                 {
1708                         { 96, 200, 0.0, "We" },
1709                         { 96, 200, 0.1, "have" },
1710                         { 96, 200, 0.2, "four" },
1711                         { 96, 200, 0.3, "lines" }
1712                 });
1713         check_verify_result (
1714                 {dir},
1715                 {
1716                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1717                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1718                 });
1719 }
1720
1721
1722 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count1)
1723 {
1724         auto const dir = path ("build/test/verify_valid_subtitle_line_count1");
1725         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1726                 dir,
1727                 {
1728                         { 96, 200, 0.0, "We" },
1729                         { 96, 200, 0.1, "have" },
1730                         { 96, 200, 0.2, "four" },
1731                 });
1732         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1733 }
1734
1735
1736 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count2)
1737 {
1738         auto const dir = path ("build/test/verify_invalid_subtitle_line_count2");
1739         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1740                 dir,
1741                 {
1742                         { 96, 300, 0.0, "We" },
1743                         { 96, 300, 0.1, "have" },
1744                         { 150, 180, 0.2, "four" },
1745                         { 150, 180, 0.3, "lines" }
1746                 });
1747         check_verify_result (
1748                 {dir},
1749                 {
1750                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1751                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1752                 });
1753 }
1754
1755
1756 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count2)
1757 {
1758         auto const dir = path ("build/test/verify_valid_subtitle_line_count2");
1759         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1760                 dir,
1761                 {
1762                         { 96, 300, 0.0, "We" },
1763                         { 96, 300, 0.1, "have" },
1764                         { 150, 180, 0.2, "four" },
1765                         { 190, 250, 0.3, "lines" }
1766                 });
1767         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1768 }
1769
1770
1771 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length1)
1772 {
1773         auto const dir = path ("build/test/verify_invalid_subtitle_line_length1");
1774         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1775                 dir,
1776                 {
1777                         { 96, 300, 0.0, "012345678901234567890123456789012345678901234567890123" }
1778                 });
1779         check_verify_result (
1780                 {dir},
1781                 {
1782                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH },
1783                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1784                 });
1785 }
1786
1787
1788 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length2)
1789 {
1790         auto const dir = path ("build/test/verify_invalid_subtitle_line_length2");
1791         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1792                 dir,
1793                 {
1794                         { 96, 300, 0.0, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
1795                 });
1796         check_verify_result (
1797                 {dir},
1798                 {
1799                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH },
1800                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1801                 });
1802 }
1803
1804
1805 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count1)
1806 {
1807         auto const dir = path ("build/test/verify_valid_closed_caption_line_count1");
1808         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1809                 dir,
1810                 {
1811                         { 96, 200, 0.0, "We" },
1812                         { 96, 200, 0.1, "have" },
1813                         { 96, 200, 0.2, "four" },
1814                         { 96, 200, 0.3, "lines" }
1815                 });
1816         check_verify_result (
1817                 {dir},
1818                 {
1819                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
1820                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1821                 });
1822 }
1823
1824
1825 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count2)
1826 {
1827         auto const dir = path ("build/test/verify_valid_closed_caption_line_count2");
1828         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1829                 dir,
1830                 {
1831                         { 96, 200, 0.0, "We" },
1832                         { 96, 200, 0.1, "have" },
1833                         { 96, 200, 0.2, "four" },
1834                 });
1835         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1836 }
1837
1838
1839 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_count3)
1840 {
1841         auto const dir = path ("build/test/verify_invalid_closed_caption_line_count3");
1842         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1843                 dir,
1844                 {
1845                         { 96, 300, 0.0, "We" },
1846                         { 96, 300, 0.1, "have" },
1847                         { 150, 180, 0.2, "four" },
1848                         { 150, 180, 0.3, "lines" }
1849                 });
1850         check_verify_result (
1851                 {dir},
1852                 {
1853                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
1854                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1855                 });
1856 }
1857
1858
1859 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count4)
1860 {
1861         auto const dir = path ("build/test/verify_valid_closed_caption_line_count4");
1862         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1863                 dir,
1864                 {
1865                         { 96, 300, 0.0, "We" },
1866                         { 96, 300, 0.1, "have" },
1867                         { 150, 180, 0.2, "four" },
1868                         { 190, 250, 0.3, "lines" }
1869                 });
1870         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1871 }
1872
1873
1874 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_length)
1875 {
1876         auto const dir = path ("build/test/verify_invalid_closed_caption_line_length");
1877         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1878                 dir,
1879                 {
1880                         { 96, 300, 0.0, "0123456789012345678901234567890123" }
1881                 });
1882         check_verify_result (
1883                 {dir},
1884                 {
1885                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH },
1886                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1887                 });
1888 }
1889
1890
1891 BOOST_AUTO_TEST_CASE (verify_invalid_sound_frame_rate)
1892 {
1893         path const dir("build/test/verify_invalid_sound_frame_rate");
1894         prepare_directory (dir);
1895
1896         auto picture = simple_picture (dir, "foo");
1897         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
1898         auto reel = make_shared<dcp::Reel>();
1899         reel->add (reel_picture);
1900         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "de-DE", 24, 96000);
1901         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
1902         reel->add (reel_sound);
1903         reel->add (simple_markers());
1904         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1905         cpl->add (reel);
1906         auto dcp = make_shared<dcp::DCP>(dir);
1907         dcp->add (cpl);
1908         dcp->write_xml (
1909                 dcp::String::compose("libdcp %1", dcp::version),
1910                 dcp::String::compose("libdcp %1", dcp::version),
1911                 dcp::LocalTime().as_string(),
1912                 "hello"
1913                 );
1914
1915         check_verify_result (
1916                 {dir},
1917                 {
1918                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE, string("96000"), canonical(dir / "audiofoo.mxf") },
1919                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1920                 });
1921 }
1922
1923
1924 BOOST_AUTO_TEST_CASE (verify_missing_cpl_annotation_text)
1925 {
1926         path const dir("build/test/verify_missing_cpl_annotation_text");
1927         auto dcp = make_simple (dir);
1928         dcp->write_xml (
1929                 dcp::String::compose("libdcp %1", dcp::version),
1930                 dcp::String::compose("libdcp %1", dcp::version),
1931                 dcp::LocalTime().as_string(),
1932                 "A Test DCP"
1933                 );
1934
1935         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
1936
1937         auto const cpl = dcp->cpls()[0];
1938
1939         {
1940                 BOOST_REQUIRE (cpl->file());
1941                 Editor e(cpl->file().get());
1942                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "");
1943         }
1944
1945         check_verify_result (
1946                 {dir},
1947                 {
1948                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
1949                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
1950                 });
1951 }
1952
1953
1954 BOOST_AUTO_TEST_CASE (verify_mismatched_cpl_annotation_text)
1955 {
1956         path const dir("build/test/verify_mismatched_cpl_annotation_text");
1957         auto dcp = make_simple (dir);
1958         dcp->write_xml (
1959                 dcp::String::compose("libdcp %1", dcp::version),
1960                 dcp::String::compose("libdcp %1", dcp::version),
1961                 dcp::LocalTime().as_string(),
1962                 "A Test DCP"
1963                 );
1964
1965         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
1966         auto const cpl = dcp->cpls()[0];
1967
1968         {
1969                 BOOST_REQUIRE (cpl->file());
1970                 Editor e(cpl->file().get());
1971                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "<AnnotationText>A Test DCP 1</AnnotationText>");
1972         }
1973
1974         check_verify_result (
1975                 {dir},
1976                 {
1977                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
1978                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
1979                 });
1980 }
1981
1982
1983 BOOST_AUTO_TEST_CASE (verify_mismatched_asset_duration)
1984 {
1985         path const dir("build/test/verify_mismatched_asset_duration");
1986         prepare_directory (dir);
1987         shared_ptr<dcp::DCP> dcp (new dcp::DCP(dir));
1988         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1989
1990         shared_ptr<dcp::MonoPictureAsset> mp = simple_picture (dir, "", 24);
1991         shared_ptr<dcp::SoundAsset> ms = simple_sound (dir, "", dcp::MXFMetadata(), "en-US", 25);
1992
1993         auto reel = make_shared<dcp::Reel>(
1994                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
1995                 make_shared<dcp::ReelSoundAsset>(ms, 0)
1996                 );
1997
1998         reel->add (simple_markers());
1999         cpl->add (reel);
2000
2001         dcp->add (cpl);
2002         dcp->write_xml (
2003                 dcp::String::compose("libdcp %1", dcp::version),
2004                 dcp::String::compose("libdcp %1", dcp::version),
2005                 dcp::LocalTime().as_string(),
2006                 "A Test DCP"
2007                 );
2008
2009         check_verify_result (
2010                 {dir},
2011                 {
2012                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION },
2013                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), canonical(cpl->file().get()) }
2014                 });
2015 }
2016
2017
2018
2019 static
2020 shared_ptr<dcp::CPL>
2021 verify_subtitles_must_be_in_all_reels_check (path dir, bool add_to_reel1, bool add_to_reel2)
2022 {
2023         prepare_directory (dir);
2024         auto dcp = make_shared<dcp::DCP>(dir);
2025         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2026
2027         auto constexpr reel_length = 192;
2028
2029         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2030         subs->set_language (dcp::LanguageTag("de-DE"));
2031         subs->set_start_time (dcp::Time());
2032         subs->add (simple_subtitle());
2033         subs->write (dir / "subs.mxf");
2034         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
2035
2036         auto reel1 = make_shared<dcp::Reel>(
2037                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2038                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2039                 );
2040
2041         if (add_to_reel1) {
2042                 reel1->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2043         }
2044
2045         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2046         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2047         reel1->add (markers1);
2048
2049         cpl->add (reel1);
2050
2051         auto reel2 = make_shared<dcp::Reel>(
2052                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2053                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2054                 );
2055
2056         if (add_to_reel2) {
2057                 reel2->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2058         }
2059
2060         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2061         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2062         reel2->add (markers2);
2063
2064         cpl->add (reel2);
2065
2066         dcp->add (cpl);
2067         dcp->write_xml (
2068                 dcp::String::compose("libdcp %1", dcp::version),
2069                 dcp::String::compose("libdcp %1", dcp::version),
2070                 dcp::LocalTime().as_string(),
2071                 "A Test DCP"
2072                 );
2073
2074         return cpl;
2075 }
2076
2077
2078 BOOST_AUTO_TEST_CASE (verify_missing_main_subtitle_from_some_reels)
2079 {
2080         {
2081                 path dir ("build/test/missing_main_subtitle_from_some_reels");
2082                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, false);
2083                 check_verify_result (
2084                         { dir },
2085                         {
2086                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS },
2087                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2088                         });
2089
2090         }
2091
2092         {
2093                 path dir ("build/test/verify_subtitles_must_be_in_all_reels2");
2094                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, true);
2095                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2096         }
2097
2098         {
2099                 path dir ("build/test/verify_subtitles_must_be_in_all_reels1");
2100                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, false, false);
2101                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2102         }
2103 }
2104
2105
2106 static
2107 shared_ptr<dcp::CPL>
2108 verify_closed_captions_must_be_in_all_reels_check (path dir, int caps_in_reel1, int caps_in_reel2)
2109 {
2110         prepare_directory (dir);
2111         auto dcp = make_shared<dcp::DCP>(dir);
2112         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2113
2114         auto constexpr reel_length = 192;
2115
2116         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2117         subs->set_language (dcp::LanguageTag("de-DE"));
2118         subs->set_start_time (dcp::Time());
2119         subs->add (simple_subtitle());
2120         subs->write (dir / "subs.mxf");
2121
2122         auto reel1 = make_shared<dcp::Reel>(
2123                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2124                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2125                 );
2126
2127         for (int i = 0; i < caps_in_reel1; ++i) {
2128                 reel1->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2129         }
2130
2131         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2132         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2133         reel1->add (markers1);
2134
2135         cpl->add (reel1);
2136
2137         auto reel2 = make_shared<dcp::Reel>(
2138                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2139                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2140                 );
2141
2142         for (int i = 0; i < caps_in_reel2; ++i) {
2143                 reel2->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2144         }
2145
2146         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2147         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2148         reel2->add (markers2);
2149
2150         cpl->add (reel2);
2151
2152         dcp->add (cpl);
2153         dcp->write_xml (
2154                 dcp::String::compose("libdcp %1", dcp::version),
2155                 dcp::String::compose("libdcp %1", dcp::version),
2156                 dcp::LocalTime().as_string(),
2157                 "A Test DCP"
2158                 );
2159
2160         return cpl;
2161 }
2162
2163
2164 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_asset_counts)
2165 {
2166         {
2167                 path dir ("build/test/mismatched_closed_caption_asset_counts");
2168                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 3, 4);
2169                 check_verify_result (
2170                         {dir},
2171                         {
2172                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS },
2173                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2174                         });
2175         }
2176
2177         {
2178                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels2");
2179                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 4, 4);
2180                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2181         }
2182
2183         {
2184                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels3");
2185                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 0, 0);
2186                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2187         }
2188 }
2189
2190
2191 template <class T>
2192 void
2193 verify_text_entry_point_check (path dir, dcp::VerificationNote::Code code, boost::function<void (shared_ptr<T>)> adjust)
2194 {
2195         prepare_directory (dir);
2196         auto dcp = make_shared<dcp::DCP>(dir);
2197         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2198
2199         auto constexpr reel_length = 192;
2200
2201         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2202         subs->set_language (dcp::LanguageTag("de-DE"));
2203         subs->set_start_time (dcp::Time());
2204         subs->add (simple_subtitle());
2205         subs->write (dir / "subs.mxf");
2206         auto reel_text = make_shared<T>(subs, dcp::Fraction(24, 1), reel_length, 0);
2207         adjust (reel_text);
2208
2209         auto reel = make_shared<dcp::Reel>(
2210                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2211                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2212                 );
2213
2214         reel->add (reel_text);
2215
2216         reel->add (simple_markers(reel_length));
2217
2218         cpl->add (reel);
2219
2220         dcp->add (cpl);
2221         dcp->write_xml (
2222                 dcp::String::compose("libdcp %1", dcp::version),
2223                 dcp::String::compose("libdcp %1", dcp::version),
2224                 dcp::LocalTime().as_string(),
2225                 "A Test DCP"
2226                 );
2227
2228         check_verify_result (
2229                 {dir},
2230                 {
2231                         { dcp::VerificationNote::Type::BV21_ERROR, code, subs->id() },
2232                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2233                 });
2234 }
2235
2236
2237 BOOST_AUTO_TEST_CASE (verify_text_entry_point)
2238 {
2239         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2240                 "build/test/verify_subtitle_entry_point_must_be_present",
2241                 dcp::VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT,
2242                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2243                         asset->unset_entry_point ();
2244                         }
2245                 );
2246
2247         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2248                 "build/test/verify_subtitle_entry_point_must_be_zero",
2249                 dcp::VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT,
2250                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2251                         asset->set_entry_point (4);
2252                         }
2253                 );
2254
2255         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2256                 "build/test/verify_closed_caption_entry_point_must_be_present",
2257                 dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT,
2258                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2259                         asset->unset_entry_point ();
2260                         }
2261                 );
2262
2263         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2264                 "build/test/verify_closed_caption_entry_point_must_be_zero",
2265                 dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT,
2266                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2267                         asset->set_entry_point (9);
2268                         }
2269                 );
2270 }
2271
2272
2273 BOOST_AUTO_TEST_CASE (verify_missing_hash)
2274 {
2275         RNGFixer fix;
2276
2277         path const dir("build/test/verify_missing_hash");
2278         auto dcp = make_simple (dir);
2279         dcp->write_xml (
2280                 dcp::String::compose("libdcp %1", dcp::version),
2281                 dcp::String::compose("libdcp %1", dcp::version),
2282                 dcp::LocalTime().as_string(),
2283                 "A Test DCP"
2284                 );
2285
2286         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2287         auto const cpl = dcp->cpls()[0];
2288         BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
2289         BOOST_REQUIRE (cpl->reels()[0]->main_picture());
2290         auto asset_id = cpl->reels()[0]->main_picture()->id();
2291
2292         {
2293                 BOOST_REQUIRE (cpl->file());
2294                 Editor e(cpl->file().get());
2295                 e.delete_first_line_containing("<Hash>");
2296         }
2297
2298         check_verify_result (
2299                 {dir},
2300                 {
2301                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2302                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, asset_id }
2303                 });
2304 }
2305
2306
2307 static
2308 void
2309 verify_markers_test (
2310         path dir,
2311         vector<pair<dcp::Marker, dcp::Time>> markers,
2312         vector<dcp::VerificationNote> test_notes
2313         )
2314 {
2315         auto dcp = make_simple (dir);
2316         dcp->cpls()[0]->set_content_kind (dcp::ContentKind::FEATURE);
2317         auto markers_asset = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 24, 0);
2318         for (auto const& i: markers) {
2319                 markers_asset->set (i.first, i.second);
2320         }
2321         dcp->cpls()[0]->reels()[0]->add(markers_asset);
2322         dcp->write_xml (
2323                 dcp::String::compose("libdcp %1", dcp::version),
2324                 dcp::String::compose("libdcp %1", dcp::version),
2325                 dcp::LocalTime().as_string(),
2326                 "A Test DCP"
2327                 );
2328
2329         check_verify_result ({dir}, test_notes);
2330 }
2331
2332
2333 BOOST_AUTO_TEST_CASE (verify_markers)
2334 {
2335         verify_markers_test (
2336                 "build/test/verify_markers_all_correct",
2337                 {
2338                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2339                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2340                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2341                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2342                 },
2343                 {}
2344                 );
2345
2346         verify_markers_test (
2347                 "build/test/verify_markers_missing_ffec",
2348                 {
2349                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2350                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2351                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2352                 },
2353                 {
2354                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE }
2355                 });
2356
2357         verify_markers_test (
2358                 "build/test/verify_markers_missing_ffmc",
2359                 {
2360                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2361                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2362                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2363                 },
2364                 {
2365                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE }
2366                 });
2367
2368         verify_markers_test (
2369                 "build/test/verify_markers_missing_ffoc",
2370                 {
2371                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2372                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2373                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2374                 },
2375                 {
2376                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC}
2377                 });
2378
2379         verify_markers_test (
2380                 "build/test/verify_markers_missing_lfoc",
2381                 {
2382                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2383                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2384                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) }
2385                 },
2386                 {
2387                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC }
2388                 });
2389
2390         verify_markers_test (
2391                 "build/test/verify_markers_incorrect_ffoc",
2392                 {
2393                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2394                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2395                         { dcp::Marker::FFOC, dcp::Time(3, 24, 24) },
2396                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2397                 },
2398                 {
2399                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_FFOC, string("3") }
2400                 });
2401
2402         verify_markers_test (
2403                 "build/test/verify_markers_incorrect_lfoc",
2404                 {
2405                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2406                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2407                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2408                         { dcp::Marker::LFOC, dcp::Time(18, 24, 24) }
2409                 },
2410                 {
2411                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_LFOC, string("18") }
2412                 });
2413 }
2414
2415
2416 BOOST_AUTO_TEST_CASE (verify_missing_cpl_metadata_version_number)
2417 {
2418         path dir = "build/test/verify_missing_cpl_metadata_version_number";
2419         prepare_directory (dir);
2420         auto dcp = make_simple (dir);
2421         auto cpl = dcp->cpls()[0];
2422         cpl->unset_version_number();
2423         dcp->write_xml (
2424                 dcp::String::compose("libdcp %1", dcp::version),
2425                 dcp::String::compose("libdcp %1", dcp::version),
2426                 dcp::LocalTime().as_string(),
2427                 "A Test DCP"
2428                 );
2429
2430         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get() }});
2431 }
2432
2433
2434 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
2435 {
2436         path dir = "build/test/verify_missing_extension_metadata1";
2437         auto dcp = make_simple (dir);
2438         dcp->write_xml (
2439                 dcp::String::compose("libdcp %1", dcp::version),
2440                 dcp::String::compose("libdcp %1", dcp::version),
2441                 dcp::LocalTime().as_string(),
2442                 "A Test DCP"
2443                 );
2444
2445         auto cpl = dcp->cpls()[0];
2446
2447         {
2448                 Editor e (cpl->file().get());
2449                 e.delete_lines ("<meta:ExtensionMetadataList>", "</meta:ExtensionMetadataList>");
2450         }
2451
2452         check_verify_result (
2453                 {dir},
2454                 {
2455                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2456                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2457                 });
2458 }
2459
2460
2461 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata2)
2462 {
2463         path dir = "build/test/verify_missing_extension_metadata2";
2464         auto dcp = make_simple (dir);
2465         dcp->write_xml (
2466                 dcp::String::compose("libdcp %1", dcp::version),
2467                 dcp::String::compose("libdcp %1", dcp::version),
2468                 dcp::LocalTime().as_string(),
2469                 "A Test DCP"
2470                 );
2471
2472         auto cpl = dcp->cpls()[0];
2473
2474         {
2475                 Editor e (cpl->file().get());
2476                 e.delete_lines ("<meta:ExtensionMetadata scope=\"http://isdcf.com/ns/cplmd/app\">", "</meta:ExtensionMetadata>");
2477         }
2478
2479         check_verify_result (
2480                 {dir},
2481                 {
2482                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2483                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2484                 });
2485 }
2486
2487
2488 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata3)
2489 {
2490         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata3";
2491         auto dcp = make_simple (dir);
2492         dcp->write_xml (
2493                 dcp::String::compose("libdcp %1", dcp::version),
2494                 dcp::String::compose("libdcp %1", dcp::version),
2495                 dcp::LocalTime().as_string(),
2496                 "A Test DCP"
2497                 );
2498
2499         auto const cpl = dcp->cpls()[0];
2500
2501         {
2502                 Editor e (cpl->file().get());
2503                 e.replace ("<meta:Name>A", "<meta:NameX>A");
2504                 e.replace ("n</meta:Name>", "n</meta:NameX>");
2505         }
2506
2507         check_verify_result (
2508                 {dir},
2509                 {
2510                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:NameX'"), cpl->file().get(), 75 },
2511                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:NameX' is not allowed for content model '(Name,PropertyList?,)'"), cpl->file().get(), 82 },
2512                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2513                 });
2514 }
2515
2516
2517 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata1)
2518 {
2519         path dir = "build/test/verify_invalid_extension_metadata1";
2520         auto dcp = make_simple (dir);
2521         dcp->write_xml (
2522                 dcp::String::compose("libdcp %1", dcp::version),
2523                 dcp::String::compose("libdcp %1", dcp::version),
2524                 dcp::LocalTime().as_string(),
2525                 "A Test DCP"
2526                 );
2527
2528         auto cpl = dcp->cpls()[0];
2529
2530         {
2531                 Editor e (cpl->file().get());
2532                 e.replace ("Application", "Fred");
2533         }
2534
2535         check_verify_result (
2536                 {dir},
2537                 {
2538                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2539                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> should be 'Application'"), cpl->file().get() },
2540                 });
2541 }
2542
2543
2544 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata2)
2545 {
2546         path dir = "build/test/verify_invalid_extension_metadata2";
2547         auto dcp = make_simple (dir);
2548         dcp->write_xml (
2549                 dcp::String::compose("libdcp %1", dcp::version),
2550                 dcp::String::compose("libdcp %1", dcp::version),
2551                 dcp::LocalTime().as_string(),
2552                 "A Test DCP"
2553                 );
2554
2555         auto cpl = dcp->cpls()[0];
2556
2557         {
2558                 Editor e (cpl->file().get());
2559                 e.replace ("DCP Constraints Profile", "Fred");
2560         }
2561
2562         check_verify_result (
2563                 {dir},
2564                 {
2565                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2566                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> property should be 'DCP Constraints Profile'"), cpl->file().get() },
2567                 });
2568 }
2569
2570
2571 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata6)
2572 {
2573         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata6";
2574         auto dcp = make_simple (dir);
2575         dcp->write_xml (
2576                 dcp::String::compose("libdcp %1", dcp::version),
2577                 dcp::String::compose("libdcp %1", dcp::version),
2578                 dcp::LocalTime().as_string(),
2579                 "A Test DCP"
2580                 );
2581
2582         auto const cpl = dcp->cpls()[0];
2583
2584         {
2585                 Editor e (cpl->file().get());
2586                 e.replace ("<meta:Value>", "<meta:ValueX>");
2587                 e.replace ("</meta:Value>", "</meta:ValueX>");
2588         }
2589
2590         check_verify_result (
2591                 {dir},
2592                 {
2593                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:ValueX'"), cpl->file().get(), 79 },
2594                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:ValueX' is not allowed for content model '(Name,Value)'"), cpl->file().get(), 80 },
2595                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2596                 });
2597 }
2598
2599
2600 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata7)
2601 {
2602         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata7";
2603         auto dcp = make_simple (dir);
2604         dcp->write_xml (
2605                 dcp::String::compose("libdcp %1", dcp::version),
2606                 dcp::String::compose("libdcp %1", dcp::version),
2607                 dcp::LocalTime().as_string(),
2608                 "A Test DCP"
2609                 );
2610
2611         auto const cpl = dcp->cpls()[0];
2612
2613         {
2614                 Editor e (cpl->file().get());
2615                 e.replace ("SMPTE-RDD-52:2020-Bv2.1", "Fred");
2616         }
2617
2618         check_verify_result (
2619                 {dir},
2620                 {
2621                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2622                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'"), cpl->file().get() },
2623                 });
2624 }
2625
2626
2627 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata8)
2628 {
2629         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata8";
2630         auto dcp = make_simple (dir);
2631         dcp->write_xml (
2632                 dcp::String::compose("libdcp %1", dcp::version),
2633                 dcp::String::compose("libdcp %1", dcp::version),
2634                 dcp::LocalTime().as_string(),
2635                 "A Test DCP"
2636                 );
2637
2638         auto const cpl = dcp->cpls()[0];
2639
2640         {
2641                 Editor e (cpl->file().get());
2642                 e.replace ("<meta:Property>", "<meta:PropertyX>");
2643                 e.replace ("</meta:Property>", "</meta:PropertyX>");
2644         }
2645
2646         check_verify_result (
2647                 {dir},
2648                 {
2649                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyX'"), cpl->file().get(), 77 },
2650                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyX' is not allowed for content model '(Property+)'"), cpl->file().get(), 81 },
2651                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2652                 });
2653 }
2654
2655
2656 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata9)
2657 {
2658         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata9";
2659         auto dcp = make_simple (dir);
2660         dcp->write_xml (
2661                 dcp::String::compose("libdcp %1", dcp::version),
2662                 dcp::String::compose("libdcp %1", dcp::version),
2663                 dcp::LocalTime().as_string(),
2664                 "A Test DCP"
2665                 );
2666
2667         auto const cpl = dcp->cpls()[0];
2668
2669         {
2670                 Editor e (cpl->file().get());
2671                 e.replace ("<meta:PropertyList>", "<meta:PropertyListX>");
2672                 e.replace ("</meta:PropertyList>", "</meta:PropertyListX>");
2673         }
2674
2675         check_verify_result (
2676                 {dir},
2677                 {
2678                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyListX'"), cpl->file().get(), 76 },
2679                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyListX' is not allowed for content model '(Name,PropertyList?,)'"), cpl->file().get(), 82 },
2680                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2681                 });
2682 }
2683
2684
2685
2686 BOOST_AUTO_TEST_CASE (verify_unsigned_cpl_with_encrypted_content)
2687 {
2688         path dir = "build/test/verify_unsigned_cpl_with_encrypted_content";
2689         prepare_directory (dir);
2690         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2691                 copy_file (i.path(), dir / i.path().filename());
2692         }
2693
2694         path const pkl = dir / ( "pkl_" + encryption_test_pkl_id + ".xml" );
2695         path const cpl = dir / ( "cpl_" + encryption_test_cpl_id + ".xml");
2696
2697         {
2698                 Editor e (cpl);
2699                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2700         }
2701
2702         check_verify_result (
2703                 {dir},
2704                 {
2705                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, encryption_test_cpl_id, canonical(cpl) },
2706                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl), },
2707                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2708                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2709                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2710                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2711                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2712                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, encryption_test_cpl_id, canonical(cpl) }
2713                 });
2714 }
2715
2716
2717 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_encrypted_content)
2718 {
2719         path dir = "build/test/unsigned_pkl_with_encrypted_content";
2720         prepare_directory (dir);
2721         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2722                 copy_file (i.path(), dir / i.path().filename());
2723         }
2724
2725         path const cpl = dir / ("cpl_" + encryption_test_cpl_id + ".xml");
2726         path const pkl = dir / ("pkl_" + encryption_test_pkl_id + ".xml");
2727         {
2728                 Editor e (pkl);
2729                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2730         }
2731
2732         check_verify_result (
2733                 {dir},
2734                 {
2735                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl) },
2736                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2737                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2738                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2739                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2740                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2741                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, encryption_test_pkl_id, canonical(pkl) },
2742                 });
2743 }
2744
2745
2746 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_unencrypted_content)
2747 {
2748         path dir = "build/test/verify_unsigned_pkl_with_unencrypted_content";
2749         prepare_directory (dir);
2750         for (auto i: directory_iterator("test/ref/DCP/dcp_test1")) {
2751                 copy_file (i.path(), dir / i.path().filename());
2752         }
2753
2754         {
2755                 Editor e (dir / dcp_test1_pkl);
2756                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2757         }
2758
2759         check_verify_result ({dir}, {});
2760 }
2761
2762
2763 BOOST_AUTO_TEST_CASE (verify_partially_encrypted)
2764 {
2765         path dir ("build/test/verify_must_not_be_partially_encrypted");
2766         prepare_directory (dir);
2767
2768         dcp::DCP d (dir);
2769
2770         auto signer = make_shared<dcp::CertificateChain>();
2771         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/ca.self-signed.pem")));
2772         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/intermediate.signed.pem")));
2773         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/leaf.signed.pem")));
2774         signer->set_key (dcp::file_to_string("test/ref/crypt/leaf.key"));
2775
2776         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2777
2778         dcp::Key key;
2779
2780         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
2781         mp->set_key (key);
2782
2783         auto writer = mp->start_write (dir / "video.mxf", false);
2784         dcp::ArrayData j2c ("test/data/flat_red.j2c");
2785         for (int i = 0; i < 24; ++i) {
2786                 writer->write (j2c.data(), j2c.size());
2787         }
2788         writer->finalize ();
2789
2790         auto ms = simple_sound (dir, "", dcp::MXFMetadata(), "de-DE");
2791
2792         auto reel = make_shared<dcp::Reel>(
2793                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2794                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2795                 );
2796
2797         reel->add (simple_markers());
2798
2799         cpl->add (reel);
2800
2801         cpl->set_content_version (
2802                 {"urn:uri:81fb54df-e1bf-4647-8788-ea7ba154375b_2012-07-17T04:45:18+00:00", "81fb54df-e1bf-4647-8788-ea7ba154375b_2012-07-17T04:45:18+00:00"}
2803                 );
2804         cpl->set_annotation_text ("A Test DCP");
2805         cpl->set_issuer ("OpenDCP 0.0.25");
2806         cpl->set_creator ("OpenDCP 0.0.25");
2807         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
2808         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
2809         cpl->set_main_sound_sample_rate (48000);
2810         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
2811         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
2812         cpl->set_version_number (1);
2813
2814         d.add (cpl);
2815
2816         d.write_xml ("OpenDCP 0.0.25", "OpenDCP 0.0.25", "2012-07-17T04:45:18+00:00", "A Test DCP", signer);
2817
2818         check_verify_result (
2819                 {dir},
2820                 {
2821                         {dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::PARTIALLY_ENCRYPTED},
2822                 });
2823 }
2824
2825
2826 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_2k)
2827 {
2828         vector<dcp::VerificationNote> notes;
2829         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV", "j2c.mxf"));
2830         auto reader = picture.start_read ();
2831         auto frame = reader->get_frame (0);
2832         verify_j2k (frame, notes);
2833         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2834 }
2835
2836
2837 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_4k)
2838 {
2839         vector<dcp::VerificationNote> notes;
2840         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "sul", "TLR"));
2841         auto reader = picture.start_read ();
2842         auto frame = reader->get_frame (0);
2843         verify_j2k (frame, notes);
2844         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2845 }
2846
2847
2848 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_libdcp)
2849 {
2850         boost::filesystem::path dir = "build/test/verify_jpeg2000_codestream_libdcp";
2851         prepare_directory (dir);
2852         auto dcp = make_simple (dir);
2853         dcp->write_xml ();
2854         vector<dcp::VerificationNote> notes;
2855         dcp::MonoPictureAsset picture (find_file(dir, "video"));
2856         auto reader = picture.start_read ();
2857         auto frame = reader->get_frame (0);
2858         verify_j2k (frame, notes);
2859         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2860 }
2861
2862
2863 /** Check that ResourceID and the XML ID being different is spotted */
2864 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_resource_id)
2865 {
2866         boost::filesystem::path const dir = "build/test/verify_mismatched_subtitle_resource_id";
2867         prepare_directory (dir);
2868
2869         ASDCP::WriterInfo writer_info;
2870         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
2871
2872         unsigned int c;
2873         auto mxf_id = dcp::make_uuid ();
2874         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
2875         BOOST_REQUIRE (c == Kumu::UUID_Length);
2876
2877         auto resource_id = dcp::make_uuid ();
2878         ASDCP::TimedText::TimedTextDescriptor descriptor;
2879         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
2880         DCP_ASSERT (c == Kumu::UUID_Length);
2881
2882         auto xml_id = dcp::make_uuid ();
2883         ASDCP::TimedText::MXFWriter writer;
2884         auto subs_mxf = dir / "subs.mxf";
2885         auto r = writer.OpenWrite(subs_mxf.c_str(), writer_info, descriptor, 4096);
2886         BOOST_REQUIRE (ASDCP_SUCCESS(r));
2887         writer.WriteTimedTextResource (dcp::String::compose(
2888                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
2889                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
2890                 "<Id>urn:uuid:%1</Id>"
2891                 "<ContentTitleText>Content</ContentTitleText>"
2892                 "<AnnotationText>Annotation</AnnotationText>"
2893                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
2894                 "<ReelNumber>1</ReelNumber>"
2895                 "<Language>en-US</Language>"
2896                 "<EditRate>25 1</EditRate>"
2897                 "<TimeCodeRate>25</TimeCodeRate>"
2898                 "<StartTime>00:00:00:00</StartTime>"
2899                 "<SubtitleList>"
2900                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
2901                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
2902                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
2903                 "</Subtitle>"
2904                 "</Font>"
2905                 "</SubtitleList>"
2906                 "</SubtitleReel>",
2907                 xml_id).c_str());
2908
2909         writer.Finalize();
2910
2911         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
2912         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
2913
2914         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
2915
2916         check_verify_result (
2917                 { dir },
2918                 {
2919                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
2920                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID },
2921                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
2922                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2923                 });
2924 }
2925
2926
2927 /** Check that ResourceID and the MXF ID being the same is spotted */
2928 BOOST_AUTO_TEST_CASE (verify_incorrect_timed_text_id)
2929 {
2930         boost::filesystem::path const dir = "build/test/verify_incorrect_timed_text_id";
2931         prepare_directory (dir);
2932
2933         ASDCP::WriterInfo writer_info;
2934         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
2935
2936         unsigned int c;
2937         auto mxf_id = dcp::make_uuid ();
2938         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
2939         BOOST_REQUIRE (c == Kumu::UUID_Length);
2940
2941         auto resource_id = mxf_id;
2942         ASDCP::TimedText::TimedTextDescriptor descriptor;
2943         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
2944         DCP_ASSERT (c == Kumu::UUID_Length);
2945
2946         auto xml_id = resource_id;
2947         ASDCP::TimedText::MXFWriter writer;
2948         auto subs_mxf = dir / "subs.mxf";
2949         auto r = writer.OpenWrite(subs_mxf.c_str(), writer_info, descriptor, 4096);
2950         BOOST_REQUIRE (ASDCP_SUCCESS(r));
2951         writer.WriteTimedTextResource (dcp::String::compose(
2952                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
2953                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
2954                 "<Id>urn:uuid:%1</Id>"
2955                 "<ContentTitleText>Content</ContentTitleText>"
2956                 "<AnnotationText>Annotation</AnnotationText>"
2957                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
2958                 "<ReelNumber>1</ReelNumber>"
2959                 "<Language>en-US</Language>"
2960                 "<EditRate>25 1</EditRate>"
2961                 "<TimeCodeRate>25</TimeCodeRate>"
2962                 "<StartTime>00:00:00:00</StartTime>"
2963                 "<SubtitleList>"
2964                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
2965                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
2966                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
2967                 "</Subtitle>"
2968                 "</Font>"
2969                 "</SubtitleList>"
2970                 "</SubtitleReel>",
2971                 xml_id).c_str());
2972
2973         writer.Finalize();
2974
2975         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
2976         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
2977
2978         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
2979
2980         check_verify_result (
2981                 { dir },
2982                 {
2983                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
2984                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID },
2985                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
2986                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2987                 });
2988 }
2989
2990
2991 /** Check a DCP with a 3D asset marked as 2D */
2992 BOOST_AUTO_TEST_CASE (verify_threed_marked_as_twod)
2993 {
2994         check_verify_result (
2995                 { private_test / "data" / "xm" },
2996                 {
2997                         {
2998                                 dcp::VerificationNote::Type::WARNING,
2999                                 dcp::VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, boost::filesystem::canonical(find_file(private_test / "data" / "xm", "j2c"))
3000                         },
3001                         {
3002                                 dcp::VerificationNote::Type::BV21_ERROR,
3003                                 dcp::VerificationNote::Code::INVALID_STANDARD
3004                         },
3005                 });
3006
3007 }
3008