Add basic verification report. v1.9.5
authorCarl Hetherington <cth@carlh.net>
Thu, 11 Apr 2024 08:50:51 +0000 (10:50 +0200)
committerCarl Hetherington <cth@carlh.net>
Mon, 15 Apr 2024 08:59:34 +0000 (10:59 +0200)
src/verify_report.cc [new file with mode: 0644]
src/verify_report.h [new file with mode: 0644]
src/wscript
test/data/text_formatter.txt [new file with mode: 0644]
test/data/text_formatter_windows.txt [new file with mode: 0644]
test/verify_report_test.cc [new file with mode: 0644]
test/wscript
tools/dcpverify.cc

diff --git a/src/verify_report.cc b/src/verify_report.cc
new file mode 100644 (file)
index 0000000..2201b8f
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+    Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of libdcp.
+
+    libdcp is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    libdcp is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with libdcp.  If not, see <http://www.gnu.org/licenses/>.
+
+    In addition, as a special exception, the copyright holders give
+    permission to link the code of portions of this program with the
+    OpenSSL library under certain conditions as described in each
+    individual source file, and distribute linked combinations
+    including the two.
+
+    You must obey the GNU General Public License in all respects
+    for all of the code used other than OpenSSL.  If you modify
+    file(s) with this exception, you may extend this exception to your
+    version of the file(s), but you are not obligated to do so.  If you
+    do not wish to do so, delete this exception statement from your
+    version.  If you delete this exception statement from all source
+    files in the program, then also delete it here.
+*/
+
+
+#include "compose.hpp"
+#include "cpl.h"
+#include "dcp.h"
+#include "file.h"
+#include "reel.h"
+#include "reel_picture_asset.h"
+#include "reel_sound_asset.h"
+#include "reel_subtitle_asset.h"
+#include "verify.h"
+#include "verify_report.h"
+
+
+using std::shared_ptr;
+using std::string;
+using std::vector;
+using boost::optional;
+using namespace dcp;
+
+
+void write_line(File& file, string format)
+{
+       file.puts(string(format + "\n").c_str());
+}
+
+
+template <typename... Args>
+void write_line(File& file, string format, Args... args)
+{
+       file.puts(String::compose(format + "\n", std::forward<Args>(args)...).c_str());
+}
+
+
+void
+dcp::verify_report(dcp::VerificationResult const& result, Formatter& formatter)
+{
+       auto document = formatter.document();
+       auto body = formatter.body();
+
+       formatter.heading("DCP verification report");
+
+       if (result.dcps.size() > 1) {
+               formatter.subheading("DCPs");
+       } else {
+               formatter.subheading("DCP");
+       }
+
+       auto reel_asset_details = [&formatter](shared_ptr<dcp::ReelAsset> asset) {
+               formatter.list_item(String::compose("UUID: %1", asset->id()));
+               formatter.list_item(String::compose("Intrinsic duration: %1", asset->intrinsic_duration()));
+               formatter.list_item(String::compose("Entry point: %1", asset->entry_point().get_value_or(0)));
+               formatter.list_item(String::compose("Duration: %1", asset->duration().get_value_or(0)));
+               if (asset->annotation_text()) {
+                       formatter.list_item(String::compose("Annotation text: %1", *asset->annotation_text()));
+               }
+       };
+
+       auto write_notes = [&formatter](dcp::VerificationResult const& result, optional<string> cpl_id) {
+               for (auto note: result.notes) {
+                       if (note.cpl_id() == cpl_id) {
+                               auto const note_as_string = dcp::note_to_string(note, formatter.process_string(), formatter.process_filename());
+                               if (note.type() == dcp::VerificationNote::Type::OK) {
+                                       formatter.list_item(note_as_string, string("ok"));
+                               } else if (note.type() == dcp::VerificationNote::Type::WARNING) {
+                                       formatter.list_item(note_as_string, string("warning"));
+                               } else if (note.type() == dcp::VerificationNote::Type::ERROR) {
+                                       formatter.list_item(note_as_string, string("error"));
+                               }
+                       }
+               }
+       };
+
+       for (auto dcp: result.dcps) {
+               auto ul = formatter.unordered_list();
+               for (auto cpl: dcp->cpls()) {
+                       formatter.list_item(String::compose("CPL ID: %1", cpl->id()));
+                       int reel_index = 1;
+                       for (auto reel: cpl->reels()) {
+                               formatter.list_item(String::compose("Reel: %1", reel_index++));
+                               auto ul2 = formatter.unordered_list();
+                               if (auto pic = reel->main_picture()) {
+                                       formatter.list_item("Main picture");
+                                       auto ul3 = formatter.unordered_list();
+                                       reel_asset_details(pic);
+                                       formatter.list_item(String::compose("Frame rate: %1", pic->frame_rate().numerator));
+                                       formatter.list_item(String::compose("Screen aspect ratio: %1x%2", pic->screen_aspect_ratio().numerator, pic->screen_aspect_ratio().denominator));
+                               }
+                               if (auto sound = reel->main_sound()) {
+                                       formatter.list_item("Main sound");
+                                       auto ul3 = formatter.unordered_list();
+                                       reel_asset_details(sound);
+                               }
+                               if (auto sub = reel->main_subtitle()) {
+                                       formatter.list_item("Main subtitle");
+                                       auto ul3 = formatter.unordered_list();
+                                       reel_asset_details(sub);
+                                       if (sub->language()) {
+                                               formatter.list_item(String::compose("Language: %1", *sub->language()));
+                                       }
+                               }
+                       }
+                       write_notes(result, cpl->id());
+               }
+       }
+
+       if (std::count_if(result.notes.begin(), result.notes.end(), [](VerificationNote const& note) { return !note.cpl_id(); }) > 0) {
+               formatter.subheading("Report");
+               write_notes(result, {});
+       }
+}
+
diff --git a/src/verify_report.h b/src/verify_report.h
new file mode 100644 (file)
index 0000000..b8ca551
--- /dev/null
@@ -0,0 +1,237 @@
+/*
+    Copyright (C) 2022 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "compose.hpp"
+#include "file.h"
+#include "verify.h"
+#include <boost/filesystem.hpp>
+#include <vector>
+
+
+namespace dcp {
+
+
+class Formatter
+{
+public:
+       Formatter(boost::filesystem::path file)
+               : _file(file, "w")
+       {}
+
+       class Wrap
+       {
+       public:
+               Wrap() = default;
+
+               Wrap(Formatter* formatter, std::string const& close)
+                       : _formatter(formatter)
+                       , _close(close)
+               {}
+
+               Wrap(Formatter* formatter, std::string const& close, std::function<void ()> closer)
+                       : _formatter(formatter)
+                       , _close(close)
+                       , _closer(closer)
+               {}
+
+               Wrap(Wrap&& other)
+               {
+                       std::swap(_formatter, other._formatter);
+                       std::swap(_close, other._close);
+                       std::swap(_closer, other._closer);
+               }
+
+               ~Wrap()
+               {
+                       if (_formatter) {
+                               _formatter->file().puts(_close.c_str());
+                       }
+                       if (_closer) {
+                               _closer();
+                       }
+               }
+
+       private:
+               Formatter* _formatter = nullptr;
+               std::string _close;
+               std::function<void ()> _closer = nullptr;
+       };
+
+       virtual Wrap document() { return {}; }
+
+       virtual void heading(std::string const& text) = 0;
+       virtual void subheading(std::string const& text) = 0;
+       virtual Wrap body() { return {}; }
+
+       virtual Wrap unordered_list() = 0;
+       virtual void list_item(std::string const& text, boost::optional<std::string> type = {}) = 0;
+
+       virtual std::function<std::string (std::string)> process_string() = 0;
+       virtual std::function<std::string (std::string)> process_filename() = 0;
+
+       dcp::File& file() {
+               return _file;
+       }
+
+protected:
+       dcp::File _file;
+};
+
+
+class TextFormatter : public Formatter
+{
+public:
+       TextFormatter(boost::filesystem::path file)
+               : Formatter(file)
+       {}
+
+       void heading(std::string const& text) override {
+               print(text);
+       }
+
+       void subheading(std::string const& text) override {
+               print("");
+               print(text);
+       }
+
+       Wrap unordered_list() override {
+               _indent++;
+               return Wrap(this, "", [this]() { _indent--; });
+       }
+
+       void list_item(std::string const& text, boost::optional<std::string> type = {}) override {
+               LIBDCP_UNUSED(type);
+               for (int i = 0; i < _indent * 2; ++i) {
+                       _file.puts(" ");
+               }
+               _file.puts("* ");
+               print(text);
+       }
+
+       std::function<std::string (std::string)> process_string() override {
+               return [](std::string s) {
+                       return s;
+               };
+       }
+
+       std::function<std::string (std::string)> process_filename() override {
+               return [](std::string s) {
+                       return s;
+               };
+       }
+
+private:
+       void print(std::string const& text) {
+               _file.puts(text.c_str());
+               _file.puts("\n");
+       }
+
+       int _indent = 0;
+};
+
+
+class HTMLFormatter : public Formatter
+{
+public:
+       HTMLFormatter(boost::filesystem::path file)
+               : Formatter(file)
+       {}
+
+       void heading(std::string const& text) override {
+               tagged("h1", text);
+       }
+
+       void subheading(std::string const& text) override {
+               tagged("h2", text);
+       }
+
+       Wrap document() override {
+               auto html = wrapped("html");
+               auto head = wrapped("head");
+               auto style = wrapped("style");
+               _file.puts("li {\n"
+                          "  margin: 2px;\n"
+                          "  padding: 2px 2px 2px 1em;\n"
+                          "}\n"
+                         );
+               _file.puts("li.ok {\n"
+                          "  background-color: #00ff00;\n"
+                          "}\n"
+                          "li.warning {\n"
+                          "  background-color: #ffa500;\n"
+                          "}\n"
+                          "li.error {\n"
+                          "  background-color: #ff0000;\n"
+                          "}\n"
+                          "ul {\n"
+                          "  list-style: none;\n"
+                          "}\n"
+                         );
+               return html;
+       }
+
+       Wrap body() override {
+               return wrapped("body");
+       }
+
+       Wrap unordered_list() override {
+               return wrapped("ul");
+       }
+
+       void list_item(std::string const& text, boost::optional<std::string> type = {}) override {
+               if (type) {
+                       _file.puts(dcp::String::compose("<li class=\"%1\">%2", *type, text).c_str());
+               } else {
+                       _file.puts(dcp::String::compose("<li>%1", text).c_str());
+               }
+       }
+
+       std::function<std::string (std::string)> process_string() override {
+               return [](std::string s) {
+                       boost::replace_all(s, "<", "&lt;");
+                       boost::replace_all(s, ">", "&gt;");
+                       return s;
+               };
+       }
+
+       std::function<std::string (std::string)> process_filename() override {
+               return [](std::string s) {
+                       return String::compose("<code>%1</code>", s);
+               };
+       }
+
+private:
+       void tagged(std::string tag, std::string content) {
+               _file.puts(String::compose("<%1>%2</%3>\n", tag, content, tag).c_str());
+       };
+
+       Wrap wrapped(std::string const& tag) {
+               _file.puts(String::compose("<%1>", tag).c_str());
+               return Wrap(this, String::compose("</%1>", tag));
+       };
+};
+
+
+extern void verify_report(dcp::VerificationResult const& result, Formatter& formatter);
+
+
+}
+
index 29eb37abf58d6ce57a52d2a8c2fc8cda190fe020..adaa57bb3104ca2aaf3661a880495c3fb86cffea 100644 (file)
@@ -125,6 +125,7 @@ def build(bld):
              v_align.cc
              verify.cc
              verify_j2k.cc
+             verify_report.cc
              version.cc
              """
 
@@ -232,6 +233,7 @@ def build(bld):
               v_align.h
               verify.h
               verify_j2k.h
+              verify_report.h
               version.h
               warnings.h
               """
diff --git a/test/data/text_formatter.txt b/test/data/text_formatter.txt
new file mode 100644 (file)
index 0000000..8861db9
--- /dev/null
@@ -0,0 +1,8 @@
+Heading
+
+Subheading
+  * Foo
+  * Bar
+    * Fred
+    * Jim
+    * Sheila
diff --git a/test/data/text_formatter_windows.txt b/test/data/text_formatter_windows.txt
new file mode 100644 (file)
index 0000000..16d78d8
--- /dev/null
@@ -0,0 +1,8 @@
+Heading\r
+\r
+Subheading\r
+  * Foo\r
+  * Bar\r
+    * Fred\r
+    * Jim\r
+    * Sheila\r
diff --git a/test/verify_report_test.cc b/test/verify_report_test.cc
new file mode 100644 (file)
index 0000000..6b28889
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+    Copyright (C) 2022 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "verify.h"
+#include "verify_report.h"
+#include <boost/test/unit_test.hpp>
+#include "test.h"
+
+
+BOOST_AUTO_TEST_CASE(verify_report_basically_ok)
+{
+       dcp::HTMLFormatter formatter("build/test/verify_report_basically_ok.html");
+       dcp::verify_report(
+               dcp::verify(
+                       { private_test / "TONEPLATES-SMPTE-PLAINTEXT_TST_F_XX-XX_ITL-TD_51-XX_2K_WOE_20111001_WOE_OV" },
+                       {},
+                       [](std::string, boost::optional<boost::filesystem::path>) {},
+                       [](float) {},
+                       {},
+                       xsd_test
+                       ),
+               formatter
+               );
+}
+
+
+BOOST_AUTO_TEST_CASE(text_formatter)
+{
+       {
+               dcp::TextFormatter fmt("build/test/text_formatter.txt");
+
+               fmt.heading("Heading");
+               fmt.subheading("Subheading");
+               auto A = fmt.unordered_list();
+               fmt.list_item("Foo");
+               fmt.list_item("Bar");
+               auto B = fmt.unordered_list();
+               fmt.list_item("Fred");
+               fmt.list_item("Jim");
+               fmt.list_item("Sheila");
+       }
+
+#ifdef LIBDCP_WINDOWS
+       check_file("test/data/text_formatter_windows.txt", "build/test/text_formatter.txt");
+#else
+       check_file("test/data/text_formatter.txt", "build/test/text_formatter.txt");
+#endif
+}
+
index e401f72094b55cd2cb7d88c60bc3d1f6f0cc26b3..92c835981e241bca21b656a161809ad47966327a 100644 (file)
@@ -117,6 +117,7 @@ def build(bld):
                  utf8_test.cc
                  v_align_test.cc
                  verify_test.cc
+                 verify_report_test.cc
                  """
     obj.target = 'tests'
     obj.install_path = ''
index 1f74709773190d73ba0789b1c72c875f8ba5660e..88f0d70576ba4b2bc91d5d93848dd6b638170d41 100644 (file)
@@ -37,6 +37,7 @@
 #include "filesystem.h"
 #include "raw_convert.h"
 #include "verify.h"
+#include "verify_report.h"
 #include "version.h"
 #include <boost/bind/bind.hpp>
 #include <boost/filesystem.hpp>
@@ -68,6 +69,7 @@ help (string n)
             << "  --ignore-bv21-smpte                          don't give the SMPTE Bv2.1 error about a DCP not being SMPTE\n"
             << "  --no-asset-hash-check                        don't check asset hashes\n"
             << "  --asset-hash-check-maximum-size <size-in-MB> only check hashes for assets smaller than this size (in MB)\n"
+            << "  -o <filename>                                write HTML report to filename\n"
             << "  -q, --quiet                                  don't report progress\n";
 }
 
@@ -80,6 +82,7 @@ main (int argc, char* argv[])
        bool ignore_missing_assets = false;
        bool ignore_bv21_smpte = false;
        bool quiet = false;
+       boost::optional<boost::filesystem::path> report_filename;
 
        dcp::VerificationOptions verification_options;
 
@@ -96,7 +99,7 @@ main (int argc, char* argv[])
                        { 0, 0, 0, 0 }
                };
 
-               int c = getopt_long (argc, argv, "VhABCD:q", long_options, &option_index);
+               int c = getopt_long (argc, argv, "VhABCD:qo:", long_options, &option_index);
 
                if (c == -1) {
                        break;
@@ -126,6 +129,9 @@ main (int argc, char* argv[])
                case 'q':
                        quiet = true;
                        break;
+               case 'o':
+                       report_filename = optarg;
+                       break;
                }
        }
 
@@ -173,8 +179,8 @@ main (int argc, char* argv[])
 
        vector<boost::filesystem::path> directories;
        directories.push_back (argv[optind]);
-       auto notes = dcp::verify(directories, {}, stage, progress, verification_options).notes;
-       dcp::filter_notes (notes, ignore_missing_assets);
+       auto result = dcp::verify(directories, {}, stage, progress, verification_options);
+       dcp::filter_notes(result.notes, ignore_missing_assets);
 
        if (!quiet) {
                cout << "\n";
@@ -183,7 +189,7 @@ main (int argc, char* argv[])
        bool failed = false;
        bool bv21_failed = false;
        bool warned = false;
-       for (auto i: notes) {
+       for (auto i: result.notes) {
                if (ignore_bv21_smpte && i.code() == dcp::VerificationNote::Code::INVALID_STANDARD) {
                        continue;
                }
@@ -217,5 +223,10 @@ main (int argc, char* argv[])
                }
        }
 
+       if (report_filename) {
+               dcp::HTMLFormatter formatter(*report_filename);
+               dcp::verify_report(result, formatter);
+       }
+
        exit (failed ? EXIT_FAILURE : EXIT_SUCCESS);
 }