946 lines
37 KiB
C++
946 lines
37 KiB
C++
#include "assets/ppi_header.h"
|
|
#include "test_harness.h"
|
|
|
|
#include <array>
|
|
#include <bit>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <span>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
using pp::assets::parse_ppi_header;
|
|
using pp::assets::create_ppi_project;
|
|
using pp::assets::create_minimal_ppi_project;
|
|
using pp::assets::decode_ppi_project_images;
|
|
using pp::assets::parse_ppi_project_index;
|
|
using pp::assets::parse_ppi_project_summary;
|
|
using pp::assets::parse_ppi_project_layout;
|
|
using pp::assets::ppi_header_size;
|
|
using pp::assets::ppi_thumbnail_byte_size;
|
|
using pp::foundation::StatusCode;
|
|
|
|
namespace {
|
|
|
|
void append_u32(std::vector<std::byte>& bytes, std::uint32_t value)
|
|
{
|
|
bytes.push_back(static_cast<std::byte>(value & 0xffU));
|
|
bytes.push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
|
|
bytes.push_back(static_cast<std::byte>((value >> 16U) & 0xffU));
|
|
bytes.push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
|
|
}
|
|
|
|
void append_u32_be(std::vector<std::byte>& bytes, std::uint32_t value)
|
|
{
|
|
bytes.push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
|
|
bytes.push_back(static_cast<std::byte>((value >> 16U) & 0xffU));
|
|
bytes.push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
|
|
bytes.push_back(static_cast<std::byte>(value & 0xffU));
|
|
}
|
|
|
|
void append_f32(std::vector<std::byte>& bytes, float value)
|
|
{
|
|
append_u32(bytes, std::bit_cast<std::uint32_t>(value));
|
|
}
|
|
|
|
void write_u32_at(std::vector<std::byte>& bytes, std::size_t offset, std::uint32_t value)
|
|
{
|
|
bytes[offset + 0U] = static_cast<std::byte>(value & 0xffU);
|
|
bytes[offset + 1U] = static_cast<std::byte>((value >> 8U) & 0xffU);
|
|
bytes[offset + 2U] = static_cast<std::byte>((value >> 16U) & 0xffU);
|
|
bytes[offset + 3U] = static_cast<std::byte>((value >> 24U) & 0xffU);
|
|
}
|
|
|
|
void write_f32_at(std::vector<std::byte>& bytes, std::size_t offset, float value)
|
|
{
|
|
write_u32_at(bytes, offset, std::bit_cast<std::uint32_t>(value));
|
|
}
|
|
|
|
void append_ascii(std::vector<std::byte>& bytes, std::string_view value)
|
|
{
|
|
for (const auto ch : value) {
|
|
bytes.push_back(static_cast<std::byte>(ch));
|
|
}
|
|
}
|
|
|
|
std::vector<std::byte> valid_header()
|
|
{
|
|
std::vector<std::byte> bytes {
|
|
std::byte { 'P' },
|
|
std::byte { 'P' },
|
|
std::byte { 'I' },
|
|
std::byte { 0 },
|
|
};
|
|
append_u32(bytes, 0);
|
|
append_u32(bytes, 4);
|
|
append_u32(bytes, 0);
|
|
append_u32(bytes, 2);
|
|
append_u32(bytes, 3);
|
|
append_u32(bytes, 1024);
|
|
append_u32(bytes, 128);
|
|
append_u32(bytes, 128);
|
|
append_u32(bytes, 4);
|
|
return bytes;
|
|
}
|
|
|
|
void append_minimal_body(std::vector<std::byte>& bytes)
|
|
{
|
|
append_u32(bytes, 64);
|
|
append_u32(bytes, 32);
|
|
append_u32(bytes, 1);
|
|
append_u32(bytes, 1);
|
|
append_u32(bytes, 0);
|
|
append_f32(bytes, 1.0F);
|
|
append_u32(bytes, 3);
|
|
append_ascii(bytes, "Ink");
|
|
append_u32(bytes, 0);
|
|
bytes.push_back(std::byte { 0 });
|
|
bytes.push_back(std::byte { 1 });
|
|
append_u32(bytes, 1);
|
|
append_u32(bytes, 100);
|
|
for (std::uint32_t i = 0; i < 6U; ++i) {
|
|
append_u32(bytes, 0);
|
|
}
|
|
append_u32(bytes, 0);
|
|
}
|
|
|
|
std::vector<std::byte> png_ihdr_payload(
|
|
std::uint32_t width,
|
|
std::uint32_t height,
|
|
std::uint8_t bit_depth = 8U,
|
|
std::uint8_t color_type = 6U)
|
|
{
|
|
std::vector<std::byte> bytes {
|
|
std::byte { 0x89 },
|
|
std::byte { 0x50 },
|
|
std::byte { 0x4e },
|
|
std::byte { 0x47 },
|
|
std::byte { 0x0d },
|
|
std::byte { 0x0a },
|
|
std::byte { 0x1a },
|
|
std::byte { 0x0a },
|
|
};
|
|
append_u32_be(bytes, 13);
|
|
bytes.push_back(std::byte { 'I' });
|
|
bytes.push_back(std::byte { 'H' });
|
|
bytes.push_back(std::byte { 'D' });
|
|
bytes.push_back(std::byte { 'R' });
|
|
append_u32_be(bytes, width);
|
|
append_u32_be(bytes, height);
|
|
bytes.push_back(static_cast<std::byte>(bit_depth));
|
|
bytes.push_back(static_cast<std::byte>(color_type));
|
|
bytes.push_back(std::byte { 0 });
|
|
bytes.push_back(std::byte { 0 });
|
|
bytes.push_back(std::byte { 0 });
|
|
append_u32_be(bytes, 0);
|
|
return bytes;
|
|
}
|
|
|
|
std::vector<std::byte> transparent_png_1x1()
|
|
{
|
|
return {
|
|
std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 },
|
|
std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a },
|
|
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x0d },
|
|
std::byte { 0x49 }, std::byte { 0x48 }, std::byte { 0x44 }, std::byte { 0x52 },
|
|
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
|
|
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
|
|
std::byte { 0x08 }, std::byte { 0x06 }, std::byte { 0x00 }, std::byte { 0x00 },
|
|
std::byte { 0x00 }, std::byte { 0x1f }, std::byte { 0x15 }, std::byte { 0xc4 },
|
|
std::byte { 0x89 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
|
std::byte { 0x0b }, std::byte { 0x49 }, std::byte { 0x44 }, std::byte { 0x41 },
|
|
std::byte { 0x54 }, std::byte { 0x78 }, std::byte { 0x9c }, std::byte { 0x63 },
|
|
std::byte { 0x60 }, std::byte { 0x00 }, std::byte { 0x02 }, std::byte { 0x00 },
|
|
std::byte { 0x00 }, std::byte { 0x05 }, std::byte { 0x00 }, std::byte { 0x01 },
|
|
std::byte { 0x7a }, std::byte { 0x5e }, std::byte { 0xab }, std::byte { 0x3f },
|
|
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
|
std::byte { 0x49 }, std::byte { 0x45 }, std::byte { 0x4e }, std::byte { 0x44 },
|
|
std::byte { 0xae }, std::byte { 0x42 }, std::byte { 0x60 }, std::byte { 0x82 },
|
|
};
|
|
}
|
|
|
|
std::vector<std::byte> minimal_project()
|
|
{
|
|
auto bytes = valid_header();
|
|
bytes.resize(ppi_header_size + (128U * 128U * 4U), std::byte { 0 });
|
|
append_minimal_body(bytes);
|
|
return bytes;
|
|
}
|
|
|
|
std::vector<std::byte> project_with_single_face_payload(
|
|
std::vector<std::byte> payload,
|
|
std::uint32_t dirty_width = 8,
|
|
std::uint32_t dirty_height = 4)
|
|
{
|
|
auto bytes = valid_header();
|
|
bytes.resize(ppi_header_size + (128U * 128U * 4U), std::byte { 0 });
|
|
|
|
append_u32(bytes, 64);
|
|
append_u32(bytes, 32);
|
|
append_u32(bytes, 1);
|
|
append_u32(bytes, 1);
|
|
append_u32(bytes, 0);
|
|
append_f32(bytes, 1.0F);
|
|
append_u32(bytes, 3);
|
|
append_ascii(bytes, "Ink");
|
|
append_u32(bytes, 0);
|
|
bytes.push_back(std::byte { 0 });
|
|
bytes.push_back(std::byte { 1 });
|
|
append_u32(bytes, 1);
|
|
append_u32(bytes, 100);
|
|
|
|
append_u32(bytes, 1);
|
|
append_u32(bytes, 2);
|
|
append_u32(bytes, 3);
|
|
append_u32(bytes, 2 + dirty_width);
|
|
append_u32(bytes, 3 + dirty_height);
|
|
append_u32(bytes, static_cast<std::uint32_t>(payload.size()));
|
|
bytes.insert(bytes.end(), payload.begin(), payload.end());
|
|
|
|
for (std::uint32_t i = 1; i < 6U; ++i) {
|
|
append_u32(bytes, 0);
|
|
}
|
|
append_u32(bytes, 0);
|
|
return bytes;
|
|
}
|
|
|
|
void parses_legacy_ppi_header(pp::tests::Harness& h)
|
|
{
|
|
const auto bytes = valid_header();
|
|
const auto header = parse_ppi_header(bytes);
|
|
|
|
PP_EXPECT(h, bytes.size() == ppi_header_size);
|
|
PP_EXPECT(h, header.ok());
|
|
PP_EXPECT(h, header.value().document_version.major == 0U);
|
|
PP_EXPECT(h, header.value().document_version.minor == 4U);
|
|
PP_EXPECT(h, header.value().software_version.fix == 3U);
|
|
PP_EXPECT(h, header.value().software_version.build == 1024U);
|
|
PP_EXPECT(h, header.value().thumbnail.width == 128U);
|
|
PP_EXPECT(h, header.value().thumbnail.height == 128U);
|
|
PP_EXPECT(h, header.value().thumbnail.components == 4U);
|
|
}
|
|
|
|
void rejects_truncated_invalid_magic_and_bad_thumbnail(pp::tests::Harness& h)
|
|
{
|
|
auto truncated = valid_header();
|
|
truncated.pop_back();
|
|
|
|
auto bad_magic = valid_header();
|
|
bad_magic[0] = std::byte { 'X' };
|
|
|
|
auto bad_thumb = valid_header();
|
|
bad_thumb[32] = std::byte { 64 };
|
|
|
|
const auto truncated_result = parse_ppi_header(truncated);
|
|
const auto magic_result = parse_ppi_header(bad_magic);
|
|
const auto thumb_result = parse_ppi_header(bad_thumb);
|
|
|
|
PP_EXPECT(h, !truncated_result.ok());
|
|
PP_EXPECT(h, truncated_result.status().code == StatusCode::out_of_range);
|
|
PP_EXPECT(h, !magic_result.ok());
|
|
PP_EXPECT(h, magic_result.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !thumb_result.ok());
|
|
PP_EXPECT(h, thumb_result.status().code == StatusCode::invalid_argument);
|
|
}
|
|
|
|
void rejects_unsupported_document_versions(pp::tests::Harness& h)
|
|
{
|
|
auto bad_major = valid_header();
|
|
bad_major[4] = std::byte { 1 };
|
|
|
|
auto bad_minor = valid_header();
|
|
bad_minor[8] = std::byte { 0 };
|
|
|
|
const auto major_result = parse_ppi_header(bad_major);
|
|
const auto minor_result = parse_ppi_header(bad_minor);
|
|
|
|
PP_EXPECT(h, !major_result.ok());
|
|
PP_EXPECT(h, major_result.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !minor_result.ok());
|
|
PP_EXPECT(h, minor_result.status().code == StatusCode::invalid_argument);
|
|
}
|
|
|
|
void parses_project_layout_with_thumbnail_and_body(pp::tests::Harness& h)
|
|
{
|
|
const auto bytes = minimal_project();
|
|
|
|
const auto layout = parse_ppi_project_layout(bytes);
|
|
|
|
PP_EXPECT(h, layout.ok());
|
|
PP_EXPECT(h, layout.value().thumbnail_offset == ppi_header_size);
|
|
PP_EXPECT(h, layout.value().thumbnail_bytes == 128U * 128U * 4U);
|
|
PP_EXPECT(h, layout.value().body_offset == ppi_header_size + (128U * 128U * 4U));
|
|
PP_EXPECT(h, layout.value().body_bytes == 73U);
|
|
}
|
|
|
|
void rejects_project_layout_with_truncated_thumbnail(pp::tests::Harness& h)
|
|
{
|
|
auto bytes = valid_header();
|
|
bytes.resize(ppi_header_size + (128U * 128U * 4U) - 1U, std::byte { 0 });
|
|
|
|
const auto layout = parse_ppi_project_layout(bytes);
|
|
|
|
PP_EXPECT(h, !layout.ok());
|
|
PP_EXPECT(h, layout.status().code == StatusCode::out_of_range);
|
|
}
|
|
|
|
void parses_minimal_project_body_summary(pp::tests::Harness& h)
|
|
{
|
|
const auto project = minimal_project();
|
|
const auto summary = parse_ppi_project_summary(project);
|
|
|
|
PP_EXPECT(h, summary.ok());
|
|
PP_EXPECT(h, summary.value().body.width == 64U);
|
|
PP_EXPECT(h, summary.value().body.height == 32U);
|
|
PP_EXPECT(h, summary.value().body.layer_count == 1U);
|
|
PP_EXPECT(h, summary.value().body.declared_frame_count == 1U);
|
|
PP_EXPECT(h, summary.value().body.total_layer_frames == 1U);
|
|
PP_EXPECT(h, summary.value().body.dirty_face_count == 0U);
|
|
PP_EXPECT(h, summary.value().body.rgba_face_payload_count == 0U);
|
|
PP_EXPECT(h, summary.value().body.compressed_face_bytes == 0U);
|
|
PP_EXPECT(h, summary.value().body.info_bytes == 0U);
|
|
}
|
|
|
|
void indexes_project_layers_frames_and_faces(pp::tests::Harness& h)
|
|
{
|
|
const auto project = project_with_single_face_payload(png_ihdr_payload(8, 4));
|
|
const auto index = parse_ppi_project_index(project);
|
|
|
|
PP_EXPECT(h, index.ok());
|
|
PP_EXPECT(h, index.value().body.layers.size() == 1U);
|
|
PP_EXPECT(h, index.value().body.layers[0].stored_order == 0U);
|
|
PP_EXPECT(h, index.value().body.layers[0].name == "Ink");
|
|
PP_EXPECT(h, index.value().body.layers[0].visible);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames.size() == 1U);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames[0].duration_ms == 100U);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].has_data);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].x0 == 2U);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].x1 == 10U);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].payload_bytes == 33U);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].png_width == 8U);
|
|
PP_EXPECT(h, !index.value().body.layers[0].frames[0].faces[1].has_data);
|
|
}
|
|
|
|
void validates_dirty_face_png_payload_metadata(pp::tests::Harness& h)
|
|
{
|
|
const auto project = project_with_single_face_payload(png_ihdr_payload(8, 4));
|
|
const auto summary = parse_ppi_project_summary(project);
|
|
|
|
PP_EXPECT(h, summary.ok());
|
|
PP_EXPECT(h, summary.value().body.dirty_face_count == 1U);
|
|
PP_EXPECT(h, summary.value().body.rgba_face_payload_count == 1U);
|
|
PP_EXPECT(h, summary.value().body.compressed_face_bytes == 33U);
|
|
}
|
|
|
|
void decodes_dirty_face_png_payloads(pp::tests::Harness& h)
|
|
{
|
|
const auto project = project_with_single_face_payload(transparent_png_1x1(), 1, 1);
|
|
const auto decoded = decode_ppi_project_images(project);
|
|
|
|
PP_EXPECT(h, decoded.ok());
|
|
PP_EXPECT(h, decoded.value().faces.size() == 1U);
|
|
PP_EXPECT(h, decoded.value().faces[0].layer_index == 0U);
|
|
PP_EXPECT(h, decoded.value().faces[0].frame_index == 0U);
|
|
PP_EXPECT(h, decoded.value().faces[0].face_index == 0U);
|
|
PP_EXPECT(h, decoded.value().faces[0].image.width == 1U);
|
|
PP_EXPECT(h, decoded.value().faces[0].image.height == 1U);
|
|
PP_EXPECT(h, decoded.value().faces[0].image.pixels.size() == 4U);
|
|
PP_EXPECT(h, decoded.value().faces[0].image.pixels[3] == 0U);
|
|
}
|
|
|
|
void rejects_metadata_only_payload_when_decoding_pixels(pp::tests::Harness& h)
|
|
{
|
|
const auto project = project_with_single_face_payload(png_ihdr_payload(8, 4));
|
|
const auto decoded = decode_ppi_project_images(project);
|
|
|
|
PP_EXPECT(h, !decoded.ok());
|
|
PP_EXPECT(h, decoded.status().code == StatusCode::invalid_argument);
|
|
}
|
|
|
|
void rejects_invalid_dirty_face_png_payloads(pp::tests::Harness& h)
|
|
{
|
|
auto mismatched_dimensions = project_with_single_face_payload(png_ihdr_payload(7, 4));
|
|
auto non_rgba = project_with_single_face_payload(png_ihdr_payload(8, 4, 8, 2));
|
|
auto bad_signature_payload = png_ihdr_payload(8, 4);
|
|
bad_signature_payload[0] = std::byte { 0 };
|
|
auto bad_signature = project_with_single_face_payload(bad_signature_payload);
|
|
|
|
const auto mismatched_result = parse_ppi_project_summary(mismatched_dimensions);
|
|
const auto non_rgba_result = parse_ppi_project_summary(non_rgba);
|
|
const auto bad_signature_result = parse_ppi_project_summary(bad_signature);
|
|
|
|
PP_EXPECT(h, !mismatched_result.ok());
|
|
PP_EXPECT(h, mismatched_result.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !non_rgba_result.ok());
|
|
PP_EXPECT(h, non_rgba_result.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !bad_signature_result.ok());
|
|
PP_EXPECT(h, bad_signature_result.status().code == StatusCode::invalid_argument);
|
|
}
|
|
|
|
void rejects_invalid_project_body_summaries(pp::tests::Harness& h)
|
|
{
|
|
auto truncated = minimal_project();
|
|
truncated.pop_back();
|
|
|
|
auto mismatched_frames = minimal_project();
|
|
mismatched_frames[ppi_header_size + (128U * 128U * 4U) + 12U] = std::byte { 2 };
|
|
|
|
auto bad_layer_name = minimal_project();
|
|
bad_layer_name[ppi_header_size + (128U * 128U * 4U) + 24U] = std::byte { 255 };
|
|
|
|
auto non_finite_opacity = minimal_project();
|
|
write_f32_at(
|
|
non_finite_opacity,
|
|
ppi_header_size + (128U * 128U * 4U) + 20U,
|
|
std::numeric_limits<float>::quiet_NaN());
|
|
|
|
auto bad_blend_mode = minimal_project();
|
|
write_u32_at(bad_blend_mode, ppi_header_size + (128U * 128U * 4U) + 31U, 99U);
|
|
|
|
const auto truncated_result = parse_ppi_project_summary(truncated);
|
|
const auto mismatched_frames_result = parse_ppi_project_summary(mismatched_frames);
|
|
const auto bad_layer_name_result = parse_ppi_project_summary(bad_layer_name);
|
|
const auto non_finite_opacity_result = parse_ppi_project_summary(non_finite_opacity);
|
|
const auto bad_blend_mode_result = parse_ppi_project_summary(bad_blend_mode);
|
|
|
|
PP_EXPECT(h, !truncated_result.ok());
|
|
PP_EXPECT(h, truncated_result.status().code == StatusCode::out_of_range);
|
|
PP_EXPECT(h, !mismatched_frames_result.ok());
|
|
PP_EXPECT(h, mismatched_frames_result.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !bad_layer_name_result.ok());
|
|
PP_EXPECT(h, bad_layer_name_result.status().code == StatusCode::out_of_range);
|
|
PP_EXPECT(h, !non_finite_opacity_result.ok());
|
|
PP_EXPECT(h, non_finite_opacity_result.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !bad_blend_mode_result.ok());
|
|
PP_EXPECT(h, bad_blend_mode_result.status().code == StatusCode::out_of_range);
|
|
}
|
|
|
|
void creates_minimal_project_for_roundtrip_load(pp::tests::Harness& h)
|
|
{
|
|
const auto project = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 256,
|
|
.height = 128,
|
|
.layer_name = "Roundtrip",
|
|
.layer_metadata = {},
|
|
.layer_count = 1,
|
|
.frame_count = 1,
|
|
.frame_duration_ms = 333,
|
|
.dirty_faces = {},
|
|
});
|
|
|
|
PP_EXPECT(h, project.ok());
|
|
const auto index = parse_ppi_project_index(project.value());
|
|
PP_EXPECT(h, index.ok());
|
|
PP_EXPECT(h, index.value().layout.thumbnail_bytes == 128U * 128U * 4U);
|
|
PP_EXPECT(h, index.value().body.summary.width == 256U);
|
|
PP_EXPECT(h, index.value().body.summary.height == 128U);
|
|
PP_EXPECT(h, index.value().body.summary.layer_count == 1U);
|
|
PP_EXPECT(h, index.value().body.summary.declared_frame_count == 1U);
|
|
PP_EXPECT(h, index.value().body.summary.dirty_face_count == 0U);
|
|
PP_EXPECT(h, index.value().body.layers[0].name == "Roundtrip");
|
|
PP_EXPECT(h, index.value().body.layers[0].opacity == 1.0F);
|
|
PP_EXPECT(h, index.value().body.layers[0].visible);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames[0].duration_ms == 333U);
|
|
}
|
|
|
|
void creates_minimal_project_with_multiple_layers(pp::tests::Harness& h)
|
|
{
|
|
const auto project = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 256,
|
|
.height = 128,
|
|
.layer_name = "Layer",
|
|
.layer_metadata = pp::assets::PpiLayerMetadataConfig {
|
|
.opacity = 0.625F,
|
|
.blend_mode = 4,
|
|
.alpha_locked = true,
|
|
.visible = false,
|
|
},
|
|
.layer_count = 2,
|
|
.frame_count = 2,
|
|
.frame_duration_ms = 111,
|
|
.dirty_faces = {},
|
|
});
|
|
|
|
PP_EXPECT(h, project.ok());
|
|
const auto index = parse_ppi_project_index(project.value());
|
|
PP_EXPECT(h, index.ok());
|
|
PP_EXPECT(h, index.value().body.summary.layer_count == 2U);
|
|
PP_EXPECT(h, index.value().body.summary.declared_frame_count == 4U);
|
|
PP_EXPECT(h, index.value().body.summary.total_layer_frames == 4U);
|
|
PP_EXPECT(h, index.value().body.layers.size() == 2U);
|
|
PP_EXPECT(h, index.value().body.layers[0].stored_order == 0U);
|
|
PP_EXPECT(h, index.value().body.layers[1].stored_order == 1U);
|
|
PP_EXPECT(h, index.value().body.layers[0].name == "Layer 1");
|
|
PP_EXPECT(h, index.value().body.layers[1].name == "Layer 2");
|
|
PP_EXPECT(h, index.value().body.layers[0].opacity == 0.625F);
|
|
PP_EXPECT(h, index.value().body.layers[1].opacity == 0.625F);
|
|
PP_EXPECT(h, index.value().body.layers[0].blend_mode == 4U);
|
|
PP_EXPECT(h, index.value().body.layers[1].blend_mode == 4U);
|
|
PP_EXPECT(h, index.value().body.layers[0].alpha_locked);
|
|
PP_EXPECT(h, index.value().body.layers[1].alpha_locked);
|
|
PP_EXPECT(h, !index.value().body.layers[0].visible);
|
|
PP_EXPECT(h, !index.value().body.layers[1].visible);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames.size() == 2U);
|
|
PP_EXPECT(h, index.value().body.layers[1].frames.size() == 2U);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames[1].duration_ms == 111U);
|
|
PP_EXPECT(h, index.value().body.layers[1].frames[1].duration_ms == 111U);
|
|
}
|
|
|
|
void creates_minimal_project_with_multiple_frames(pp::tests::Harness& h)
|
|
{
|
|
const auto project = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 256,
|
|
.height = 128,
|
|
.layer_name = "Frames",
|
|
.layer_metadata = {},
|
|
.layer_count = 1,
|
|
.frame_count = 3,
|
|
.frame_duration_ms = 111,
|
|
.dirty_faces = {},
|
|
});
|
|
|
|
PP_EXPECT(h, project.ok());
|
|
const auto index = parse_ppi_project_index(project.value());
|
|
PP_EXPECT(h, index.ok());
|
|
PP_EXPECT(h, index.value().body.summary.declared_frame_count == 3U);
|
|
PP_EXPECT(h, index.value().body.summary.total_layer_frames == 3U);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames.size() == 3U);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames[0].duration_ms == 111U);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames[1].duration_ms == 111U);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames[2].duration_ms == 111U);
|
|
}
|
|
|
|
void creates_explicit_project_with_layer_frame_metadata(pp::tests::Harness& h)
|
|
{
|
|
const pp::assets::PpiFrameConfig base_frames[] {
|
|
{ .duration_ms = 100 },
|
|
{ .duration_ms = 250 },
|
|
};
|
|
const pp::assets::PpiFrameConfig paint_frames[] {
|
|
{ .duration_ms = 333 },
|
|
};
|
|
const pp::assets::PpiLayerConfig layers[] {
|
|
{
|
|
.name = "Base",
|
|
.metadata = pp::assets::PpiLayerMetadataConfig {
|
|
.opacity = 1.0F,
|
|
.blend_mode = 0,
|
|
.alpha_locked = false,
|
|
.visible = true,
|
|
},
|
|
.frames = std::span<const pp::assets::PpiFrameConfig>(base_frames, 2),
|
|
},
|
|
{
|
|
.name = "Paint",
|
|
.metadata = pp::assets::PpiLayerMetadataConfig {
|
|
.opacity = 0.5F,
|
|
.blend_mode = 4,
|
|
.alpha_locked = true,
|
|
.visible = false,
|
|
},
|
|
.frames = std::span<const pp::assets::PpiFrameConfig>(paint_frames, 1),
|
|
},
|
|
};
|
|
|
|
const auto project = create_ppi_project(pp::assets::PpiProjectConfig {
|
|
.width = 256,
|
|
.height = 128,
|
|
.layers = std::span<const pp::assets::PpiLayerConfig>(layers, 2),
|
|
.dirty_faces = {},
|
|
});
|
|
|
|
PP_EXPECT(h, project.ok());
|
|
const auto index = parse_ppi_project_index(project.value());
|
|
PP_EXPECT(h, index.ok());
|
|
PP_EXPECT(h, index.value().body.summary.layer_count == 2U);
|
|
PP_EXPECT(h, index.value().body.summary.declared_frame_count == 3U);
|
|
PP_EXPECT(h, index.value().body.summary.total_layer_frames == 3U);
|
|
PP_EXPECT(h, index.value().body.layers[0].name == "Base");
|
|
PP_EXPECT(h, index.value().body.layers[0].frames.size() == 2U);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames[0].duration_ms == 100U);
|
|
PP_EXPECT(h, index.value().body.layers[0].frames[1].duration_ms == 250U);
|
|
PP_EXPECT(h, index.value().body.layers[1].name == "Paint");
|
|
PP_EXPECT(h, index.value().body.layers[1].opacity == 0.5F);
|
|
PP_EXPECT(h, index.value().body.layers[1].blend_mode == 4U);
|
|
PP_EXPECT(h, index.value().body.layers[1].alpha_locked);
|
|
PP_EXPECT(h, !index.value().body.layers[1].visible);
|
|
PP_EXPECT(h, index.value().body.layers[1].frames.size() == 1U);
|
|
PP_EXPECT(h, index.value().body.layers[1].frames[0].duration_ms == 333U);
|
|
}
|
|
|
|
void creates_minimal_project_with_dirty_face_payload(pp::tests::Harness& h)
|
|
{
|
|
const auto png_payload = transparent_png_1x1();
|
|
const pp::assets::PpiDirtyFacePayloadConfig dirty_faces[] {
|
|
{
|
|
.layer_index = 0,
|
|
.frame_index = 0,
|
|
.face_index = 2,
|
|
.x = 4,
|
|
.y = 5,
|
|
.width = 1,
|
|
.height = 1,
|
|
.png_rgba8 = std::span<const std::byte>(png_payload.data(), png_payload.size()),
|
|
},
|
|
};
|
|
const auto project = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 256,
|
|
.height = 128,
|
|
.layer_name = "Payload",
|
|
.layer_metadata = {},
|
|
.layer_count = 1,
|
|
.frame_count = 1,
|
|
.frame_duration_ms = 333,
|
|
.dirty_faces = std::span<const pp::assets::PpiDirtyFacePayloadConfig>(dirty_faces, 1),
|
|
});
|
|
|
|
PP_EXPECT(h, project.ok());
|
|
const auto decoded = decode_ppi_project_images(project.value());
|
|
PP_EXPECT(h, decoded.ok());
|
|
PP_EXPECT(h, decoded.value().project.body.summary.dirty_face_count == 1U);
|
|
PP_EXPECT(h, decoded.value().project.body.summary.rgba_face_payload_count == 1U);
|
|
PP_EXPECT(h, decoded.value().project.body.summary.compressed_face_bytes == png_payload.size());
|
|
PP_EXPECT(h, decoded.value().faces.size() == 1U);
|
|
PP_EXPECT(h, decoded.value().faces[0].face_index == 2U);
|
|
PP_EXPECT(h, decoded.value().faces[0].descriptor.x0 == 4U);
|
|
PP_EXPECT(h, decoded.value().faces[0].descriptor.y0 == 5U);
|
|
PP_EXPECT(h, decoded.value().faces[0].image.width == 1U);
|
|
PP_EXPECT(h, decoded.value().faces[0].image.height == 1U);
|
|
PP_EXPECT(h, decoded.value().faces[0].image.pixels.size() == 4U);
|
|
}
|
|
|
|
void creates_minimal_project_with_targeted_dirty_face_payloads(pp::tests::Harness& h)
|
|
{
|
|
const auto png_payload = transparent_png_1x1();
|
|
const pp::assets::PpiDirtyFacePayloadConfig dirty_faces[] {
|
|
{
|
|
.layer_index = 0,
|
|
.frame_index = 1,
|
|
.face_index = 2,
|
|
.x = 4,
|
|
.y = 5,
|
|
.width = 1,
|
|
.height = 1,
|
|
.png_rgba8 = std::span<const std::byte>(png_payload.data(), png_payload.size()),
|
|
},
|
|
{
|
|
.layer_index = 1,
|
|
.frame_index = 0,
|
|
.face_index = 2,
|
|
.x = 6,
|
|
.y = 7,
|
|
.width = 1,
|
|
.height = 1,
|
|
.png_rgba8 = std::span<const std::byte>(png_payload.data(), png_payload.size()),
|
|
},
|
|
};
|
|
const auto project = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 256,
|
|
.height = 128,
|
|
.layer_name = "Payload",
|
|
.layer_metadata = {},
|
|
.layer_count = 2,
|
|
.frame_count = 2,
|
|
.frame_duration_ms = 333,
|
|
.dirty_faces = std::span<const pp::assets::PpiDirtyFacePayloadConfig>(dirty_faces, 2),
|
|
});
|
|
|
|
PP_EXPECT(h, project.ok());
|
|
const auto decoded = decode_ppi_project_images(project.value());
|
|
PP_EXPECT(h, decoded.ok());
|
|
PP_EXPECT(h, decoded.value().project.body.summary.dirty_face_count == 2U);
|
|
PP_EXPECT(h, decoded.value().faces.size() == 2U);
|
|
PP_EXPECT(h, decoded.value().faces[0].layer_index == 0U);
|
|
PP_EXPECT(h, decoded.value().faces[0].frame_index == 1U);
|
|
PP_EXPECT(h, decoded.value().faces[0].face_index == 2U);
|
|
PP_EXPECT(h, decoded.value().faces[0].descriptor.x0 == 4U);
|
|
PP_EXPECT(h, decoded.value().faces[1].layer_index == 1U);
|
|
PP_EXPECT(h, decoded.value().faces[1].frame_index == 0U);
|
|
PP_EXPECT(h, decoded.value().faces[1].face_index == 2U);
|
|
PP_EXPECT(h, decoded.value().faces[1].descriptor.x0 == 6U);
|
|
}
|
|
|
|
void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h)
|
|
{
|
|
const auto png_payload = transparent_png_1x1();
|
|
const pp::assets::PpiDirtyFacePayloadConfig duplicate_faces[] {
|
|
{
|
|
.layer_index = 0,
|
|
.frame_index = 0,
|
|
.face_index = 0,
|
|
.x = 0,
|
|
.y = 0,
|
|
.width = 1,
|
|
.height = 1,
|
|
.png_rgba8 = std::span<const std::byte>(png_payload.data(), png_payload.size()),
|
|
},
|
|
{
|
|
.layer_index = 0,
|
|
.frame_index = 0,
|
|
.face_index = 0,
|
|
.x = 1,
|
|
.y = 1,
|
|
.width = 1,
|
|
.height = 1,
|
|
.png_rgba8 = std::span<const std::byte>(png_payload.data(), png_payload.size()),
|
|
},
|
|
};
|
|
const pp::assets::PpiDirtyFacePayloadConfig bad_layer_faces[] {
|
|
{
|
|
.layer_index = 1,
|
|
.frame_index = 0,
|
|
.face_index = 0,
|
|
.x = 0,
|
|
.y = 0,
|
|
.width = 1,
|
|
.height = 1,
|
|
.png_rgba8 = std::span<const std::byte>(png_payload.data(), png_payload.size()),
|
|
},
|
|
};
|
|
const pp::assets::PpiDirtyFacePayloadConfig bad_frame_faces[] {
|
|
{
|
|
.layer_index = 0,
|
|
.frame_index = 1,
|
|
.face_index = 0,
|
|
.x = 0,
|
|
.y = 0,
|
|
.width = 1,
|
|
.height = 1,
|
|
.png_rgba8 = std::span<const std::byte>(png_payload.data(), png_payload.size()),
|
|
},
|
|
};
|
|
const auto no_size = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 0,
|
|
.height = 128,
|
|
.layer_name = "Ink",
|
|
.layer_metadata = {},
|
|
.layer_count = 1,
|
|
.frame_count = 1,
|
|
.frame_duration_ms = 100,
|
|
.dirty_faces = {},
|
|
});
|
|
const auto no_name = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 128,
|
|
.height = 128,
|
|
.layer_name = "",
|
|
.layer_metadata = {},
|
|
.layer_count = 1,
|
|
.frame_count = 1,
|
|
.frame_duration_ms = 100,
|
|
.dirty_faces = {},
|
|
});
|
|
const auto no_duration = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 128,
|
|
.height = 128,
|
|
.layer_name = "Ink",
|
|
.layer_metadata = {},
|
|
.layer_count = 1,
|
|
.frame_count = 1,
|
|
.frame_duration_ms = 0,
|
|
.dirty_faces = {},
|
|
});
|
|
const auto no_frames = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 128,
|
|
.height = 128,
|
|
.layer_name = "Ink",
|
|
.layer_metadata = {},
|
|
.layer_count = 1,
|
|
.frame_count = 0,
|
|
.frame_duration_ms = 100,
|
|
.dirty_faces = {},
|
|
});
|
|
const auto no_layers = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 128,
|
|
.height = 128,
|
|
.layer_name = "Ink",
|
|
.layer_metadata = {},
|
|
.layer_count = 0,
|
|
.frame_count = 1,
|
|
.frame_duration_ms = 100,
|
|
.dirty_faces = {},
|
|
});
|
|
const auto bad_opacity = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 128,
|
|
.height = 128,
|
|
.layer_name = "Ink",
|
|
.layer_metadata = pp::assets::PpiLayerMetadataConfig {
|
|
.opacity = 1.25F,
|
|
},
|
|
.layer_count = 1,
|
|
.frame_count = 1,
|
|
.frame_duration_ms = 100,
|
|
.dirty_faces = {},
|
|
});
|
|
const auto non_finite_opacity = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 128,
|
|
.height = 128,
|
|
.layer_name = "Ink",
|
|
.layer_metadata = pp::assets::PpiLayerMetadataConfig {
|
|
.opacity = std::numeric_limits<float>::quiet_NaN(),
|
|
},
|
|
.layer_count = 1,
|
|
.frame_count = 1,
|
|
.frame_duration_ms = 100,
|
|
.dirty_faces = {},
|
|
});
|
|
const auto bad_blend_mode = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 128,
|
|
.height = 128,
|
|
.layer_name = "Ink",
|
|
.layer_metadata = pp::assets::PpiLayerMetadataConfig {
|
|
.blend_mode = 99,
|
|
},
|
|
.layer_count = 1,
|
|
.frame_count = 1,
|
|
.frame_duration_ms = 100,
|
|
.dirty_faces = {},
|
|
});
|
|
const auto duplicate_dirty_face = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 128,
|
|
.height = 128,
|
|
.layer_name = "Ink",
|
|
.layer_metadata = {},
|
|
.layer_count = 1,
|
|
.frame_count = 1,
|
|
.frame_duration_ms = 100,
|
|
.dirty_faces = std::span<const pp::assets::PpiDirtyFacePayloadConfig>(duplicate_faces, 2),
|
|
});
|
|
const auto bad_layer_dirty_face = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 128,
|
|
.height = 128,
|
|
.layer_name = "Ink",
|
|
.layer_metadata = {},
|
|
.layer_count = 1,
|
|
.frame_count = 1,
|
|
.frame_duration_ms = 100,
|
|
.dirty_faces = std::span<const pp::assets::PpiDirtyFacePayloadConfig>(bad_layer_faces, 1),
|
|
});
|
|
const auto bad_frame_dirty_face = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
|
.width = 128,
|
|
.height = 128,
|
|
.layer_name = "Ink",
|
|
.layer_metadata = {},
|
|
.layer_count = 1,
|
|
.frame_count = 1,
|
|
.frame_duration_ms = 100,
|
|
.dirty_faces = std::span<const pp::assets::PpiDirtyFacePayloadConfig>(bad_frame_faces, 1),
|
|
});
|
|
|
|
PP_EXPECT(h, !no_size.ok());
|
|
PP_EXPECT(h, no_size.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !no_name.ok());
|
|
PP_EXPECT(h, no_name.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !no_duration.ok());
|
|
PP_EXPECT(h, no_duration.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !no_frames.ok());
|
|
PP_EXPECT(h, no_frames.status().code == StatusCode::out_of_range);
|
|
PP_EXPECT(h, !no_layers.ok());
|
|
PP_EXPECT(h, no_layers.status().code == StatusCode::out_of_range);
|
|
PP_EXPECT(h, !bad_opacity.ok());
|
|
PP_EXPECT(h, bad_opacity.status().code == StatusCode::out_of_range);
|
|
PP_EXPECT(h, !non_finite_opacity.ok());
|
|
PP_EXPECT(h, non_finite_opacity.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !bad_blend_mode.ok());
|
|
PP_EXPECT(h, bad_blend_mode.status().code == StatusCode::out_of_range);
|
|
PP_EXPECT(h, !duplicate_dirty_face.ok());
|
|
PP_EXPECT(h, duplicate_dirty_face.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !bad_layer_dirty_face.ok());
|
|
PP_EXPECT(h, bad_layer_dirty_face.status().code == StatusCode::out_of_range);
|
|
PP_EXPECT(h, !bad_frame_dirty_face.ok());
|
|
PP_EXPECT(h, bad_frame_dirty_face.status().code == StatusCode::out_of_range);
|
|
}
|
|
|
|
void rejects_invalid_explicit_project_writer_inputs(pp::tests::Harness& h)
|
|
{
|
|
const pp::assets::PpiFrameConfig valid_frames[] {
|
|
{ .duration_ms = 100 },
|
|
};
|
|
const pp::assets::PpiFrameConfig invalid_frames[] {
|
|
{ .duration_ms = 0 },
|
|
};
|
|
const pp::assets::PpiLayerConfig unnamed_layers[] {
|
|
{
|
|
.name = "",
|
|
.metadata = {},
|
|
.frames = std::span<const pp::assets::PpiFrameConfig>(valid_frames, 1),
|
|
},
|
|
};
|
|
const pp::assets::PpiLayerConfig frameless_layers[] {
|
|
{
|
|
.name = "Ink",
|
|
.metadata = {},
|
|
.frames = {},
|
|
},
|
|
};
|
|
const pp::assets::PpiLayerConfig invalid_duration_layers[] {
|
|
{
|
|
.name = "Ink",
|
|
.metadata = {},
|
|
.frames = std::span<const pp::assets::PpiFrameConfig>(invalid_frames, 1),
|
|
},
|
|
};
|
|
|
|
const auto no_layers = create_ppi_project(pp::assets::PpiProjectConfig {
|
|
.width = 128,
|
|
.height = 64,
|
|
.layers = {},
|
|
.dirty_faces = {},
|
|
});
|
|
const auto unnamed = create_ppi_project(pp::assets::PpiProjectConfig {
|
|
.width = 128,
|
|
.height = 64,
|
|
.layers = std::span<const pp::assets::PpiLayerConfig>(unnamed_layers, 1),
|
|
.dirty_faces = {},
|
|
});
|
|
const auto frameless = create_ppi_project(pp::assets::PpiProjectConfig {
|
|
.width = 128,
|
|
.height = 64,
|
|
.layers = std::span<const pp::assets::PpiLayerConfig>(frameless_layers, 1),
|
|
.dirty_faces = {},
|
|
});
|
|
const auto invalid_duration = create_ppi_project(pp::assets::PpiProjectConfig {
|
|
.width = 128,
|
|
.height = 64,
|
|
.layers = std::span<const pp::assets::PpiLayerConfig>(invalid_duration_layers, 1),
|
|
.dirty_faces = {},
|
|
});
|
|
|
|
PP_EXPECT(h, !no_layers.ok());
|
|
PP_EXPECT(h, no_layers.status().code == StatusCode::out_of_range);
|
|
PP_EXPECT(h, !unnamed.ok());
|
|
PP_EXPECT(h, unnamed.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !frameless.ok());
|
|
PP_EXPECT(h, frameless.status().code == StatusCode::out_of_range);
|
|
PP_EXPECT(h, !invalid_duration.ok());
|
|
PP_EXPECT(h, invalid_duration.status().code == StatusCode::invalid_argument);
|
|
}
|
|
|
|
}
|
|
|
|
int main()
|
|
{
|
|
pp::tests::Harness harness;
|
|
harness.run("parses_legacy_ppi_header", parses_legacy_ppi_header);
|
|
harness.run("rejects_truncated_invalid_magic_and_bad_thumbnail", rejects_truncated_invalid_magic_and_bad_thumbnail);
|
|
harness.run("rejects_unsupported_document_versions", rejects_unsupported_document_versions);
|
|
harness.run("parses_project_layout_with_thumbnail_and_body", parses_project_layout_with_thumbnail_and_body);
|
|
harness.run("rejects_project_layout_with_truncated_thumbnail", rejects_project_layout_with_truncated_thumbnail);
|
|
harness.run("parses_minimal_project_body_summary", parses_minimal_project_body_summary);
|
|
harness.run("indexes_project_layers_frames_and_faces", indexes_project_layers_frames_and_faces);
|
|
harness.run("validates_dirty_face_png_payload_metadata", validates_dirty_face_png_payload_metadata);
|
|
harness.run("decodes_dirty_face_png_payloads", decodes_dirty_face_png_payloads);
|
|
harness.run("rejects_metadata_only_payload_when_decoding_pixels", rejects_metadata_only_payload_when_decoding_pixels);
|
|
harness.run("rejects_invalid_dirty_face_png_payloads", rejects_invalid_dirty_face_png_payloads);
|
|
harness.run("rejects_invalid_project_body_summaries", rejects_invalid_project_body_summaries);
|
|
harness.run("creates_minimal_project_for_roundtrip_load", creates_minimal_project_for_roundtrip_load);
|
|
harness.run("creates_minimal_project_with_multiple_layers", creates_minimal_project_with_multiple_layers);
|
|
harness.run("creates_minimal_project_with_multiple_frames", creates_minimal_project_with_multiple_frames);
|
|
harness.run("creates_explicit_project_with_layer_frame_metadata", creates_explicit_project_with_layer_frame_metadata);
|
|
harness.run("creates_minimal_project_with_dirty_face_payload", creates_minimal_project_with_dirty_face_payload);
|
|
harness.run("creates_minimal_project_with_targeted_dirty_face_payloads", creates_minimal_project_with_targeted_dirty_face_payloads);
|
|
harness.run("rejects_invalid_minimal_project_writer_inputs", rejects_invalid_minimal_project_writer_inputs);
|
|
harness.run("rejects_invalid_explicit_project_writer_inputs", rejects_invalid_explicit_project_writer_inputs);
|
|
return harness.finish();
|
|
}
|