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