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