When reading a DCP set up asset hashes from the CPL/PKL, not by digesting the actual...
[libdcp.git] / test / dcp_test.cc
1 /*
2     Copyright (C) 2013-2020 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
6     libdcp is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     libdcp is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with libdcp.  If not, see <http://www.gnu.org/licenses/>.
18
19     In addition, as a special exception, the copyright holders give
20     permission to link the code of portions of this program with the
21     OpenSSL library under certain conditions as described in each
22     individual source file, and distribute linked combinations
23     including the two.
24
25     You must obey the GNU General Public License in all respects
26     for all of the code used other than OpenSSL.  If you modify
27     file(s) with this exception, you may extend this exception to your
28     version of the file(s), but you are not obligated to do so.  If you
29     do not wish to do so, delete this exception statement from your
30     version.  If you delete this exception statement from all source
31     files in the program, then also delete it here.
32 */
33
34
35 #include "atmos_asset.h"
36 #include "compose.hpp"
37 #include "cpl.h"
38 #include "dcp.h"
39 #include "metadata.h"
40 #include "mono_picture_asset.h"
41 #include "picture_asset_writer.h"
42 #include "reel.h"
43 #include "reel_atmos_asset.h"
44 #include "reel_markers_asset.h"
45 #include "reel_mono_picture_asset.h"
46 #include "reel_picture_asset.h"
47 #include "reel_sound_asset.h"
48 #include "reel_stereo_picture_asset.h"
49 #include "sound_asset.h"
50 #include "sound_asset_writer.h"
51 #include "stereo_picture_asset.h"
52 #include "test.h"
53 #include <asdcp/KM_util.h>
54 #include <sndfile.h>
55 #include <boost/test/unit_test.hpp>
56
57
58 using std::dynamic_pointer_cast;
59 using std::make_shared;
60 using std::shared_ptr;
61 using std::string;
62 using std::vector;
63 #if BOOST_VERSION >= 106100
64 using namespace boost::placeholders;
65 #endif
66
67
68 /** Test creation of a 2D SMPTE DCP from very simple inputs */
69 BOOST_AUTO_TEST_CASE (dcp_test1)
70 {
71         RNGFixer fixer;
72
73         auto dcp = make_simple("build/test/DCP/dcp_test1");
74         dcp->set_issuer("OpenDCP 0.0.25");
75         dcp->set_creator("OpenDCP 0.0.25");
76         dcp->set_issue_date("2012-07-17T04:45:18+00:00");
77         dcp->set_annotation_text("A Test DCP");
78         dcp->write_xml();
79
80         /* build/test/DCP/dcp_test1 is checked against test/ref/DCP/dcp_test1 by run/tests */
81 }
82
83 /** Test creation of a 3D DCP from very simple inputs */
84 BOOST_AUTO_TEST_CASE (dcp_test2)
85 {
86         RNGFixer fix;
87
88         /* Some known metadata */
89         dcp::MXFMetadata mxf_meta;
90         mxf_meta.company_name = "OpenDCP";
91         mxf_meta.product_name = "OpenDCP";
92         mxf_meta.product_version = "0.0.25";
93
94         /* We're making build/test/DCP/dcp_test2 */
95         boost::filesystem::remove_all ("build/test/DCP/dcp_test2");
96         boost::filesystem::create_directories ("build/test/DCP/dcp_test2");
97         dcp::DCP d ("build/test/DCP/dcp_test2");
98         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
99         cpl->set_content_version (
100                 dcp::ContentVersion("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")
101                 );
102         cpl->set_issuer ("OpenDCP 0.0.25");
103         cpl->set_creator ("OpenDCP 0.0.25");
104         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
105         cpl->set_annotation_text ("A Test DCP");
106
107         auto mp = make_shared<dcp::StereoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
108         mp->set_metadata (mxf_meta);
109         auto picture_writer = mp->start_write("build/test/DCP/dcp_test2/video.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
110         dcp::ArrayData j2c ("test/data/flat_red.j2c");
111         for (int i = 0; i < 24; ++i) {
112                 /* Left */
113                 picture_writer->write (j2c.data (), j2c.size ());
114                 /* Right */
115                 picture_writer->write (j2c.data (), j2c.size ());
116         }
117         picture_writer->finalize ();
118
119         auto ms = make_shared<dcp::SoundAsset>(dcp::Fraction(24, 1), 48000, 1, dcp::LanguageTag("en-GB"), dcp::Standard::SMPTE);
120         ms->set_metadata (mxf_meta);
121         auto sound_writer = ms->start_write("build/test/DCP/dcp_test2/audio.mxf", {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
122
123         SF_INFO info;
124         info.format = 0;
125         auto sndfile = sf_open ("test/data/1s_24-bit_48k_silence.wav", SFM_READ, &info);
126         BOOST_CHECK (sndfile);
127         float buffer[4096*6];
128         float* channels[1];
129         channels[0] = buffer;
130         while (true) {
131                 auto N = sf_readf_float (sndfile, buffer, 4096);
132                 sound_writer->write(channels, 1, N);
133                 if (N < 4096) {
134                         break;
135                 }
136         }
137
138         sound_writer->finalize ();
139
140         cpl->add (make_shared<dcp::Reel>(
141                           make_shared<dcp::ReelStereoPictureAsset>(mp, 0),
142                           make_shared<dcp::ReelSoundAsset>(ms, 0)
143                           ));
144
145         d.add (cpl);
146
147         d.set_issuer("OpenDCP 0.0.25");
148         d.set_creator("OpenDCP 0.0.25");
149         d.set_issue_date("2012-07-17T04:45:18+00:00");
150         d.set_annotation_text("Created by libdcp");
151         d.write_xml();
152
153         /* build/test/DCP/dcp_test2 is checked against test/ref/DCP/dcp_test2 by run/tests */
154 }
155
156 static void
157 note (dcp::NoteType, string)
158 {
159
160 }
161
162 /** Test comparison of a DCP with itself */
163 BOOST_AUTO_TEST_CASE (dcp_test3)
164 {
165         dcp::DCP A ("test/ref/DCP/dcp_test1");
166         A.read ();
167         dcp::DCP B ("test/ref/DCP/dcp_test1");
168         B.read ();
169
170         BOOST_CHECK (A.equals (B, dcp::EqualityOptions(), boost::bind (&note, _1, _2)));
171 }
172
173 /** Test comparison of a DCP with a different DCP */
174 BOOST_AUTO_TEST_CASE (dcp_test4)
175 {
176         dcp::DCP A ("test/ref/DCP/dcp_test1");
177         A.read ();
178         dcp::DCP B ("test/ref/DCP/dcp_test2");
179         B.read ();
180
181         BOOST_CHECK (!A.equals (B, dcp::EqualityOptions(), boost::bind (&note, _1, _2)));
182 }
183
184 static
185 void
186 test_rewriting_sound(string name, bool modify)
187 {
188         using namespace boost::filesystem;
189
190         dcp::DCP A ("test/ref/DCP/dcp_test1");
191         A.read ();
192
193         BOOST_REQUIRE (!A.cpls().empty());
194         BOOST_REQUIRE (!A.cpls().front()->reels().empty());
195         auto A_picture = dynamic_pointer_cast<dcp::ReelMonoPictureAsset>(A.cpls().front()->reels().front()->main_picture());
196         BOOST_REQUIRE (A_picture);
197         auto A_sound = dynamic_pointer_cast<dcp::ReelSoundAsset>(A.cpls().front()->reels().front()->main_sound());
198
199         string const picture = "j2c_5279f9aa-94d7-42a6-b0e0-e4eaec4e2a15.mxf";
200
201         remove_all ("build/test/" + name);
202         dcp::DCP B ("build/test/" + name);
203         auto reel = make_shared<dcp::Reel>();
204
205         BOOST_REQUIRE (A_picture->mono_asset());
206         BOOST_REQUIRE (A_picture->mono_asset()->file());
207         copy_file (A_picture->mono_asset()->file().get(), path("build") / "test" / name / picture);
208         reel->add(make_shared<dcp::ReelMonoPictureAsset>(make_shared<dcp::MonoPictureAsset>(path("build") / "test" / name / picture), 0));
209
210         auto reader = A_sound->asset()->start_read();
211         auto sound = make_shared<dcp::SoundAsset>(A_sound->asset()->edit_rate(), A_sound->asset()->sampling_rate(), A_sound->asset()->channels(), dcp::LanguageTag("en-US"), dcp::Standard::SMPTE);
212         auto writer = sound->start_write(path("build") / "test" / name / "pcm_8246f87f-e1df-4c42-a290-f3b3069ff021.mxf", {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
213
214         bool need_to_modify = modify;
215         for (int i = 0; i < A_sound->asset()->intrinsic_duration(); ++i) {
216                 auto sf = reader->get_frame (i);
217                 float* out[sf->channels()];
218                 for (int j = 0; j < sf->channels(); ++j) {
219                         out[j] = new float[sf->samples()];
220                 }
221                 for (int j = 0; j < sf->samples(); ++j) {
222                         for (int k = 0; k < sf->channels(); ++k) {
223                                 out[k][j] = static_cast<float>(sf->get(k, j)) / (1 << 23);
224                                 if (need_to_modify) {
225                                         out[k][j] += 1.0 / (1 << 23);
226                                         need_to_modify = false;
227                                 }
228                         }
229                 }
230                 writer->write(out, sf->channels(), sf->samples());
231                 for (int j = 0; j < sf->channels(); ++j) {
232                         delete[] out[j];
233                 }
234         }
235         writer->finalize();
236
237         reel->add(make_shared<dcp::ReelSoundAsset>(sound, 0));
238         reel->add(simple_markers());
239
240         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
241         cpl->add (reel);
242
243         B.add (cpl);
244         B.write_xml ();
245
246         dcp::EqualityOptions eq;
247         eq.reel_hashes_can_differ = true;
248         eq.max_audio_sample_error = 0;
249         if (modify) {
250                 BOOST_CHECK (!A.equals(B, eq, boost::bind(&note, _1, _2)));
251         } else {
252                 BOOST_CHECK (A.equals(B, eq, boost::bind(&note, _1, _2)));
253         }
254 }
255
256 /** Test comparison of a DCP with another that has the same picture and the same (but re-written) sound */
257 BOOST_AUTO_TEST_CASE (dcp_test9)
258 {
259         test_rewriting_sound ("dcp_test9", false);
260 }
261
262 /** Test comparison of a DCP with another that has the same picture and very slightly modified sound */
263 BOOST_AUTO_TEST_CASE (dcp_test10)
264 {
265         test_rewriting_sound ("dcp_test10", true);
266 }
267
268 /** Test creation of a 2D DCP with an Atmos track */
269 BOOST_AUTO_TEST_CASE (dcp_test5)
270 {
271         RNGFixer fix;
272
273         /* Some known metadata */
274         dcp::MXFMetadata mxf_meta;
275         mxf_meta.company_name = "OpenDCP";
276         mxf_meta.product_name = "OpenDCP";
277         mxf_meta.product_version = "0.0.25";
278
279         /* We're making build/test/DCP/dcp_test5 */
280         boost::filesystem::remove_all ("build/test/DCP/dcp_test5");
281         boost::filesystem::create_directories ("build/test/DCP/dcp_test5");
282         dcp::DCP d ("build/test/DCP/dcp_test5");
283         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
284         cpl->set_content_version (
285                 dcp::ContentVersion("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")
286                 );
287         cpl->set_issuer ("OpenDCP 0.0.25");
288         cpl->set_creator ("OpenDCP 0.0.25");
289         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
290         cpl->set_annotation_text ("A Test DCP");
291
292         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
293         mp->set_metadata (mxf_meta);
294         auto picture_writer = mp->start_write("build/test/DCP/dcp_test5/video.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
295         dcp::ArrayData j2c ("test/data/flat_red.j2c");
296         for (int i = 0; i < 24; ++i) {
297                 picture_writer->write (j2c.data (), j2c.size ());
298         }
299         picture_writer->finalize ();
300
301         auto ms = make_shared<dcp::SoundAsset>(dcp::Fraction(24, 1), 48000, 1, dcp::LanguageTag("en-GB"), dcp::Standard::SMPTE);
302         ms->set_metadata (mxf_meta);
303         auto sound_writer = ms->start_write("build/test/DCP/dcp_test5/audio.mxf", {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
304
305         SF_INFO info;
306         info.format = 0;
307         SNDFILE* sndfile = sf_open ("test/data/1s_24-bit_48k_silence.wav", SFM_READ, &info);
308         BOOST_CHECK (sndfile);
309         float buffer[4096*6];
310         float* channels[1];
311         channels[0] = buffer;
312         while (true) {
313                 sf_count_t N = sf_readf_float (sndfile, buffer, 4096);
314                 sound_writer->write(channels, 1, N);
315                 if (N < 4096) {
316                         break;
317                 }
318         }
319
320         sound_writer->finalize ();
321
322         auto am = make_shared<dcp::AtmosAsset>(private_test / "20160218_NameOfFilm_FTR_OV_EN_A_dcs_r01.mxf");
323
324         cpl->add(make_shared<dcp::Reel>(
325                         make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
326                         make_shared<dcp::ReelSoundAsset>(ms, 0),
327                         shared_ptr<dcp::ReelSubtitleAsset>(),
328                         shared_ptr<dcp::ReelMarkersAsset>(),
329                         make_shared<dcp::ReelAtmosAsset>(am, 0)
330                         ));
331
332         d.add (cpl);
333
334         d.set_issuer("OpenDCP 0.0.25");
335         d.set_creator("OpenDCP 0.0.25");
336         d.set_issue_date("2012-07-17T04:45:18+00:00");
337         d.set_annotation_text("Created by libdcp");
338         d.write_xml();
339
340         /* build/test/DCP/dcp_test5 is checked against test/ref/DCP/dcp_test5 by run/tests */
341 }
342
343 /** Basic tests of reading a 2D DCP with an Atmos track */
344 BOOST_AUTO_TEST_CASE (dcp_test6)
345 {
346         dcp::DCP dcp ("test/ref/DCP/dcp_test5");
347         dcp.read ();
348
349         BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 1U);
350         BOOST_REQUIRE_EQUAL (dcp.cpls()[0]->reels().size(), 1U);
351         BOOST_CHECK (dcp.cpls().front()->reels().front()->main_picture());
352         BOOST_CHECK (dcp.cpls().front()->reels().front()->main_sound());
353         BOOST_CHECK (!dcp.cpls().front()->reels().front()->main_subtitle());
354         BOOST_CHECK (dcp.cpls().front()->reels().front()->atmos());
355 }
356
357 /** Test creation of a 2D Interop DCP from very simple inputs */
358 BOOST_AUTO_TEST_CASE (dcp_test7)
359 {
360         RNGFixer fix;
361
362         auto dcp = make_simple("build/test/DCP/dcp_test7", 1, 24, dcp::Standard::INTEROP);
363         dcp->set_issuer("OpenDCP 0.0.25");
364         dcp->set_creator("OpenDCP 0.0.25");
365         dcp->set_issue_date("2012-07-17T04:45:18+00:00");
366         dcp->set_annotation_text("Created by libdcp");
367         dcp->write_xml();
368
369         /* build/test/DCP/dcp_test7 is checked against test/ref/DCP/dcp_test7 by run/tests */
370 }
371
372 /** Test reading of a DCP with multiple CPLs */
373 BOOST_AUTO_TEST_CASE (dcp_test8)
374 {
375         dcp::DCP dcp (private_test / "data/SMPTE_TST-B1PB2P_S_EN-EN-CCAP_5171-HI-VI_2K_ISDCF_20151123_DPPT_SMPTE_combo/");
376         dcp.read ();
377
378         BOOST_REQUIRE_EQUAL (dcp.cpls().size(), 2U);
379 }
380
381
382 /** Test reading a DCP whose ASSETMAP contains assets not used by any PKL */
383 BOOST_AUTO_TEST_CASE (dcp_things_in_assetmap_not_in_pkl)
384 {
385         dcp::DCP dcp ("test/data/extra_assetmap");
386         BOOST_CHECK_NO_THROW (dcp.read());
387 }
388
389
390 /** Test that writing the XML for a DCP with no CPLs throws */
391 BOOST_AUTO_TEST_CASE (dcp_with_no_cpls)
392 {
393         dcp::DCP dcp ("build/test/dcp_with_no_cpls");
394         BOOST_REQUIRE_THROW (dcp.write_xml(), dcp::MiscError);
395 }
396
397
398 /** Test that writing the XML for a DCP with Interop CPLs makes a SMPTE assetmap */
399 BOOST_AUTO_TEST_CASE (dcp_with_interop_cpls)
400 {
401         boost::filesystem::path path = "build/test/dcp_with_interop_cpls";
402         boost::filesystem::remove_all (path);
403         dcp::DCP dcp (path);
404         auto cpl1 = make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::INTEROP);
405         cpl1->add(make_shared<dcp::Reel>());
406         dcp.add(cpl1);
407         auto cpl2 = make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::INTEROP);
408         cpl2->add(make_shared<dcp::Reel>());
409         dcp.add(cpl2);
410         dcp.write_xml ();
411         BOOST_REQUIRE (boost::filesystem::exists(path / "ASSETMAP"));
412         BOOST_REQUIRE (!boost::filesystem::exists(path / "ASSETMAP.xml"));
413 }
414
415
416 /** Test that writing the XML for a DCP with SMPTE CPLs makes a SMPTE assetmap */
417 BOOST_AUTO_TEST_CASE (dcp_with_smpte_cpls)
418 {
419         boost::filesystem::path path = "build/test/dcp_with_smpte_cpls";
420         boost::filesystem::remove_all (path);
421         dcp::DCP dcp (path);
422         auto cpl1 = make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
423         cpl1->add(make_shared<dcp::Reel>());
424         dcp.add(cpl1);
425         auto cpl2 = make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE);
426         cpl2->add(make_shared<dcp::Reel>());
427         dcp.add(cpl2);
428         dcp.write_xml ();
429         BOOST_REQUIRE (!boost::filesystem::exists(path / "ASSETMAP"));
430         BOOST_REQUIRE (boost::filesystem::exists(path / "ASSETMAP.xml"));
431 }
432
433
434 /** Test that writing the XML for a DCP with mixed-standard CPLs throws */
435 BOOST_AUTO_TEST_CASE (dcp_with_mixed_cpls)
436 {
437         dcp::DCP dcp ("build/test/dcp_with_mixed_cpls");
438         dcp.add(make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE));
439         dcp.add(make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::INTEROP));
440         dcp.add(make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE));
441         BOOST_REQUIRE_THROW (dcp.write_xml(), dcp::MiscError);
442 }
443
444
445 BOOST_AUTO_TEST_CASE (dcp_add_kdm_test)
446 {
447         /* Some CPLs, each with a reel */
448
449         shared_ptr<dcp::CPL> cpls[] = {
450                 make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE),
451                 make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE),
452                 make_shared<dcp::CPL>("", dcp::ContentKind::FEATURE, dcp::Standard::SMPTE)
453         };
454
455         shared_ptr<dcp::Reel> reels[] = {
456                 make_shared<dcp::Reel>(),
457                 make_shared<dcp::Reel>(),
458                 make_shared<dcp::Reel>()
459         };
460
461         for (auto i = 0; i < 3; ++i) {
462                 cpls[i]->add(reels[i]);
463         }
464
465         dcp::DCP dcp ("build/test/dcp_add_kdm_test");
466         dcp.add(cpls[0]);
467         dcp.add(cpls[1]);
468         dcp.add(cpls[2]);
469
470         /* Simple KDM with one key that should be given to cpls[0] */
471
472         auto kdm_1 = dcp::DecryptedKDM({}, {}, "", "", "");
473         auto kdm_1_uuid = dcp::make_uuid();
474         kdm_1.add_key (dcp::DecryptedKDMKey(string("MDIK"), kdm_1_uuid, dcp::Key(), cpls[0]->id(), dcp::Standard::SMPTE));
475         dcp.add (kdm_1);
476         BOOST_REQUIRE_EQUAL (reels[0]->_kdms.size(), 1U);
477         BOOST_CHECK_EQUAL (reels[0]->_kdms[0].keys().size(), 1U);
478         BOOST_CHECK_EQUAL (reels[0]->_kdms[0].keys()[0].id(), kdm_1_uuid);
479         BOOST_CHECK_EQUAL (reels[1]->_kdms.size(), 0U);
480         BOOST_CHECK_EQUAL (reels[2]->_kdms.size(), 0U);
481
482         /* KDM with two keys that should be given to cpls[1] and cpls[2] */
483
484         auto kdm_2 = dcp::DecryptedKDM({}, {}, "", "", "");
485         auto kdm_2_uuid_1 = dcp::make_uuid();
486         kdm_2.add_key (dcp::DecryptedKDMKey(string("MDIK"), kdm_2_uuid_1, dcp::Key(), cpls[1]->id(), dcp::Standard::SMPTE));
487         auto kdm_2_uuid_2 = dcp::make_uuid();
488         kdm_2.add_key (dcp::DecryptedKDMKey(string("MDIK"), kdm_2_uuid_2, dcp::Key(), cpls[2]->id(), dcp::Standard::SMPTE));
489         dcp.add (kdm_2);
490         /* Unchanged from previous test */
491         BOOST_CHECK (reels[0]->_kdms.size() == 1);
492         /* kdm_2 should have been added to both the other CPLs */
493         BOOST_REQUIRE_EQUAL (reels[1]->_kdms.size(), 1U);
494         BOOST_REQUIRE_EQUAL (reels[1]->_kdms[0].keys().size(), 2U);
495         BOOST_CHECK_EQUAL (reels[1]->_kdms[0].keys()[0].id(), kdm_2_uuid_1);
496         BOOST_CHECK_EQUAL (reels[1]->_kdms[0].keys()[1].id(), kdm_2_uuid_2);
497         BOOST_REQUIRE_EQUAL (reels[2]->_kdms.size(), 1U);
498         BOOST_REQUIRE_EQUAL (reels[2]->_kdms[0].keys().size(), 2U);
499         BOOST_CHECK_EQUAL (reels[2]->_kdms[0].keys()[0].id(), kdm_2_uuid_1);
500         BOOST_CHECK_EQUAL (reels[2]->_kdms[0].keys()[1].id(), kdm_2_uuid_2);
501 }
502
503
504 BOOST_AUTO_TEST_CASE(hashes_preserved_when_loading_corrupted_dcp)
505 {
506         boost::filesystem::path const dir = "build/test/hashes_preserved_when_loading_corrupted_dcp";
507         boost::filesystem::remove_all(dir);
508
509         auto dcp = make_simple(dir / "1");
510         dcp->write_xml();
511
512         auto asset_1_id = dcp::MonoPictureAsset(dir / "1" / "video.mxf").id();
513         auto asset_1_hash = dcp::MonoPictureAsset(dir / "1" / "video.mxf").hash();
514
515         /* Replace the hash in the CPL (the one that corresponds to the actual file)
516          * with an incorrect one new_hash.
517          */
518         string new_hash;
519         {
520                 Editor editor(find_file(dir / "1", "cpl_"));
521                 auto const after = "<Duration>24</Duration>";
522                 editor.delete_lines_after(after, 1);
523
524                 if (asset_1_hash[0] == 'A') {
525                         new_hash = 'B' + asset_1_hash.substr(1);
526                 } else {
527                         new_hash = 'A' + asset_1_hash.substr(1);
528                 }
529
530                 editor.insert(after, dcp::String::compose("      <Hash>%1</Hash>", new_hash));
531         }
532
533         dcp::DCP read_back(dir / "1");
534         read_back.read();
535
536         BOOST_REQUIRE_EQUAL(read_back.cpls().size(), 1U);
537         auto cpl = read_back.cpls()[0];
538         BOOST_REQUIRE_EQUAL(cpl->reels().size(), 1U);
539         auto reel = cpl->reels()[0];
540         BOOST_REQUIRE(reel->main_picture());
541         /* Now the asset should think it has the wrong hash written to the PKL file; it shouldn't have
542          * checked the file again.
543          */
544         BOOST_CHECK_EQUAL(reel->main_picture()->asset_ref()->hash(), new_hash);
545 }