diff --git a/enterprise/index/enterprise_index.cc b/enterprise/index/enterprise_index.cc index 9b0dd69c8..447c95cf9 100644 --- a/enterprise/index/enterprise_index.cc +++ b/enterprise/index/enterprise_index.cc @@ -14,7 +14,7 @@ auto load_custom_lint_rules( std::unordered_set &custom_names, const sourcemeta::blaze::Configuration &configuration, const sourcemeta::one::Resolver &resolver, - const sourcemeta::one::Build::DynamicCallback &callback) -> void { + const sourcemeta::one::BuildDynamicCallback &callback) -> void { const auto default_dialect{ configuration.default_dialect.value_or(std::string{})}; for (const auto &rule_path : configuration.lint.rules) { diff --git a/enterprise/index/include/sourcemeta/one/enterprise_index.h b/enterprise/index/include/sourcemeta/one/enterprise_index.h index a122d83ae..1c9600341 100644 --- a/enterprise/index/include/sourcemeta/one/enterprise_index.h +++ b/enterprise/index/include/sourcemeta/one/enterprise_index.h @@ -18,7 +18,7 @@ auto load_custom_lint_rules( std::unordered_set &custom_names, const sourcemeta::blaze::Configuration &configuration, const sourcemeta::one::Resolver &resolver, - const sourcemeta::one::Build::DynamicCallback &callback) -> void; + const sourcemeta::one::BuildDynamicCallback &callback) -> void; } // namespace sourcemeta::one diff --git a/src/build/CMakeLists.txt b/src/build/CMakeLists.txt index 13fa43ff7..ee744759a 100644 --- a/src/build/CMakeLists.txt +++ b/src/build/CMakeLists.txt @@ -1,5 +1,6 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT one NAME build - SOURCES build.cc build_state.h) + PRIVATE_HEADERS state.h + SOURCES delta.cc state.cc) target_link_libraries(sourcemeta_one_build - PUBLIC sourcemeta::core::json - PRIVATE sourcemeta::core::jsonschema sourcemeta::core::io) + PUBLIC sourcemeta::core::json sourcemeta::one::resolver + PRIVATE sourcemeta::core::io) diff --git a/src/build/build.cc b/src/build/build.cc deleted file mode 100644 index 5123569b6..000000000 --- a/src/build/build.cc +++ /dev/null @@ -1,176 +0,0 @@ -#include - -#include "build_state.h" - -#include - -#include // std::ranges::none_of -#include // assert -#include // std::ofstream - -#include // std::unique_lock -#include // std::string - -using mark_type = sourcemeta::one::Build::mark_type; -using Entry = sourcemeta::one::Build::Entry; - -static auto mark_locked(const std::unordered_map &entries, - const std::filesystem::path &path) - -> std::optional { - assert(path.is_absolute()); - const auto match{entries.find(path.native())}; - if (match != entries.end() && match->second.file_mark.has_value()) { - return match->second.file_mark; - } - - try { - return std::filesystem::last_write_time(path); - } catch (const std::filesystem::filesystem_error &) { - return std::nullopt; - } -} - -namespace sourcemeta::one { - -Build::Build(const std::filesystem::path &output_root) - : root{(static_cast(std::filesystem::create_directories(output_root)), - std::filesystem::canonical(output_root))}, - root_string{this->root.string()} { - const auto state_path{this->root / STATE_FILENAME}; - if (!std::filesystem::exists(state_path)) { - // First run or crash recovery: scan directory for orphaned files - for (const auto &entry : - std::filesystem::recursive_directory_iterator(this->root)) { - auto &map_entry{this->entries_[entry.path().native()]}; - map_entry.tracked = false; - map_entry.is_directory = entry.is_directory(); - } - - return; - } - - try { - this->has_previous_run = load_state(state_path, this->entries_); - } catch (...) { - this->entries_.clear(); - } -} - -auto Build::has_dependencies(const std::filesystem::path &path) const -> bool { - assert(path.is_absolute()); - std::shared_lock lock{this->mutex}; - const auto match{this->entries_.find(path.native())}; - return match != this->entries_.end() && - (!match->second.static_dependencies.empty() || - !match->second.dynamic_dependencies.empty()); -} - -auto Build::finish() -> void { - const auto state_path{this->root / STATE_FILENAME}; - - { - std::shared_lock lock{this->mutex}; - save_state(state_path, this->entries_); - } - - this->track(state_path); - - // Remove untracked files inside the output directory - std::shared_lock read_lock{this->mutex}; - for (const auto &entry : this->entries_) { - if (!entry.second.tracked && - entry.first.size() > this->root_string.size() && - entry.first.starts_with(this->root_string)) { - std::filesystem::remove_all(entry.first); - } - } -} - -auto Build::dispatch_is_cached(const Entry &entry, - const bool static_dependencies_match) const - -> bool { - if (!static_dependencies_match) { - return false; - } - - const auto check_mtime{[this, &entry]( - const std::filesystem::path &dependency_path) { - const auto dependency_mark{mark_locked(this->entries_, dependency_path)}; - return !dependency_mark.has_value() || - dependency_mark.value() > entry.file_mark.value(); - }}; - - return std::ranges::none_of(entry.static_dependencies, check_mtime) && - std::ranges::none_of(entry.dynamic_dependencies, check_mtime); -} - -auto Build::dispatch_commit( - const std::filesystem::path &destination, - std::vector &&static_dependencies, - std::vector &&dynamic_dependencies) -> void { - assert(destination.is_absolute()); - assert(std::filesystem::exists(destination)); - this->refresh(destination); - this->track(destination); - std::unique_lock lock{this->mutex}; - auto &entry{this->entries_[destination.native()]}; - entry.static_dependencies = std::move(static_dependencies); - entry.dynamic_dependencies = std::move(dynamic_dependencies); -} - -auto Build::refresh(const std::filesystem::path &path) -> void { - const auto value{std::filesystem::file_time_type::clock::now()}; - std::unique_lock lock{this->mutex}; - this->entries_[path.native()].file_mark = value; -} - -auto Build::track(const std::filesystem::path &path) -> void { - assert(path.is_absolute()); - const auto &path_string{path.native()}; - std::unique_lock lock{this->mutex}; - this->entries_[path_string].tracked = true; - auto parent_key{path_string}; - while (true) { - const auto slash{parent_key.rfind('/')}; - if (slash == std::string::npos || slash < this->root_string.size()) { - break; - } - - parent_key = parent_key.substr(0, slash); - auto &parent_entry{this->entries_[parent_key]}; - if (parent_entry.tracked) { - break; - } - - parent_entry.tracked = true; - parent_entry.is_directory = true; - } -} - -auto Build::is_untracked_file(const std::filesystem::path &path) const -> bool { - std::shared_lock lock{this->mutex}; - const auto match{this->entries_.find(path.native())}; - return match == this->entries_.cend() || !match->second.tracked; -} - -auto Build::write_json_if_different(const std::filesystem::path &path, - const sourcemeta::core::JSON &document) - -> void { - if (std::filesystem::exists(path)) { - const auto current{sourcemeta::core::read_json(path)}; - if (current == document) { - this->track(path); - return; - } - } - - assert(path.is_absolute()); - std::filesystem::create_directories(path.parent_path()); - std::ofstream stream{path}; - assert(!stream.fail()); - sourcemeta::core::stringify(document, stream); - this->track(path); - this->refresh(path); -} - -} // namespace sourcemeta::one diff --git a/src/build/build_state.h b/src/build/build_state.h deleted file mode 100644 index aae0cbebe..000000000 --- a/src/build/build_state.h +++ /dev/null @@ -1,185 +0,0 @@ -#ifndef SOURCEMETA_ONE_BUILD_STATE_H_ -#define SOURCEMETA_ONE_BUILD_STATE_H_ - -#include -#include - -#include // std::chrono::nanoseconds, std::chrono::duration_cast -#include // std::int64_t, std::uint32_t, std::uint8_t -#include // std::memcpy -#include // std::filesystem::path -#include // std::ofstream -#include // std::string -#include // std::string_view -#include // std::unordered_map - -namespace { - -auto read_uint32(const std::uint8_t *data, std::size_t &offset) - -> std::uint32_t { - std::uint32_t value; - std::memcpy(&value, data + offset, sizeof(value)); - offset += sizeof(value); - return value; -} - -auto read_int64(const std::uint8_t *data, std::size_t &offset) -> std::int64_t { - std::int64_t value; - std::memcpy(&value, data + offset, sizeof(value)); - offset += sizeof(value); - return value; -} - -auto append_uint32(std::string &buffer, const std::uint32_t value) -> void { - buffer.append(reinterpret_cast(&value), sizeof(value)); -} - -auto append_int64(std::string &buffer, const std::int64_t value) -> void { - buffer.append(reinterpret_cast(&value), sizeof(value)); -} - -auto append_string(std::string &buffer, const std::string &value) -> void { - append_uint32(buffer, static_cast(value.size())); - buffer.append(value); -} - -} // anonymous namespace - -namespace sourcemeta::one { - -static constexpr std::string_view STATE_FILENAME{"state.bin"}; -static constexpr std::uint32_t STATE_MAGIC{0x44455053}; -static constexpr std::uint32_t STATE_VERSION{1}; -static constexpr std::uint8_t STATE_FLAG_HAS_DEPENDENCIES{0x01}; -static constexpr std::uint8_t STATE_FLAG_HAS_MARK{0x02}; - -using mark_type = Build::mark_type; -using Entry = Build::Entry; - -inline auto load_state(const std::filesystem::path &path, - std::unordered_map &entries) - -> bool { - const sourcemeta::core::FileView view{path}; - const auto *data{view.as()}; - const auto file_size{view.size()}; - - if (file_size < 12) { - return false; - } - - std::size_t offset{0}; - if (read_uint32(data, offset) != STATE_MAGIC) { - return false; - } - - if (read_uint32(data, offset) != STATE_VERSION) { - return false; - } - - const auto entry_count{read_uint32(data, offset)}; - for (std::uint32_t index = 0; index < entry_count; ++index) { - const auto path_length{read_uint32(data, offset)}; - std::string entry_path{reinterpret_cast(data + offset), - path_length}; - offset += path_length; - - const auto flags{data[offset++]}; - auto &map_entry{entries[std::move(entry_path)]}; - - if ((flags & STATE_FLAG_HAS_DEPENDENCIES) != 0) { - const auto static_count{read_uint32(data, offset)}; - map_entry.static_dependencies.reserve(static_count); - for (std::uint32_t static_index = 0; static_index < static_count; - ++static_index) { - const auto dep_length{read_uint32(data, offset)}; - map_entry.static_dependencies.emplace_back(std::string{ - reinterpret_cast(data + offset), dep_length}); - offset += dep_length; - } - - const auto dynamic_count{read_uint32(data, offset)}; - map_entry.dynamic_dependencies.reserve(dynamic_count); - for (std::uint32_t dynamic_index = 0; dynamic_index < dynamic_count; - ++dynamic_index) { - const auto dep_length{read_uint32(data, offset)}; - map_entry.dynamic_dependencies.emplace_back(std::string{ - reinterpret_cast(data + offset), dep_length}); - offset += dep_length; - } - } - - if ((flags & STATE_FLAG_HAS_MARK) != 0) { - const auto nanoseconds{read_int64(data, offset)}; - map_entry.file_mark = - mark_type{std::chrono::duration_cast( - std::chrono::nanoseconds{nanoseconds})}; - } - } - - return true; -} - -inline auto save_state(const std::filesystem::path &path, - const std::unordered_map &entries) - -> void { - std::string buffer; - buffer.resize(12); - std::memcpy(buffer.data(), &STATE_MAGIC, sizeof(STATE_MAGIC)); - std::memcpy(buffer.data() + 4, &STATE_VERSION, sizeof(STATE_VERSION)); - - std::uint32_t count{0}; - for (const auto &entry : entries) { - if (!entry.second.tracked) { - continue; - } - - count += 1; - append_string(buffer, entry.first); - - const bool has_dependencies{!entry.second.static_dependencies.empty() || - !entry.second.dynamic_dependencies.empty()}; - const bool has_mark{entry.second.file_mark.has_value()}; - - std::uint8_t flags{0}; - if (has_dependencies) { - flags |= STATE_FLAG_HAS_DEPENDENCIES; - } - if (has_mark) { - flags |= STATE_FLAG_HAS_MARK; - } - - buffer.push_back(static_cast(flags)); - - if (has_dependencies) { - append_uint32(buffer, static_cast( - entry.second.static_dependencies.size())); - for (const auto &dependency : entry.second.static_dependencies) { - append_string(buffer, dependency.native()); - } - - append_uint32(buffer, static_cast( - entry.second.dynamic_dependencies.size())); - for (const auto &dependency : entry.second.dynamic_dependencies) { - append_string(buffer, dependency.native()); - } - } - - if (has_mark) { - const auto nanoseconds{ - std::chrono::duration_cast( - entry.second.file_mark.value().time_since_epoch()) - .count()}; - append_int64(buffer, static_cast(nanoseconds)); - } - } - - std::memcpy(buffer.data() + 8, &count, sizeof(count)); - - std::ofstream stream{path, std::ios::binary}; - assert(!stream.fail()); - stream.write(buffer.data(), static_cast(buffer.size())); -} - -} // namespace sourcemeta::one - -#endif diff --git a/src/build/delta.cc b/src/build/delta.cc new file mode 100644 index 000000000..df3d0e48a --- /dev/null +++ b/src/build/delta.cc @@ -0,0 +1,854 @@ +#include + +#include // std::ranges::sort, std::ranges::all_of +#include // assert +#include // std::string +#include // std::string_view +#include // std::unordered_map +#include // std::unordered_set +#include // std::vector + +// TODO: Refactor this mess + +namespace sourcemeta::one { + +static constexpr auto SENTINEL = "%"; + +static auto schema_base_path(const std::filesystem::path &output, + const std::filesystem::path &relative_path) + -> std::filesystem::path { + return output / "schemas" / relative_path / SENTINEL; +} + +static auto explorer_base_path(const std::filesystem::path &output, + const std::filesystem::path &relative_path) + -> std::filesystem::path { + return output / "explorer" / relative_path / SENTINEL; +} + +struct Target { + BuildPlan::Action::Type action; + std::filesystem::path destination; + std::vector dependencies; + std::string_view data; +}; + +using TargetMap = std::unordered_map; + +static auto declare_target(TargetMap &targets, BuildPlan::Action::Type action, + std::filesystem::path destination, + std::vector dependencies, + const std::string_view data = {}) -> void { + const auto key{destination.string()}; + targets.emplace(key, Target{.action = action, + .destination = std::move(destination), + .dependencies = std::move(dependencies), + .data = data}); +} + +static auto +declare_schema_targets(TargetMap &targets, const std::filesystem::path &output, + const std::filesystem::path &source, + const std::filesystem::path &relative_path, + const bool evaluate, + const std::filesystem::path &configuration_path, + const std::string_view uri) -> void { + const auto base{schema_base_path(output, relative_path)}; + const auto explorer_base{explorer_base_path(output, relative_path)}; + const auto schema_metapack{base / "schema.metapack"}; + const auto dependencies_metapack{base / "dependencies.metapack"}; + const auto bundle_metapack{base / "bundle.metapack"}; + const auto health_metapack{base / "health.metapack"}; + + declare_target(targets, BuildPlan::Action::Type::Materialise, schema_metapack, + {source, configuration_path}, uri); + declare_target(targets, BuildPlan::Action::Type::Positions, + base / "positions.metapack", {schema_metapack}, uri); + declare_target(targets, BuildPlan::Action::Type::Locations, + base / "locations.metapack", {schema_metapack}, uri); + declare_target(targets, BuildPlan::Action::Type::Dependencies, + dependencies_metapack, {schema_metapack}, uri); + declare_target(targets, BuildPlan::Action::Type::Stats, + base / "stats.metapack", {schema_metapack}, uri); + declare_target(targets, BuildPlan::Action::Type::Health, health_metapack, + {schema_metapack, dependencies_metapack}, uri); + declare_target(targets, BuildPlan::Action::Type::Bundle, bundle_metapack, + {schema_metapack, dependencies_metapack}, uri); + declare_target(targets, BuildPlan::Action::Type::Editor, + base / "editor.metapack", {bundle_metapack}, uri); + + if (evaluate) { + declare_target(targets, BuildPlan::Action::Type::BlazeExhaustive, + base / "blaze-exhaustive.metapack", {bundle_metapack}, uri); + declare_target(targets, BuildPlan::Action::Type::BlazeFast, + base / "blaze-fast.metapack", {bundle_metapack}, uri); + } + + declare_target(targets, BuildPlan::Action::Type::SchemaMetadata, + explorer_base / "schema.metapack", + {schema_metapack, health_metapack, dependencies_metapack}, + uri); +} + +enum class DirtyState : std::uint8_t { Unknown, Clean, Dirty }; + +static auto is_dirty(const std::string &target_path, const TargetMap &targets, + const BuildState &entries, + const std::unordered_set &force_dirty, + std::unordered_map &cache) + -> bool { + const auto cached{cache.find(target_path)}; + if (cached != cache.end()) { + return cached->second == DirtyState::Dirty; + } + + cache[target_path] = DirtyState::Clean; + + const auto target_match{targets.find(target_path)}; + if (target_match == targets.end()) { + return false; + } + + if (force_dirty.contains(target_path)) { + cache[target_path] = DirtyState::Dirty; + return true; + } + + const auto *state_entry{entries.entry(target_path)}; + if (state_entry == nullptr) { + cache[target_path] = DirtyState::Dirty; + return true; + } + + for (const auto &dependency : target_match->second.dependencies) { + if (is_dirty(dependency.string(), targets, entries, force_dirty, cache)) { + cache[target_path] = DirtyState::Dirty; + return true; + } + } + + for (const auto &dependency : state_entry->dependencies) { + if (is_dirty(dependency.string(), targets, entries, force_dirty, cache)) { + cache[target_path] = DirtyState::Dirty; + return true; + } + } + + return false; +} + +static auto compute_wave(const std::string &path, const TargetMap &targets, + const std::unordered_set &dirty_set, + std::unordered_map &cache) + -> std::size_t { + const auto cached{cache.find(path)}; + if (cached != cache.end()) { + return cached->second; + } + + cache[path] = 0; + + const auto target_match{targets.find(path)}; + if (target_match == targets.end()) { + return 0; + } + + std::size_t max_dep_wave{0}; + for (const auto &dependency : target_match->second.dependencies) { + const auto &dep_string{dependency.string()}; + if (dirty_set.contains(dep_string)) { + const auto dep_wave{compute_wave(dep_string, targets, dirty_set, cache)}; + if (dep_wave + 1 > max_dep_wave) { + max_dep_wave = dep_wave + 1; + } + } + } + + cache[path] = max_dep_wave; + return max_dep_wave; +} + +static auto collect_affected_directories( + const std::filesystem::path &schemas_path, + const std::vector &affected_relative_paths) + -> std::vector { + std::unordered_set directory_set; + directory_set.insert(schemas_path.string()); + + for (const auto &relative_path : affected_relative_paths) { + auto current{(schemas_path / relative_path).parent_path()}; + while (current != schemas_path) { + directory_set.insert(current.string()); + current = current.parent_path(); + } + } + + std::vector result; + result.reserve(directory_set.size()); + for (const auto &entry : directory_set) { + result.emplace_back(entry); + } + + std::ranges::sort(result, [](const std::filesystem::path &left, + const std::filesystem::path &right) { + const auto left_depth{std::distance(left.begin(), left.end())}; + const auto right_depth{std::distance(right.begin(), right.end())}; + if (left_depth == right_depth) { + return left < right; + } + + return left_depth > right_depth; + }); + + return result; +} + +auto delta(const BuildPlan::Type build_type, const BuildState &entries, + const std::filesystem::path &output, const Resolver::Views &schemas, + const std::string_view version, + const std::string_view current_version, + const std::string_view comment, + const sourcemeta::core::JSON &configuration, + const sourcemeta::core::JSON ¤t_configuration, + const std::vector &changed, + const std::vector &removed) -> BuildPlan { + assert(output.is_absolute()); + assert(std::ranges::all_of(schemas, [](const auto &entry) { + return entry.second.path.is_absolute() && + !entry.second.relative_path.is_absolute(); + })); + assert(std::ranges::all_of( + changed, [](const auto &path) { return path.is_absolute(); })); + assert(std::ranges::all_of( + removed, [](const auto &path) { return path.is_absolute(); })); + + assert(!version.empty()); + assert(!configuration.is_null()); + const auto version_path{output / "version.json"}; + const auto configuration_path{output / "configuration.json"}; + const auto comment_path{output / "comment.json"}; + assert(current_version.empty() == !entries.contains(version_path.native())); + assert(current_configuration.is_null() == + !entries.contains(configuration_path.native())); + const auto version_changed{current_version != version}; + const auto configuration_changed{current_configuration.is_null() || + configuration != current_configuration}; + const auto is_full{version_changed || configuration_changed}; + const auto schemas_path{output / "schemas"}; + const auto explorer_path{output / "explorer"}; + const auto dependency_tree_path{output / "dependency-tree.metapack"}; + + std::unordered_set removed_uris; + if (!changed.empty() || !removed.empty()) { + std::unordered_set removed_set; + removed_set.reserve(removed.size()); + for (const auto &path : removed) { + removed_set.insert(path.string()); + } + + for (const auto &[uri, info] : schemas) { + if (removed_set.contains(info.path.string())) { + removed_uris.insert(uri); + } + } + } + + TargetMap targets; + + std::vector all_relative_paths; + all_relative_paths.reserve(schemas.size()); + + for (const auto &[uri, info] : schemas) { + if (removed_uris.contains(uri)) { + continue; + } + + declare_schema_targets(targets, output, info.path, info.relative_path, + info.evaluate, configuration_path, uri); + all_relative_paths.emplace_back(info.relative_path); + } + + std::unordered_set force_dirty; + + if (is_full) { + for (const auto &[uri, info] : schemas) { + if (removed_uris.contains(uri)) { + continue; + } + + force_dirty.insert( + (schema_base_path(output, info.relative_path) / "schema.metapack") + .string()); + } + } else if (!changed.empty()) { + std::unordered_set changed_set; + changed_set.reserve(changed.size()); + for (const auto &path : changed) { + changed_set.insert(path.string()); + } + + for (const auto &[uri, info] : schemas) { + if (removed_uris.contains(uri)) { + continue; + } + + if (!changed_set.contains(info.path.string())) { + continue; + } + + const auto metapack_path{ + (schema_base_path(output, info.relative_path) / "schema.metapack") + .string()}; + if (entries.is_stale(metapack_path, info.mtime)) { + force_dirty.insert(metapack_path); + } + } + } else if (!is_full) { + for (const auto &[uri, info] : schemas) { + if (removed_uris.contains(uri)) { + continue; + } + + const auto metapack_path{ + (schema_base_path(output, info.relative_path) / "schema.metapack") + .string()}; + if (entries.is_stale(metapack_path, info.mtime)) { + force_dirty.insert(metapack_path); + } + } + } + + std::unordered_map dirty_cache; + std::unordered_set dirty_set; + + for (const auto &[target_path, target] : targets) { + if (is_dirty(target_path, targets, entries, force_dirty, dirty_cache)) { + dirty_set.insert(target_path); + } + } + + bool has_missing_web{false}; + if (build_type == BuildPlan::Type::Full && dirty_set.empty() && !is_full) { + for (const auto &[uri, info] : schemas) { + if (removed_uris.contains(uri)) { + continue; + } + + const auto web_schema_path{ + (explorer_base_path(output, info.relative_path) / + "schema-html.metapack") + .string()}; + if (!entries.contains(web_schema_path)) { + has_missing_web = true; + break; + } + } + } + + bool has_potential_stale{false}; + { + std::unordered_set current_schema_bases; + for (const auto &[uri, info] : schemas) { + current_schema_bases.insert( + schema_base_path(output, info.relative_path).string()); + } + + const auto schemas_prefix{schemas_path.string() + "/"}; + for (const auto &[entry_path, entry] : entries) { + if (!entry_path.starts_with(schemas_prefix)) { + continue; + } + + const auto sentinel_pos{entry_path.find("/%/")}; + if (sentinel_pos == std::string::npos) { + continue; + } + + const auto base{entry_path.substr(0, sentinel_pos + 2)}; + if (!current_schema_bases.contains(base)) { + has_potential_stale = true; + break; + } + } + } + + if (!is_full && dirty_set.empty() && removed_uris.empty() && + !has_missing_web && !has_potential_stale) { + if (!comment.empty()) { + BuildPlan plan; + plan.output = output; + plan.type = build_type; + plan.waves.push_back({{.type = BuildPlan::Action::Type::Comment, + .destination = comment_path, + .dependencies = {}, + .data = comment}}); + plan.size = 1; + return plan; + } + + if (comment.empty() && entries.contains(comment_path.native())) { + BuildPlan plan; + plan.output = output; + plan.type = build_type; + plan.waves.push_back({{.type = BuildPlan::Action::Type::Remove, + .destination = comment_path, + .dependencies = {}, + .data = {}}}); + plan.size = 1; + return plan; + } + + return {.output = output, .type = build_type, .waves = {}}; + } + + const auto has_schema_work{is_full || !dirty_set.empty() || + !removed_uris.empty() || has_missing_web}; + + std::vector affected_relative_paths; + if (has_schema_work) { + for (const auto &[uri, info] : schemas) { + if (removed_uris.contains(uri)) { + continue; + } + + if (is_full) { + affected_relative_paths.emplace_back(info.relative_path); + continue; + } + + const auto base{schema_base_path(output, info.relative_path)}; + if (dirty_set.contains((base / "schema.metapack").string())) { + affected_relative_paths.emplace_back(info.relative_path); + } + } + + std::vector all_dependencies; + all_dependencies.reserve(schemas.size()); + for (const auto &[uri, info] : schemas) { + if (removed_uris.contains(uri)) { + continue; + } + + all_dependencies.emplace_back( + schema_base_path(output, info.relative_path) / + "dependencies.metapack"); + } + + declare_target(targets, BuildPlan::Action::Type::DependencyTree, + dependency_tree_path, all_dependencies); + dirty_set.insert(dependency_tree_path.string()); + + for (const auto &[uri, info] : schemas) { + if (removed_uris.contains(uri)) { + continue; + } + + const auto dependents_path{schema_base_path(output, info.relative_path) / + "dependents.metapack"}; + declare_target(targets, BuildPlan::Action::Type::Dependents, + dependents_path, {dependency_tree_path}, uri); + + const auto base{schema_base_path(output, info.relative_path)}; + if (is_full || dirty_set.contains((base / "schema.metapack").string()) || + !entries.contains(dependents_path.native())) { + dirty_set.insert(dependents_path.string()); + } + } + + std::vector all_summaries; + all_summaries.reserve(schemas.size()); + for (const auto &[uri, info] : schemas) { + if (removed_uris.contains(uri)) { + continue; + } + + all_summaries.emplace_back( + explorer_base_path(output, info.relative_path) / "schema.metapack"); + } + + const auto search_path{explorer_path / SENTINEL / "search.metapack"}; + declare_target(targets, BuildPlan::Action::Type::SearchIndex, search_path, + all_summaries); + dirty_set.insert(search_path.string()); + + const auto affected_dirs{ + collect_affected_directories(schemas_path, affected_relative_paths)}; + for (const auto &directory : affected_dirs) { + const auto relative{std::filesystem::relative(directory, schemas_path)}; + const auto destination{ + (explorer_path / relative / SENTINEL / "directory.metapack") + .lexically_normal()}; + + std::vector directory_dependencies; + + for (const auto &schema_relative : affected_relative_paths) { + if (schema_relative.parent_path() == relative || + (relative == "." && !schema_relative.has_parent_path())) { + directory_dependencies.emplace_back( + explorer_base_path(output, schema_relative) / "schema.metapack"); + } + } + + for (const auto &child_directory : affected_dirs) { + if (child_directory == directory) { + continue; + } + + if (child_directory.parent_path() == directory) { + const auto child_relative{ + std::filesystem::relative(child_directory, schemas_path)}; + directory_dependencies.emplace_back( + (explorer_path / child_relative / SENTINEL / "directory.metapack") + .lexically_normal()); + } + } + + declare_target(targets, BuildPlan::Action::Type::DirectoryList, + destination, std::move(directory_dependencies)); + dirty_set.insert(destination.string()); + } + + if (build_type == BuildPlan::Type::Full) { + for (const auto &directory : affected_dirs) { + const auto relative{std::filesystem::relative(directory, schemas_path)}; + const auto directory_metapack{ + (explorer_path / relative / SENTINEL / "directory.metapack") + .lexically_normal()}; + if (relative == ".") { + const auto web_index_path{explorer_path / SENTINEL / + "directory-html.metapack"}; + declare_target(targets, BuildPlan::Action::Type::WebIndex, + web_index_path, {directory_metapack}); + dirty_set.insert(web_index_path.string()); + if (is_full) { + const auto not_found_path{explorer_path / SENTINEL / + "404.metapack"}; + declare_target(targets, BuildPlan::Action::Type::WebNotFound, + not_found_path, {configuration_path}); + dirty_set.insert(not_found_path.string()); + } + } else { + const auto web_dir_path{explorer_path / relative / SENTINEL / + "directory-html.metapack"}; + declare_target(targets, BuildPlan::Action::Type::WebDirectory, + web_dir_path, {directory_metapack}); + dirty_set.insert(web_dir_path.string()); + } + } + + for (const auto &[uri, info] : schemas) { + if (removed_uris.contains(uri)) { + continue; + } + + const auto schema_base{schema_base_path(output, info.relative_path)}; + const auto explorer_base{ + explorer_base_path(output, info.relative_path)}; + const auto web_schema_path{explorer_base / "schema-html.metapack"}; + declare_target(targets, BuildPlan::Action::Type::WebSchema, + web_schema_path, + {explorer_base / "schema.metapack", + schema_base / "dependencies.metapack", + schema_base / "health.metapack", + schema_base / "dependents.metapack"}, + uri); + + if (is_full || + dirty_set.contains((schema_base / "schema.metapack").string()) || + !entries.contains(web_schema_path.native())) { + dirty_set.insert(web_schema_path.string()); + } + } + } + } // has_schema_work + + std::unordered_map wave_cache; + std::size_t max_wave{0}; + + for (const auto &target_path : dirty_set) { + if (!targets.contains(target_path)) { + continue; + } + + const auto wave{compute_wave(target_path, targets, dirty_set, wave_cache)}; + if (wave > max_wave) { + max_wave = wave; + } + } + + std::vector> dag_waves; + if (!dirty_set.empty()) { + dag_waves.resize(max_wave + 1); + for (const auto &target_path : dirty_set) { + if (!targets.contains(target_path)) { + continue; + } + + const auto &target{targets.at(target_path)}; + const auto wave_it{wave_cache.find(target_path)}; + const auto wave{wave_it != wave_cache.end() ? wave_it->second + : std::size_t{0}}; + dag_waves[wave].push_back({target.action, target.destination, + target.dependencies, target.data}); + } + } + + std::vector remove_wave; + + for (const auto &uri : removed_uris) { + const auto match{schemas.find(uri)}; + if (match == schemas.end()) { + continue; + } + + remove_wave.push_back( + {BuildPlan::Action::Type::Remove, + schema_base_path(output, match->second.relative_path), + {}, + {}}); + remove_wave.push_back( + {BuildPlan::Action::Type::Remove, + explorer_base_path(output, match->second.relative_path), + {}, + {}}); + } + + for (const auto &[uri, info] : schemas) { + if (removed_uris.contains(uri) || info.evaluate) { + continue; + } + + const auto base{schema_base_path(output, info.relative_path)}; + const auto exhaustive_path{base / "blaze-exhaustive.metapack"}; + const auto fast_path{base / "blaze-fast.metapack"}; + const auto schema_dirty{ + dirty_set.contains((base / "schema.metapack").string())}; + if (schema_dirty || is_full) { + if (entries.contains(exhaustive_path.native())) { + remove_wave.push_back( + {BuildPlan::Action::Type::Remove, exhaustive_path, {}, {}}); + } + if (entries.contains(fast_path.native())) { + remove_wave.push_back( + {BuildPlan::Action::Type::Remove, fast_path, {}, {}}); + } + } + } + + std::unordered_set known_bases; + for (const auto &[uri, info] : schemas) { + known_bases.insert(schema_base_path(output, info.relative_path).string()); + known_bases.insert(explorer_base_path(output, info.relative_path).string()); + auto current{info.relative_path}; + while (current.has_parent_path() && current.parent_path() != current) { + current = current.parent_path(); + known_bases.insert((explorer_path / current / SENTINEL).string()); + } + } + known_bases.insert((explorer_path / SENTINEL).string()); + + std::unordered_set known_ancestors; + known_ancestors.reserve(known_bases.size() * 3); + for (const auto &base : known_bases) { + auto slash_pos{base.rfind('/')}; + while (slash_pos != std::string::npos && slash_pos > 0) { + const auto ancestor{base.substr(0, slash_pos)}; + if (!known_ancestors.insert(ancestor).second) { + break; + } + slash_pos = ancestor.rfind('/'); + } + } + + const auto output_prefix{output.string() + "/"}; + const auto version_string{version_path.string()}; + const auto dependency_tree_string{dependency_tree_path.string()}; + const auto routes_path_string{(output / "routes.bin").string()}; + const auto comment_string{comment_path.string()}; + const auto configuration_string{configuration_path.string()}; + + std::unordered_set stale_roots; + for (const auto &[entry_path, entry] : entries) { + if (!entry_path.starts_with(output_prefix)) { + continue; + } + + if (entry_path == version_string || entry_path == dependency_tree_string || + entry_path == routes_path_string || entry_path == comment_string || + entry_path == configuration_string) { + continue; + } + + bool is_known{false}; + const auto sentinel_pos{entry_path.find("/%/")}; + if (sentinel_pos != std::string::npos) { + is_known = known_bases.contains(entry_path.substr(0, sentinel_pos + 2)); + } + + if (!is_known) { + is_known = known_bases.contains(entry_path) || + known_ancestors.contains(entry_path); + } + + if (!is_known) { + auto stale_end{entry_path.size()}; + while (true) { + const auto slash_pos{entry_path.rfind('/', stale_end - 1)}; + if (slash_pos == std::string::npos || + slash_pos < output_prefix.size()) { + break; + } + const auto parent{entry_path.substr(0, slash_pos)}; + if (known_bases.contains(parent) || known_ancestors.contains(parent)) { + break; + } + stale_end = slash_pos; + } + + stale_roots.insert(entry_path.substr(0, stale_end)); + } + } + + for (auto &root : stale_roots) { + remove_wave.push_back( + {BuildPlan::Action::Type::Remove, std::filesystem::path{root}, {}, {}}); + } + + bool web_removed{false}; + if (build_type == BuildPlan::Type::Headless) { + const auto explorer_prefix{explorer_path.string() + "/"}; + for (const auto &[entry_path, entry] : entries) { + if (!entry_path.starts_with(explorer_prefix)) { + continue; + } + + const auto last_slash{entry_path.rfind('/')}; + if (last_slash == std::string::npos) { + continue; + } + + const std::string_view filename{entry_path.data() + last_slash + 1, + entry_path.size() - last_slash - 1}; + if (filename == "directory-html.metapack" || + filename == "schema-html.metapack" || filename == "404.metapack") { + remove_wave.push_back({BuildPlan::Action::Type::Remove, + std::filesystem::path{entry_path}, + {}, + {}}); + web_removed = true; + } + } + } + + bool web_added{false}; + if (build_type == BuildPlan::Type::Full && !is_full) { + const auto explorer_prefix{explorer_path.string() + "/"}; + bool had_web_entries{false}; + for (const auto &[entry_path, entry] : entries) { + if (!entry_path.starts_with(explorer_prefix)) { + continue; + } + const auto last_slash{entry_path.rfind('/')}; + if (last_slash == std::string::npos) { + continue; + } + const std::string_view filename{entry_path.data() + last_slash + 1, + entry_path.size() - last_slash - 1}; + if (filename == "directory-html.metapack" || + filename == "schema-html.metapack" || filename == "404.metapack") { + had_web_entries = true; + break; + } + } + web_added = !had_web_entries; + } + + BuildPlan plan; + plan.output = output; + plan.type = build_type; + + if (is_full) { + std::vector init_wave; + init_wave.push_back({.type = BuildPlan::Action::Type::Version, + .destination = version_path, + .dependencies = {}, + .data = version}); + init_wave.push_back({.type = BuildPlan::Action::Type::Configuration, + .destination = configuration_path, + .dependencies = {}, + .data = {}}); + if (!comment.empty()) { + init_wave.push_back({.type = BuildPlan::Action::Type::Comment, + .destination = comment_path, + .dependencies = {}, + .data = comment}); + } else if (entries.contains(comment_string)) { + remove_wave.push_back( + {BuildPlan::Action::Type::Remove, comment_path, {}, {}}); + } + plan.waves.push_back(std::move(init_wave)); + } else if (!comment.empty()) { + plan.waves.push_back({{.type = BuildPlan::Action::Type::Comment, + .destination = comment_path, + .dependencies = {}, + .data = comment}}); + } else if (entries.contains(comment_string)) { + remove_wave.push_back( + {BuildPlan::Action::Type::Remove, comment_path, {}, {}}); + } + + for (auto &wave : dag_waves) { + if (!wave.empty()) { + plan.waves.push_back(std::move(wave)); + } + } + + if (is_full || web_added || web_removed) { + std::vector routes_wave; + routes_wave.push_back( + {BuildPlan::Action::Type::Routes, + output / "routes.bin", + {configuration_path}, + build_type == BuildPlan::Type::Full ? "Full" : "Headless"}); + plan.waves.push_back(std::move(routes_wave)); + } + + if (!remove_wave.empty()) { + std::ranges::sort(remove_wave, [](const BuildPlan::Action &left, + const BuildPlan::Action &right) { + return left.destination < right.destination; + }); + std::vector deduped_remove; + deduped_remove.reserve(remove_wave.size()); + for (auto &entry : remove_wave) { + const auto &path{entry.destination.string()}; + bool is_child{false}; + for (const auto &kept : deduped_remove) { + const auto &parent{kept.destination.string()}; + if (path.size() > parent.size() && path[parent.size()] == '/' && + path.starts_with(parent)) { + is_child = true; + break; + } + } + + if (!is_child) { + deduped_remove.push_back(std::move(entry)); + } + } + + plan.waves.push_back(std::move(deduped_remove)); + } + + for (auto &wave : plan.waves) { + std::ranges::sort(wave, [](const BuildPlan::Action &left, + const BuildPlan::Action &right) { + return left.destination < right.destination; + }); + plan.size += wave.size(); + } + + return plan; +} + +} // namespace sourcemeta::one diff --git a/src/build/include/sourcemeta/one/build.h b/src/build/include/sourcemeta/one/build.h index 95490761a..dc79dc968 100644 --- a/src/build/include/sourcemeta/one/build.h +++ b/src/build/include/sourcemeta/one/build.h @@ -6,195 +6,76 @@ #endif #include +#include -#include // std::array -#include // std::filesystem -#include // std::function, std::reference_wrapper, std::cref -#include // std::optional -#include // std::shared_mutex, std::shared_lock -#include // std::string -#include // std::is_same_v -#include // std::unordered_map -#include // std::move -#include // std::vector +#include -namespace sourcemeta::one { - -class SOURCEMETA_ONE_BUILD_EXPORT Build { -public: - using Dependencies = - std::vector>; - - using DynamicCallback = std::function; - - template - using Handler = - std::function; - - using mark_type = std::filesystem::file_time_type; - - Build(const std::filesystem::path &output_root); - - auto path() const -> const std::filesystem::path & { return this->root; } - - [[nodiscard]] auto has_dependencies(const std::filesystem::path &path) const - -> bool; - auto finish() -> void; - auto refresh(const std::filesystem::path &path) -> void; - - template - auto dispatch(const Handler &handler, - const std::filesystem::path &destination, - const Context &context, - const std::vector &dependencies) - -> bool { - const auto &destination_string{destination.native()}; - std::shared_lock lock{this->mutex}; - const auto cached_match{this->entries_.find(destination_string)}; - if (cached_match != this->entries_.end()) { - const auto &entry{cached_match->second}; - if (entry.file_mark.has_value() && - (!entry.static_dependencies.empty() || - !entry.dynamic_dependencies.empty())) { - bool static_dependencies_match{dependencies.size() == - entry.static_dependencies.size()}; - if (static_dependencies_match) { - for (std::size_t index = 0; index < dependencies.size(); ++index) { - if (dependencies[index] != entry.static_dependencies[index]) { - static_dependencies_match = false; - break; - } - } - } - - if (this->dispatch_is_cached(entry, static_dependencies_match)) { - lock.unlock(); - this->track(destination); - return false; - } - } - } - - lock.unlock(); - - Dependencies static_dependency_references; - static_dependency_references.reserve(dependencies.size()); - for (const auto &dependency : dependencies) { - static_dependency_references.emplace_back(std::cref(dependency)); - } - - std::vector dynamic_dependencies; - handler( - destination, static_dependency_references, - [&](const auto &new_dependency) { - dynamic_dependencies.emplace_back(new_dependency); - }, - context); +#include // std::uint8_t +#include // std::filesystem::path +#include // std::function +#include // std::string_view +#include // std::vector - this->dispatch_commit(destination, - std::vector( - dependencies.begin(), dependencies.end()), - std::move(dynamic_dependencies)); - return true; - } - - template - requires(sizeof...(DependencyTypes) == 0 || - (std::is_same_v && ...)) - auto dispatch(const Handler &handler, - const std::filesystem::path &destination, - const Context &context, const DependencyTypes &...dependencies) - -> bool { - const auto &destination_string{destination.native()}; - std::shared_lock lock{this->mutex}; - const auto cached_match{this->entries_.find(destination_string)}; - if (cached_match != this->entries_.end()) { - const auto &entry{cached_match->second}; - if (entry.file_mark.has_value() && - (!entry.static_dependencies.empty() || - !entry.dynamic_dependencies.empty())) { - const std::array - dependency_pointers{{&dependencies...}}; - constexpr auto dependency_count{sizeof...(DependencyTypes)}; - bool static_dependencies_match{entry.static_dependencies.size() == - dependency_count}; - if (static_dependencies_match) { - for (std::size_t index = 0; index < dependency_count; ++index) { - if (*dependency_pointers[index] != - entry.static_dependencies[index]) { - static_dependencies_match = false; - break; - } - } - } - - if (this->dispatch_is_cached(entry, static_dependencies_match)) { - lock.unlock(); - this->track(destination); - return false; - } - } - } - - lock.unlock(); - - const Dependencies static_dependency_references{std::cref(dependencies)...}; - - std::vector dynamic_dependencies; - handler( - destination, static_dependency_references, - [&](const auto &new_dependency) { - dynamic_dependencies.emplace_back(new_dependency); - }, - context); - - std::vector stored_static_dependencies; - stored_static_dependencies.reserve(sizeof...(DependencyTypes)); - ((void)stored_static_dependencies.emplace_back(dependencies), ...); - - this->dispatch_commit(destination, std::move(stored_static_dependencies), - std::move(dynamic_dependencies)); - return true; - } +namespace sourcemeta::one { - struct Entry { - std::optional file_mark; - std::vector static_dependencies; - std::vector dynamic_dependencies; - bool tracked{false}; - bool is_directory{false}; +struct BuildPlan { + enum class Type : std::uint8_t { Headless, Full }; + + struct Action { + enum class Type : std::uint8_t { + Materialise, + Positions, + Locations, + Dependencies, + Stats, + Health, + Bundle, + Editor, + BlazeExhaustive, + BlazeFast, + SchemaMetadata, + DependencyTree, + Dependents, + SearchIndex, + DirectoryList, + WebIndex, + WebNotFound, + WebDirectory, + WebSchema, + Comment, + Configuration, + Version, + Routes, + Remove + }; + + using Dependencies = std::vector; + + Type type; + std::filesystem::path destination; + Dependencies dependencies; + std::string_view data; }; - auto track(const std::filesystem::path &path) -> void; - [[nodiscard]] auto is_untracked_file(const std::filesystem::path &path) const - -> bool; - - auto entries() const -> const std::unordered_map & { - return this->entries_; - } - - auto write_json_if_different(const std::filesystem::path &path, - const sourcemeta::core::JSON &document) -> void; - -private: - [[nodiscard]] auto dispatch_is_cached(const Entry &entry, - bool static_dependencies_match) const - -> bool; - auto - dispatch_commit(const std::filesystem::path &destination, - std::vector &&static_dependencies, - std::vector &&dynamic_dependencies) - -> void; - - std::filesystem::path root; - std::string root_string; - std::unordered_map entries_; - mutable std::shared_mutex mutex; - bool has_previous_run{false}; + std::filesystem::path output; + Type type; + std::vector> waves; + std::size_t size{0}; }; +using BuildDynamicCallback = std::function; + +SOURCEMETA_ONE_BUILD_EXPORT +auto delta(const BuildPlan::Type build_type, const BuildState &entries, + const std::filesystem::path &output, const Resolver::Views &schemas, + const std::string_view version, + const std::string_view current_version, + const std::string_view comment, + const sourcemeta::core::JSON &configuration, + const sourcemeta::core::JSON ¤t_configuration, + const std::vector &changed, + const std::vector &removed) -> BuildPlan; + } // namespace sourcemeta::one #endif // SOURCEMETA_ONE_BUILD_H_ diff --git a/src/build/include/sourcemeta/one/build_state.h b/src/build/include/sourcemeta/one/build_state.h new file mode 100644 index 000000000..889ff76a7 --- /dev/null +++ b/src/build/include/sourcemeta/one/build_state.h @@ -0,0 +1,76 @@ +#ifndef SOURCEMETA_ONE_BUILD_STATE_H_ +#define SOURCEMETA_ONE_BUILD_STATE_H_ + +#ifndef SOURCEMETA_ONE_BUILD_EXPORT +#include +#endif + +#include // std::filesystem::path, std::filesystem::file_time_type +#include // std::string +#include // std::unordered_map +#include // std::vector + +namespace sourcemeta::one { + +class SOURCEMETA_ONE_BUILD_EXPORT BuildState { +public: + struct Entry { + std::filesystem::file_time_type file_mark; + std::vector dependencies; + }; + + using Container = std::unordered_map; + + BuildState() = default; + + auto load(const std::filesystem::path &path) -> void; + auto save(const std::filesystem::path &path) const -> void; + + [[nodiscard]] auto empty() const -> bool { return this->data.empty(); } + [[nodiscard]] auto size() const -> std::size_t { return this->data.size(); } + + [[nodiscard]] auto contains(const Container::key_type &key) const -> bool { + return this->data.contains(key); + } + + [[nodiscard]] auto entry(const Container::key_type &key) const + -> const Entry * { + const auto match{this->data.find(key)}; + return match == this->data.end() ? nullptr : &match->second; + } + + [[nodiscard]] auto + is_stale(const Container::key_type &key, + const std::filesystem::file_time_type source_mtime) const -> bool { + const auto *result{this->entry(key)}; + return result == nullptr || source_mtime > result->file_mark; + } + + auto commit(const std::filesystem::path &path, + std::vector dependencies) -> void { + auto &result{this->data[path.native()]}; + result.file_mark = std::filesystem::file_time_type::clock::now(); + result.dependencies = std::move(dependencies); + } + + auto forget(const Container::key_type &key) -> void { this->data.erase(key); } + + [[nodiscard]] auto begin() const -> Container::const_iterator { + return this->data.begin(); + } + + [[nodiscard]] auto end() const -> Container::const_iterator { + return this->data.end(); + } + + auto emplace(const std::filesystem::path &path, Entry entry) -> void { + this->data[path.native()] = std::move(entry); + } + +private: + Container data; +}; + +} // namespace sourcemeta::one + +#endif // SOURCEMETA_ONE_BUILD_STATE_H_ diff --git a/src/build/state.cc b/src/build/state.cc new file mode 100644 index 000000000..426bd79a9 --- /dev/null +++ b/src/build/state.cc @@ -0,0 +1,150 @@ +#include + +#include + +#include // assert +#include // std::chrono::nanoseconds, std::chrono::duration_cast +#include // std::int64_t, std::uint32_t, std::uint8_t +#include // std::memcpy +#include // std::ofstream +#include // std::string + +namespace { + +constexpr std::uint32_t STATE_MAGIC{0x44455053}; +constexpr std::uint32_t STATE_VERSION{1}; +constexpr std::uint8_t STATE_FLAG_HAS_DEPENDENCIES{0x01}; + +auto read_uint32(const std::uint8_t *data, std::size_t &offset) + -> std::uint32_t { + std::uint32_t value; + std::memcpy(&value, data + offset, sizeof(value)); + offset += sizeof(value); + return value; +} + +auto read_int64(const std::uint8_t *data, std::size_t &offset) -> std::int64_t { + std::int64_t value; + std::memcpy(&value, data + offset, sizeof(value)); + offset += sizeof(value); + return value; +} + +auto append_uint32(std::string &buffer, const std::uint32_t value) -> void { + buffer.append(reinterpret_cast(&value), sizeof(value)); +} + +auto append_int64(std::string &buffer, const std::int64_t value) -> void { + buffer.append(reinterpret_cast(&value), sizeof(value)); +} + +auto append_string(std::string &buffer, const std::string &value) -> void { + append_uint32(buffer, static_cast(value.size())); + buffer.append(value); +} + +} // anonymous namespace + +namespace sourcemeta::one { + +using mark_type = std::filesystem::file_time_type; + +auto BuildState::load(const std::filesystem::path &path) -> void { + if (!std::filesystem::exists(path)) { + return; + } + + try { + const sourcemeta::core::FileView view{path}; + const auto *file_data{view.as()}; + const auto file_size{view.size()}; + + if (file_size < 12) { + return; + } + + std::size_t offset{0}; + if (read_uint32(file_data, offset) != STATE_MAGIC) { + return; + } + + if (read_uint32(file_data, offset) != STATE_VERSION) { + return; + } + + const auto entry_count{read_uint32(file_data, offset)}; + for (std::uint32_t index = 0; index < entry_count; ++index) { + const auto path_length{read_uint32(file_data, offset)}; + std::string entry_path{reinterpret_cast(file_data + offset), + path_length}; + offset += path_length; + + const auto flags{file_data[offset++]}; + auto &map_entry{this->data[entry_path]}; + + if ((flags & STATE_FLAG_HAS_DEPENDENCIES) != 0) { + const auto dependency_count{read_uint32(file_data, offset)}; + map_entry.dependencies.reserve(dependency_count); + for (std::uint32_t dependency_index = 0; + dependency_index < dependency_count; ++dependency_index) { + const auto dependency_length{read_uint32(file_data, offset)}; + map_entry.dependencies.emplace_back( + std::string{reinterpret_cast(file_data + offset), + dependency_length}); + offset += dependency_length; + } + } + + const auto nanoseconds{read_int64(file_data, offset)}; + map_entry.file_mark = + mark_type{std::chrono::duration_cast( + std::chrono::nanoseconds{nanoseconds})}; + } + } catch (...) { + this->data.clear(); + throw; + } +} + +auto BuildState::save(const std::filesystem::path &path) const -> void { + std::string buffer; + buffer.resize(12); + std::memcpy(buffer.data(), &STATE_MAGIC, sizeof(STATE_MAGIC)); + std::memcpy(buffer.data() + 4, &STATE_VERSION, sizeof(STATE_VERSION)); + + std::uint32_t count{0}; + for (const auto &[entry_path, entry] : *this) { + count += 1; + + append_string(buffer, entry_path); + + const bool has_dependencies{!entry.dependencies.empty()}; + std::uint8_t flags{0}; + if (has_dependencies) { + flags |= STATE_FLAG_HAS_DEPENDENCIES; + } + + buffer.push_back(static_cast(flags)); + + if (has_dependencies) { + append_uint32(buffer, + static_cast(entry.dependencies.size())); + for (const auto &dependency : entry.dependencies) { + append_string(buffer, dependency.native()); + } + } + + const auto nanoseconds{std::chrono::duration_cast( + entry.file_mark.time_since_epoch()) + .count()}; + append_int64(buffer, static_cast(nanoseconds)); + } + + std::memcpy(buffer.data() + 8, &count, sizeof(count)); + + std::ofstream stream{path, std::ios::binary}; + assert(!stream.fail()); + stream.write(buffer.data(), static_cast(buffer.size())); +} + +} // namespace sourcemeta::one diff --git a/src/index/explorer.h b/src/index/explorer.h index 7c7640cdf..e5099f7ef 100644 --- a/src/index/explorer.h +++ b/src/index/explorer.h @@ -108,31 +108,29 @@ inflate_metadata(const sourcemeta::one::Configuration &configuration, namespace sourcemeta::one { struct GENERATE_EXPLORER_SCHEMA_METADATA { - using Context = std::tuple< - std::reference_wrapper, - std::reference_wrapper, - std::filesystem::path>; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &callback, - const Context &context) -> void { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &callback, + sourcemeta::one::Resolver &resolver, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; + const auto &resolver_entry{resolver.entry(action.data)}; const auto schema{ - sourcemeta::one::read_json_with_metadata(dependencies.front().get())}; + sourcemeta::one::read_json_with_metadata(action.dependencies.front())}; const auto id{sourcemeta::core::identify( - schema.data, [&callback, &context](const auto identifier) { - return std::get<0>(context).get()(identifier, callback); + schema.data, [&callback, &resolver](const auto identifier) { + return resolver(identifier, callback); })}; assert(!id.empty()); auto result{sourcemeta::core::JSON::make_object()}; result.assign("bytes", sourcemeta::core::JSON{schema.bytes}); result.assign("identifier", sourcemeta::core::JSON{std::string{id}}); - result.assign("path", - sourcemeta::core::JSON{"/" + std::get<2>(context).string()}); + result.assign("path", sourcemeta::core::JSON{ + "/" + resolver_entry.relative_path.string()}); const auto base_dialect{sourcemeta::core::base_dialect( - schema.data, [&callback, &context](const auto identifier) { - return std::get<0>(context).get()(identifier, callback); + schema.data, [&callback, &resolver](const auto identifier) { + return resolver(identifier, callback); })}; assert(base_dialect.has_value()); result.assign("baseDialect", @@ -157,8 +155,8 @@ struct GENERATE_EXPLORER_SCHEMA_METADATA { const auto *examples{schema.data.try_at("examples")}; if (examples && examples->is_array() && !examples->empty()) { const auto vocabularies{sourcemeta::core::vocabularies( - [&callback, &context](const auto identifier) { - return std::get<0>(context).get()(identifier, callback); + [&callback, &resolver](const auto identifier) { + return resolver(identifier, callback); }, base_dialect.value(), dialect)}; const auto &walker_result{ @@ -179,15 +177,15 @@ struct GENERATE_EXPLORER_SCHEMA_METADATA { result.assign("examples", std::move(examples_array)); } - const auto health{sourcemeta::one::read_json(dependencies.at(1).get())}; + const auto health{sourcemeta::one::read_json(action.dependencies.at(1))}; result.assign("health", health.at("score")); const auto schema_dependencies{ - sourcemeta::one::read_json(dependencies.at(2).get())}; + sourcemeta::one::read_json(action.dependencies.at(2))}; result.assign("dependencies", sourcemeta::core::to_json(schema_dependencies.size())); - const auto &collection{std::get<1>(context).get()}; + const auto &collection{*resolver_entry.collection}; if (collection.extra.defines("x-sourcemeta-one:alert")) { assert(collection.extra.at("x-sourcemeta-one:alert").is_string()); @@ -204,13 +202,14 @@ struct GENERATE_EXPLORER_SCHEMA_METADATA { result.assign("provenance", sourcemeta::core::JSON{nullptr}); } - result.assign("breadcrumb", make_breadcrumb(std::get<2>(context), false)); + result.assign("breadcrumb", + make_breadcrumb(resolver_entry.relative_path, false)); const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); + std::filesystem::create_directories(action.destination.parent_path()); sourcemeta::one::write_pretty_json( - destination, result, "application/json", + action.destination, result, "application/json", sourcemeta::one::Encoding::GZIP, sourcemeta::core::JSON{nullptr}, std::chrono::duration_cast(timestamp_end - timestamp_start)); @@ -218,17 +217,17 @@ struct GENERATE_EXPLORER_SCHEMA_METADATA { }; struct GENERATE_EXPLORER_SEARCH_INDEX { - using Context = std::nullptr_t; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &, - const Context &) -> void { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; std::vector result; - result.reserve(dependencies.size()); + result.reserve(action.dependencies.size()); - for (const auto &dependency : dependencies) { - auto metadata_json{sourcemeta::one::read_json(dependency.get())}; + for (const auto &dependency : action.dependencies) { + auto metadata_json{sourcemeta::one::read_json(dependency)}; if (!sourcemeta::core::is_schema(metadata_json)) { continue; } @@ -270,9 +269,9 @@ struct GENERATE_EXPLORER_SEARCH_INDEX { const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); + std::filesystem::create_directories(action.destination.parent_path()); sourcemeta::one::write_jsonl( - destination, result, "application/jsonl", + action.destination, result, "application/jsonl", // We don't want to compress this one so we can // quickly skim through it while streaming it sourcemeta::one::Encoding::Identity, sourcemeta::core::JSON{nullptr}, @@ -282,92 +281,81 @@ struct GENERATE_EXPLORER_SEARCH_INDEX { }; struct GENERATE_EXPLORER_DIRECTORY_LIST { - struct Context { - const std::filesystem::path &directory; - const sourcemeta::one::Configuration &configuration; - const sourcemeta::one::Build &output; - const std::filesystem::path &explorer_path; - const std::filesystem::path &schemas_path; - }; - - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &, - const sourcemeta::one::Build::DynamicCallback &callback, - const Context &context) -> void { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &configuration, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; - assert( - context.directory.string().starts_with(context.schemas_path.string())); auto entries{sourcemeta::core::JSON::make_array()}; - std::vector scores; - if (std::filesystem::exists(context.directory)) { - for (const auto &entry : - std::filesystem::directory_iterator{context.directory}) { - const auto entry_relative_path{entry.path().string().substr( - context.schemas_path.string().size() + 1)}; - assert(!entry_relative_path.starts_with('/')); - - const auto directory_metapack_path{context.explorer_path / - entry_relative_path / "%" / - "directory.metapack"}; - - if (entry.is_directory() && entry.path().filename() != "%" && - std::filesystem::exists(directory_metapack_path) && - !context.output.is_untracked_file(entry.path())) { - auto entry_json{sourcemeta::core::JSON::make_object()}; - callback(directory_metapack_path); - auto directory_json{ - sourcemeta::one::read_json(directory_metapack_path)}; - assert(directory_json.is_object()); - assert(directory_json.defines("health")); - assert(directory_json.at("health").is_integer()); - scores.emplace_back(directory_json.at("health").to_integer()); - entry_json.assign("health", directory_json.at("health")); - - entry_json.assign("name", - sourcemeta::core::JSON{entry.path().filename()}); - inflate_metadata(context.configuration, entry_relative_path, - entry_json); - - entry_json.assign("type", sourcemeta::core::JSON{"directory"}); - entry_json.assign( - "path", sourcemeta::core::JSON{ - entry.path().string().substr( - context.schemas_path.string().size()) + - // This is to distinguish links to a directory or to a - // schema, in case both entries have the same name - "/"}); - entries.push_back(std::move(entry_json)); - } + const auto directory_path{action.destination.parent_path().parent_path()}; + std::filesystem::path relative_path; + auto current{directory_path}; + while (current.has_filename()) { + if (current.filename() == "explorer") { + relative_path = std::filesystem::relative(directory_path, current); + break; + } + + current = current.parent_path(); + } - if (entry.is_directory() && - std::filesystem::exists(entry.path() / "%" / "schema.metapack")) { - auto entry_json{sourcemeta::core::JSON::make_object()}; - auto name{entry.path().filename()}; - entry_json.assign("name", sourcemeta::core::JSON{std::move(name)}); - auto schema_nav_path{context.explorer_path / entry_relative_path}; - schema_nav_path /= "%"; - schema_nav_path /= "schema.metapack"; - - auto nav{sourcemeta::one::read_json(schema_nav_path)}; - - entry_json.merge(nav.as_object()); - assert(!entry_json.defines("entries")); - // No need to show these on children - entry_json.erase("breadcrumb"); - entry_json.erase("examples"); - entry_json.assign("type", sourcemeta::core::JSON{"schema"}); - - assert(entry_json.defines("path")); - std::filesystem::path url{entry_json.at("path").to_string()}; - entry_json.at("path").into(sourcemeta::core::JSON{url}); - - assert(entry_json.defines("health")); - assert(entry_json.at("health").is_integer()); - scores.emplace_back(entry_json.at("health").to_integer()); - entries.push_back(std::move(entry_json)); + for (const auto &dependency : action.dependencies) { + const auto filename{dependency.filename().string()}; + const auto child_name{ + dependency.parent_path().parent_path().filename().string()}; + + if (filename == "directory.metapack") { + auto directory_json{sourcemeta::one::read_json(dependency)}; + assert(directory_json.is_object()); + assert(directory_json.defines("health")); + assert(directory_json.at("health").is_integer()); + scores.emplace_back(directory_json.at("health").to_integer()); + + auto entry_json{sourcemeta::core::JSON::make_object()}; + entry_json.assign("health", directory_json.at("health")); + entry_json.assign("name", sourcemeta::core::JSON{child_name}); + entry_json.assign("type", sourcemeta::core::JSON{"directory"}); + assert(directory_json.defines("path")); + entry_json.assign("path", + sourcemeta::core::JSON{ + directory_json.at("path").to_string() + "/"}); + if (directory_json.defines("title")) { + entry_json.assign("title", directory_json.at("title")); + } + if (directory_json.defines("description")) { + entry_json.assign("description", directory_json.at("description")); + } + if (directory_json.defines("email")) { + entry_json.assign("email", directory_json.at("email")); + } + if (directory_json.defines("github")) { + entry_json.assign("github", directory_json.at("github")); } + if (directory_json.defines("website")) { + entry_json.assign("website", directory_json.at("website")); + } + entries.push_back(std::move(entry_json)); + } else if (filename == "schema.metapack") { + auto nav{sourcemeta::one::read_json(dependency)}; + auto entry_json{sourcemeta::core::JSON::make_object()}; + entry_json.assign("name", sourcemeta::core::JSON{child_name}); + entry_json.merge(nav.as_object()); + assert(!entry_json.defines("entries")); + entry_json.erase("breadcrumb"); + entry_json.erase("examples"); + entry_json.assign("type", sourcemeta::core::JSON{"schema"}); + + assert(entry_json.defines("path")); + std::filesystem::path url{entry_json.at("path").to_string()}; + entry_json.at("path").into(sourcemeta::core::JSON{url}); + + assert(entry_json.defines("health")); + assert(entry_json.at("health").is_integer()); + scores.emplace_back(entry_json.at("health").to_integer()); + entries.push_back(std::move(entry_json)); } } @@ -385,7 +373,6 @@ struct GENERATE_EXPLORER_DIRECTORY_LIST { sort_keys.push_back({type, try_parse_version(name), name}); } - // Sort indices so we can reorder the JSON array accordingly std::vector indices(entries.size()); for (std::size_t index = 0; index < indices.size(); index++) { indices[index] = index; @@ -416,39 +403,37 @@ struct GENERATE_EXPLORER_DIRECTORY_LIST { auto meta{sourcemeta::core::JSON::make_object()}; - const auto page_key{ - std::filesystem::relative(context.directory, context.schemas_path)}; - inflate_metadata(context.configuration, page_key, meta); - - const auto accumulated_health = - static_cast(std::lround(static_cast(std::accumulate( - scores.cbegin(), scores.cend(), 0LL)) / - static_cast(scores.size()))); + inflate_metadata(configuration, relative_path, meta); - meta.assign("health", sourcemeta::core::JSON{accumulated_health}); + if (!scores.empty()) { + const auto accumulated_health = static_cast( + std::lround(static_cast(std::accumulate(scores.cbegin(), + scores.cend(), 0LL)) / + static_cast(scores.size()))); + meta.assign("health", sourcemeta::core::JSON{accumulated_health}); + } else { + meta.assign("health", sourcemeta::core::JSON{0}); + } meta.assign("entries", std::move(entries)); - const std::filesystem::path relative_path{context.directory.string().substr( - std::min(context.schemas_path.string().size() + 1, - context.directory.string().size()))}; - meta.assign("path", sourcemeta::core::JSON{std::string{"/"} + - relative_path.string()}); - - if (relative_path.string().empty()) { - meta.assign("url", sourcemeta::core::JSON{context.configuration.url}); + if (relative_path == ".") { + meta.assign("path", sourcemeta::core::JSON{"/"}); + meta.assign("url", sourcemeta::core::JSON{configuration.url}); + meta.assign("breadcrumb", make_breadcrumb(std::filesystem::path{}, true)); } else { - meta.assign("url", sourcemeta::core::JSON{context.configuration.url + - "/" + relative_path.string()}); + meta.assign("path", sourcemeta::core::JSON{std::string{"/"} + + relative_path.string()}); + meta.assign("url", sourcemeta::core::JSON{configuration.url + "/" + + relative_path.string()}); + meta.assign("breadcrumb", make_breadcrumb(relative_path, true)); } - meta.assign("breadcrumb", make_breadcrumb(relative_path, true)); const auto timestamp_end{std::chrono::steady_clock::now()}; - - std::filesystem::create_directories(destination.parent_path()); + std::filesystem::create_directories(action.destination.parent_path()); sourcemeta::one::write_pretty_json( - destination, meta, "application/json", sourcemeta::one::Encoding::GZIP, - sourcemeta::core::JSON{nullptr}, + action.destination, meta, "application/json", + sourcemeta::one::Encoding::GZIP, sourcemeta::core::JSON{nullptr}, std::chrono::duration_cast(timestamp_end - timestamp_start)); } diff --git a/src/index/generators.h b/src/index/generators.h index d1c13e7e7..0454a2e93 100644 --- a/src/index/generators.h +++ b/src/index/generators.h @@ -25,6 +25,7 @@ #include // assert #include // std::filesystem +#include // std::ofstream #include // std::unique_ptr #include // std::mutex, std::lock_guard #include // std::queue @@ -36,19 +37,59 @@ namespace sourcemeta::one { +struct GENERATE_VERSION { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { + std::filesystem::create_directories(action.destination.parent_path()); + std::ofstream stream{action.destination}; + assert(!stream.fail()); + sourcemeta::core::stringify( + sourcemeta::core::JSON{std::string{action.data}}, stream); + } +}; + +struct GENERATE_COMMENT { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { + std::filesystem::create_directories(action.destination.parent_path()); + std::ofstream stream{action.destination}; + assert(!stream.fail()); + sourcemeta::core::stringify( + sourcemeta::core::JSON{std::string{action.data}}, stream); + } +}; + +struct GENERATE_CONFIGURATION { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &raw_configuration) -> void { + std::filesystem::create_directories(action.destination.parent_path()); + std::ofstream stream{action.destination}; + assert(!stream.fail()); + sourcemeta::core::stringify(raw_configuration, stream); + } +}; + struct GENERATE_MATERIALISED_SCHEMA { - using Context = std::pair>; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &, - const sourcemeta::one::Build::DynamicCallback &callback, - const Context &data) -> void { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &callback, + sourcemeta::one::Resolver &resolver, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; - auto schema{data.second.get()(data.first)}; + auto schema{resolver(action.data)}; assert(schema.has_value()); const auto dialect_identifier{sourcemeta::core::dialect(schema.value())}; assert(!dialect_identifier.empty()); - const auto metaschema{data.second.get()(dialect_identifier)}; + const auto metaschema{resolver(dialect_identifier)}; assert(metaschema.has_value()); // Validate the schemas against their meta-schemas @@ -56,7 +97,7 @@ struct GENERATE_MATERIALISED_SCHEMA { sourcemeta::blaze::Evaluator evaluator; const auto result{evaluator.validate( GENERATE_MATERIALISED_SCHEMA::compile(std::string{dialect_identifier}, - metaschema.value(), data.second), + metaschema.value(), resolver), schema.value(), std::ref(output))}; if (!result) { throw MetaschemaError(output); @@ -64,19 +105,20 @@ struct GENERATE_MATERIALISED_SCHEMA { sourcemeta::core::format( schema.value(), sourcemeta::core::schema_walker, - [&callback, &data](const auto identifier) { - return data.second.get()(identifier, callback); + [&callback, &resolver](const auto identifier) { + return resolver(identifier, callback); }, dialect_identifier); const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); + std::filesystem::create_directories(action.destination.parent_path()); sourcemeta::one::write_pretty_json( - destination, schema.value(), "application/schema+json", + action.destination, schema.value(), "application/schema+json", sourcemeta::one::Encoding::GZIP, sourcemeta::core::JSON{std::string{dialect_identifier}}, std::chrono::duration_cast(timestamp_end - timestamp_start)); + resolver.cache_path(action.data, action.destination); } private: @@ -103,19 +145,19 @@ struct GENERATE_MATERIALISED_SCHEMA { }; struct GENERATE_POINTER_POSITIONS { - using Context = sourcemeta::one::Resolver; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &, - const Context &) -> void { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; sourcemeta::core::PointerPositionTracker tracker; - sourcemeta::one::read_json(dependencies.front().get(), std::ref(tracker)); + sourcemeta::one::read_json(action.dependencies.front(), std::ref(tracker)); const auto result{sourcemeta::core::to_json(tracker)}; const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); + std::filesystem::create_directories(action.destination.parent_path()); sourcemeta::one::write_pretty_json( - destination, result, "application/json", + action.destination, result, "application/json", sourcemeta::one::Encoding::GZIP, sourcemeta::core::JSON{nullptr}, std::chrono::duration_cast(timestamp_end - timestamp_start)); @@ -123,14 +165,14 @@ struct GENERATE_POINTER_POSITIONS { }; struct GENERATE_FRAME_LOCATIONS { - using Context = sourcemeta::one::Resolver; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &callback, - const Context &resolver) -> void { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &callback, + sourcemeta::one::Resolver &resolver, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; sourcemeta::core::PointerPositionTracker tracker; - const auto contents{sourcemeta::one::read_json(dependencies.front().get(), + const auto contents{sourcemeta::one::read_json(action.dependencies.front(), std::ref(tracker))}; sourcemeta::core::SchemaFrame frame{ sourcemeta::core::SchemaFrame::Mode::Locations}; @@ -140,9 +182,9 @@ struct GENERATE_FRAME_LOCATIONS { }); const auto result{frame.to_json(tracker).at("locations")}; const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); + std::filesystem::create_directories(action.destination.parent_path()); sourcemeta::one::write_pretty_json( - destination, result, "application/json", + action.destination, result, "application/json", sourcemeta::one::Encoding::GZIP, sourcemeta::core::JSON{nullptr}, std::chrono::duration_cast(timestamp_end - timestamp_start)); @@ -150,13 +192,14 @@ struct GENERATE_FRAME_LOCATIONS { }; struct GENERATE_DEPENDENCIES { - using Context = sourcemeta::one::Resolver; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &callback, - const Context &resolver) -> void { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &callback, + sourcemeta::one::Resolver &resolver, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; - const auto contents{sourcemeta::one::read_json(dependencies.front().get())}; + const auto contents{ + sourcemeta::one::read_json(action.dependencies.front())}; auto result{sourcemeta::core::JSON::make_array()}; sourcemeta::core::dependencies( contents, sourcemeta::core::schema_walker, @@ -175,9 +218,9 @@ struct GENERATE_DEPENDENCIES { assert(result.unique()); const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); + std::filesystem::create_directories(action.destination.parent_path()); sourcemeta::one::write_pretty_json( - destination, result, "application/json", + action.destination, result, "application/json", sourcemeta::one::Encoding::GZIP, sourcemeta::core::JSON{nullptr}, std::chrono::duration_cast(timestamp_end - timestamp_start)); @@ -195,11 +238,11 @@ struct GENERATE_DEPENDENCIES { }; struct GENERATE_DEPENDENCY_TREE { - using Context = sourcemeta::one::Resolver; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &, - const Context &) -> void { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; // Direct dependencies @@ -207,8 +250,8 @@ struct GENERATE_DEPENDENCY_TREE { std::unordered_map>; DirectMap direct; - for (const auto &dependency : dependencies) { - const auto contents{sourcemeta::one::read_json(dependency.get())}; + for (const auto &dependency : action.dependencies) { + const auto contents{sourcemeta::one::read_json(dependency)}; assert(contents.is_array()); for (const auto &entry : contents.as_array()) { direct[entry.at("to").to_string()].emplace( @@ -248,9 +291,9 @@ struct GENERATE_DEPENDENCY_TREE { auto result{sourcemeta::core::to_json(transitive)}; const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); + std::filesystem::create_directories(action.destination.parent_path()); sourcemeta::one::write_pretty_json( - destination, result, "application/json", + action.destination, result, "application/json", sourcemeta::one::Encoding::GZIP, sourcemeta::core::JSON{nullptr}, std::chrono::duration_cast(timestamp_end - timestamp_start)); @@ -258,16 +301,17 @@ struct GENERATE_DEPENDENCY_TREE { }; struct GENERATE_DEPENDENTS { - using Context = sourcemeta::core::JSON::String; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &, - const Context &context) -> void { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; - const auto contents{sourcemeta::one::read_json(dependencies.front().get())}; + const auto contents{ + sourcemeta::one::read_json(action.dependencies.front())}; assert(contents.is_object()); auto result{sourcemeta::core::JSON::make_array()}; - const auto *match{contents.try_at(context)}; + const auto *match{contents.try_at(std::string{action.data})}; if (match) { assert(match->is_array()); for (const auto &entry : match->as_array()) { @@ -280,9 +324,9 @@ struct GENERATE_DEPENDENTS { const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); + std::filesystem::create_directories(action.destination.parent_path()); sourcemeta::one::write_pretty_json( - destination, result, "application/json", + action.destination, result, "application/json", sourcemeta::one::Encoding::GZIP, sourcemeta::core::JSON{nullptr}, std::chrono::duration_cast(timestamp_end - timestamp_start)); @@ -290,19 +334,16 @@ struct GENERATE_DEPENDENTS { }; struct GENERATE_HEALTH { - using Context = - std::pair, - std::reference_wrapper>; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &callback, - const Context &context) -> void { - const auto &resolver{context.first.get()}; - const auto &configuration{context.second.get()}; + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &callback, + sourcemeta::one::Resolver &resolver, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; - const auto contents{sourcemeta::one::read_json(dependencies.front().get())}; - - auto &cache_entry{bundle_for(configuration, resolver, callback)}; + const auto contents{ + sourcemeta::one::read_json(action.dependencies.front())}; + const auto &collection{*resolver.entry(action.data).collection}; + auto &cache_entry{bundle_for(collection, resolver, callback)}; auto errors{sourcemeta::core::JSON::make_array()}; const auto result{cache_entry.bundle.check( contents, sourcemeta::core::schema_walker, @@ -339,9 +380,9 @@ struct GENERATE_HEALTH { report.assign("errors", std::move(errors)); const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); + std::filesystem::create_directories(action.destination.parent_path()); sourcemeta::one::write_pretty_json( - destination, report, "application/json", + action.destination, report, "application/json", sourcemeta::one::Encoding::GZIP, sourcemeta::core::JSON{nullptr}, std::chrono::duration_cast(timestamp_end - timestamp_start)); @@ -356,7 +397,7 @@ struct GENERATE_HEALTH { static auto bundle_for( const sourcemeta::blaze::Configuration &configuration, [[maybe_unused]] const sourcemeta::one::Resolver &resolver, - [[maybe_unused]] const sourcemeta::one::Build::DynamicCallback &callback) + [[maybe_unused]] const sourcemeta::one::BuildDynamicCallback &callback) -> CacheEntry & { static std::mutex cache_mutex; static std::unordered_map> cache; @@ -390,13 +431,13 @@ struct GENERATE_HEALTH { }; struct GENERATE_BUNDLE { - using Context = sourcemeta::one::Resolver; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &callback, - const Context &resolver) -> void { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &callback, + sourcemeta::one::Resolver &resolver, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; - auto schema{sourcemeta::one::read_json(dependencies.front().get())}; + auto schema{sourcemeta::one::read_json(action.dependencies.front())}; sourcemeta::core::bundle(schema, sourcemeta::core::schema_walker, [&callback, &resolver](const auto identifier) { return resolver(identifier, callback); @@ -411,9 +452,9 @@ struct GENERATE_BUNDLE { dialect_identifier); const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); + std::filesystem::create_directories(action.destination.parent_path()); sourcemeta::one::write_pretty_json( - destination, schema, "application/schema+json", + action.destination, schema, "application/schema+json", sourcemeta::one::Encoding::GZIP, sourcemeta::core::JSON{std::string{dialect_identifier}}, std::chrono::duration_cast(timestamp_end - @@ -422,13 +463,13 @@ struct GENERATE_BUNDLE { }; struct GENERATE_EDITOR { - using Context = sourcemeta::one::Resolver; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &callback, - const Context &resolver) -> void { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &callback, + sourcemeta::one::Resolver &resolver, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; - auto schema{sourcemeta::one::read_json(dependencies.front().get())}; + auto schema{sourcemeta::one::read_json(action.dependencies.front())}; sourcemeta::core::for_editor(schema, sourcemeta::core::schema_walker, [&callback, &resolver](const auto identifier) { return resolver(identifier, callback); @@ -443,9 +484,9 @@ struct GENERATE_EDITOR { dialect_identifier); const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); + std::filesystem::create_directories(action.destination.parent_path()); sourcemeta::one::write_pretty_json( - destination, schema, "application/schema+json", + action.destination, schema, "application/schema+json", sourcemeta::one::Encoding::GZIP, sourcemeta::core::JSON{std::string{dialect_identifier}}, std::chrono::duration_cast(timestamp_end - @@ -453,41 +494,60 @@ struct GENERATE_EDITOR { } }; -struct GENERATE_BLAZE_TEMPLATE { - using Context = sourcemeta::blaze::Mode; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &, - const Context &mode) -> void { - const auto timestamp_start{std::chrono::steady_clock::now()}; - const auto contents{sourcemeta::one::read_json(dependencies.front().get())}; - sourcemeta::core::SchemaFrame frame{ - sourcemeta::core::SchemaFrame::Mode::References}; - frame.analyse(contents, sourcemeta::core::schema_walker, - sourcemeta::core::schema_resolver); - const auto schema_template{sourcemeta::blaze::compile( - contents, sourcemeta::core::schema_walker, - sourcemeta::core::schema_resolver, - sourcemeta::blaze::default_schema_compiler, frame, frame.root(), mode)}; - const auto result{sourcemeta::blaze::to_json(schema_template)}; - const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); - sourcemeta::one::write_json( - destination, result, "application/json", - sourcemeta::one::Encoding::GZIP, sourcemeta::core::JSON{nullptr}, - std::chrono::duration_cast(timestamp_end - - timestamp_start)); +static auto generate_blaze_template( + const std::filesystem::path &destination, + const sourcemeta::one::BuildPlan::Action::Dependencies &dependencies, + const sourcemeta::blaze::Mode mode) -> void { + const auto timestamp_start{std::chrono::steady_clock::now()}; + const auto contents{sourcemeta::one::read_json(dependencies.front())}; + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(contents, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + const auto schema_template{sourcemeta::blaze::compile( + contents, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::blaze::default_schema_compiler, frame, frame.root(), mode)}; + const auto result{sourcemeta::blaze::to_json(schema_template)}; + const auto timestamp_end{std::chrono::steady_clock::now()}; + std::filesystem::create_directories(destination.parent_path()); + sourcemeta::one::write_json( + destination, result, "application/json", sourcemeta::one::Encoding::GZIP, + sourcemeta::core::JSON{nullptr}, + std::chrono::duration_cast(timestamp_end - + timestamp_start)); +} + +struct GENERATE_BLAZE_TEMPLATE_EXHAUSTIVE { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { + generate_blaze_template(action.destination, action.dependencies, + sourcemeta::blaze::Mode::Exhaustive); + } +}; + +struct GENERATE_BLAZE_TEMPLATE_FAST { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { + generate_blaze_template(action.destination, action.dependencies, + sourcemeta::blaze::Mode::FastValidation); } }; struct GENERATE_STATS { - using Context = sourcemeta::one::Resolver; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &callback, - const Context &resolver) -> void { + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &callback, + sourcemeta::one::Resolver &resolver, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; - const auto schema{sourcemeta::one::read_json(dependencies.front().get())}; + const auto schema{sourcemeta::one::read_json(action.dependencies.front())}; std::map> result; @@ -513,23 +573,56 @@ struct GENERATE_STATS { } const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); + std::filesystem::create_directories(action.destination.parent_path()); sourcemeta::one::write_pretty_json( - destination, sourcemeta::core::to_json(result), "application/json", - sourcemeta::one::Encoding::GZIP, sourcemeta::core::JSON{nullptr}, + action.destination, sourcemeta::core::to_json(result), + "application/json", sourcemeta::one::Encoding::GZIP, + sourcemeta::core::JSON{nullptr}, std::chrono::duration_cast(timestamp_end - timestamp_start)); } }; struct GENERATE_URITEMPLATE_ROUTES { - using Context = sourcemeta::core::URITemplateRouter; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &, - const sourcemeta::one::Build::DynamicCallback &, - const Context &router) -> void { - std::filesystem::create_directories(destination.parent_path()); - sourcemeta::core::URITemplateRouterView::save(router, destination); + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &, + const sourcemeta::core::JSON &) -> void { + sourcemeta::core::URITemplateRouter router; + router.add("/self/v1/api/list", sourcemeta::one::HANDLER_SELF_V1_API_LIST); + router.add("/self/v1/api/list/{+path}", + sourcemeta::one::HANDLER_SELF_V1_API_LIST_PATH); + router.add("/self/v1/api/schemas/dependencies/{+schema}", + sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_DEPENDENCIES); + router.add("/self/v1/api/schemas/dependents/{+schema}", + sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_DEPENDENTS); + router.add("/self/v1/api/schemas/health/{+schema}", + sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_HEALTH); + router.add("/self/v1/api/schemas/locations/{+schema}", + sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_LOCATIONS); + router.add("/self/v1/api/schemas/positions/{+schema}", + sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_POSITIONS); + router.add("/self/v1/api/schemas/stats/{+schema}", + sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_STATS); + router.add("/self/v1/api/schemas/metadata/{+schema}", + sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_METADATA); + router.add("/self/v1/api/schemas/evaluate/{+schema}", + sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_EVALUATE); + router.add("/self/v1/api/schemas/trace/{+schema}", + sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_TRACE); + router.add("/self/v1/api/schemas/search", + sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_SEARCH); + router.add("/self/v1/health", sourcemeta::one::HANDLER_SELF_V1_HEALTH); + router.add("/self/v1/api/{+any}", + sourcemeta::one::HANDLER_SELF_V1_API_DEFAULT); + + if (action.data == "Full") { + router.add("/self/static/{+path}", sourcemeta::one::HANDLER_SELF_STATIC); + } + + std::filesystem::create_directories(action.destination.parent_path()); + sourcemeta::core::URITemplateRouterView::save(router, action.destination); } }; diff --git a/src/index/index.cc b/src/index/index.cc index 3c0a10db5..b8fcb0d76 100644 --- a/src/index/index.cc +++ b/src/index/index.cc @@ -16,21 +16,21 @@ #include "explorer.h" #include "generators.h" -#include // std::sort -#include // assert -#include // std::chrono -#include // EXIT_FAILURE, EXIT_SUCCESS -#include // std::exception -#include // std::filesystem -#include // std::reference_wrapper, std::cref -#include // std::setw, std::setfill -#include // std::cerr, std::cout -#include // std::optional -#include // std::string -#include // std::string_view -#include // std::unordered_map -#include // std::unordered_set -#include // std::vector +#include // std::sort +#include // std::array +#include // std::atomic +#include // assert +#include // std::chrono +#include // EXIT_FAILURE, EXIT_SUCCESS +#include // std::exception +#include // std::filesystem +#include // std::reference_wrapper, std::cref +#include // std::setw, std::setfill +#include // std::cerr, std::cout +#include // std::mutex, std::lock_guard +#include // std::string +#include // std::string_view +#include // std::vector // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) #define PROFILE_INIT(state) \ @@ -48,19 +48,37 @@ std::chrono::steady_clock::now() - (state).second)); \ (state).second = std::chrono::steady_clock::now() -// We rely on this special prefix to avoid file system collisions. The reason it -// works is that URIs cannot have "%" without percent-encoding it as "%25", and -// the resolver will not unescape it back when computing the relative path to an -// entry -constexpr auto SENTINEL{"%"}; - -static auto attribute_not_disabled( - const sourcemeta::one::Configuration::Collection &collection, - const sourcemeta::core::JSON::String &property) -> bool { - return !collection.extra.defines(property) || - !collection.extra.at(property).is_boolean() || - collection.extra.at(property).to_boolean(); -} +using BuildHandlerFunction = void (*)( + const sourcemeta::one::BuildPlan::Action &, + const sourcemeta::one::BuildDynamicCallback &, sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &, const sourcemeta::core::JSON &); + +static constexpr std::array HANDLERS{{ + &sourcemeta::one::GENERATE_MATERIALISED_SCHEMA::handler, + &sourcemeta::one::GENERATE_POINTER_POSITIONS::handler, + &sourcemeta::one::GENERATE_FRAME_LOCATIONS::handler, + &sourcemeta::one::GENERATE_DEPENDENCIES::handler, + &sourcemeta::one::GENERATE_STATS::handler, + &sourcemeta::one::GENERATE_HEALTH::handler, + &sourcemeta::one::GENERATE_BUNDLE::handler, + &sourcemeta::one::GENERATE_EDITOR::handler, + &sourcemeta::one::GENERATE_BLAZE_TEMPLATE_EXHAUSTIVE::handler, + &sourcemeta::one::GENERATE_BLAZE_TEMPLATE_FAST::handler, + &sourcemeta::one::GENERATE_EXPLORER_SCHEMA_METADATA::handler, + &sourcemeta::one::GENERATE_DEPENDENCY_TREE::handler, + &sourcemeta::one::GENERATE_DEPENDENTS::handler, + &sourcemeta::one::GENERATE_EXPLORER_SEARCH_INDEX::handler, + &sourcemeta::one::GENERATE_EXPLORER_DIRECTORY_LIST::handler, + &sourcemeta::one::GENERATE_WEB_INDEX::handler, + &sourcemeta::one::GENERATE_WEB_NOT_FOUND::handler, + &sourcemeta::one::GENERATE_WEB_DIRECTORY::handler, + &sourcemeta::one::GENERATE_WEB_SCHEMA::handler, + &sourcemeta::one::GENERATE_COMMENT::handler, + &sourcemeta::one::GENERATE_CONFIGURATION::handler, + &sourcemeta::one::GENERATE_VERSION::handler, + &sourcemeta::one::GENERATE_URITEMPLATE_ROUTES::handler, + nullptr, +}}; static auto print_progress(std::mutex &mutex, const std::size_t threads, const std::string_view title, @@ -74,25 +92,6 @@ static auto print_progress(std::mutex &mutex, const std::size_t threads, << " [" << std::this_thread::get_id() << "/" << threads << "]\n"; } -template -static auto DISPATCH(const std::filesystem::path &destination, - const typename Handler::Context &context, - std::mutex &mutex, const std::string_view title, - const std::string_view prefix, - const std::string_view suffix, - sourcemeta::one::Build &output, const Deps &...deps) - -> bool { - const auto was_built{output.dispatch( - Handler::handler, destination, context, deps...)}; - if (!was_built) { - std::lock_guard lock{mutex}; - std::cerr << "(skip) " << title << ": " << prefix << " [" << suffix - << "]\n"; - } - - return was_built; -} - static auto index_main(const std::string_view &program, const sourcemeta::core::Options &app) -> int { if (!app.contains("skip-banner")) { @@ -179,40 +178,43 @@ static auto index_main(const std::string_view &program, } ///////////////////////////////////////////////////////////////////////////// - // (5) Prepare the output directory + // (5) Prepare the output directory and load previous state ///////////////////////////////////////////////////////////////////////////// - sourcemeta::one::Build output{output_path}; + std::filesystem::create_directories(output_path); + const auto canonical_output{std::filesystem::canonical(output_path)}; - // We do this so that targets can be re-built if the One version changes - const auto mark_version_path{output.path() / "version.json"}; - // Note we only write back if the content changed in order to not accidentally - // bump up the file modified time - output.write_json_if_different( - mark_version_path, sourcemeta::core::JSON{sourcemeta::one::version()}); + sourcemeta::one::BuildState entries; + const auto state_path{canonical_output / "state.bin"}; + entries.load(state_path); - ///////////////////////////////////////////////////////////////////////////// - // (6) Store the full configuration file for target dependencies - ///////////////////////////////////////////////////////////////////////////// + // Only trust on-disk files when the state was loaded successfully, + // otherwise the entries map and the on-disk artefacts are out of sync - // For targets that depend on the contents of the configuration or on anything - // potentially derived from the configuration, such as the resolver - const auto mark_configuration_path{output.path() / "configuration.json"}; - // Note we only write back if the content changed in order to not accidentally - // bump up the file modified time - output.write_json_if_different(mark_configuration_path, raw_configuration); - - ///////////////////////////////////////////////////////////////////////////// - // (7) Store the optional comment for informational purposes - ///////////////////////////////////////////////////////////////////////////// + std::string current_version; + const auto version_path{canonical_output / "version.json"}; + if (!entries.empty() && std::filesystem::exists(version_path)) { + const auto version_json{sourcemeta::core::read_json(version_path)}; + current_version = version_json.to_string(); + } - const auto comment_path{output.path() / "comment.json"}; - if (app.contains("comment")) { - output.write_json_if_different( - comment_path, - sourcemeta::core::JSON{std::string{app.at("comment").at(0)}}); + auto current_configuration{sourcemeta::core::JSON{nullptr}}; + const auto configuration_json_path{canonical_output / "configuration.json"}; + if (!entries.empty() && std::filesystem::exists(configuration_json_path)) { + current_configuration = + sourcemeta::core::read_json(configuration_json_path); } + // Determine comment + const std::string comment{app.contains("comment") + ? std::string{app.at("comment").at(0)} + : std::string{}}; + + // Determine build type + const auto build_type{configuration.html.has_value() + ? sourcemeta::one::BuildPlan::Type::Full + : sourcemeta::one::BuildPlan::Type::Headless}; + PROFILE_END(profiling, "Startup"); // Mainly to not screw up the logs @@ -222,7 +224,7 @@ static auto index_main(const std::string_view &program, : std::thread::hardware_concurrency()}; ///////////////////////////////////////////////////////////////////////////// - // (8) First pass to locate all of the schemas we will be indexing + // (6) First pass to locate all of the schemas we will be indexing // NOTE: No files are generated. We only want to know what's out there ///////////////////////////////////////////////////////////////////////////// @@ -267,7 +269,7 @@ static auto index_main(const std::string_view &program, PROFILE_END(profiling, "Detect"); ///////////////////////////////////////////////////////////////////////////// - // (9) Resolve all detected schemas in parallel + // (7) Resolve all detected schemas in parallel ///////////////////////////////////////////////////////////////////////////// sourcemeta::one::Resolver resolver; @@ -292,513 +294,91 @@ static auto index_main(const std::string_view &program, PROFILE_END(profiling, "Resolve"); ///////////////////////////////////////////////////////////////////////////// - // (10) Do a first analysis pass on the schemas and materialise them for - // further analysis. We do this so that we don't end up rebasing the same - // schemas over and over again depending on the order of analysis later on + // (8) Build schema info map and compute the delta plan ///////////////////////////////////////////////////////////////////////////// - const auto schemas_path{output.path() / "schemas"}; - const auto display_schemas_path{ - std::filesystem::relative(schemas_path, output.path())}; - sourcemeta::core::parallel_for_each( - resolver.begin(), resolver.end(), - [&schemas_path, &resolver, &mutex, &output, &mark_configuration_path, - &mark_version_path](const auto &schema, const auto threads, - const auto cursor) { - print_progress(mutex, threads, "Ingesting", schema.first, cursor, - resolver.size()); - const auto destination{schemas_path / schema.second.relative_path / - SENTINEL / "schema.metapack"}; - DISPATCH( - destination, {schema.first, resolver}, mutex, "Ingesting", - schema.first, "materialise", output, schema.second.path, - mark_configuration_path, mark_version_path); - - // Mark the materialised schema in the resolver - resolver.cache_path(schema.first, destination); - }, - concurrency); + const std::vector changed; + const std::vector removed; + auto plan{sourcemeta::one::delta(build_type, entries, canonical_output, + resolver.data(), sourcemeta::one::version(), + current_version, comment, raw_configuration, + current_configuration, changed, removed)}; - PROFILE_END(profiling, "Ingest"); + PROFILE_END(profiling, "Delta"); ///////////////////////////////////////////////////////////////////////////// - // (11) Generate all the artifacts that purely depend on the schemas + // (9) Execute the plan wave by wave ///////////////////////////////////////////////////////////////////////////// // Give it a generous thread stack size, otherwise we might overflow // the small-by-default thread stack with Blaze constexpr auto THREAD_STACK_SIZE{8 * 1024 * 1024}; - const auto explorer_path{output.path() / "explorer"}; - const auto display_explorer_path{ - std::filesystem::relative(explorer_path, output.path())}; - sourcemeta::core::parallel_for_each( - resolver.begin(), resolver.end(), - [&schemas_path, &explorer_path, &resolver, &mutex, &output, - &mark_configuration_path, &mark_version_path]( - const auto &schema, const auto threads, const auto cursor) { - print_progress(mutex, threads, "Analysing", schema.first, cursor, - resolver.size()); - const auto base_path{schemas_path / schema.second.relative_path / - SENTINEL}; - const auto schema_metapack{base_path / "schema.metapack"}; - const auto dependencies_metapack{base_path / "dependencies.metapack"}; - const auto bundle_metapack{base_path / "bundle.metapack"}; - - const auto health_metapack{base_path / "health.metapack"}; - - DISPATCH( - base_path / "positions.metapack", resolver, mutex, "Analysing", - schema.first, "positions", output, schema_metapack, - mark_version_path); - - DISPATCH( - base_path / "locations.metapack", resolver, mutex, "Analysing", - schema.first, "locations", output, schema_metapack, - mark_version_path); - - DISPATCH( - dependencies_metapack, resolver, mutex, "Analysing", schema.first, - "dependencies", output, schema_metapack, mark_version_path); - - DISPATCH( - base_path / "stats.metapack", resolver, mutex, "Analysing", - schema.first, "stats", output, schema_metapack, mark_version_path); - - DISPATCH( - health_metapack, - {std::ref(resolver), std::cref(schema.second.collection.get())}, - mutex, "Analysing", schema.first, "health", output, schema_metapack, - dependencies_metapack, mark_version_path); - - DISPATCH( - bundle_metapack, resolver, mutex, "Analysing", schema.first, - "bundle", output, schema_metapack, dependencies_metapack, - mark_version_path); - - DISPATCH( - base_path / "editor.metapack", resolver, mutex, "Analysing", - schema.first, "editor", output, bundle_metapack, mark_version_path); - - if (attribute_not_disabled(schema.second.collection.get(), - "x-sourcemeta-one:evaluate")) { - DISPATCH( - base_path / "blaze-exhaustive.metapack", - sourcemeta::blaze::Mode::Exhaustive, mutex, "Analysing", - schema.first, "blaze-exhaustive", output, bundle_metapack, - mark_version_path); - DISPATCH( - base_path / "blaze-fast.metapack", - sourcemeta::blaze::Mode::FastValidation, mutex, "Analysing", - schema.first, "blaze-fast", output, bundle_metapack, - mark_version_path); - } - - DISPATCH( - explorer_path / schema.second.relative_path / SENTINEL / - "schema.metapack", - {resolver, schema.second.collection.get(), - schema.second.relative_path}, - mutex, "Analysing", schema.first, "metadata", output, - schema_metapack, health_metapack, dependencies_metapack, - mark_configuration_path, mark_version_path); - }, - concurrency, THREAD_STACK_SIZE); - - PROFILE_END(profiling, "Analyse"); - - ///////////////////////////////////////////////////////////////////////////// - // (12) Scan the generated files so far to prepare for more complex targets - ///////////////////////////////////////////////////////////////////////////// - - // This is a pretty fast step that will be useful for us to properly declare - // dependencies for HTML and navigational targets - - print_progress(mutex, concurrency, "Reviewing", display_schemas_path.string(), - 1, 3); - std::vector directories; - // The top-level is itself a directory - directories.emplace_back(schemas_path); - std::vector summaries; - std::unordered_map> - directory_summaries; - std::vector dependencies; - if (std::filesystem::exists(schemas_path)) { - const auto &entries{output.entries()}; - const auto &schemas_path_string{schemas_path.native()}; - const auto schemas_prefix_size{schemas_path_string.size() + 1}; - - // Collect tracked entries under schemas_path into a sorted vector - // so that dependency ordering is deterministic across runs - struct SchemaEntry { - const std::string *path; - bool is_directory; - }; - - std::vector schema_entries; - std::unordered_map child_counts; - std::unordered_set has_sentinel; - - for (const auto &[path_key, entry] : entries) { - if (!entry.tracked || path_key.size() <= schemas_path_string.size() || - !path_key.starts_with(schemas_path_string) || - path_key[schemas_path_string.size()] != '/') { - continue; - } - - schema_entries.push_back({&path_key, entry.is_directory}); - - const auto last_slash{path_key.rfind('/')}; - if (last_slash == std::string::npos) { - continue; - } - - child_counts[path_key.substr(0, last_slash)]++; - if (entry.is_directory && - std::string_view{path_key}.substr(last_slash + 1) == SENTINEL) { - has_sentinel.insert(path_key.substr(0, last_slash)); - } - } - - std::ranges::sort(schema_entries, [](const auto &left, const auto &right) { - return *left.path < *right.path; - }); - - for (const auto &schema_entry : schema_entries) { - const auto &path_key{*schema_entry.path}; - if (path_key.size() <= schemas_prefix_size) { - continue; - } - - const auto last_slash{path_key.rfind('/')}; - const std::string_view filename{path_key.data() + last_slash + 1, - path_key.size() - last_slash - 1}; - - if (schema_entry.is_directory && filename != SENTINEL) { - const auto count_match{child_counts.find(path_key)}; - const auto children{ - count_match != child_counts.end() ? count_match->second : 0u}; - if (children == 0 || - (has_sentinel.count(path_key) > 0 && children == 1)) { - continue; - } - - directories.emplace_back(path_key); - const std::string_view relative{path_key.data() + schemas_prefix_size, - path_key.size() - schemas_prefix_size}; - const auto child_directory_metapack{explorer_path / - std::filesystem::path{relative} / - SENTINEL / "directory.metapack"}; - directory_summaries[path_key.substr(0, last_slash)].emplace_back( - child_directory_metapack); - } else if (!schema_entry.is_directory && filename == "schema.metapack") { - const std::string_view parent_path{path_key.data(), last_slash}; - const auto parent_last_slash{parent_path.rfind('/')}; - if (parent_last_slash == std::string_view::npos) { - continue; - } - - const std::string_view parent_filename{ - parent_path.data() + parent_last_slash + 1, - parent_path.size() - parent_last_slash - 1}; - if (parent_filename != SENTINEL) { - continue; - } - - const std::string_view relative{path_key.data() + schemas_prefix_size, - path_key.size() - schemas_prefix_size}; - const auto explorer_summary_path{explorer_path / - std::filesystem::path{relative}}; - summaries.emplace_back(explorer_summary_path); - - const auto grandparent_slash{ - parent_path.substr(0, parent_last_slash).rfind('/')}; - if (grandparent_slash != std::string_view::npos) { - directory_summaries[std::string{ - parent_path.substr(0, grandparent_slash)}] - .emplace_back(explorer_summary_path); - } - - dependencies.emplace_back(std::filesystem::path{parent_path} / - "dependencies.metapack"); - } - } - - // Re-order the directories so that the most nested ones come first, as we - // often need to process directories in that order - std::ranges::sort(directories, [](const std::filesystem::path &left, - const std::filesystem::path &right) { - const auto left_depth{std::distance(left.begin(), left.end())}; - const auto right_depth{std::distance(right.begin(), right.end())}; - if (left_depth == right_depth) { - return left < right; - } else { - return left_depth > right_depth; - } - }); - } - - print_progress(mutex, concurrency, "Reviewing", display_schemas_path.string(), - 2, 3); - - const auto dependency_tree_path{output.path() / "dependency-tree.metapack"}; - - // Read the old dependency tree before DISPATCH overwrites it, so we can - // diff against the new tree to find which schemas' dependents changed - std::optional old_dependency_tree; - if (std::filesystem::exists(dependency_tree_path)) { - old_dependency_tree = sourcemeta::one::read_json(dependency_tree_path); - } - - const auto dependency_tree_rebuilt{ - DISPATCH( - dependency_tree_path, resolver, mutex, "Reviewing", - display_schemas_path.string(), "dependencies", output, dependencies)}; - - // Determine which schemas' dependents actually changed by diffing - // the old and new dependency trees per key. We keep new_dependency_tree - // alive so affected_dependents can hold string_views into its keys. - std::optional new_dependency_tree; - std::unordered_set affected_dependents; - if (dependency_tree_rebuilt) { - new_dependency_tree = sourcemeta::one::read_json(dependency_tree_path); - if (old_dependency_tree.has_value()) { - for (const auto &entry : new_dependency_tree->as_object()) { - const auto *old_value{old_dependency_tree->try_at(entry.first)}; - if (!old_value || *old_value != entry.second) { - affected_dependents.insert(entry.first); - } - } - - for (const auto &entry : old_dependency_tree->as_object()) { - if (!new_dependency_tree->defines(entry.first)) { - affected_dependents.insert(entry.first); - } - } - } else { - for (const auto &entry : new_dependency_tree->as_object()) { - affected_dependents.insert(entry.first); - } - } - } - - struct ReworkEntry { - std::reference_wrapper uri; - std::filesystem::path dependents_path; - }; - - std::vector rework_entries; - for (const auto &schema : resolver) { - auto dependents_path{schemas_path / schema.second.relative_path / SENTINEL / - "dependents.metapack"}; - if (affected_dependents.contains(schema.first) || - !std::filesystem::exists(dependents_path) || - !output.has_dependencies(dependents_path)) { - rework_entries.push_back( - {std::cref(schema.first), std::move(dependents_path)}); - } else { - output.track(dependents_path); - } - } - - print_progress(mutex, concurrency, "Reviewing", display_schemas_path.string(), - 3, 3); - - PROFILE_END(profiling, "Review"); - - ///////////////////////////////////////////////////////////////////////////// - // (13) A further pass on the schemas after review - ///////////////////////////////////////////////////////////////////////////// - - sourcemeta::core::parallel_for_each( - rework_entries.begin(), rework_entries.end(), - [&rework_entries, &mutex, &output, &dependency_tree_path]( - const auto &entry, const auto threads, const auto cursor) { - print_progress(mutex, threads, "Reworking", entry.uri.get(), cursor, - rework_entries.size()); - DISPATCH( - entry.dependents_path, entry.uri.get(), mutex, "Reworking", - entry.uri.get(), "dependents", output, dependency_tree_path); - }, - concurrency, THREAD_STACK_SIZE); - - PROFILE_END(profiling, "Rework"); - - ///////////////////////////////////////////////////////////////////////////// - // (14) Generate the search index - ///////////////////////////////////////////////////////////////////////////// - - print_progress(mutex, concurrency, "Producing", - display_explorer_path.string(), 0, 100); - DISPATCH( - explorer_path / SENTINEL / "search.metapack", nullptr, mutex, "Producing", - display_explorer_path.string(), "search", output, summaries); - - PROFILE_END(profiling, "Search"); - - ///////////////////////////////////////////////////////////////////////////// - // (15) Generate the JSON-based explorer - ///////////////////////////////////////////////////////////////////////////// - - // Note that we can't parallelise this loop, as we need to do it bottom-up - for (std::size_t cursor = 0; cursor < directories.size(); cursor++) { - const auto &entry{directories[cursor]}; - const auto relative_path{std::filesystem::relative(entry, schemas_path)}; - print_progress(mutex, 1, "Producing", relative_path.string(), cursor + 1, - directories.size()); - const auto destination{ - (explorer_path / relative_path / SENTINEL / "directory.metapack") - .lexically_normal()}; - auto &local_summaries{directory_summaries[entry.string()]}; - local_summaries.emplace_back(mark_configuration_path); - local_summaries.emplace_back(mark_version_path); - DISPATCH( - destination, - {.directory = entry, - .configuration = configuration, - .output = output, - .explorer_path = explorer_path, - .schemas_path = schemas_path}, - mutex, "Producing", relative_path.string(), "directory", output, - local_summaries); - local_summaries.pop_back(); - local_summaries.pop_back(); - } + std::atomic progress_counter{0}; + std::mutex entries_mutex; - PROFILE_END(profiling, "Produce"); - - ///////////////////////////////////////////////////////////////////////////// - // (16) Generate the HTML web interface - ///////////////////////////////////////////////////////////////////////////// - - if (configuration.html.has_value()) { + for (auto &wave : plan.waves) { + // TODO: Maybe we can optimise this for waves of a single action + // by avoiding the creation of a whole thread plus the locks? sourcemeta::core::parallel_for_each( - directories.begin(), directories.end(), - [&configuration, &schemas_path, &explorer_path, &directories, - &summaries, &mutex, &output, &mark_configuration_path, - &mark_version_path](const auto &entry, const auto threads, - const auto cursor) { - const auto relative_path{ - std::filesystem::relative(entry, schemas_path)}; - print_progress(mutex, threads, "Rendering", relative_path.string(), - cursor, directories.size() + summaries.size()); - - if (relative_path == ".") { - const auto directory_metapack{explorer_path / SENTINEL / - "directory.metapack"}; - DISPATCH( - explorer_path / SENTINEL / "directory-html.metapack", - configuration, mutex, "Rendering", relative_path.string(), - "index", output, directory_metapack, mark_configuration_path, - mark_version_path); - DISPATCH( - explorer_path / SENTINEL / "404.metapack", configuration, mutex, - "Rendering", relative_path.string(), "not-found", output, - mark_configuration_path, mark_version_path); - } else { - const auto directory_metapack{explorer_path / relative_path / - SENTINEL / "directory.metapack"}; - DISPATCH( - explorer_path / relative_path / SENTINEL / - "directory-html.metapack", - configuration, mutex, "Rendering", relative_path.string(), - "directory", output, directory_metapack, - mark_configuration_path, mark_version_path); + wave.begin(), wave.end(), + [&](auto &action, const auto threads, const auto) { + const auto current{ + progress_counter.fetch_add(1, std::memory_order_relaxed) + 1}; + const std::string_view destination_view{action.destination.native()}; + print_progress( + mutex, threads, + action.type == sourcemeta::one::BuildPlan::Action::Type::Remove + ? "Disposing" + : "Producing", + destination_view.substr(canonical_output.native().size() + 1), + current, plan.size); + + if (action.type == sourcemeta::one::BuildPlan::Action::Type::Remove) { + std::filesystem::remove_all(action.destination); + std::lock_guard lock{entries_mutex}; + entries.forget(action.destination.native()); + return; } - }, - concurrency); - sourcemeta::core::parallel_for_each( - summaries.begin(), summaries.end(), - [&configuration, &schemas_path, &explorer_path, &directories, - &summaries, &mutex, &output, &mark_configuration_path, - &mark_version_path](const auto &entry, const auto threads, - const auto cursor) { - const auto relative_path{ - std::filesystem::relative(entry, explorer_path) - .parent_path() - .parent_path()}; - print_progress(mutex, threads, "Rendering", relative_path.string(), - cursor + directories.size(), - summaries.size() + directories.size()); - const auto schema_path{schemas_path / relative_path / SENTINEL}; - const auto schema_deps_metapack{schema_path / - "dependencies.metapack"}; - const auto schema_health_metapack{schema_path / "health.metapack"}; - const auto schema_dependents_metapack{schema_path / - "dependents.metapack"}; - DISPATCH( - entry.parent_path() / "schema-html.metapack", configuration, - mutex, "Rendering", relative_path.string(), "schema", output, - entry, schema_deps_metapack, schema_health_metapack, - schema_dependents_metapack, mark_configuration_path, - mark_version_path); + const auto handler{HANDLERS[static_cast(action.type)]}; + assert(handler); + handler( + action, + [&action](const auto &path) { + action.dependencies.emplace_back(path); + }, + resolver, configuration, raw_configuration); + + { + std::lock_guard lock{entries_mutex}; + entries.commit(action.destination, std::move(action.dependencies)); + } }, - concurrency); + concurrency, THREAD_STACK_SIZE); } - PROFILE_END(profiling, "Render"); + PROFILE_END(profiling, "Build"); ///////////////////////////////////////////////////////////////////////////// - // (17) Generate the pre computed routes + // (9) Save state ///////////////////////////////////////////////////////////////////////////// - sourcemeta::core::URITemplateRouter router; - router.add("/self/v1/api/list", sourcemeta::one::HANDLER_SELF_V1_API_LIST); - router.add("/self/v1/api/list/{+path}", - sourcemeta::one::HANDLER_SELF_V1_API_LIST_PATH); - router.add("/self/v1/api/schemas/dependencies/{+schema}", - sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_DEPENDENCIES); - router.add("/self/v1/api/schemas/dependents/{+schema}", - sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_DEPENDENTS); - router.add("/self/v1/api/schemas/health/{+schema}", - sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_HEALTH); - router.add("/self/v1/api/schemas/locations/{+schema}", - sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_LOCATIONS); - router.add("/self/v1/api/schemas/positions/{+schema}", - sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_POSITIONS); - router.add("/self/v1/api/schemas/stats/{+schema}", - sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_STATS); - router.add("/self/v1/api/schemas/metadata/{+schema}", - sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_METADATA); - router.add("/self/v1/api/schemas/evaluate/{+schema}", - sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_EVALUATE); - router.add("/self/v1/api/schemas/trace/{+schema}", - sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_TRACE); - router.add("/self/v1/api/schemas/search", - sourcemeta::one::HANDLER_SELF_V1_API_SCHEMAS_SEARCH); - router.add("/self/v1/health", sourcemeta::one::HANDLER_SELF_V1_HEALTH); - router.add("/self/v1/api/{+any}", - sourcemeta::one::HANDLER_SELF_V1_API_DEFAULT); - - if (configuration.html.has_value()) { - router.add("/self/static/{+path}", sourcemeta::one::HANDLER_SELF_STATIC); - } - - const auto routes_path{output.path() / "routes.bin"}; - const auto display_routes_path{ - std::filesystem::relative(routes_path, output.path())}; - DISPATCH( - routes_path, router, mutex, "Producing", display_routes_path.string(), - "routes", output, mark_configuration_path, mark_version_path); + entries.save(state_path); - PROFILE_END(profiling, "Routes"); + PROFILE_END(profiling, "Cleanup"); ///////////////////////////////////////////////////////////////////////////// - // Finish generation + // (10) Compute metrics ///////////////////////////////////////////////////////////////////////////// - output.finish(); - - PROFILE_END(profiling, "Cleanup"); - // TODO: Add a test for this if (app.contains("profile")) { std::cerr << "Profiling...\n"; std::vector> durations; for (const auto &entry : - std::filesystem::recursive_directory_iterator{output.path()}) { + std::filesystem::recursive_directory_iterator{canonical_output}) { if (entry.is_regular_file() && entry.path().extension() == ".metapack") { try { const auto file{sourcemeta::one::read_stream_raw(entry.path())}; @@ -823,7 +403,7 @@ static auto index_main(const std::string_view &program, index++) { std::cout << durations[index].second.count() << "ms " << std::filesystem::relative(durations[index].first, - output.path()) + canonical_output) .string() << "\n"; } diff --git a/src/resolver/include/sourcemeta/one/resolver.h b/src/resolver/include/sourcemeta/one/resolver.h index 958948494..b13830fa7 100644 --- a/src/resolver/include/sourcemeta/one/resolver.h +++ b/src/resolver/include/sourcemeta/one/resolver.h @@ -7,9 +7,9 @@ #include -#include // std::filesystem -#include // std::reference_wrapper -#include // std::optional +#include // std::filesystem::path, std::filesystem::file_time_type +#include // std::function, std::reference_wrapper +#include // std::optional #include // std::shared_mutex #include // std::string_view #include // std::unordered_map @@ -41,25 +41,32 @@ class Resolver { std::reference_wrapper, std::reference_wrapper>; - auto cache_path(const sourcemeta::core::JSON::String &uri, - const std::filesystem::path &path) -> void; - - [[nodiscard]] auto begin() const -> auto { return this->views.begin(); } - [[nodiscard]] auto end() const -> auto { return this->views.end(); } - [[nodiscard]] auto size() const -> auto { return this->views.size(); } + auto cache_path(std::string_view uri, const std::filesystem::path &path) + -> void; struct Entry { - std::optional cache_path; std::filesystem::path path; - sourcemeta::core::JSON::String dialect; // This is the collection name plus the final schema path component std::filesystem::path relative_path; - sourcemeta::core::JSON::String original_identifier; - std::reference_wrapper collection; + std::filesystem::file_time_type mtime; + bool evaluate{true}; + std::optional cache_path{}; + sourcemeta::core::JSON::String dialect{}; + sourcemeta::core::JSON::String original_identifier{}; + const Configuration::Collection *collection{nullptr}; }; + using Views = std::unordered_map; + + [[nodiscard]] auto begin() const -> auto { return this->views.begin(); } + [[nodiscard]] auto end() const -> auto { return this->views.end(); } + [[nodiscard]] auto size() const -> auto { return this->views.size(); } + [[nodiscard]] auto data() const -> const Views & { return this->views; } + + [[nodiscard]] auto entry(std::string_view identifier) const -> const Entry &; + private: - std::unordered_map views; + Views views; std::shared_mutex mutex; }; diff --git a/src/resolver/resolver.cc b/src/resolver/resolver.cc index afe0ffa45..203724699 100644 --- a/src/resolver/resolver.cc +++ b/src/resolver/resolver.cc @@ -188,7 +188,7 @@ auto Resolver::operator()( // This is safe, as at this point we have validated all schemas // against their meta-schemas assert(maybe_ref->is_string()); - normalise_ref(result->second.collection.get(), entry.second.base, + normalise_ref(*result->second.collection, entry.second.base, subschema, "$ref", maybe_ref->to_string()); } @@ -200,7 +200,7 @@ auto Resolver::operator()( // This is safe, as at this point we have validated all schemas // against their meta-schemas assert(maybe_dynamic_ref->is_string()); - normalise_ref(result->second.collection.get(), entry.second.base, + normalise_ref(*result->second.collection, entry.second.base, subschema, "$dynamicRef", maybe_dynamic_ref->to_string()); } @@ -308,17 +308,27 @@ auto Resolver::add(const sourcemeta::core::JSON::String &server_url, ///////////////////////////////////////////////////////////////////////////// // (5) Safely one the schema entry in the resolver ///////////////////////////////////////////////////////////////////////////// + + // TODO: Computing this for every schema seems like a waste. + // Can we compute it properly at the collection level once? + const auto evaluate{ + !collection.extra.defines("x-sourcemeta-one:evaluate") || + !collection.extra.at("x-sourcemeta-one:evaluate").is_boolean() || + collection.extra.at("x-sourcemeta-one:evaluate").to_boolean()}; + std::unique_lock lock{this->mutex}; auto result{this->views.emplace( new_identifier, - Entry{.cache_path = std::nullopt, - .path = path, - .dialect = std::move(current_dialect), + Entry{.path = path, .relative_path = sourcemeta::core::URI{new_identifier} .relative_to(server_url) .recompose(), + .mtime = std::filesystem::last_write_time(path), + .evaluate = evaluate, + .cache_path = std::nullopt, + .dialect = std::move(current_dialect), .original_identifier = identifier, - .collection = collection})}; + .collection = &collection})}; lock.unlock(); if (!result.second && result.first->second.path != path) { throw sourcemeta::core::SchemaFrameError( @@ -327,12 +337,21 @@ auto Resolver::add(const sourcemeta::core::JSON::String &server_url, return {result.first->second.original_identifier, result.first->first}; } -auto Resolver::cache_path(const sourcemeta::core::JSON::String &uri, +auto Resolver::entry(const std::string_view identifier) const -> const Entry & { + const auto result{this->views.find(std::string{identifier})}; + assert(result != this->views.cend()); + assert(!result->second.dialect.empty()); + assert(!result->second.original_identifier.empty()); + assert(result->second.collection != nullptr); + return result->second; +} + +auto Resolver::cache_path(const std::string_view uri, const std::filesystem::path &path) -> void { assert(std::filesystem::exists(path)); // As we are modifying the actual map std::unique_lock lock{this->mutex}; - auto entry{this->views.find(uri)}; + auto entry{this->views.find(std::string{uri})}; assert(entry != this->views.cend()); assert(!entry->second.cache_path.has_value()); entry->second.cache_path = path; diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt index 883c10849..3a8399e40 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -9,6 +9,7 @@ target_link_libraries(sourcemeta_one_web PUBLIC sourcemeta::one::build) target_link_libraries(sourcemeta_one_web PRIVATE sourcemeta::core::json) target_link_libraries(sourcemeta_one_web PRIVATE sourcemeta::core::html) target_link_libraries(sourcemeta_one_web PUBLIC sourcemeta::one::configuration) +target_link_libraries(sourcemeta_one_web PUBLIC sourcemeta::one::resolver) target_link_libraries(sourcemeta_one_web PRIVATE sourcemeta::one::shared) sourcemeta_esbuild_bundle( diff --git a/src/web/include/sourcemeta/one/web.h b/src/web/include/sourcemeta/one/web.h index 72d42dd51..7243e5bb0 100644 --- a/src/web/include/sourcemeta/one/web.h +++ b/src/web/include/sourcemeta/one/web.h @@ -3,41 +3,40 @@ #include #include - -#include // std::filesystem +#include namespace sourcemeta::one { struct GENERATE_WEB_DIRECTORY { - using Context = Configuration; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &, - const Context &configuration) -> void; + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &configuration, + const sourcemeta::core::JSON &) -> void; }; struct GENERATE_WEB_NOT_FOUND { - using Context = Configuration; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &, - const sourcemeta::one::Build::DynamicCallback &, - const Context &configuration) -> void; + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &configuration, + const sourcemeta::core::JSON &) -> void; }; struct GENERATE_WEB_INDEX { - using Context = Configuration; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &, - const Context &configuration) -> void; + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &configuration, + const sourcemeta::core::JSON &) -> void; }; struct GENERATE_WEB_SCHEMA { - using Context = Configuration; - static auto handler(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &, - const Context &configuration) -> void; + static auto handler(const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, + sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &configuration, + const sourcemeta::core::JSON &) -> void; }; } // namespace sourcemeta::one diff --git a/src/web/pages/directory.cc b/src/web/pages/directory.cc index fe7443aed..6bdc670ea 100644 --- a/src/web/pages/directory.cc +++ b/src/web/pages/directory.cc @@ -12,13 +12,13 @@ namespace sourcemeta::one { auto GENERATE_WEB_DIRECTORY::handler( - const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &, - const Context &configuration) -> void { + const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &configuration, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; - const auto directory{read_json(dependencies.front().get())}; + const auto directory{read_json(action.dependencies.front())}; const auto &canonical{directory.at("url").to_string()}; const auto &title{directory.defines("title") ? directory.at("title").to_string() @@ -36,9 +36,9 @@ auto GENERATE_WEB_DIRECTORY::handler( html::make_file_manager(directory)); const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); - write_text(destination, html_content.str(), "text/html", Encoding::GZIP, - sourcemeta::core::JSON{nullptr}, + std::filesystem::create_directories(action.destination.parent_path()); + write_text(action.destination, html_content.str(), "text/html", + Encoding::GZIP, sourcemeta::core::JSON{nullptr}, std::chrono::duration_cast( timestamp_end - timestamp_start)); } diff --git a/src/web/pages/index.cc b/src/web/pages/index.cc index b50ef52e8..a929339be 100644 --- a/src/web/pages/index.cc +++ b/src/web/pages/index.cc @@ -29,13 +29,13 @@ auto make_hero(const sourcemeta::one::Configuration &configuration) namespace sourcemeta::one { auto GENERATE_WEB_INDEX::handler( - const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &, - const Context &configuration) -> void { + const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &configuration, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; - const auto directory{read_json(dependencies.front().get())}; + const auto directory{read_json(action.dependencies.front())}; const auto &canonical{directory.at("url").to_string()}; const auto title{configuration.html->name + " Schemas"}; const auto &description{configuration.html->description}; @@ -46,9 +46,9 @@ auto GENERATE_WEB_INDEX::handler( html::make_file_manager(directory)); const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); - write_text(destination, html_content.str(), "text/html", Encoding::GZIP, - sourcemeta::core::JSON{nullptr}, + std::filesystem::create_directories(action.destination.parent_path()); + write_text(action.destination, html_content.str(), "text/html", + Encoding::GZIP, sourcemeta::core::JSON{nullptr}, std::chrono::duration_cast( timestamp_end - timestamp_start)); } diff --git a/src/web/pages/not_found.cc b/src/web/pages/not_found.cc index 4ec8b714c..953542d15 100644 --- a/src/web/pages/not_found.cc +++ b/src/web/pages/not_found.cc @@ -13,10 +13,10 @@ namespace sourcemeta::one { auto GENERATE_WEB_NOT_FOUND::handler( - const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &, - const sourcemeta::one::Build::DynamicCallback &, - const Context &configuration) -> void { + const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &configuration, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; const auto &canonical{configuration.url}; @@ -35,9 +35,9 @@ auto GENERATE_WEB_NOT_FOUND::handler( html::a({{"href", "/"}}, "Get back to the home page"))); const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); - write_text(destination, html_content.str(), "text/html", Encoding::GZIP, - sourcemeta::core::JSON{nullptr}, + std::filesystem::create_directories(action.destination.parent_path()); + write_text(action.destination, html_content.str(), "text/html", + Encoding::GZIP, sourcemeta::core::JSON{nullptr}, std::chrono::duration_cast( timestamp_end - timestamp_start)); } diff --git a/src/web/pages/schema.cc b/src/web/pages/schema.cc index 3e1b15f4f..fa210a71d 100644 --- a/src/web/pages/schema.cc +++ b/src/web/pages/schema.cc @@ -17,13 +17,13 @@ namespace sourcemeta::one { auto GENERATE_WEB_SCHEMA::handler( - const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &, - const Context &configuration) -> void { + const sourcemeta::one::BuildPlan::Action &action, + const sourcemeta::one::BuildDynamicCallback &, sourcemeta::one::Resolver &, + const sourcemeta::one::Configuration &configuration, + const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; - const auto meta{read_json(dependencies.front().get())}; + const auto meta{read_json(action.dependencies.front())}; const auto &canonical{meta.at("identifier").to_string()}; const auto &title{meta.defines("title") ? meta.at("title").to_string() : meta.at("path").to_string()}; @@ -196,11 +196,11 @@ auto GENERATE_WEB_SCHEMA::handler( {"data-sourcemeta-ui-editor-language", "json"}}, "Loading schema...")); - const auto dependencies_json{read_json(dependencies.at(1).get())}; - const auto health{read_json(dependencies.at(2).get())}; + const auto dependencies_json{read_json(action.dependencies.at(1))}; + const auto health{read_json(action.dependencies.at(2))}; assert(health.is_object()); assert(health.defines("errors")); - const auto dependents_json{read_json(dependencies.at(3).get())}; + const auto dependents_json{read_json(action.dependencies.at(3))}; assert(dependents_json.is_array()); // Collect unique dependent schemas, preferring direct over indirect. @@ -490,9 +490,9 @@ auto GENERATE_WEB_SCHEMA::handler( container_children)); const auto timestamp_end{std::chrono::steady_clock::now()}; - std::filesystem::create_directories(destination.parent_path()); - write_text(destination, html_content.str(), "text/html", Encoding::GZIP, - sourcemeta::core::JSON{nullptr}, + std::filesystem::create_directories(action.destination.parent_path()); + write_text(action.destination, html_content.str(), "text/html", + Encoding::GZIP, sourcemeta::core::JSON{nullptr}, std::chrono::duration_cast( timestamp_end - timestamp_start)); } diff --git a/test/cli/index/common/bundle-ref-no-fragment.sh b/test/cli/index/common/bundle-ref-no-fragment.sh index 59d0176c7..29318a5bb 100755 --- a/test/cli/index/common/bundle-ref-no-fragment.sh +++ b/test/cli/index/common/bundle-ref-no-fragment.sh @@ -33,7 +33,7 @@ cat << 'EOF' > "$TMP/schemas/test.json" } EOF -"$1" --skip-banner "$TMP/one.json" "$TMP/output" 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" +"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" test "$CODE" = "1" || exit 1 # Remove thread information @@ -49,8 +49,11 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/test.json (#1) (100%) Resolving: test.json -(100%) Ingesting: https://sourcemeta.com/example/schemas/test -(100%) Analysing: https://sourcemeta.com/example/schemas/test +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 12%) Producing: explorer/%/404.metapack +( 16%) Producing: schemas/example/schemas/test/%/schema.metapack +( 20%) Producing: schemas/example/schemas/test/%/dependencies.metapack error: Could not resolve schema reference https://sourcemeta.com/example/schemas/test#foo at schema location "/allOf/0/\$ref" diff --git a/test/cli/index/common/comment-removed-on-rebuild.sh b/test/cli/index/common/comment-removed-on-rebuild.sh index 6cf850e52..f68fb620a 100755 --- a/test/cli/index/common/comment-removed-on-rebuild.sh +++ b/test/cli/index/common/comment-removed-on-rebuild.sh @@ -30,12 +30,15 @@ remove_threads_information "$TMP/output.txt" cat << EOF > "$TMP/expected.txt" Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(100%) Reviewing: schemas -( 0%) Producing: explorer -(100%) Producing: . -(100%) Rendering: . +( 11%) Producing: comment.json +( 22%) Producing: configuration.json +( 33%) Producing: version.json +( 44%) Producing: dependency-tree.metapack +( 55%) Producing: explorer/%/404.metapack +( 66%) Producing: explorer/%/directory.metapack +( 77%) Producing: explorer/%/search.metapack +( 88%) Producing: explorer/%/directory-html.metapack +(100%) Producing: routes.bin EOF diff "$TMP/output.txt" "$TMP/expected.txt" @@ -51,16 +54,7 @@ remove_threads_information "$TMP/output.txt" cat << EOF > "$TMP/expected.txt" Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(100%) Reviewing: schemas -( 0%) Producing: explorer -(100%) Producing: . -(skip) Producing: . [directory] -(100%) Rendering: . -(skip) Rendering: . [index] -(skip) Rendering: . [not-found] -(skip) Producing: routes.bin [routes] +(100%) Disposing: comment.json EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/common/comment-updated-on-rebuild.sh b/test/cli/index/common/comment-updated-on-rebuild.sh index dbe41f0d5..712107973 100755 --- a/test/cli/index/common/comment-updated-on-rebuild.sh +++ b/test/cli/index/common/comment-updated-on-rebuild.sh @@ -30,12 +30,15 @@ remove_threads_information "$TMP/output.txt" cat << EOF > "$TMP/expected.txt" Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(100%) Reviewing: schemas -( 0%) Producing: explorer -(100%) Producing: . -(100%) Rendering: . +( 11%) Producing: comment.json +( 22%) Producing: configuration.json +( 33%) Producing: version.json +( 44%) Producing: dependency-tree.metapack +( 55%) Producing: explorer/%/404.metapack +( 66%) Producing: explorer/%/directory.metapack +( 77%) Producing: explorer/%/search.metapack +( 88%) Producing: explorer/%/directory-html.metapack +(100%) Producing: routes.bin EOF diff "$TMP/output.txt" "$TMP/expected.txt" @@ -51,16 +54,7 @@ remove_threads_information "$TMP/output.txt" cat << EOF > "$TMP/expected.txt" Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(100%) Reviewing: schemas -( 0%) Producing: explorer -(100%) Producing: . -(skip) Producing: . [directory] -(100%) Rendering: . -(skip) Rendering: . [index] -(skip) Rendering: . [not-found] -(skip) Producing: routes.bin [routes] +(100%) Producing: comment.json EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/common/external-reference.sh b/test/cli/index/common/external-reference.sh index 9df81b33e..6779d9d23 100755 --- a/test/cli/index/common/external-reference.sh +++ b/test/cli/index/common/external-reference.sh @@ -33,7 +33,7 @@ cat << 'EOF' > "$TMP/schemas/test.json" } EOF -"$1" --skip-banner "$TMP/one.json" "$TMP/output" 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" +"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" test "$CODE" = "1" || exit 1 # Remove thread information @@ -49,8 +49,11 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/test.json (#1) (100%) Resolving: test.json -(100%) Ingesting: https://sourcemeta.com/example/schemas/test -(100%) Analysing: https://sourcemeta.com/example/schemas/test +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 12%) Producing: explorer/%/404.metapack +( 16%) Producing: schemas/example/schemas/test/%/schema.metapack +( 20%) Producing: schemas/example/schemas/test/%/dependencies.metapack error: Could not resolve the reference to an external schema https://sourcemeta.com/external diff --git a/test/cli/index/common/extra-files-on-rebuild.sh b/test/cli/index/common/extra-files-on-rebuild.sh index f4350cb90..55b4bf45b 100755 --- a/test/cli/index/common/extra-files-on-rebuild.sh +++ b/test/cli/index/common/extra-files-on-rebuild.sh @@ -32,7 +32,50 @@ cat << 'EOF' > "$TMP/schemas/test.json" } EOF -"$1" "$TMP/one.json" "$TMP/output" +remove_threads_information() { + expr='s/ \[[^]]*[^a-z-][^]]*\]//g' + if [ "$(uname -s)" = "Darwin" ]; then + sed -i '' "$expr" "$1" + else + sed -i "$expr" "$1" + fi +} + +"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" +remove_threads_information "$TMP/output.txt" + +cat << EOF > "$TMP/expected.txt" +Writing output to: $(realpath "$TMP")/output +Using configuration: $(realpath "$TMP")/one.json +Detecting: $(realpath "$TMP")/schemas/test.json (#1) +(100%) Resolving: test.json +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 12%) Producing: explorer/%/404.metapack +( 16%) Producing: schemas/example/schemas/old/%/schema.metapack +( 20%) Producing: schemas/example/schemas/old/%/dependencies.metapack +( 24%) Producing: schemas/example/schemas/old/%/locations.metapack +( 28%) Producing: schemas/example/schemas/old/%/positions.metapack +( 32%) Producing: schemas/example/schemas/old/%/stats.metapack +( 36%) Producing: dependency-tree.metapack +( 40%) Producing: schemas/example/schemas/old/%/bundle.metapack +( 44%) Producing: schemas/example/schemas/old/%/health.metapack +( 48%) Producing: explorer/example/schemas/old/%/schema.metapack +( 52%) Producing: schemas/example/schemas/old/%/blaze-exhaustive.metapack +( 56%) Producing: schemas/example/schemas/old/%/blaze-fast.metapack +( 60%) Producing: schemas/example/schemas/old/%/dependents.metapack +( 64%) Producing: schemas/example/schemas/old/%/editor.metapack +( 68%) Producing: explorer/%/search.metapack +( 72%) Producing: explorer/example/schemas/%/directory.metapack +( 76%) Producing: explorer/example/schemas/old/%/schema-html.metapack +( 80%) Producing: explorer/example/%/directory.metapack +( 84%) Producing: explorer/example/schemas/%/directory-html.metapack +( 88%) Producing: explorer/%/directory.metapack +( 92%) Producing: explorer/example/%/directory-html.metapack +( 96%) Producing: explorer/%/directory-html.metapack +(100%) Producing: routes.bin +EOF +diff "$TMP/output.txt" "$TMP/expected.txt" exists() { [ -f "$1" ] || (echo "File MUST exist: $1" 1>&2 && exit 1) @@ -55,7 +98,39 @@ cat << 'EOF' > "$TMP/schemas/test.json" } EOF -"$1" "$TMP/one.json" "$TMP/output" +"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" +remove_threads_information "$TMP/output.txt" + +cat << EOF > "$TMP/expected.txt" +Writing output to: $(realpath "$TMP")/output +Using configuration: $(realpath "$TMP")/one.json +Detecting: $(realpath "$TMP")/schemas/test.json (#1) +(100%) Resolving: test.json +( 4%) Producing: schemas/example/schemas/new/%/schema.metapack +( 8%) Producing: schemas/example/schemas/new/%/dependencies.metapack +( 13%) Producing: schemas/example/schemas/new/%/locations.metapack +( 17%) Producing: schemas/example/schemas/new/%/positions.metapack +( 21%) Producing: schemas/example/schemas/new/%/stats.metapack +( 26%) Producing: dependency-tree.metapack +( 30%) Producing: schemas/example/schemas/new/%/bundle.metapack +( 34%) Producing: schemas/example/schemas/new/%/health.metapack +( 39%) Producing: explorer/example/schemas/new/%/schema.metapack +( 43%) Producing: schemas/example/schemas/new/%/blaze-exhaustive.metapack +( 47%) Producing: schemas/example/schemas/new/%/blaze-fast.metapack +( 52%) Producing: schemas/example/schemas/new/%/dependents.metapack +( 56%) Producing: schemas/example/schemas/new/%/editor.metapack +( 60%) Producing: explorer/%/search.metapack +( 65%) Producing: explorer/example/schemas/%/directory.metapack +( 69%) Producing: explorer/example/schemas/new/%/schema-html.metapack +( 73%) Producing: explorer/example/%/directory.metapack +( 78%) Producing: explorer/example/schemas/%/directory-html.metapack +( 82%) Producing: explorer/%/directory.metapack +( 86%) Producing: explorer/example/%/directory-html.metapack +( 91%) Producing: explorer/%/directory-html.metapack +( 95%) Disposing: explorer/example/schemas/old +(100%) Disposing: schemas/example/schemas/old +EOF +diff "$TMP/output.txt" "$TMP/expected.txt" exists "$TMP/output/explorer/example/schemas/new/%/schema.metapack" exists "$TMP/output/schemas/example/schemas/new/%/bundle.metapack" diff --git a/test/cli/index/common/invalid-schema-top-level-ref-draft7.sh b/test/cli/index/common/invalid-schema-top-level-ref-draft7.sh index 988f62342..092572518 100755 --- a/test/cli/index/common/invalid-schema-top-level-ref-draft7.sh +++ b/test/cli/index/common/invalid-schema-top-level-ref-draft7.sh @@ -34,7 +34,7 @@ cat << 'EOF' > "$TMP/schemas/test.json" } EOF -"$1" --skip-banner "$TMP/one.json" "$TMP/output" 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" +"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" test "$CODE" = "1" || exit 1 # Remove thread information @@ -50,7 +50,10 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/test.json (#1) (100%) Resolving: test.json -(100%) Ingesting: https://sourcemeta.com/example/schemas/test +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 12%) Producing: explorer/%/404.metapack +( 16%) Producing: schemas/example/schemas/test/%/schema.metapack error: A schema with a top-level \`\$ref\` in JSON Schema Draft 7 and older dialects ignores every sibling keywords (like identifiers and meta-schema declarations) and therefore many operations, like bundling, are not possible without undefined behavior https://sourcemeta.com/example/schemas/test EOF diff --git a/test/cli/index/common/invalid-schema.sh b/test/cli/index/common/invalid-schema.sh index 3c394ceb5..a55666b51 100755 --- a/test/cli/index/common/invalid-schema.sh +++ b/test/cli/index/common/invalid-schema.sh @@ -33,7 +33,7 @@ cat << 'EOF' > "$TMP/schemas/test.json" } EOF -"$1" --skip-banner "$TMP/one.json" "$TMP/output" 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" +"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" test "$CODE" = "1" || exit 1 # Remove thread information @@ -49,7 +49,10 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/test.json (#1) (100%) Resolving: test.json -(100%) Ingesting: https://sourcemeta.com/example/schemas/test +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 12%) Producing: explorer/%/404.metapack +( 16%) Producing: schemas/example/schemas/test/%/schema.metapack error: The schema does not adhere to its metaschema The integer value 1 was expected to equal one of the following values: "array", "boolean", "integer", "null", "number", "object", and "string" at instance location "/type" diff --git a/test/cli/index/common/rebuild-cache.sh b/test/cli/index/common/rebuild-cache.sh index a09b36ea5..d00f30ffc 100755 --- a/test/cli/index/common/rebuild-cache.sh +++ b/test/cli/index/common/rebuild-cache.sh @@ -48,20 +48,31 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/foo.json (#1) (100%) Resolving: foo.json -(100%) Ingesting: https://sourcemeta.com/example/schemas/foo -(100%) Analysing: https://sourcemeta.com/example/schemas/foo -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(100%) Reviewing: schemas -(100%) Reworking: https://sourcemeta.com/example/schemas/foo -( 0%) Producing: explorer -( 33%) Producing: example/schemas -( 66%) Producing: example -(100%) Producing: . -( 25%) Rendering: example/schemas -( 50%) Rendering: example -( 75%) Rendering: . -(100%) Rendering: example/schemas/foo +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 12%) Producing: explorer/%/404.metapack +( 16%) Producing: schemas/example/schemas/foo/%/schema.metapack +( 20%) Producing: schemas/example/schemas/foo/%/dependencies.metapack +( 24%) Producing: schemas/example/schemas/foo/%/locations.metapack +( 28%) Producing: schemas/example/schemas/foo/%/positions.metapack +( 32%) Producing: schemas/example/schemas/foo/%/stats.metapack +( 36%) Producing: dependency-tree.metapack +( 40%) Producing: schemas/example/schemas/foo/%/bundle.metapack +( 44%) Producing: schemas/example/schemas/foo/%/health.metapack +( 48%) Producing: explorer/example/schemas/foo/%/schema.metapack +( 52%) Producing: schemas/example/schemas/foo/%/blaze-exhaustive.metapack +( 56%) Producing: schemas/example/schemas/foo/%/blaze-fast.metapack +( 60%) Producing: schemas/example/schemas/foo/%/dependents.metapack +( 64%) Producing: schemas/example/schemas/foo/%/editor.metapack +( 68%) Producing: explorer/%/search.metapack +( 72%) Producing: explorer/example/schemas/%/directory.metapack +( 76%) Producing: explorer/example/schemas/foo/%/schema-html.metapack +( 80%) Producing: explorer/example/%/directory.metapack +( 84%) Producing: explorer/example/schemas/%/directory-html.metapack +( 88%) Producing: explorer/%/directory.metapack +( 92%) Producing: explorer/example/%/directory-html.metapack +( 96%) Producing: explorer/%/directory-html.metapack +(100%) Producing: routes.bin EOF diff "$TMP/output.txt" "$TMP/expected.txt" @@ -74,41 +85,6 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/foo.json (#1) (100%) Resolving: foo.json -(100%) Ingesting: https://sourcemeta.com/example/schemas/foo -(skip) Ingesting: https://sourcemeta.com/example/schemas/foo [materialise] -(100%) Analysing: https://sourcemeta.com/example/schemas/foo -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [positions] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [locations] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [dependencies] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [stats] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [health] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [bundle] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [editor] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [blaze-exhaustive] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [blaze-fast] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [metadata] -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(skip) Reviewing: schemas [dependencies] -(100%) Reviewing: schemas -( 0%) Producing: explorer -(skip) Producing: explorer [search] -( 33%) Producing: example/schemas -(skip) Producing: example/schemas [directory] -( 66%) Producing: example -(skip) Producing: example [directory] -(100%) Producing: . -(skip) Producing: . [directory] -( 25%) Rendering: example/schemas -(skip) Rendering: example/schemas [directory] -( 50%) Rendering: example -(skip) Rendering: example [directory] -( 75%) Rendering: . -(skip) Rendering: . [index] -(skip) Rendering: . [not-found] -(100%) Rendering: example/schemas/foo -(skip) Rendering: example/schemas/foo [schema] -(skip) Producing: routes.bin [routes] EOF diff "$TMP/output.txt" "$TMP/expected.txt" @@ -127,21 +103,27 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/foo.json (#1) (100%) Resolving: foo.json -(100%) Ingesting: https://sourcemeta.com/example/schemas/foo -(100%) Analysing: https://sourcemeta.com/example/schemas/foo -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(100%) Reviewing: schemas -( 0%) Producing: explorer -( 33%) Producing: example/schemas -( 66%) Producing: example -(100%) Producing: . -( 25%) Rendering: example/schemas -( 50%) Rendering: example -( 75%) Rendering: . -(skip) Rendering: . [not-found] -(100%) Rendering: example/schemas/foo -(skip) Producing: routes.bin [routes] +( 4%) Producing: schemas/example/schemas/foo/%/schema.metapack +( 9%) Producing: schemas/example/schemas/foo/%/dependencies.metapack +( 14%) Producing: schemas/example/schemas/foo/%/locations.metapack +( 19%) Producing: schemas/example/schemas/foo/%/positions.metapack +( 23%) Producing: schemas/example/schemas/foo/%/stats.metapack +( 28%) Producing: dependency-tree.metapack +( 33%) Producing: schemas/example/schemas/foo/%/bundle.metapack +( 38%) Producing: schemas/example/schemas/foo/%/health.metapack +( 42%) Producing: explorer/example/schemas/foo/%/schema.metapack +( 47%) Producing: schemas/example/schemas/foo/%/blaze-exhaustive.metapack +( 52%) Producing: schemas/example/schemas/foo/%/blaze-fast.metapack +( 57%) Producing: schemas/example/schemas/foo/%/dependents.metapack +( 61%) Producing: schemas/example/schemas/foo/%/editor.metapack +( 66%) Producing: explorer/%/search.metapack +( 71%) Producing: explorer/example/schemas/%/directory.metapack +( 76%) Producing: explorer/example/schemas/foo/%/schema-html.metapack +( 80%) Producing: explorer/example/%/directory.metapack +( 85%) Producing: explorer/example/schemas/%/directory-html.metapack +( 90%) Producing: explorer/%/directory.metapack +( 95%) Producing: explorer/example/%/directory-html.metapack +(100%) Producing: explorer/%/directory-html.metapack EOF diff "$TMP/output.txt" "$TMP/expected.txt" @@ -154,40 +136,5 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/foo.json (#1) (100%) Resolving: foo.json -(100%) Ingesting: https://sourcemeta.com/example/schemas/foo -(skip) Ingesting: https://sourcemeta.com/example/schemas/foo [materialise] -(100%) Analysing: https://sourcemeta.com/example/schemas/foo -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [positions] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [locations] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [dependencies] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [stats] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [health] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [bundle] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [editor] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [blaze-exhaustive] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [blaze-fast] -(skip) Analysing: https://sourcemeta.com/example/schemas/foo [metadata] -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(skip) Reviewing: schemas [dependencies] -(100%) Reviewing: schemas -( 0%) Producing: explorer -(skip) Producing: explorer [search] -( 33%) Producing: example/schemas -(skip) Producing: example/schemas [directory] -( 66%) Producing: example -(skip) Producing: example [directory] -(100%) Producing: . -(skip) Producing: . [directory] -( 25%) Rendering: example/schemas -(skip) Rendering: example/schemas [directory] -( 50%) Rendering: example -(skip) Rendering: example [directory] -( 75%) Rendering: . -(skip) Rendering: . [index] -(skip) Rendering: . [not-found] -(100%) Rendering: example/schemas/foo -(skip) Rendering: example/schemas/foo [schema] -(skip) Producing: routes.bin [routes] EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/common/rebuild-deleted-deps.sh b/test/cli/index/common/rebuild-deleted-deps.sh index 0776cd9ec..b186ddcfd 100755 --- a/test/cli/index/common/rebuild-deleted-deps.sh +++ b/test/cli/index/common/rebuild-deleted-deps.sh @@ -32,12 +32,56 @@ cat << 'EOF' > "$TMP/schemas/a.json" } EOF +remove_threads_information() { + expr='s/ \[[^]]*[^a-z-][^]]*\]//g' + if [ "$(uname -s)" = "Darwin" ]; then + sed -i '' "$expr" "$1" + else + sed -i "$expr" "$1" + fi +} + "$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 > /dev/null 2>&1 test -f "$TMP/output/state.bin" rm "$TMP/output/state.bin" test ! -f "$TMP/output/state.bin" -"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 > /dev/null 2>&1 +"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" +remove_threads_information "$TMP/output.txt" + +cat << EOF > "$TMP/expected.txt" +Writing output to: $(realpath "$TMP")/output +Using configuration: $(realpath "$TMP")/one.json +Detecting: $(realpath "$TMP")/schemas/a.json (#1) +(100%) Resolving: a.json +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 12%) Producing: explorer/%/404.metapack +( 16%) Producing: schemas/example/schemas/a/%/schema.metapack +( 20%) Producing: schemas/example/schemas/a/%/dependencies.metapack +( 24%) Producing: schemas/example/schemas/a/%/locations.metapack +( 28%) Producing: schemas/example/schemas/a/%/positions.metapack +( 32%) Producing: schemas/example/schemas/a/%/stats.metapack +( 36%) Producing: dependency-tree.metapack +( 40%) Producing: schemas/example/schemas/a/%/bundle.metapack +( 44%) Producing: schemas/example/schemas/a/%/health.metapack +( 48%) Producing: explorer/example/schemas/a/%/schema.metapack +( 52%) Producing: schemas/example/schemas/a/%/blaze-exhaustive.metapack +( 56%) Producing: schemas/example/schemas/a/%/blaze-fast.metapack +( 60%) Producing: schemas/example/schemas/a/%/dependents.metapack +( 64%) Producing: schemas/example/schemas/a/%/editor.metapack +( 68%) Producing: explorer/%/search.metapack +( 72%) Producing: explorer/example/schemas/%/directory.metapack +( 76%) Producing: explorer/example/schemas/a/%/schema-html.metapack +( 80%) Producing: explorer/example/%/directory.metapack +( 84%) Producing: explorer/example/schemas/%/directory-html.metapack +( 88%) Producing: explorer/%/directory.metapack +( 92%) Producing: explorer/example/%/directory-html.metapack +( 96%) Producing: explorer/%/directory-html.metapack +(100%) Producing: routes.bin +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" test -f "$TMP/output/state.bin" diff --git a/test/cli/index/common/rebuild-modify-cache.sh b/test/cli/index/common/rebuild-modify-cache.sh index fe1716ea5..f56b996ec 100755 --- a/test/cli/index/common/rebuild-modify-cache.sh +++ b/test/cli/index/common/rebuild-modify-cache.sh @@ -63,41 +63,6 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/a.json (#1) (100%) Resolving: a.json -(100%) Ingesting: https://sourcemeta.com/example/schemas/a -(skip) Ingesting: https://sourcemeta.com/example/schemas/a [materialise] -(100%) Analysing: https://sourcemeta.com/example/schemas/a -(skip) Analysing: https://sourcemeta.com/example/schemas/a [positions] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [locations] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [dependencies] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [stats] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [health] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [bundle] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [editor] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [blaze-exhaustive] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [blaze-fast] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [metadata] -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(skip) Reviewing: schemas [dependencies] -(100%) Reviewing: schemas -( 0%) Producing: explorer -(skip) Producing: explorer [search] -( 33%) Producing: example/schemas -(skip) Producing: example/schemas [directory] -( 66%) Producing: example -(skip) Producing: example [directory] -(100%) Producing: . -(skip) Producing: . [directory] -( 25%) Rendering: example/schemas -(skip) Rendering: example/schemas [directory] -( 50%) Rendering: example -(skip) Rendering: example [directory] -( 75%) Rendering: . -(skip) Rendering: . [index] -(skip) Rendering: . [not-found] -(100%) Rendering: example/schemas/a -(skip) Rendering: example/schemas/a [schema] -(skip) Producing: routes.bin [routes] EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/common/rebuild-nested-directories.sh b/test/cli/index/common/rebuild-nested-directories.sh index 3722ee44b..8cb27a570 100755 --- a/test/cli/index/common/rebuild-nested-directories.sh +++ b/test/cli/index/common/rebuild-nested-directories.sh @@ -83,8 +83,7 @@ remove_threads_information() { "$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 > /dev/null 2>&1 # Run 2: add a fifth schema to left/left-a -# Sibling directories (left-b, right, right-a, right-b) should be fully cached -# Only left/left-a, left, and root directory listings should rebuild +# Only the new schema's artifacts and affected directories should rebuild cat << 'EOF' > "$TMP/schemas-left-a/s5.json" { "$schema": "http://json-schema.org/draft-07/schema#", @@ -94,66 +93,76 @@ EOF "$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" remove_threads_information "$TMP/output.txt" -grep '(skip)' "$TMP/output.txt" | LC_ALL=C sort > "$TMP/actual_skips.txt" -cat << 'EOF' | LC_ALL=C sort > "$TMP/expected_skips.txt" -(skip) Ingesting: https://sourcemeta.com/left/left-a/s1 [materialise] -(skip) Ingesting: https://sourcemeta.com/left/left-b/s2 [materialise] -(skip) Ingesting: https://sourcemeta.com/right/right-a/s3 [materialise] -(skip) Ingesting: https://sourcemeta.com/right/right-b/s4 [materialise] -(skip) Analysing: https://sourcemeta.com/left/left-a/s1 [positions] -(skip) Analysing: https://sourcemeta.com/left/left-a/s1 [locations] -(skip) Analysing: https://sourcemeta.com/left/left-a/s1 [dependencies] -(skip) Analysing: https://sourcemeta.com/left/left-a/s1 [stats] -(skip) Analysing: https://sourcemeta.com/left/left-a/s1 [health] -(skip) Analysing: https://sourcemeta.com/left/left-a/s1 [bundle] -(skip) Analysing: https://sourcemeta.com/left/left-a/s1 [editor] -(skip) Analysing: https://sourcemeta.com/left/left-a/s1 [blaze-exhaustive] -(skip) Analysing: https://sourcemeta.com/left/left-a/s1 [blaze-fast] -(skip) Analysing: https://sourcemeta.com/left/left-a/s1 [metadata] -(skip) Analysing: https://sourcemeta.com/left/left-b/s2 [positions] -(skip) Analysing: https://sourcemeta.com/left/left-b/s2 [locations] -(skip) Analysing: https://sourcemeta.com/left/left-b/s2 [dependencies] -(skip) Analysing: https://sourcemeta.com/left/left-b/s2 [stats] -(skip) Analysing: https://sourcemeta.com/left/left-b/s2 [health] -(skip) Analysing: https://sourcemeta.com/left/left-b/s2 [bundle] -(skip) Analysing: https://sourcemeta.com/left/left-b/s2 [editor] -(skip) Analysing: https://sourcemeta.com/left/left-b/s2 [blaze-exhaustive] -(skip) Analysing: https://sourcemeta.com/left/left-b/s2 [blaze-fast] -(skip) Analysing: https://sourcemeta.com/left/left-b/s2 [metadata] -(skip) Analysing: https://sourcemeta.com/right/right-a/s3 [positions] -(skip) Analysing: https://sourcemeta.com/right/right-a/s3 [locations] -(skip) Analysing: https://sourcemeta.com/right/right-a/s3 [dependencies] -(skip) Analysing: https://sourcemeta.com/right/right-a/s3 [stats] -(skip) Analysing: https://sourcemeta.com/right/right-a/s3 [health] -(skip) Analysing: https://sourcemeta.com/right/right-a/s3 [bundle] -(skip) Analysing: https://sourcemeta.com/right/right-a/s3 [editor] -(skip) Analysing: https://sourcemeta.com/right/right-a/s3 [blaze-exhaustive] -(skip) Analysing: https://sourcemeta.com/right/right-a/s3 [blaze-fast] -(skip) Analysing: https://sourcemeta.com/right/right-a/s3 [metadata] -(skip) Analysing: https://sourcemeta.com/right/right-b/s4 [positions] -(skip) Analysing: https://sourcemeta.com/right/right-b/s4 [locations] -(skip) Analysing: https://sourcemeta.com/right/right-b/s4 [dependencies] -(skip) Analysing: https://sourcemeta.com/right/right-b/s4 [stats] -(skip) Analysing: https://sourcemeta.com/right/right-b/s4 [health] -(skip) Analysing: https://sourcemeta.com/right/right-b/s4 [bundle] -(skip) Analysing: https://sourcemeta.com/right/right-b/s4 [editor] -(skip) Analysing: https://sourcemeta.com/right/right-b/s4 [blaze-exhaustive] -(skip) Analysing: https://sourcemeta.com/right/right-b/s4 [blaze-fast] -(skip) Analysing: https://sourcemeta.com/right/right-b/s4 [metadata] -(skip) Producing: left/left-b [directory] -(skip) Producing: right [directory] -(skip) Producing: right/right-a [directory] -(skip) Producing: right/right-b [directory] -(skip) Producing: routes.bin [routes] -(skip) Rendering: . [not-found] -(skip) Rendering: left/left-a/s1 [schema] -(skip) Rendering: left/left-b [directory] -(skip) Rendering: left/left-b/s2 [schema] -(skip) Rendering: right [directory] -(skip) Rendering: right/right-a [directory] -(skip) Rendering: right/right-a/s3 [schema] -(skip) Rendering: right/right-b [directory] -(skip) Rendering: right/right-b/s4 [schema] +cat << EOF > "$TMP/expected_a.txt" +Writing output to: $(realpath "$TMP")/output +Using configuration: $(realpath "$TMP")/one.json +Detecting: $(realpath "$TMP")/schemas-left-b/s2.json (#1) +Detecting: $(realpath "$TMP")/schemas-left-a/s1.json (#2) +Detecting: $(realpath "$TMP")/schemas-left-a/s5.json (#3) +Detecting: $(realpath "$TMP")/schemas-right-b/s4.json (#4) +Detecting: $(realpath "$TMP")/schemas-right-a/s3.json (#5) +( 20%) Resolving: s2.json +( 40%) Resolving: s1.json +( 60%) Resolving: s5.json +( 80%) Resolving: s4.json +(100%) Resolving: s3.json +( 4%) Producing: schemas/left/left-a/s5/%/schema.metapack +( 9%) Producing: schemas/left/left-a/s5/%/dependencies.metapack +( 14%) Producing: schemas/left/left-a/s5/%/locations.metapack +( 19%) Producing: schemas/left/left-a/s5/%/positions.metapack +( 23%) Producing: schemas/left/left-a/s5/%/stats.metapack +( 28%) Producing: dependency-tree.metapack +( 33%) Producing: schemas/left/left-a/s5/%/bundle.metapack +( 38%) Producing: schemas/left/left-a/s5/%/health.metapack +( 42%) Producing: explorer/left/left-a/s5/%/schema.metapack +( 47%) Producing: schemas/left/left-a/s5/%/blaze-exhaustive.metapack +( 52%) Producing: schemas/left/left-a/s5/%/blaze-fast.metapack +( 57%) Producing: schemas/left/left-a/s5/%/dependents.metapack +( 61%) Producing: schemas/left/left-a/s5/%/editor.metapack +( 66%) Producing: explorer/%/search.metapack +( 71%) Producing: explorer/left/left-a/%/directory.metapack +( 76%) Producing: explorer/left/left-a/s5/%/schema-html.metapack +( 80%) Producing: explorer/left/%/directory.metapack +( 85%) Producing: explorer/left/left-a/%/directory-html.metapack +( 90%) Producing: explorer/%/directory.metapack +( 95%) Producing: explorer/left/%/directory-html.metapack +(100%) Producing: explorer/%/directory-html.metapack EOF -diff "$TMP/actual_skips.txt" "$TMP/expected_skips.txt" +cat << EOF > "$TMP/expected_b.txt" +Writing output to: $(realpath "$TMP")/output +Using configuration: $(realpath "$TMP")/one.json +Detecting: $(realpath "$TMP")/schemas-right-b/s4.json (#1) +Detecting: $(realpath "$TMP")/schemas-right-a/s3.json (#2) +Detecting: $(realpath "$TMP")/schemas-left-b/s2.json (#3) +Detecting: $(realpath "$TMP")/schemas-left-a/s1.json (#4) +Detecting: $(realpath "$TMP")/schemas-left-a/s5.json (#5) +( 20%) Resolving: s4.json +( 40%) Resolving: s3.json +( 60%) Resolving: s2.json +( 80%) Resolving: s1.json +(100%) Resolving: s5.json +( 4%) Producing: schemas/left/left-a/s5/%/schema.metapack +( 9%) Producing: schemas/left/left-a/s5/%/dependencies.metapack +( 14%) Producing: schemas/left/left-a/s5/%/locations.metapack +( 19%) Producing: schemas/left/left-a/s5/%/positions.metapack +( 23%) Producing: schemas/left/left-a/s5/%/stats.metapack +( 28%) Producing: dependency-tree.metapack +( 33%) Producing: schemas/left/left-a/s5/%/bundle.metapack +( 38%) Producing: schemas/left/left-a/s5/%/health.metapack +( 42%) Producing: explorer/left/left-a/s5/%/schema.metapack +( 47%) Producing: schemas/left/left-a/s5/%/blaze-exhaustive.metapack +( 52%) Producing: schemas/left/left-a/s5/%/blaze-fast.metapack +( 57%) Producing: schemas/left/left-a/s5/%/dependents.metapack +( 61%) Producing: schemas/left/left-a/s5/%/editor.metapack +( 66%) Producing: explorer/%/search.metapack +( 71%) Producing: explorer/left/left-a/%/directory.metapack +( 76%) Producing: explorer/left/left-a/s5/%/schema-html.metapack +( 80%) Producing: explorer/left/%/directory.metapack +( 85%) Producing: explorer/left/left-a/%/directory-html.metapack +( 90%) Producing: explorer/%/directory.metapack +( 95%) Producing: explorer/left/%/directory-html.metapack +(100%) Producing: explorer/%/directory-html.metapack +EOF + +diff "$TMP/output.txt" "$TMP/expected_a.txt" || diff "$TMP/output.txt" "$TMP/expected_b.txt" diff --git a/test/cli/index/common/rebuild-one-to-zero.sh b/test/cli/index/common/rebuild-one-to-zero.sh index 4128fef05..f486cea78 100755 --- a/test/cli/index/common/rebuild-one-to-zero.sh +++ b/test/cli/index/common/rebuild-one-to-zero.sh @@ -46,18 +46,29 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/test.json (#1) (100%) Resolving: test.json -(100%) Ingesting: https://sourcemeta.com/schemas/test -(100%) Analysing: https://sourcemeta.com/schemas/test -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(100%) Reviewing: schemas -(100%) Reworking: https://sourcemeta.com/schemas/test -( 0%) Producing: explorer -( 50%) Producing: schemas -(100%) Producing: . -( 33%) Rendering: schemas -( 66%) Rendering: . -(100%) Rendering: schemas/test +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 13%) Producing: explorer/%/404.metapack +( 17%) Producing: schemas/schemas/test/%/schema.metapack +( 21%) Producing: schemas/schemas/test/%/dependencies.metapack +( 26%) Producing: schemas/schemas/test/%/locations.metapack +( 30%) Producing: schemas/schemas/test/%/positions.metapack +( 34%) Producing: schemas/schemas/test/%/stats.metapack +( 39%) Producing: dependency-tree.metapack +( 43%) Producing: schemas/schemas/test/%/bundle.metapack +( 47%) Producing: schemas/schemas/test/%/health.metapack +( 52%) Producing: explorer/schemas/test/%/schema.metapack +( 56%) Producing: schemas/schemas/test/%/blaze-exhaustive.metapack +( 60%) Producing: schemas/schemas/test/%/blaze-fast.metapack +( 65%) Producing: schemas/schemas/test/%/dependents.metapack +( 69%) Producing: schemas/schemas/test/%/editor.metapack +( 73%) Producing: explorer/%/search.metapack +( 78%) Producing: explorer/schemas/%/directory.metapack +( 82%) Producing: explorer/schemas/test/%/schema-html.metapack +( 86%) Producing: explorer/%/directory.metapack +( 91%) Producing: explorer/schemas/%/directory-html.metapack +( 95%) Producing: explorer/%/directory-html.metapack +(100%) Producing: routes.bin EOF diff "$TMP/output.txt" "$TMP/expected.txt" @@ -112,14 +123,8 @@ remove_threads_information "$TMP/output.txt" cat << EOF > "$TMP/expected.txt" Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(100%) Reviewing: schemas -( 0%) Producing: explorer -(100%) Producing: . -(100%) Rendering: . -(skip) Rendering: . [not-found] -(skip) Producing: routes.bin [routes] +( 50%) Disposing: explorer/schemas +(100%) Disposing: schemas EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/common/rebuild-two-to-three-with-ref.sh b/test/cli/index/common/rebuild-two-to-three-with-ref.sh index 24963de10..0e2519912 100755 --- a/test/cli/index/common/rebuild-two-to-three-with-ref.sh +++ b/test/cli/index/common/rebuild-two-to-three-with-ref.sh @@ -52,6 +52,7 @@ remove_threads_information() { "$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 > /dev/null 2>&1 # Run 2: add a third schema that references schema a +# Only the new schema's artifacts and affected directories should rebuild cat << 'EOF' > "$TMP/schemas/c.json" { "$schema": "http://json-schema.org/draft-07/schema#", @@ -64,33 +65,36 @@ EOF "$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" remove_threads_information "$TMP/output.txt" -grep '(skip)' "$TMP/output.txt" | LC_ALL=C sort > "$TMP/actual_skips.txt" -cat << 'EOF' | LC_ALL=C sort > "$TMP/expected_skips.txt" -(skip) Ingesting: https://sourcemeta.com/example/schemas/a [materialise] -(skip) Ingesting: https://sourcemeta.com/example/schemas/b [materialise] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [positions] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [locations] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [dependencies] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [stats] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [health] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [bundle] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [editor] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [blaze-exhaustive] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [blaze-fast] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [metadata] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [positions] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [locations] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [dependencies] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [stats] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [health] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [bundle] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [editor] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [blaze-exhaustive] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [blaze-fast] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [metadata] -(skip) Rendering: . [not-found] -(skip) Rendering: example/schemas/b [schema] -(skip) Producing: routes.bin [routes] +cat << EOF > "$TMP/expected.txt" +Writing output to: $(realpath "$TMP")/output +Using configuration: $(realpath "$TMP")/one.json +Detecting: $(realpath "$TMP")/schemas/a.json (#1) +Detecting: $(realpath "$TMP")/schemas/c.json (#2) +Detecting: $(realpath "$TMP")/schemas/b.json (#3) +( 33%) Resolving: a.json +( 66%) Resolving: c.json +(100%) Resolving: b.json +( 4%) Producing: schemas/example/schemas/c/%/schema.metapack +( 9%) Producing: schemas/example/schemas/c/%/dependencies.metapack +( 14%) Producing: schemas/example/schemas/c/%/locations.metapack +( 19%) Producing: schemas/example/schemas/c/%/positions.metapack +( 23%) Producing: schemas/example/schemas/c/%/stats.metapack +( 28%) Producing: dependency-tree.metapack +( 33%) Producing: schemas/example/schemas/c/%/bundle.metapack +( 38%) Producing: schemas/example/schemas/c/%/health.metapack +( 42%) Producing: explorer/example/schemas/c/%/schema.metapack +( 47%) Producing: schemas/example/schemas/c/%/blaze-exhaustive.metapack +( 52%) Producing: schemas/example/schemas/c/%/blaze-fast.metapack +( 57%) Producing: schemas/example/schemas/c/%/dependents.metapack +( 61%) Producing: schemas/example/schemas/c/%/editor.metapack +( 66%) Producing: explorer/%/search.metapack +( 71%) Producing: explorer/example/schemas/%/directory.metapack +( 76%) Producing: explorer/example/schemas/c/%/schema-html.metapack +( 80%) Producing: explorer/example/%/directory.metapack +( 85%) Producing: explorer/example/schemas/%/directory-html.metapack +( 90%) Producing: explorer/%/directory.metapack +( 95%) Producing: explorer/example/%/directory-html.metapack +(100%) Producing: explorer/%/directory-html.metapack EOF -diff "$TMP/actual_skips.txt" "$TMP/expected_skips.txt" +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/common/rebuild-two-to-three.sh b/test/cli/index/common/rebuild-two-to-three.sh index d3430f8bc..01373e66c 100755 --- a/test/cli/index/common/rebuild-two-to-three.sh +++ b/test/cli/index/common/rebuild-two-to-three.sh @@ -52,8 +52,7 @@ remove_threads_information() { "$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 > /dev/null 2>&1 # Run 2: add a third schema -# The existing schemas' dependents and HTML should be cache hits -# because the new schema does not affect the dependency tree +# Only the new schema's artifacts and affected directories should rebuild cat << 'EOF' > "$TMP/schemas/c.json" { "$schema": "http://json-schema.org/draft-07/schema#", @@ -63,50 +62,39 @@ EOF "$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" remove_threads_information "$TMP/output.txt" -grep '(skip)' "$TMP/output.txt" | LC_ALL=C sort > "$TMP/actual_skips.txt" -cat << 'EOF' | LC_ALL=C sort > "$TMP/expected_skips.txt" -(skip) Ingesting: https://sourcemeta.com/example/schemas/a [materialise] -(skip) Ingesting: https://sourcemeta.com/example/schemas/b [materialise] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [positions] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [locations] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [dependencies] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [stats] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [health] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [bundle] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [editor] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [blaze-exhaustive] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [blaze-fast] -(skip) Analysing: https://sourcemeta.com/example/schemas/a [metadata] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [positions] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [locations] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [dependencies] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [stats] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [health] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [bundle] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [editor] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [blaze-exhaustive] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [blaze-fast] -(skip) Analysing: https://sourcemeta.com/example/schemas/b [metadata] -(skip) Rendering: . [not-found] -(skip) Rendering: example/schemas/a [schema] -(skip) Rendering: example/schemas/b [schema] -(skip) Producing: routes.bin [routes] +cat << EOF > "$TMP/expected.txt" +Writing output to: $(realpath "$TMP")/output +Using configuration: $(realpath "$TMP")/one.json +Detecting: $(realpath "$TMP")/schemas/a.json (#1) +Detecting: $(realpath "$TMP")/schemas/c.json (#2) +Detecting: $(realpath "$TMP")/schemas/b.json (#3) +( 33%) Resolving: a.json +( 66%) Resolving: c.json +(100%) Resolving: b.json +( 4%) Producing: schemas/example/schemas/c/%/schema.metapack +( 9%) Producing: schemas/example/schemas/c/%/dependencies.metapack +( 14%) Producing: schemas/example/schemas/c/%/locations.metapack +( 19%) Producing: schemas/example/schemas/c/%/positions.metapack +( 23%) Producing: schemas/example/schemas/c/%/stats.metapack +( 28%) Producing: dependency-tree.metapack +( 33%) Producing: schemas/example/schemas/c/%/bundle.metapack +( 38%) Producing: schemas/example/schemas/c/%/health.metapack +( 42%) Producing: explorer/example/schemas/c/%/schema.metapack +( 47%) Producing: schemas/example/schemas/c/%/blaze-exhaustive.metapack +( 52%) Producing: schemas/example/schemas/c/%/blaze-fast.metapack +( 57%) Producing: schemas/example/schemas/c/%/dependents.metapack +( 61%) Producing: schemas/example/schemas/c/%/editor.metapack +( 66%) Producing: explorer/%/search.metapack +( 71%) Producing: explorer/example/schemas/%/directory.metapack +( 76%) Producing: explorer/example/schemas/c/%/schema-html.metapack +( 80%) Producing: explorer/example/%/directory.metapack +( 85%) Producing: explorer/example/schemas/%/directory-html.metapack +( 90%) Producing: explorer/%/directory.metapack +( 95%) Producing: explorer/example/%/directory-html.metapack +(100%) Producing: explorer/%/directory-html.metapack EOF -diff "$TMP/actual_skips.txt" "$TMP/expected_skips.txt" - -# Run 3: re-index with no changes. All three schemas should be fully cached. -"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" -remove_threads_information "$TMP/output.txt" -grep '(skip) Ingesting:' "$TMP/output.txt" | sort > "$TMP/ingest_actual.txt" - -cat << 'EOF' | sort > "$TMP/ingest_expected.txt" -(skip) Ingesting: https://sourcemeta.com/example/schemas/a [materialise] -(skip) Ingesting: https://sourcemeta.com/example/schemas/b [materialise] -(skip) Ingesting: https://sourcemeta.com/example/schemas/c [materialise] -EOF - -diff "$TMP/ingest_actual.txt" "$TMP/ingest_expected.txt" +diff "$TMP/output.txt" "$TMP/expected.txt" cd "$TMP/output" find . -mindepth 1 | LC_ALL=C sort > "$TMP/manifest.txt" diff --git a/test/cli/index/common/rebuild-zero-to-one.sh b/test/cli/index/common/rebuild-zero-to-one.sh index e2733814e..6e0bdc9ff 100755 --- a/test/cli/index/common/rebuild-zero-to-one.sh +++ b/test/cli/index/common/rebuild-zero-to-one.sh @@ -36,12 +36,14 @@ remove_threads_information "$TMP/output.txt" cat << EOF > "$TMP/expected.txt" Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(100%) Reviewing: schemas -( 0%) Producing: explorer -(100%) Producing: . -(100%) Rendering: . +( 12%) Producing: configuration.json +( 25%) Producing: version.json +( 37%) Producing: dependency-tree.metapack +( 50%) Producing: explorer/%/404.metapack +( 62%) Producing: explorer/%/directory.metapack +( 75%) Producing: explorer/%/search.metapack +( 87%) Producing: explorer/%/directory-html.metapack +(100%) Producing: routes.bin EOF diff "$TMP/output.txt" "$TMP/expected.txt" @@ -82,20 +84,25 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/test.json (#1) (100%) Resolving: test.json -(100%) Ingesting: https://sourcemeta.com/schemas/test -(100%) Analysing: https://sourcemeta.com/schemas/test -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(100%) Reviewing: schemas -(100%) Reworking: https://sourcemeta.com/schemas/test -( 0%) Producing: explorer -( 50%) Producing: schemas -(100%) Producing: . -( 33%) Rendering: schemas -( 66%) Rendering: . -(skip) Rendering: . [not-found] -(100%) Rendering: schemas/test -(skip) Producing: routes.bin [routes] +( 5%) Producing: schemas/schemas/test/%/schema.metapack +( 10%) Producing: schemas/schemas/test/%/dependencies.metapack +( 15%) Producing: schemas/schemas/test/%/locations.metapack +( 21%) Producing: schemas/schemas/test/%/positions.metapack +( 26%) Producing: schemas/schemas/test/%/stats.metapack +( 31%) Producing: dependency-tree.metapack +( 36%) Producing: schemas/schemas/test/%/bundle.metapack +( 42%) Producing: schemas/schemas/test/%/health.metapack +( 47%) Producing: explorer/schemas/test/%/schema.metapack +( 52%) Producing: schemas/schemas/test/%/blaze-exhaustive.metapack +( 57%) Producing: schemas/schemas/test/%/blaze-fast.metapack +( 63%) Producing: schemas/schemas/test/%/dependents.metapack +( 68%) Producing: schemas/schemas/test/%/editor.metapack +( 73%) Producing: explorer/%/search.metapack +( 78%) Producing: explorer/schemas/%/directory.metapack +( 84%) Producing: explorer/schemas/test/%/schema-html.metapack +( 89%) Producing: explorer/%/directory.metapack +( 94%) Producing: explorer/schemas/%/directory-html.metapack +(100%) Producing: explorer/%/directory-html.metapack EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/common/verbose-long.sh b/test/cli/index/common/verbose-long.sh index 45c88b582..3ad225c3f 100755 --- a/test/cli/index/common/verbose-long.sh +++ b/test/cli/index/common/verbose-long.sh @@ -49,19 +49,30 @@ Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/foo.json (#1) (100%) Resolving: foo.json https://example.com/foo => https://sourcemeta.com/example/schemas/foo -(100%) Ingesting: https://sourcemeta.com/example/schemas/foo -(100%) Analysing: https://sourcemeta.com/example/schemas/foo -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(100%) Reviewing: schemas -(100%) Reworking: https://sourcemeta.com/example/schemas/foo -( 0%) Producing: explorer -( 33%) Producing: example/schemas -( 66%) Producing: example -(100%) Producing: . -( 25%) Rendering: example/schemas -( 50%) Rendering: example -( 75%) Rendering: . -(100%) Rendering: example/schemas/foo +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 12%) Producing: explorer/%/404.metapack +( 16%) Producing: schemas/example/schemas/foo/%/schema.metapack +( 20%) Producing: schemas/example/schemas/foo/%/dependencies.metapack +( 24%) Producing: schemas/example/schemas/foo/%/locations.metapack +( 28%) Producing: schemas/example/schemas/foo/%/positions.metapack +( 32%) Producing: schemas/example/schemas/foo/%/stats.metapack +( 36%) Producing: dependency-tree.metapack +( 40%) Producing: schemas/example/schemas/foo/%/bundle.metapack +( 44%) Producing: schemas/example/schemas/foo/%/health.metapack +( 48%) Producing: explorer/example/schemas/foo/%/schema.metapack +( 52%) Producing: schemas/example/schemas/foo/%/blaze-exhaustive.metapack +( 56%) Producing: schemas/example/schemas/foo/%/blaze-fast.metapack +( 60%) Producing: schemas/example/schemas/foo/%/dependents.metapack +( 64%) Producing: schemas/example/schemas/foo/%/editor.metapack +( 68%) Producing: explorer/%/search.metapack +( 72%) Producing: explorer/example/schemas/%/directory.metapack +( 76%) Producing: explorer/example/schemas/foo/%/schema-html.metapack +( 80%) Producing: explorer/example/%/directory.metapack +( 84%) Producing: explorer/example/schemas/%/directory-html.metapack +( 88%) Producing: explorer/%/directory.metapack +( 92%) Producing: explorer/example/%/directory-html.metapack +( 96%) Producing: explorer/%/directory-html.metapack +(100%) Producing: routes.bin EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/common/verbose-short.sh b/test/cli/index/common/verbose-short.sh index 914a7ba04..f64513fe1 100755 --- a/test/cli/index/common/verbose-short.sh +++ b/test/cli/index/common/verbose-short.sh @@ -49,19 +49,30 @@ Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/foo.json (#1) (100%) Resolving: foo.json https://example.com/foo => https://sourcemeta.com/example/schemas/foo -(100%) Ingesting: https://sourcemeta.com/example/schemas/foo -(100%) Analysing: https://sourcemeta.com/example/schemas/foo -( 33%) Reviewing: schemas -( 66%) Reviewing: schemas -(100%) Reviewing: schemas -(100%) Reworking: https://sourcemeta.com/example/schemas/foo -( 0%) Producing: explorer -( 33%) Producing: example/schemas -( 66%) Producing: example -(100%) Producing: . -( 25%) Rendering: example/schemas -( 50%) Rendering: example -( 75%) Rendering: . -(100%) Rendering: example/schemas/foo +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 12%) Producing: explorer/%/404.metapack +( 16%) Producing: schemas/example/schemas/foo/%/schema.metapack +( 20%) Producing: schemas/example/schemas/foo/%/dependencies.metapack +( 24%) Producing: schemas/example/schemas/foo/%/locations.metapack +( 28%) Producing: schemas/example/schemas/foo/%/positions.metapack +( 32%) Producing: schemas/example/schemas/foo/%/stats.metapack +( 36%) Producing: dependency-tree.metapack +( 40%) Producing: schemas/example/schemas/foo/%/bundle.metapack +( 44%) Producing: schemas/example/schemas/foo/%/health.metapack +( 48%) Producing: explorer/example/schemas/foo/%/schema.metapack +( 52%) Producing: schemas/example/schemas/foo/%/blaze-exhaustive.metapack +( 56%) Producing: schemas/example/schemas/foo/%/blaze-fast.metapack +( 60%) Producing: schemas/example/schemas/foo/%/dependents.metapack +( 64%) Producing: schemas/example/schemas/foo/%/editor.metapack +( 68%) Producing: explorer/%/search.metapack +( 72%) Producing: explorer/example/schemas/%/directory.metapack +( 76%) Producing: explorer/example/schemas/foo/%/schema-html.metapack +( 80%) Producing: explorer/example/%/directory.metapack +( 84%) Producing: explorer/example/schemas/%/directory-html.metapack +( 88%) Producing: explorer/%/directory.metapack +( 92%) Producing: explorer/example/%/directory-html.metapack +( 96%) Producing: explorer/%/directory-html.metapack +(100%) Producing: routes.bin EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/community/lint-rule-enterprise-only.sh b/test/cli/index/community/lint-rule-enterprise-only.sh index 1c66ab6ae..8df2d4200 100755 --- a/test/cli/index/community/lint-rule-enterprise-only.sh +++ b/test/cli/index/community/lint-rule-enterprise-only.sh @@ -29,7 +29,7 @@ cat << EOF > "$TMP/one.json" } EOF -"$1" --skip-banner "$TMP/one.json" "$TMP/output" 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" +"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" test "$CODE" = "1" || exit 1 # Remove thread information @@ -45,8 +45,17 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/test.json (#1) (100%) Resolving: test.json -(100%) Ingesting: https://example.com/test/test -(100%) Analysing: https://example.com/test/test +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 13%) Producing: explorer/%/404.metapack +( 17%) Producing: schemas/test/test/%/schema.metapack +( 21%) Producing: schemas/test/test/%/dependencies.metapack +( 26%) Producing: schemas/test/test/%/locations.metapack +( 30%) Producing: schemas/test/test/%/positions.metapack +( 34%) Producing: schemas/test/test/%/stats.metapack +( 39%) Producing: dependency-tree.metapack +( 43%) Producing: schemas/test/test/%/bundle.metapack +( 47%) Producing: schemas/test/test/%/health.metapack error: Custom linter rules are only available on the enterprise edition EOF diff --git a/test/cli/index/enterprise/lint-rule-invalid-title.sh b/test/cli/index/enterprise/lint-rule-invalid-title.sh index 357330855..ab7494fc6 100755 --- a/test/cli/index/enterprise/lint-rule-invalid-title.sh +++ b/test/cli/index/enterprise/lint-rule-invalid-title.sh @@ -29,7 +29,7 @@ cat << EOF > "$TMP/one.json" } EOF -"$1" --skip-banner "$TMP/one.json" "$TMP/output" 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" +"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" test "$CODE" = "1" || exit 1 # Remove thread information @@ -45,8 +45,17 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/test.json (#1) (100%) Resolving: test.json -(100%) Ingesting: https://example.com/test/test -(100%) Analysing: https://example.com/test/test +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 13%) Producing: explorer/%/404.metapack +( 17%) Producing: schemas/test/test/%/schema.metapack +( 21%) Producing: schemas/test/test/%/dependencies.metapack +( 26%) Producing: schemas/test/test/%/locations.metapack +( 30%) Producing: schemas/test/test/%/positions.metapack +( 34%) Producing: schemas/test/test/%/stats.metapack +( 39%) Producing: dependency-tree.metapack +( 43%) Producing: schemas/test/test/%/bundle.metapack +( 47%) Producing: schemas/test/test/%/health.metapack error: The schema rule name must match ^[a-z0-9_/]+\$ at name My Invalid Rule! EOF diff --git a/test/cli/index/enterprise/lint-rule-no-dialect.sh b/test/cli/index/enterprise/lint-rule-no-dialect.sh index 0826f07ca..4e2ed6ea0 100755 --- a/test/cli/index/enterprise/lint-rule-no-dialect.sh +++ b/test/cli/index/enterprise/lint-rule-no-dialect.sh @@ -29,7 +29,7 @@ cat << EOF > "$TMP/one.json" } EOF -"$1" --skip-banner "$TMP/one.json" "$TMP/output" 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" +"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" test "$CODE" = "1" || exit 1 # Remove thread information @@ -45,8 +45,17 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/test.json (#1) (100%) Resolving: test.json -(100%) Ingesting: https://example.com/test/test -(100%) Analysing: https://example.com/test/test +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 13%) Producing: explorer/%/404.metapack +( 17%) Producing: schemas/test/test/%/schema.metapack +( 21%) Producing: schemas/test/test/%/dependencies.metapack +( 26%) Producing: schemas/test/test/%/locations.metapack +( 30%) Producing: schemas/test/test/%/positions.metapack +( 34%) Producing: schemas/test/test/%/stats.metapack +( 39%) Producing: dependency-tree.metapack +( 43%) Producing: schemas/test/test/%/bundle.metapack +( 47%) Producing: schemas/test/test/%/health.metapack error: Could not determine the base dialect of the schema EOF diff --git a/test/cli/index/enterprise/lint-rule-no-title.sh b/test/cli/index/enterprise/lint-rule-no-title.sh index 1ad4fa3ae..882cba00e 100755 --- a/test/cli/index/enterprise/lint-rule-no-title.sh +++ b/test/cli/index/enterprise/lint-rule-no-title.sh @@ -29,7 +29,7 @@ cat << EOF > "$TMP/one.json" } EOF -"$1" --skip-banner "$TMP/one.json" "$TMP/output" 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" +"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" test "$CODE" = "1" || exit 1 # Remove thread information @@ -45,8 +45,17 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/test.json (#1) (100%) Resolving: test.json -(100%) Ingesting: https://example.com/test/test -(100%) Analysing: https://example.com/test/test +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 13%) Producing: explorer/%/404.metapack +( 17%) Producing: schemas/test/test/%/schema.metapack +( 21%) Producing: schemas/test/test/%/dependencies.metapack +( 26%) Producing: schemas/test/test/%/locations.metapack +( 30%) Producing: schemas/test/test/%/positions.metapack +( 34%) Producing: schemas/test/test/%/stats.metapack +( 39%) Producing: dependency-tree.metapack +( 43%) Producing: schemas/test/test/%/bundle.metapack +( 47%) Producing: schemas/test/test/%/health.metapack error: The schema rule is missing a title EOF diff --git a/test/cli/index/enterprise/lint-rule-not-found.sh b/test/cli/index/enterprise/lint-rule-not-found.sh index 07f8f3daa..a87ae153c 100755 --- a/test/cli/index/enterprise/lint-rule-not-found.sh +++ b/test/cli/index/enterprise/lint-rule-not-found.sh @@ -25,7 +25,7 @@ cat << EOF > "$TMP/one.json" } EOF -"$1" --skip-banner "$TMP/one.json" "$TMP/output" 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" +"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?" test "$CODE" = "1" || exit 1 # Remove thread information @@ -41,8 +41,17 @@ Writing output to: $(realpath "$TMP")/output Using configuration: $(realpath "$TMP")/one.json Detecting: $(realpath "$TMP")/schemas/test.json (#1) (100%) Resolving: test.json -(100%) Ingesting: https://example.com/test/test -(100%) Analysing: https://example.com/test/test +( 4%) Producing: configuration.json +( 8%) Producing: version.json +( 13%) Producing: explorer/%/404.metapack +( 17%) Producing: schemas/test/test/%/schema.metapack +( 21%) Producing: schemas/test/test/%/dependencies.metapack +( 26%) Producing: schemas/test/test/%/locations.metapack +( 30%) Producing: schemas/test/test/%/positions.metapack +( 34%) Producing: schemas/test/test/%/stats.metapack +( 39%) Producing: dependency-tree.metapack +( 43%) Producing: schemas/test/test/%/bundle.metapack +( 47%) Producing: schemas/test/test/%/health.metapack error: could not locate the requested file at $(realpath "$TMP")/rules/does-not-exist.json EOF diff --git a/test/unit/build/CMakeLists.txt b/test/unit/build/CMakeLists.txt index 9bf13d726..2d79ce0e5 100644 --- a/test/unit/build/CMakeLists.txt +++ b/test/unit/build/CMakeLists.txt @@ -1,9 +1,8 @@ sourcemeta_googletest(NAMESPACE sourcemeta PROJECT one NAME build SOURCES build_test_utils.h - build_e2e_test.cc) + build_delta_test.cc + build_state_test.cc) target_link_libraries(sourcemeta_one_build_unit PRIVATE sourcemeta::one::build) -target_compile_definitions(sourcemeta_one_build_unit - PRIVATE TEST_DIRECTORY="${CMAKE_CURRENT_SOURCE_DIR}") target_compile_definitions(sourcemeta_one_build_unit PRIVATE BINARY_DIRECTORY="${CMAKE_CURRENT_BINARY_DIR}") diff --git a/test/unit/build/build_delta_test.cc b/test/unit/build/build_delta_test.cc new file mode 100644 index 000000000..ad92563f2 --- /dev/null +++ b/test/unit/build/build_delta_test.cc @@ -0,0 +1,4538 @@ +#include + +#include +#include + +#include "build_test_utils.h" + +#include // std::filesystem::path +#include // std::string +#include // std::unordered_map +#include // std::vector + +TEST(Build_delta, full_empty_registry) { + const std::filesystem::path output{"/output"}; + const sourcemeta::one::BuildState entries; + const sourcemeta::one::Resolver::Views schemas; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON{nullptr}, changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 4, 8); + + EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json", + ""); + EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0"); + + EXPECT_ACTION(plan, 1, 0, 4, DependencyTree, + output / "dependency-tree.metapack", ""); + EXPECT_ACTION(plan, 1, 1, 4, WebNotFound, + output / "explorer" / "%" / "404.metapack", "", + output / "configuration.json"); + EXPECT_ACTION(plan, 1, 2, 4, DirectoryList, + output / "explorer" / "%" / "directory.metapack", ""); + EXPECT_ACTION(plan, 1, 3, 4, SearchIndex, + output / "explorer" / "%" / "search.metapack", ""); + + EXPECT_ACTION(plan, 2, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 3, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_TOTAL_FILES(plan, entries, output / "configuration.json", + output / "version.json", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "404.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, full_single_schema) { + const std::filesystem::path output{"/output"}; + const sourcemeta::one::BuildState entries; + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON{nullptr}, changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 21); + + EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json", + ""); + EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0"); + + EXPECT_ACTION(plan, 1, 0, 2, WebNotFound, + output / "explorer" / "%" / "404.metapack", "", + output / "configuration.json"); + EXPECT_ACTION(plan, 1, 1, 2, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 2, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 3, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 4, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 4, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 4, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 4, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 4, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 5, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 1, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 7, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "configuration.json", output / "version.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, incremental_changed_same_mtime) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + ADD_SCHEMA_ENTRIES(entries, output, "foo", true, true, MTIME(100)); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "foo" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}}; + const std::vector changed{"/src/foo.json"}; + const std::vector removed; + + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 0, 0); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "explorer" / "foo" / "%" / "directory.metapack", + output / "explorer" / "foo" / "%" / "directory-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, incremental_missing_schema_metapack) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "bar" / "%" / "dependencies.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "bar" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/bar", {"/src/bar.json", "bar", MTIME(100)}}}; + const std::vector changed; + const std::vector removed; + + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 18); + + EXPECT_ACTION(plan, 0, 0, 1, Materialise, + output / "schemas" / "bar" / "%" / "schema.metapack", + "https://example.com/bar", + std::filesystem::path{"/"} / "src" / "bar.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 4, Dependencies, + output / "schemas" / "bar" / "%" / "dependencies.metapack", + "https://example.com/bar", + output / "schemas" / "bar" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, Locations, + output / "schemas" / "bar" / "%" / "locations.metapack", + "https://example.com/bar", + output / "schemas" / "bar" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, Positions, + output / "schemas" / "bar" / "%" / "positions.metapack", + "https://example.com/bar", + output / "schemas" / "bar" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Stats, + output / "schemas" / "bar" / "%" / "stats.metapack", + "https://example.com/bar", + output / "schemas" / "bar" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 2, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "bar" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 3, Bundle, + output / "schemas" / "bar" / "%" / "bundle.metapack", + "https://example.com/bar", + output / "schemas" / "bar" / "%" / "schema.metapack", + output / "schemas" / "bar" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 3, Health, + output / "schemas" / "bar" / "%" / "health.metapack", + "https://example.com/bar", + output / "schemas" / "bar" / "%" / "schema.metapack", + output / "schemas" / "bar" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 5, SchemaMetadata, + output / "explorer" / "bar" / "%" / "schema.metapack", + "https://example.com/bar", + output / "schemas" / "bar" / "%" / "schema.metapack", + output / "schemas" / "bar" / "%" / "health.metapack", + output / "schemas" / "bar" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 5, BlazeExhaustive, + output / "schemas" / "bar" / "%" / "blaze-exhaustive.metapack", + "https://example.com/bar", + output / "schemas" / "bar" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 2, 5, BlazeFast, + output / "schemas" / "bar" / "%" / "blaze-fast.metapack", + "https://example.com/bar", + output / "schemas" / "bar" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 3, 5, Dependents, + output / "schemas" / "bar" / "%" / "dependents.metapack", + "https://example.com/bar", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 4, 5, Editor, + output / "schemas" / "bar" / "%" / "editor.metapack", + "https://example.com/bar", + output / "schemas" / "bar" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 4, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "bar" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 1, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "bar" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 3, WebSchema, + output / "explorer" / "bar" / "%" / "schema-html.metapack", + "https://example.com/bar", + output / "explorer" / "bar" / "%" / "schema.metapack", + output / "schemas" / "bar" / "%" / "dependencies.metapack", + output / "schemas" / "bar" / "%" / "health.metapack", + output / "schemas" / "bar" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "bar" / "%" / "schema.metapack", + output / "schemas" / "bar" / "%" / "dependencies.metapack", + output / "schemas" / "bar" / "%" / "locations.metapack", + output / "schemas" / "bar" / "%" / "positions.metapack", + output / "schemas" / "bar" / "%" / "stats.metapack", + output / "schemas" / "bar" / "%" / "bundle.metapack", + output / "schemas" / "bar" / "%" / "health.metapack", + output / "schemas" / "bar" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "bar" / "%" / "blaze-fast.metapack", + output / "schemas" / "bar" / "%" / "editor.metapack", + output / "schemas" / "bar" / "%" / "dependents.metapack", + output / "explorer" / "bar" / "%" / "schema.metapack", + output / "explorer" / "bar" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, incremental_one_schema_changed) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "bar" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "bar" / "%" / "dependencies.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "bar" / "%" / "locations.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "bar" / "%" / "positions.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "bar" / "%" / "stats.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "bar" / "%" / "bundle.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "bar" / "%" / "health.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "bar" / "%" / + "blaze-exhaustive.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "bar" / "%" / "blaze-fast.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "bar" / "%" / "editor.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "bar" / "%" / "dependents.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "bar" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "bar" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "bar" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "bar" / "%" / "schema-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(200)}}, + {"https://example.com/bar", {"/src/bar.json", "bar", MTIME(100)}}}; + const std::vector changed{"/src/foo.json"}; + const std::vector removed; + + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 6, 17); + + EXPECT_ACTION(plan, 0, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION_UNORDERED( + plan, 2, 0, 3, DependencyTree, output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "bar" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 4, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION_UNORDERED( + plan, 4, 1, 3, SearchIndex, output / "explorer" / "%" / "search.metapack", + "", output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "bar" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "bar" / "%" / "schema.metapack", + output / "schemas" / "bar" / "%" / "dependencies.metapack", + output / "schemas" / "bar" / "%" / "locations.metapack", + output / "schemas" / "bar" / "%" / "positions.metapack", + output / "schemas" / "bar" / "%" / "stats.metapack", + output / "schemas" / "bar" / "%" / "bundle.metapack", + output / "schemas" / "bar" / "%" / "health.metapack", + output / "schemas" / "bar" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "bar" / "%" / "blaze-fast.metapack", + output / "schemas" / "bar" / "%" / "editor.metapack", + output / "schemas" / "bar" / "%" / "dependents.metapack", + output / "explorer" / "bar" / "%" / "schema.metapack", + output / "explorer" / "bar" / "%" / "schema-html.metapack", + output / "explorer" / "bar" / "%" / "directory.metapack", + output / "explorer" / "bar" / "%" / "directory-html.metapack", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, incremental_removed_schema) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}}; + const std::vector changed; + const std::vector removed{"/src/foo.json"}; + + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 4, 7); + + EXPECT_ACTION(plan, 0, 0, 3, DependencyTree, + output / "dependency-tree.metapack", ""); + EXPECT_ACTION(plan, 0, 1, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", ""); + EXPECT_ACTION(plan, 0, 2, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", ""); + + EXPECT_ACTION(plan, 1, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 2, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_ACTION(plan, 3, 0, 2, Remove, output / "explorer" / "foo" / "%", ""); + EXPECT_ACTION(plan, 3, 1, 2, Remove, output / "schemas" / "foo" / "%", ""); + + EXPECT_TOTAL_FILES(plan, entries, output / "version.json", + output / "configuration.json", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, full_stale_file_in_entries) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "schemas" / "ghost" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON{nullptr}, changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 9, 22); + + EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json", + ""); + EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0"); + + EXPECT_ACTION(plan, 1, 0, 2, WebNotFound, + output / "explorer" / "%" / "404.metapack", "", + output / "configuration.json"); + EXPECT_ACTION(plan, 1, 1, 2, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 2, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 3, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 4, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 4, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 4, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 4, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 4, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 5, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 1, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 7, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_ACTION(plan, 8, 0, 1, Remove, output / "schemas" / "ghost", ""); + + EXPECT_TOTAL_FILES( + plan, entries, output / "configuration.json", output / "version.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, full_stale_directory_in_entries) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "schemas" / "ghost" / "%", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON{nullptr}, changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 9, 22); + + EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json", + ""); + EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0"); + + EXPECT_ACTION(plan, 1, 0, 2, WebNotFound, + output / "explorer" / "%" / "404.metapack", "", + output / "configuration.json"); + EXPECT_ACTION(plan, 1, 1, 2, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 2, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 3, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 4, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 4, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 4, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 4, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 4, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 5, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 1, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 7, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_ACTION(plan, 8, 0, 1, Remove, output / "schemas" / "ghost", ""); + + EXPECT_TOTAL_FILES( + plan, entries, output / "configuration.json", output / "version.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, full_with_comment) { + const std::filesystem::path output{"/output"}; + const sourcemeta::one::BuildState entries; + const sourcemeta::one::Resolver::Views schemas; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "", "Hello world", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON{nullptr}, changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 4, 9); + + EXPECT_ACTION(plan, 0, 0, 3, Comment, output / "comment.json", "Hello world"); + EXPECT_ACTION(plan, 0, 1, 3, Configuration, output / "configuration.json", + ""); + EXPECT_ACTION(plan, 0, 2, 3, Version, output / "version.json", "1.0.0"); + + EXPECT_ACTION(plan, 1, 0, 4, DependencyTree, + output / "dependency-tree.metapack", ""); + EXPECT_ACTION(plan, 1, 1, 4, WebNotFound, + output / "explorer" / "%" / "404.metapack", "", + output / "configuration.json"); + EXPECT_ACTION(plan, 1, 2, 4, DirectoryList, + output / "explorer" / "%" / "directory.metapack", ""); + EXPECT_ACTION(plan, 1, 3, 4, SearchIndex, + output / "explorer" / "%" / "search.metapack", ""); + + EXPECT_ACTION(plan, 2, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 3, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_TOTAL_FILES(plan, entries, output / "comment.json", + output / "configuration.json", output / "version.json", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "404.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, full_without_comment_removes_existing) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "comment.json", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON{nullptr}, changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 5, 9); + + EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json", + ""); + EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0"); + + EXPECT_ACTION(plan, 1, 0, 4, DependencyTree, + output / "dependency-tree.metapack", ""); + EXPECT_ACTION(plan, 1, 1, 4, WebNotFound, + output / "explorer" / "%" / "404.metapack", "", + output / "configuration.json"); + EXPECT_ACTION(plan, 1, 2, 4, DirectoryList, + output / "explorer" / "%" / "directory.metapack", ""); + EXPECT_ACTION(plan, 1, 3, 4, SearchIndex, + output / "explorer" / "%" / "search.metapack", ""); + + EXPECT_ACTION(plan, 2, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 3, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_ACTION(plan, 4, 0, 1, Remove, output / "comment.json", ""); + + EXPECT_TOTAL_FILES(plan, entries, output / "configuration.json", + output / "version.json", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "404.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, incremental_with_comment) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}}; + const std::vector changed{"/src/foo.json"}; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "Hello world", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 19); + + EXPECT_ACTION(plan, 0, 0, 1, Comment, output / "comment.json", "Hello world"); + + EXPECT_ACTION(plan, 1, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 2, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 3, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 4, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 4, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 4, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 4, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 4, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 5, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 1, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 7, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_TOTAL_FILES(plan, entries, output / "version.json", + output / "configuration.json", output / "comment.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / + "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, incremental_empty_comment_removes_existing) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "comment.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}}; + const std::vector changed{"/src/foo.json"}; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 19); + + EXPECT_ACTION(plan, 0, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 2, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 4, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 1, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_ACTION(plan, 7, 0, 1, Remove, output / "comment.json", ""); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, incremental_no_changes_adds_comment) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "hello", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 1, 1); + + EXPECT_ACTION(plan, 0, 0, 1, Comment, output / "comment.json", "hello"); + + EXPECT_TOTAL_FILES(plan, entries, output / "version.json", + output / "configuration.json", output / "comment.json", + output / "routes.bin", output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "404.metapack", + output / "explorer" / "%" / "directory-html.metapack"); +} + +TEST(Build_delta, incremental_no_changes_removes_comment) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "comment.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 1, 1); + + EXPECT_ACTION(plan, 0, 0, 1, Remove, output / "comment.json", ""); + + EXPECT_TOTAL_FILES(plan, entries, output / "version.json", + output / "configuration.json", output / "routes.bin", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "404.metapack", + output / "explorer" / "%" / "directory-html.metapack"); +} + +TEST(Build_delta, incremental_schema_removed_cleans_stale_entries) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + ADD_SCHEMA_ENTRIES(entries, output, "foo", true, true, MTIME(100)); + const sourcemeta::one::Resolver::Views schemas; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 1, 2); + + EXPECT_ACTION(plan, 0, 0, 2, Remove, output / "explorer" / "foo", ""); + EXPECT_ACTION(plan, 0, 1, 2, Remove, output / "schemas", ""); + + EXPECT_TOTAL_FILES(plan, entries, output / "version.json", + output / "configuration.json", output / "routes.bin", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "404.metapack", + output / "explorer" / "%" / "directory-html.metapack"); +} + +TEST(Build_delta, remove_wave_deduplicates_children_of_removed_directories) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + ADD_SCHEMA_ENTRIES(entries, output, "foo", true, true, MTIME(100)); + const sourcemeta::one::Resolver::Views schemas; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 1, 2); + + EXPECT_ACTION(plan, 0, 0, 2, Remove, output / "explorer" / "foo", ""); + EXPECT_ACTION(plan, 0, 1, 2, Remove, output / "schemas", ""); + + EXPECT_TOTAL_FILES(plan, entries, output / "version.json", + output / "configuration.json", output / "routes.bin", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "404.metapack", + output / "explorer" / "%" / "directory-html.metapack"); +} + +TEST(Build_delta, full_config_change_to_empty_schemas) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + ADD_SCHEMA_ENTRIES(entries, output, "foo", true, true, MTIME(100)); + const sourcemeta::one::Resolver::Views schemas; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON{{"a", sourcemeta::core::JSON{1}}}, + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 5, 10); + + EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json", + ""); + EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0"); + + EXPECT_ACTION(plan, 1, 0, 4, DependencyTree, + output / "dependency-tree.metapack", ""); + EXPECT_ACTION(plan, 1, 1, 4, WebNotFound, + output / "explorer" / "%" / "404.metapack", "", + output / "configuration.json"); + EXPECT_ACTION(plan, 1, 2, 4, DirectoryList, + output / "explorer" / "%" / "directory.metapack", ""); + EXPECT_ACTION(plan, 1, 3, 4, SearchIndex, + output / "explorer" / "%" / "search.metapack", ""); + + EXPECT_ACTION(plan, 2, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 3, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_ACTION(plan, 4, 0, 2, Remove, output / "explorer" / "foo", ""); + EXPECT_ACTION(plan, 4, 1, 2, Remove, output / "schemas", ""); + + EXPECT_TOTAL_FILES(plan, entries, output / "version.json", + output / "configuration.json", output / "routes.bin", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "404.metapack", + output / "explorer" / "%" / "directory-html.metapack"); +} + +TEST(Build_delta, full_single_schema_evaluate_false) { + const std::filesystem::path output{"/output"}; + const sourcemeta::one::BuildState entries; + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100), false}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON{nullptr}, changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 19); + + EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json", + ""); + EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0"); + + EXPECT_ACTION(plan, 1, 0, 2, WebNotFound, + output / "explorer" / "%" / "404.metapack", "", + output / "configuration.json"); + EXPECT_ACTION(plan, 1, 1, 2, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 2, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 3, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 4, 0, 3, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 4, 1, 3, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 4, 2, 3, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 5, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 1, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 7, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "configuration.json", output / "version.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, full_evaluate_false_removes_existing_blaze) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "schemas" / "foo" / "%" / + "blaze-exhaustive.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100), false}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON{nullptr}, changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 9, 21); + + EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json", + ""); + EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0"); + + EXPECT_ACTION(plan, 1, 0, 2, WebNotFound, + output / "explorer" / "%" / "404.metapack", "", + output / "configuration.json"); + EXPECT_ACTION(plan, 1, 1, 2, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 2, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 3, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 4, 0, 3, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 4, 1, 3, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 4, 2, 3, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 5, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 1, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 7, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_ACTION(plan, 8, 0, 2, Remove, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + ""); + EXPECT_ACTION(plan, 8, 1, 2, Remove, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", ""); + + EXPECT_TOTAL_FILES( + plan, entries, output / "configuration.json", output / "version.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, incremental_evaluate_false) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100), false}}}; + const std::vector changed{"/src/foo.json"}; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 16); + + EXPECT_ACTION(plan, 0, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 2, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 3, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 3, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 2, 3, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 4, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 1, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_TOTAL_FILES(plan, entries, output / "version.json", + output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, incremental_missing_blaze_exhaustive) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + ADD_SCHEMA_ENTRIES(entries, output, "foo", true, true, MTIME(100)); + entries.forget( + (output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack") + .string()); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "foo" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(40)}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 2, 5); + + EXPECT_ACTION(plan, 0, 0, 4, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 0, 1, 4, DirectoryList, + output / "explorer" / "%" / "directory.metapack", ""); + EXPECT_ACTION(plan, 0, 2, 4, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 0, 3, 4, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 1, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "explorer" / "foo" / "%" / "directory.metapack", + output / "explorer" / "foo" / "%" / "directory-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, incremental_missing_bundle) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + ADD_SCHEMA_ENTRIES(entries, output, "foo", true, true, MTIME(100)); + entries.forget( + (output / "schemas" / "foo" / "%" / "bundle.metapack").string()); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "foo" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(40)}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 2, 8); + + EXPECT_ACTION(plan, 0, 0, 4, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 0, 1, 4, DirectoryList, + output / "explorer" / "%" / "directory.metapack", ""); + EXPECT_ACTION(plan, 0, 2, 4, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 0, 3, 4, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 1, 0, 4, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "explorer" / "foo" / "%" / "directory.metapack", + output / "explorer" / "foo" / "%" / "directory-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, incremental_missing_web_schema) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + ADD_SCHEMA_ENTRIES(entries, output, "foo", true, true, MTIME(100)); + entries.forget( + (output / "explorer" / "foo" / "%" / "schema-html.metapack").string()); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "foo" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(40)}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 2, 5); + + EXPECT_ACTION(plan, 0, 0, 4, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 0, 1, 4, DirectoryList, + output / "explorer" / "%" / "directory.metapack", ""); + EXPECT_ACTION(plan, 0, 2, 4, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 0, 3, 4, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 1, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "explorer" / "foo" / "%" / "directory.metapack", + output / "explorer" / "foo" / "%" / "directory-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, incremental_missing_web_not_checked_headless) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + ADD_SCHEMA_ENTRIES(entries, output, "foo", true, false, MTIME(100)); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "foo" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(40)}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas, + "1.0.0", "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 0, 0); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "directory.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", output / "routes.bin"); +} + +TEST(Build_delta, mtime_nothing_changed) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + ADD_SCHEMA_ENTRIES(entries, output, "foo", true, true, MTIME(50)); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "explorer" / "foo" / "%" / "directory.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(50), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(40)}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 0, 0); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "explorer" / "foo" / "%" / "directory.metapack", + output / "explorer" / "foo" / "%" / "directory-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, mtime_source_newer) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + ADD_SCHEMA_ENTRIES(entries, output, "foo", true, true, MTIME(10)); + ADD_SCHEMA_ENTRIES(entries, output, "bar", true, true, MTIME(50)); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(50), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(50)}}, + {"https://example.com/bar", {"/src/bar.json", "bar", MTIME(40)}}}; + const std::vector changed; + const std::vector removed; + + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 6, 17); + + EXPECT_ACTION(plan, 0, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION_UNORDERED( + plan, 2, 0, 3, DependencyTree, output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "bar" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 4, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION_UNORDERED( + plan, 4, 1, 3, SearchIndex, output / "explorer" / "%" / "search.metapack", + "", output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "bar" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "explorer" / "foo" / "%" / "directory-html.metapack", + output / "schemas" / "bar" / "%" / "schema.metapack", + output / "schemas" / "bar" / "%" / "dependencies.metapack", + output / "schemas" / "bar" / "%" / "locations.metapack", + output / "schemas" / "bar" / "%" / "positions.metapack", + output / "schemas" / "bar" / "%" / "stats.metapack", + output / "schemas" / "bar" / "%" / "bundle.metapack", + output / "schemas" / "bar" / "%" / "health.metapack", + output / "schemas" / "bar" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "bar" / "%" / "blaze-fast.metapack", + output / "schemas" / "bar" / "%" / "editor.metapack", + output / "schemas" / "bar" / "%" / "dependents.metapack", + output / "explorer" / "bar" / "%" / "schema.metapack", + output / "explorer" / "bar" / "%" / "schema-html.metapack", + output / "explorer" / "bar" / "%" / "directory-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, mtime_no_entry) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + ADD_SCHEMA_ENTRIES(entries, output, "bar", true, true, MTIME(50)); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(50), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(40)}}, + {"https://example.com/bar", {"/src/bar.json", "bar", MTIME(40)}}}; + const std::vector changed; + const std::vector removed; + + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 6, 17); + + EXPECT_ACTION(plan, 0, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION_UNORDERED( + plan, 2, 0, 3, DependencyTree, output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "bar" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 4, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION_UNORDERED( + plan, 4, 1, 3, SearchIndex, output / "explorer" / "%" / "search.metapack", + "", output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "bar" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "schemas" / "bar" / "%" / "schema.metapack", + output / "schemas" / "bar" / "%" / "dependencies.metapack", + output / "schemas" / "bar" / "%" / "locations.metapack", + output / "schemas" / "bar" / "%" / "positions.metapack", + output / "schemas" / "bar" / "%" / "stats.metapack", + output / "schemas" / "bar" / "%" / "bundle.metapack", + output / "schemas" / "bar" / "%" / "health.metapack", + output / "schemas" / "bar" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "bar" / "%" / "blaze-fast.metapack", + output / "schemas" / "bar" / "%" / "editor.metapack", + output / "schemas" / "bar" / "%" / "dependents.metapack", + output / "explorer" / "bar" / "%" / "schema.metapack", + output / "explorer" / "bar" / "%" / "schema-html.metapack", + output / "explorer" / "bar" / "%" / "directory-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, mtime_no_file_mark) { + const std::filesystem::path output{"/output"}; + + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + ADD_SCHEMA_ENTRIES(entries, output, "foo", true, true, MTIME(100)); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.forget( + (output / "schemas" / "foo" / "%" / "schema.metapack").string()); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(40)}}}; + const std::vector changed; + const std::vector removed; + + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 6, 17); + + EXPECT_ACTION(plan, 0, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 2, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 4, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 1, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "explorer" / "foo" / "%" / "directory-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "%" / "404.metapack", output / "routes.bin"); +} + +TEST(Build_delta, incremental_reverse_dep_direct) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace( + output / "schemas" / "b" / "%" / "dependencies.metapack", + {.file_mark = MTIME(100), + .dependencies = {output / "schemas" / "a" / "%" / "schema.metapack"}}); + entries.emplace(output / "explorer" / "a" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "b" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/a", {"/src/a.json", "a", MTIME(100)}}, + {"https://example.com/b", {"/src/b.json", "b", MTIME(100)}}}; + const std::vector changed{"/src/a.json"}; + const std::vector removed; + + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 31); + + EXPECT_ACTION(plan, 0, 0, 2, Materialise, + output / "schemas" / "a" / "%" / "schema.metapack", + "https://example.com/a", + std::filesystem::path{"/"} / "src" / "a.json", + output / "configuration.json"); + EXPECT_ACTION(plan, 0, 1, 2, Materialise, + output / "schemas" / "b" / "%" / "schema.metapack", + "https://example.com/b", + std::filesystem::path{"/"} / "src" / "b.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 8, Dependencies, + output / "schemas" / "a" / "%" / "dependencies.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 8, Locations, + output / "schemas" / "a" / "%" / "locations.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 8, Positions, + output / "schemas" / "a" / "%" / "positions.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 8, Stats, + output / "schemas" / "a" / "%" / "stats.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 4, 8, Dependencies, + output / "schemas" / "b" / "%" / "dependencies.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 5, 8, Locations, + output / "schemas" / "b" / "%" / "locations.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 6, 8, Positions, + output / "schemas" / "b" / "%" / "positions.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 7, 8, Stats, + output / "schemas" / "b" / "%" / "stats.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack"); + + EXPECT_ACTION_UNORDERED( + plan, 2, 0, 5, DependencyTree, output / "dependency-tree.metapack", "", + output / "schemas" / "a" / "%" / "dependencies.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 5, Bundle, + output / "schemas" / "a" / "%" / "bundle.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 5, Health, + output / "schemas" / "a" / "%" / "health.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 3, 5, Bundle, + output / "schemas" / "b" / "%" / "bundle.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 4, 5, Health, + output / "schemas" / "b" / "%" / "health.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 10, SchemaMetadata, + output / "explorer" / "a" / "%" / "schema.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "health.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 10, SchemaMetadata, + output / "explorer" / "b" / "%" / "schema.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "health.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 2, 10, BlazeExhaustive, + output / "schemas" / "a" / "%" / "blaze-exhaustive.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 3, 10, BlazeFast, + output / "schemas" / "a" / "%" / "blaze-fast.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 4, 10, Dependents, + output / "schemas" / "a" / "%" / "dependents.metapack", + "https://example.com/a", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 5, 10, Editor, + output / "schemas" / "a" / "%" / "editor.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 6, 10, BlazeExhaustive, + output / "schemas" / "b" / "%" / "blaze-exhaustive.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 7, 10, BlazeFast, + output / "schemas" / "b" / "%" / "blaze-fast.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 8, 10, Dependents, + output / "schemas" / "b" / "%" / "dependents.metapack", + "https://example.com/b", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 9, 10, Editor, + output / "schemas" / "b" / "%" / "editor.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "bundle.metapack"); + + EXPECT_ACTION_UNORDERED(plan, 4, 0, 4, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "a" / "%" / "schema.metapack", + output / "explorer" / "b" / "%" / "schema.metapack"); + EXPECT_ACTION_UNORDERED(plan, 4, 1, 4, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "a" / "%" / "schema.metapack", + output / "explorer" / "b" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 4, WebSchema, + output / "explorer" / "a" / "%" / "schema-html.metapack", + "https://example.com/a", + output / "explorer" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack", + output / "schemas" / "a" / "%" / "health.metapack", + output / "schemas" / "a" / "%" / "dependents.metapack"); + EXPECT_ACTION(plan, 4, 3, 4, WebSchema, + output / "explorer" / "b" / "%" / "schema-html.metapack", + "https://example.com/b", + output / "explorer" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack", + output / "schemas" / "b" / "%" / "health.metapack", + output / "schemas" / "b" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack", + output / "schemas" / "a" / "%" / "locations.metapack", + output / "schemas" / "a" / "%" / "positions.metapack", + output / "schemas" / "a" / "%" / "stats.metapack", + output / "schemas" / "a" / "%" / "bundle.metapack", + output / "schemas" / "a" / "%" / "health.metapack", + output / "schemas" / "a" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "a" / "%" / "blaze-fast.metapack", + output / "schemas" / "a" / "%" / "editor.metapack", + output / "schemas" / "a" / "%" / "dependents.metapack", + output / "explorer" / "a" / "%" / "schema.metapack", + output / "explorer" / "a" / "%" / "schema-html.metapack", + output / "schemas" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack", + output / "schemas" / "b" / "%" / "locations.metapack", + output / "schemas" / "b" / "%" / "positions.metapack", + output / "schemas" / "b" / "%" / "stats.metapack", + output / "schemas" / "b" / "%" / "bundle.metapack", + output / "schemas" / "b" / "%" / "health.metapack", + output / "schemas" / "b" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "b" / "%" / "blaze-fast.metapack", + output / "schemas" / "b" / "%" / "editor.metapack", + output / "schemas" / "b" / "%" / "dependents.metapack", + output / "explorer" / "b" / "%" / "schema.metapack", + output / "explorer" / "b" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, incremental_reverse_dep_transitive) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace( + output / "schemas" / "b" / "%" / "dependencies.metapack", + {.file_mark = MTIME(100), + .dependencies = {output / "schemas" / "a" / "%" / "schema.metapack"}}); + entries.emplace( + output / "schemas" / "c" / "%" / "dependencies.metapack", + {.file_mark = MTIME(100), + .dependencies = {output / "schemas" / "b" / "%" / "schema.metapack"}}); + entries.emplace(output / "explorer" / "a" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "b" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "c" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/a", {"/src/a.json", "a", MTIME(100)}}, + {"https://example.com/b", {"/src/b.json", "b", MTIME(100)}}, + {"https://example.com/c", {"/src/c.json", "c", MTIME(100)}}}; + const std::vector changed{"/src/a.json"}; + const std::vector removed; + + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 44); + + EXPECT_ACTION(plan, 0, 0, 3, Materialise, + output / "schemas" / "a" / "%" / "schema.metapack", + "https://example.com/a", + std::filesystem::path{"/"} / "src" / "a.json", + output / "configuration.json"); + EXPECT_ACTION(plan, 0, 1, 3, Materialise, + output / "schemas" / "b" / "%" / "schema.metapack", + "https://example.com/b", + std::filesystem::path{"/"} / "src" / "b.json", + output / "configuration.json"); + EXPECT_ACTION(plan, 0, 2, 3, Materialise, + output / "schemas" / "c" / "%" / "schema.metapack", + "https://example.com/c", + std::filesystem::path{"/"} / "src" / "c.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 12, Dependencies, + output / "schemas" / "a" / "%" / "dependencies.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 12, Locations, + output / "schemas" / "a" / "%" / "locations.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 12, Positions, + output / "schemas" / "a" / "%" / "positions.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 12, Stats, + output / "schemas" / "a" / "%" / "stats.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 4, 12, Dependencies, + output / "schemas" / "b" / "%" / "dependencies.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 5, 12, Locations, + output / "schemas" / "b" / "%" / "locations.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 6, 12, Positions, + output / "schemas" / "b" / "%" / "positions.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 7, 12, Stats, + output / "schemas" / "b" / "%" / "stats.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 8, 12, Dependencies, + output / "schemas" / "c" / "%" / "dependencies.metapack", + "https://example.com/c", + output / "schemas" / "c" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 9, 12, Locations, + output / "schemas" / "c" / "%" / "locations.metapack", + "https://example.com/c", + output / "schemas" / "c" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 10, 12, Positions, + output / "schemas" / "c" / "%" / "positions.metapack", + "https://example.com/c", + output / "schemas" / "c" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 11, 12, Stats, + output / "schemas" / "c" / "%" / "stats.metapack", + "https://example.com/c", + output / "schemas" / "c" / "%" / "schema.metapack"); + + EXPECT_ACTION_UNORDERED( + plan, 2, 0, 7, DependencyTree, output / "dependency-tree.metapack", "", + output / "schemas" / "a" / "%" / "dependencies.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack", + output / "schemas" / "c" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 7, Bundle, + output / "schemas" / "a" / "%" / "bundle.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 7, Health, + output / "schemas" / "a" / "%" / "health.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 3, 7, Bundle, + output / "schemas" / "b" / "%" / "bundle.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 4, 7, Health, + output / "schemas" / "b" / "%" / "health.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 5, 7, Bundle, + output / "schemas" / "c" / "%" / "bundle.metapack", + "https://example.com/c", + output / "schemas" / "c" / "%" / "schema.metapack", + output / "schemas" / "c" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 6, 7, Health, + output / "schemas" / "c" / "%" / "health.metapack", + "https://example.com/c", + output / "schemas" / "c" / "%" / "schema.metapack", + output / "schemas" / "c" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 15, SchemaMetadata, + output / "explorer" / "a" / "%" / "schema.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "health.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 15, SchemaMetadata, + output / "explorer" / "b" / "%" / "schema.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "health.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 2, 15, SchemaMetadata, + output / "explorer" / "c" / "%" / "schema.metapack", + "https://example.com/c", + output / "schemas" / "c" / "%" / "schema.metapack", + output / "schemas" / "c" / "%" / "health.metapack", + output / "schemas" / "c" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 3, 15, BlazeExhaustive, + output / "schemas" / "a" / "%" / "blaze-exhaustive.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 4, 15, BlazeFast, + output / "schemas" / "a" / "%" / "blaze-fast.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 5, 15, Dependents, + output / "schemas" / "a" / "%" / "dependents.metapack", + "https://example.com/a", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 6, 15, Editor, + output / "schemas" / "a" / "%" / "editor.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 7, 15, BlazeExhaustive, + output / "schemas" / "b" / "%" / "blaze-exhaustive.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 8, 15, BlazeFast, + output / "schemas" / "b" / "%" / "blaze-fast.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 9, 15, Dependents, + output / "schemas" / "b" / "%" / "dependents.metapack", + "https://example.com/b", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 10, 15, Editor, + output / "schemas" / "b" / "%" / "editor.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 11, 15, BlazeExhaustive, + output / "schemas" / "c" / "%" / "blaze-exhaustive.metapack", + "https://example.com/c", + output / "schemas" / "c" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 12, 15, BlazeFast, + output / "schemas" / "c" / "%" / "blaze-fast.metapack", + "https://example.com/c", + output / "schemas" / "c" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 13, 15, Dependents, + output / "schemas" / "c" / "%" / "dependents.metapack", + "https://example.com/c", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 14, 15, Editor, + output / "schemas" / "c" / "%" / "editor.metapack", + "https://example.com/c", + output / "schemas" / "c" / "%" / "bundle.metapack"); + + EXPECT_ACTION_UNORDERED(plan, 4, 0, 5, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "a" / "%" / "schema.metapack", + output / "explorer" / "b" / "%" / "schema.metapack", + output / "explorer" / "c" / "%" / "schema.metapack"); + EXPECT_ACTION_UNORDERED(plan, 4, 1, 5, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "a" / "%" / "schema.metapack", + output / "explorer" / "b" / "%" / "schema.metapack", + output / "explorer" / "c" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 5, WebSchema, + output / "explorer" / "a" / "%" / "schema-html.metapack", + "https://example.com/a", + output / "explorer" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack", + output / "schemas" / "a" / "%" / "health.metapack", + output / "schemas" / "a" / "%" / "dependents.metapack"); + EXPECT_ACTION(plan, 4, 3, 5, WebSchema, + output / "explorer" / "b" / "%" / "schema-html.metapack", + "https://example.com/b", + output / "explorer" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack", + output / "schemas" / "b" / "%" / "health.metapack", + output / "schemas" / "b" / "%" / "dependents.metapack"); + EXPECT_ACTION(plan, 4, 4, 5, WebSchema, + output / "explorer" / "c" / "%" / "schema-html.metapack", + "https://example.com/c", + output / "explorer" / "c" / "%" / "schema.metapack", + output / "schemas" / "c" / "%" / "dependencies.metapack", + output / "schemas" / "c" / "%" / "health.metapack", + output / "schemas" / "c" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack", + output / "schemas" / "a" / "%" / "locations.metapack", + output / "schemas" / "a" / "%" / "positions.metapack", + output / "schemas" / "a" / "%" / "stats.metapack", + output / "schemas" / "a" / "%" / "bundle.metapack", + output / "schemas" / "a" / "%" / "health.metapack", + output / "schemas" / "a" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "a" / "%" / "blaze-fast.metapack", + output / "schemas" / "a" / "%" / "editor.metapack", + output / "schemas" / "a" / "%" / "dependents.metapack", + output / "explorer" / "a" / "%" / "schema.metapack", + output / "explorer" / "a" / "%" / "schema-html.metapack", + output / "schemas" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack", + output / "schemas" / "b" / "%" / "locations.metapack", + output / "schemas" / "b" / "%" / "positions.metapack", + output / "schemas" / "b" / "%" / "stats.metapack", + output / "schemas" / "b" / "%" / "bundle.metapack", + output / "schemas" / "b" / "%" / "health.metapack", + output / "schemas" / "b" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "b" / "%" / "blaze-fast.metapack", + output / "schemas" / "b" / "%" / "editor.metapack", + output / "schemas" / "b" / "%" / "dependents.metapack", + output / "explorer" / "b" / "%" / "schema.metapack", + output / "explorer" / "b" / "%" / "schema-html.metapack", + output / "schemas" / "c" / "%" / "schema.metapack", + output / "schemas" / "c" / "%" / "dependencies.metapack", + output / "schemas" / "c" / "%" / "locations.metapack", + output / "schemas" / "c" / "%" / "positions.metapack", + output / "schemas" / "c" / "%" / "stats.metapack", + output / "schemas" / "c" / "%" / "bundle.metapack", + output / "schemas" / "c" / "%" / "health.metapack", + output / "schemas" / "c" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "c" / "%" / "blaze-fast.metapack", + output / "schemas" / "c" / "%" / "editor.metapack", + output / "schemas" / "c" / "%" / "dependents.metapack", + output / "explorer" / "c" / "%" / "schema.metapack", + output / "explorer" / "c" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, mtime_reverse_dep) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "a" / "%" / "schema.metapack", + {.file_mark = MTIME(10), .dependencies = {}}); + entries.emplace(output / "schemas" / "b" / "%" / "schema.metapack", + {.file_mark = MTIME(50), .dependencies = {}}); + entries.emplace( + output / "schemas" / "b" / "%" / "dependencies.metapack", + {.file_mark = MTIME(100), + .dependencies = {output / "schemas" / "a" / "%" / "schema.metapack"}}); + entries.emplace(output / "explorer" / "a" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "b" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/a", {"/src/a.json", "a", MTIME(30)}}, + {"https://example.com/b", {"/src/b.json", "b", MTIME(30)}}}; + const std::vector changed; + const std::vector removed; + + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 30); + + EXPECT_ACTION(plan, 0, 0, 5, Materialise, + output / "schemas" / "a" / "%" / "schema.metapack", + "https://example.com/a", + std::filesystem::path{"/"} / "src" / "a.json", + output / "configuration.json"); + EXPECT_ACTION(plan, 0, 1, 5, Dependencies, + output / "schemas" / "b" / "%" / "dependencies.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 0, 2, 5, Locations, + output / "schemas" / "b" / "%" / "locations.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 0, 3, 5, Positions, + output / "schemas" / "b" / "%" / "positions.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 0, 4, 5, Stats, + output / "schemas" / "b" / "%" / "stats.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 1, 0, 6, Dependencies, + output / "schemas" / "a" / "%" / "dependencies.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 6, Locations, + output / "schemas" / "a" / "%" / "locations.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 6, Positions, + output / "schemas" / "a" / "%" / "positions.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 6, Stats, + output / "schemas" / "a" / "%" / "stats.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 4, 6, Bundle, + output / "schemas" / "b" / "%" / "bundle.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 1, 5, 6, Health, + output / "schemas" / "b" / "%" / "health.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack"); + + EXPECT_ACTION_UNORDERED( + plan, 2, 0, 7, DependencyTree, output / "dependency-tree.metapack", "", + output / "schemas" / "a" / "%" / "dependencies.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 7, SchemaMetadata, + output / "explorer" / "b" / "%" / "schema.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "health.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 7, Bundle, + output / "schemas" / "a" / "%" / "bundle.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 3, 7, Health, + output / "schemas" / "a" / "%" / "health.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 4, 7, BlazeExhaustive, + output / "schemas" / "b" / "%" / "blaze-exhaustive.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 2, 5, 7, BlazeFast, + output / "schemas" / "b" / "%" / "blaze-fast.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 2, 6, 7, Editor, + output / "schemas" / "b" / "%" / "editor.metapack", + "https://example.com/b", + output / "schemas" / "b" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 3, 0, 6, SchemaMetadata, + output / "explorer" / "a" / "%" / "schema.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "health.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 6, BlazeExhaustive, + output / "schemas" / "a" / "%" / "blaze-exhaustive.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 2, 6, BlazeFast, + output / "schemas" / "a" / "%" / "blaze-fast.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 3, 6, Dependents, + output / "schemas" / "a" / "%" / "dependents.metapack", + "https://example.com/a", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 4, 6, Editor, + output / "schemas" / "a" / "%" / "editor.metapack", + "https://example.com/a", + output / "schemas" / "a" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 5, 6, Dependents, + output / "schemas" / "b" / "%" / "dependents.metapack", + "https://example.com/b", output / "dependency-tree.metapack"); + + EXPECT_ACTION(plan, 4, 0, 4, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "a" / "%" / "schema.metapack"); + EXPECT_ACTION_UNORDERED(plan, 4, 1, 4, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "a" / "%" / "schema.metapack", + output / "explorer" / "b" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 4, WebSchema, + output / "explorer" / "a" / "%" / "schema-html.metapack", + "https://example.com/a", + output / "explorer" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack", + output / "schemas" / "a" / "%" / "health.metapack", + output / "schemas" / "a" / "%" / "dependents.metapack"); + EXPECT_ACTION(plan, 4, 3, 4, WebSchema, + output / "explorer" / "b" / "%" / "schema-html.metapack", + "https://example.com/b", + output / "explorer" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack", + output / "schemas" / "b" / "%" / "health.metapack", + output / "schemas" / "b" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "a" / "%" / "schema.metapack", + output / "schemas" / "a" / "%" / "dependencies.metapack", + output / "schemas" / "a" / "%" / "locations.metapack", + output / "schemas" / "a" / "%" / "positions.metapack", + output / "schemas" / "a" / "%" / "stats.metapack", + output / "schemas" / "a" / "%" / "bundle.metapack", + output / "schemas" / "a" / "%" / "health.metapack", + output / "schemas" / "a" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "a" / "%" / "blaze-fast.metapack", + output / "schemas" / "a" / "%" / "editor.metapack", + output / "schemas" / "a" / "%" / "dependents.metapack", + output / "explorer" / "a" / "%" / "schema.metapack", + output / "explorer" / "a" / "%" / "schema-html.metapack", + output / "schemas" / "b" / "%" / "schema.metapack", + output / "schemas" / "b" / "%" / "dependencies.metapack", + output / "schemas" / "b" / "%" / "locations.metapack", + output / "schemas" / "b" / "%" / "positions.metapack", + output / "schemas" / "b" / "%" / "stats.metapack", + output / "schemas" / "b" / "%" / "bundle.metapack", + output / "schemas" / "b" / "%" / "health.metapack", + output / "schemas" / "b" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "b" / "%" / "blaze-fast.metapack", + output / "schemas" / "b" / "%" / "editor.metapack", + output / "schemas" / "b" / "%" / "dependents.metapack", + output / "explorer" / "b" / "%" / "schema.metapack", + output / "explorer" / "b" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, incremental_evaluate_false_removes_existing_blaze) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / + "blaze-exhaustive.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100), false}}}; + const std::vector changed{"/src/foo.json"}; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 18); + + EXPECT_ACTION(plan, 0, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 2, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 3, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 3, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 2, 3, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 4, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 1, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_ACTION(plan, 7, 0, 2, Remove, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + ""); + EXPECT_ACTION(plan, 7, 1, 2, Remove, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", ""); + + EXPECT_TOTAL_FILES(plan, entries, output / "version.json", + output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, headless_full_empty_registry) { + const std::filesystem::path output{"/output"}; + const sourcemeta::one::BuildState entries; + const sourcemeta::one::Resolver::Views schemas; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas, + "1.0.0", "", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON{nullptr}, changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 3, 6); + + EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json", + ""); + EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0"); + + EXPECT_ACTION(plan, 1, 0, 3, DependencyTree, + output / "dependency-tree.metapack", ""); + EXPECT_ACTION(plan, 1, 1, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", ""); + EXPECT_ACTION(plan, 1, 2, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", ""); + + EXPECT_ACTION(plan, 2, 0, 1, Routes, output / "routes.bin", "Headless", + output / "configuration.json"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "configuration.json", output / "version.json", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", output / "routes.bin"); +} + +TEST(Build_delta, headless_full_single_schema) { + const std::filesystem::path output{"/output"}; + const sourcemeta::one::BuildState entries; + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas, + "1.0.0", "", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON{nullptr}, changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 7, 18); + + EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json", + ""); + EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0"); + + EXPECT_ACTION(plan, 1, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 2, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 3, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 4, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 4, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 4, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 4, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 4, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 5, 0, 2, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 1, 2, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, Routes, output / "routes.bin", "Headless", + output / "configuration.json"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "configuration.json", output / "version.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", output / "routes.bin"); +} + +TEST(Build_delta, headless_incremental) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}}; + const std::vector changed{"/src/foo.json"}; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas, + "1.0.0", "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 5, 15); + + EXPECT_ACTION(plan, 0, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 2, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 4, 0, 2, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 1, 2, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack"); +} + +TEST(Build_delta, full_to_headless_removes_web) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "foo" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "foo" / "%" / "schema-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(200)}}}; + const std::vector changed{"/src/foo.json"}; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas, + "1.0.0", "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 7, 20); + + EXPECT_ACTION(plan, 0, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 2, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 4, 0, 2, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 1, 2, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, Routes, output / "routes.bin", "Headless", + output / "configuration.json"); + + EXPECT_ACTION(plan, 6, 0, 4, Remove, + output / "explorer" / "%" / "404.metapack", ""); + EXPECT_ACTION(plan, 6, 1, 4, Remove, + output / "explorer" / "%" / "directory-html.metapack", ""); + EXPECT_ACTION(plan, 6, 2, 4, Remove, + output / "explorer" / "foo" / "%" / "directory-html.metapack", + ""); + EXPECT_ACTION(plan, 6, 3, 4, Remove, + output / "explorer" / "foo" / "%" / "schema-html.metapack", ""); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", output / "routes.bin"); +} + +TEST(Build_delta, headless_to_full_incremental) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}}; + const std::vector changed{"/src/foo.json"}; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 18); + + EXPECT_ACTION(plan, 0, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 2, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 4, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 1, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, headless_to_full_full_rebuild) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / "dependencies.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / "locations.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / "positions.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / "stats.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / "bundle.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / "health.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / "editor.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / + "blaze-exhaustive.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "foo" / "%" / "schema.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "schemas" / "foo" / "%" / "dependents.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "foo" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(200)}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 18); + + EXPECT_ACTION(plan, 0, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 2, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 3, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 4, 0, 3, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 1, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 3, WebSchema, + output / "explorer" / "foo" / "%" / "schema-html.metapack", + "https://example.com/foo", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack"); + + EXPECT_ACTION(plan, 5, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, Routes, output / "routes.bin", "Full", + output / "configuration.json"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "explorer" / "foo" / "%" / "schema-html.metapack", + output / "explorer" / "foo" / "%" / "directory.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, full_to_headless_full_rebuild) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "foo" / "%" / "schema-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas, + "2.0.0", "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 8, 21); + + EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json", + ""); + EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "2.0.0"); + + EXPECT_ACTION(plan, 1, 0, 1, Materialise, + output / "schemas" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + std::filesystem::path{"/"} / "src" / "foo.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 2, 0, 4, Dependencies, + output / "schemas" / "foo" / "%" / "dependencies.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 1, 4, Locations, + output / "schemas" / "foo" / "%" / "locations.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 2, 4, Positions, + output / "schemas" / "foo" / "%" / "positions.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 2, 3, 4, Stats, + output / "schemas" / "foo" / "%" / "stats.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 3, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 3, Bundle, + output / "schemas" / "foo" / "%" / "bundle.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 2, 3, Health, + output / "schemas" / "foo" / "%" / "health.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + + EXPECT_ACTION(plan, 4, 0, 5, SchemaMetadata, + output / "explorer" / "foo" / "%" / "schema.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 4, 1, 5, BlazeExhaustive, + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 4, 2, 5, BlazeFast, + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + EXPECT_ACTION(plan, 4, 3, 5, Dependents, + output / "schemas" / "foo" / "%" / "dependents.metapack", + "https://example.com/foo", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 4, 4, 5, Editor, + output / "schemas" / "foo" / "%" / "editor.metapack", + "https://example.com/foo", + output / "schemas" / "foo" / "%" / "bundle.metapack"); + + EXPECT_ACTION(plan, 5, 0, 2, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + EXPECT_ACTION(plan, 5, 1, 2, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "foo" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, Routes, output / "routes.bin", "Headless", + output / "configuration.json"); + + EXPECT_ACTION(plan, 7, 0, 3, Remove, + output / "explorer" / "%" / "404.metapack", ""); + EXPECT_ACTION(plan, 7, 1, 3, Remove, + output / "explorer" / "%" / "directory-html.metapack", ""); + EXPECT_ACTION(plan, 7, 2, 3, Remove, + output / "explorer" / "foo" / "%" / "schema-html.metapack", ""); + + EXPECT_TOTAL_FILES( + plan, entries, output / "configuration.json", output / "version.json", + output / "schemas" / "foo" / "%" / "schema.metapack", + output / "schemas" / "foo" / "%" / "dependencies.metapack", + output / "schemas" / "foo" / "%" / "locations.metapack", + output / "schemas" / "foo" / "%" / "positions.metapack", + output / "schemas" / "foo" / "%" / "stats.metapack", + output / "schemas" / "foo" / "%" / "bundle.metapack", + output / "schemas" / "foo" / "%" / "health.metapack", + output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack", + output / "schemas" / "foo" / "%" / "blaze-fast.metapack", + output / "schemas" / "foo" / "%" / "editor.metapack", + output / "schemas" / "foo" / "%" / "dependents.metapack", + output / "explorer" / "foo" / "%" / "schema.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", output / "routes.bin"); +} + +TEST(Build_delta, full_single_schema_nested_path_headless) { + const std::filesystem::path output{"/output"}; + const sourcemeta::one::BuildState entries; + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/test", + {"/src/test.json", "example/test", MTIME(100)}}}; + const std::vector changed; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas, + "1.0.0", "", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON{nullptr}, changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 8, 19); + + EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json", + ""); + EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0"); + + EXPECT_ACTION(plan, 1, 0, 1, Materialise, + output / "schemas" / "example" / "test" / "%" / + "schema.metapack", + "https://example.com/test", + std::filesystem::path{"/"} / "src" / "test.json", + output / "configuration.json"); + + EXPECT_ACTION( + plan, 2, 0, 4, Dependencies, + output / "schemas" / "example" / "test" / "%" / "dependencies.metapack", + "https://example.com/test", + output / "schemas" / "example" / "test" / "%" / "schema.metapack"); + EXPECT_ACTION( + plan, 2, 1, 4, Locations, + output / "schemas" / "example" / "test" / "%" / "locations.metapack", + "https://example.com/test", + output / "schemas" / "example" / "test" / "%" / "schema.metapack"); + EXPECT_ACTION( + plan, 2, 2, 4, Positions, + output / "schemas" / "example" / "test" / "%" / "positions.metapack", + "https://example.com/test", + output / "schemas" / "example" / "test" / "%" / "schema.metapack"); + EXPECT_ACTION( + plan, 2, 3, 4, Stats, + output / "schemas" / "example" / "test" / "%" / "stats.metapack", + "https://example.com/test", + output / "schemas" / "example" / "test" / "%" / "schema.metapack"); + + EXPECT_ACTION( + plan, 3, 0, 3, DependencyTree, output / "dependency-tree.metapack", "", + output / "schemas" / "example" / "test" / "%" / "dependencies.metapack"); + EXPECT_ACTION( + plan, 3, 1, 3, Bundle, + output / "schemas" / "example" / "test" / "%" / "bundle.metapack", + "https://example.com/test", + output / "schemas" / "example" / "test" / "%" / "schema.metapack", + output / "schemas" / "example" / "test" / "%" / "dependencies.metapack"); + EXPECT_ACTION( + plan, 3, 2, 3, Health, + output / "schemas" / "example" / "test" / "%" / "health.metapack", + "https://example.com/test", + output / "schemas" / "example" / "test" / "%" / "schema.metapack", + output / "schemas" / "example" / "test" / "%" / "dependencies.metapack"); + + EXPECT_ACTION( + plan, 4, 0, 5, SchemaMetadata, + output / "explorer" / "example" / "test" / "%" / "schema.metapack", + "https://example.com/test", + output / "schemas" / "example" / "test" / "%" / "schema.metapack", + output / "schemas" / "example" / "test" / "%" / "health.metapack", + output / "schemas" / "example" / "test" / "%" / "dependencies.metapack"); + EXPECT_ACTION(plan, 4, 1, 5, BlazeExhaustive, + output / "schemas" / "example" / "test" / "%" / + "blaze-exhaustive.metapack", + "https://example.com/test", + output / "schemas" / "example" / "test" / "%" / + "bundle.metapack"); + EXPECT_ACTION( + plan, 4, 2, 5, BlazeFast, + output / "schemas" / "example" / "test" / "%" / "blaze-fast.metapack", + "https://example.com/test", + output / "schemas" / "example" / "test" / "%" / "bundle.metapack"); + EXPECT_ACTION( + plan, 4, 3, 5, Dependents, + output / "schemas" / "example" / "test" / "%" / "dependents.metapack", + "https://example.com/test", output / "dependency-tree.metapack"); + EXPECT_ACTION( + plan, 4, 4, 5, Editor, + output / "schemas" / "example" / "test" / "%" / "editor.metapack", + "https://example.com/test", + output / "schemas" / "example" / "test" / "%" / "bundle.metapack"); + + EXPECT_ACTION( + plan, 5, 0, 2, SearchIndex, output / "explorer" / "%" / "search.metapack", + "", output / "explorer" / "example" / "test" / "%" / "schema.metapack"); + EXPECT_ACTION( + plan, 5, 1, 2, DirectoryList, + output / "explorer" / "example" / "%" / "directory.metapack", "", + output / "explorer" / "example" / "test" / "%" / "schema.metapack"); + + EXPECT_ACTION(plan, 6, 0, 1, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "example" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 7, 0, 1, Routes, output / "routes.bin", "Headless", + output / "configuration.json"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "configuration.json", output / "version.json", + output / "schemas" / "example" / "test" / "%" / "schema.metapack", + output / "schemas" / "example" / "test" / "%" / "dependencies.metapack", + output / "schemas" / "example" / "test" / "%" / "locations.metapack", + output / "schemas" / "example" / "test" / "%" / "positions.metapack", + output / "schemas" / "example" / "test" / "%" / "stats.metapack", + output / "schemas" / "example" / "test" / "%" / "bundle.metapack", + output / "schemas" / "example" / "test" / "%" / "health.metapack", + output / "schemas" / "example" / "test" / "%" / + "blaze-exhaustive.metapack", + output / "schemas" / "example" / "test" / "%" / "blaze-fast.metapack", + output / "schemas" / "example" / "test" / "%" / "editor.metapack", + output / "schemas" / "example" / "test" / "%" / "dependents.metapack", + output / "explorer" / "example" / "test" / "%" / "schema.metapack", + output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "example" / "%" / "directory.metapack", + output / "routes.bin"); +} + +TEST(Build_delta, incremental_add_schema_preserves_intermediate_dirs) { + const std::filesystem::path output{"/output"}; + sourcemeta::one::BuildState entries; + + entries.emplace(output / "version.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "configuration.json", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "routes.bin", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "dependency-tree.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "search.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "404.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "%" / "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "example" / "%" / "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "example" / "%" / + "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "example" / "schemas" / "%" / + "directory.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + entries.emplace(output / "explorer" / "example" / "schemas" / "%" / + "directory-html.metapack", + {.file_mark = MTIME(100), .dependencies = {}}); + ADD_SCHEMA_ENTRIES(entries, output, "example/schemas/a", true, true, + MTIME(100)); + ADD_SCHEMA_ENTRIES(entries, output, "example/schemas/b", true, true, + MTIME(100)); + + const sourcemeta::one::Resolver::Views schemas{ + {"https://example.com/a", + {"/src/a.json", "example/schemas/a", MTIME(100)}}, + {"https://example.com/b", + {"/src/b.json", "example/schemas/b", MTIME(100)}}, + {"https://example.com/c", + {"/src/c.json", "example/schemas/c", MTIME(200)}}}; + const std::vector changed{"/src/c.json"}; + const std::vector removed; + const auto plan{sourcemeta::one::delta( + sourcemeta::one::BuildPlan::Type::Full, entries, output, schemas, "1.0.0", + "1.0.0", "", sourcemeta::core::JSON::make_object(), + sourcemeta::core::JSON::make_object(), changed, removed)}; + + EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 21); + + EXPECT_ACTION(plan, 0, 0, 1, Materialise, + output / "schemas" / "example" / "schemas" / "c" / "%" / + "schema.metapack", + "https://example.com/c", + std::filesystem::path{"/"} / "src" / "c.json", + output / "configuration.json"); + + EXPECT_ACTION(plan, 1, 0, 4, Dependencies, + output / "schemas" / "example" / "schemas" / "c" / "%" / + "dependencies.metapack", + "https://example.com/c", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "schema.metapack"); + EXPECT_ACTION(plan, 1, 1, 4, Locations, + output / "schemas" / "example" / "schemas" / "c" / "%" / + "locations.metapack", + "https://example.com/c", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "schema.metapack"); + EXPECT_ACTION(plan, 1, 2, 4, Positions, + output / "schemas" / "example" / "schemas" / "c" / "%" / + "positions.metapack", + "https://example.com/c", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "schema.metapack"); + EXPECT_ACTION(plan, 1, 3, 4, Stats, + output / "schemas" / "example" / "schemas" / "c" / "%" / + "stats.metapack", + "https://example.com/c", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "schema.metapack"); + + EXPECT_ACTION(plan, 2, 0, 3, DependencyTree, + output / "dependency-tree.metapack", "", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "dependencies.metapack", + output / "schemas" / "example" / "schemas" / "b" / "%" / + "dependencies.metapack", + output / "schemas" / "example" / "schemas" / "a" / "%" / + "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 1, 3, Bundle, + output / "schemas" / "example" / "schemas" / "c" / "%" / + "bundle.metapack", + "https://example.com/c", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "schema.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "dependencies.metapack"); + EXPECT_ACTION(plan, 2, 2, 3, Health, + output / "schemas" / "example" / "schemas" / "c" / "%" / + "health.metapack", + "https://example.com/c", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "schema.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "dependencies.metapack"); + + EXPECT_ACTION(plan, 3, 0, 5, SchemaMetadata, + output / "explorer" / "example" / "schemas" / "c" / "%" / + "schema.metapack", + "https://example.com/c", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "schema.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "health.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "dependencies.metapack"); + EXPECT_ACTION(plan, 3, 1, 5, BlazeExhaustive, + output / "schemas" / "example" / "schemas" / "c" / "%" / + "blaze-exhaustive.metapack", + "https://example.com/c", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "bundle.metapack"); + EXPECT_ACTION(plan, 3, 2, 5, BlazeFast, + output / "schemas" / "example" / "schemas" / "c" / "%" / + "blaze-fast.metapack", + "https://example.com/c", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "bundle.metapack"); + EXPECT_ACTION(plan, 3, 3, 5, Dependents, + output / "schemas" / "example" / "schemas" / "c" / "%" / + "dependents.metapack", + "https://example.com/c", output / "dependency-tree.metapack"); + EXPECT_ACTION(plan, 3, 4, 5, Editor, + output / "schemas" / "example" / "schemas" / "c" / "%" / + "editor.metapack", + "https://example.com/c", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "bundle.metapack"); + + EXPECT_ACTION(plan, 4, 0, 3, SearchIndex, + output / "explorer" / "%" / "search.metapack", "", + output / "explorer" / "example" / "schemas" / "c" / "%" / + "schema.metapack", + output / "explorer" / "example" / "schemas" / "b" / "%" / + "schema.metapack", + output / "explorer" / "example" / "schemas" / "a" / "%" / + "schema.metapack"); + EXPECT_ACTION(plan, 4, 1, 3, DirectoryList, + output / "explorer" / "example" / "schemas" / "%" / + "directory.metapack", + "", + output / "explorer" / "example" / "schemas" / "c" / "%" / + "schema.metapack"); + EXPECT_ACTION(plan, 4, 2, 3, WebSchema, + output / "explorer" / "example" / "schemas" / "c" / "%" / + "schema-html.metapack", + "https://example.com/c", + output / "explorer" / "example" / "schemas" / "c" / "%" / + "schema.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "dependencies.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "health.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "dependents.metapack"); + + EXPECT_ACTION( + plan, 5, 0, 2, DirectoryList, + output / "explorer" / "example" / "%" / "directory.metapack", "", + output / "explorer" / "example" / "schemas" / "%" / "directory.metapack"); + EXPECT_ACTION(plan, 5, 1, 2, WebDirectory, + output / "explorer" / "example" / "schemas" / "%" / + "directory-html.metapack", + "", + output / "explorer" / "example" / "schemas" / "%" / + "directory.metapack"); + + EXPECT_ACTION(plan, 6, 0, 2, DirectoryList, + output / "explorer" / "%" / "directory.metapack", "", + output / "explorer" / "example" / "%" / "directory.metapack"); + EXPECT_ACTION( + plan, 6, 1, 2, WebDirectory, + output / "explorer" / "example" / "%" / "directory-html.metapack", "", + output / "explorer" / "example" / "%" / "directory.metapack"); + + EXPECT_ACTION(plan, 7, 0, 1, WebIndex, + output / "explorer" / "%" / "directory-html.metapack", "", + output / "explorer" / "%" / "directory.metapack"); + + EXPECT_TOTAL_FILES( + plan, entries, output / "version.json", output / "configuration.json", + output / "routes.bin", output / "dependency-tree.metapack", + output / "explorer" / "%" / "search.metapack", + output / "explorer" / "%" / "directory.metapack", + output / "explorer" / "%" / "404.metapack", + output / "explorer" / "%" / "directory-html.metapack", + output / "explorer" / "example" / "%" / "directory.metapack", + output / "explorer" / "example" / "%" / "directory-html.metapack", + output / "explorer" / "example" / "schemas" / "%" / "directory.metapack", + output / "explorer" / "example" / "schemas" / "%" / + "directory-html.metapack", + output / "schemas" / "example" / "schemas" / "a" / "%" / + "schema.metapack", + output / "schemas" / "example" / "schemas" / "a" / "%" / + "dependencies.metapack", + output / "schemas" / "example" / "schemas" / "a" / "%" / + "locations.metapack", + output / "schemas" / "example" / "schemas" / "a" / "%" / + "positions.metapack", + output / "schemas" / "example" / "schemas" / "a" / "%" / "stats.metapack", + output / "schemas" / "example" / "schemas" / "a" / "%" / + "bundle.metapack", + output / "schemas" / "example" / "schemas" / "a" / "%" / + "health.metapack", + output / "schemas" / "example" / "schemas" / "a" / "%" / + "editor.metapack", + output / "schemas" / "example" / "schemas" / "a" / "%" / + "dependents.metapack", + output / "explorer" / "example" / "schemas" / "a" / "%" / + "schema.metapack", + output / "schemas" / "example" / "schemas" / "a" / "%" / + "blaze-exhaustive.metapack", + output / "schemas" / "example" / "schemas" / "a" / "%" / + "blaze-fast.metapack", + output / "explorer" / "example" / "schemas" / "a" / "%" / + "schema-html.metapack", + output / "explorer" / "example" / "schemas" / "a" / "%" / + "directory-html.metapack", + output / "schemas" / "example" / "schemas" / "b" / "%" / + "schema.metapack", + output / "schemas" / "example" / "schemas" / "b" / "%" / + "dependencies.metapack", + output / "schemas" / "example" / "schemas" / "b" / "%" / + "locations.metapack", + output / "schemas" / "example" / "schemas" / "b" / "%" / + "positions.metapack", + output / "schemas" / "example" / "schemas" / "b" / "%" / "stats.metapack", + output / "schemas" / "example" / "schemas" / "b" / "%" / + "bundle.metapack", + output / "schemas" / "example" / "schemas" / "b" / "%" / + "health.metapack", + output / "schemas" / "example" / "schemas" / "b" / "%" / + "editor.metapack", + output / "schemas" / "example" / "schemas" / "b" / "%" / + "dependents.metapack", + output / "explorer" / "example" / "schemas" / "b" / "%" / + "schema.metapack", + output / "schemas" / "example" / "schemas" / "b" / "%" / + "blaze-exhaustive.metapack", + output / "schemas" / "example" / "schemas" / "b" / "%" / + "blaze-fast.metapack", + output / "explorer" / "example" / "schemas" / "b" / "%" / + "schema-html.metapack", + output / "explorer" / "example" / "schemas" / "b" / "%" / + "directory-html.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "schema.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "dependencies.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "locations.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "positions.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / "stats.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "bundle.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "health.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "editor.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "dependents.metapack", + output / "explorer" / "example" / "schemas" / "c" / "%" / + "schema.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "blaze-exhaustive.metapack", + output / "schemas" / "example" / "schemas" / "c" / "%" / + "blaze-fast.metapack", + output / "explorer" / "example" / "schemas" / "c" / "%" / + "schema-html.metapack"); +} diff --git a/test/unit/build/build_e2e_test.cc b/test/unit/build/build_e2e_test.cc deleted file mode 100644 index 98c400eec..000000000 --- a/test/unit/build/build_e2e_test.cc +++ /dev/null @@ -1,651 +0,0 @@ -#include - -#include - -#include -#include // std::chrono::seconds -#include - -#include "build_test_utils.h" - -auto read_uint64_t(const std::filesystem::path &path) -> std::uint64_t { - return std::stoull(READ_FILE(path)); -} - -auto write_uint64_t(const std::filesystem::path &path, - const std::uint64_t input) -> void { - WRITE_FILE(path, std::to_string(input)); -} - -static auto -handler_multiply(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &, - const std::uint64_t &value) -> void { - assert(dependencies.size() == 1); - write_uint64_t(destination, - read_uint64_t(dependencies.front().get()) * value); -} - -static auto -handler_sum(const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &, - const std::nullptr_t &) -> void { - std::uint64_t total{0}; - for (const auto &dependency : dependencies) { - total += read_uint64_t(dependency.get()); - } - - write_uint64_t(destination, total); -} - -static auto handler_mirror_context_node( - const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &, - const sourcemeta::one::Build::DynamicCallback &callback, - const std::filesystem::path &context) -> void { - const auto input{read_uint64_t(context)}; - callback(context); - write_uint64_t(destination, input); -} - -static auto handler_mirror_context_node_without_callback( - const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &, - const sourcemeta::one::Build::DynamicCallback &, - const std::filesystem::path &context) -> void { - const auto input{read_uint64_t(context)}; - write_uint64_t(destination, input); -} - -static auto handler_sum_with_dynamic( - const std::filesystem::path &destination, - const sourcemeta::one::Build::Dependencies &dependencies, - const sourcemeta::one::Build::DynamicCallback &callback, - const std::filesystem::path &context) -> void { - std::uint64_t total{0}; - for (const auto &dependency : dependencies) { - total += read_uint64_t(dependency.get()); - } - - total += read_uint64_t(context); - callback(context); - write_uint64_t(destination, total); -} - -TEST(Build_e2e, simple_cache_miss_hit) { - const auto base_path{std::filesystem::path{BINARY_DIRECTORY} / - "simple_cache_miss_hit"}; - std::filesystem::remove_all(base_path); - const auto output_path{base_path / "output"}; - const auto input_path{base_path / "input"}; - std::filesystem::create_directories(input_path); - write_uint64_t(input_path / "initial.txt", 8); - - const auto initial_txt{input_path / "initial.txt"}; - const auto first_txt{output_path / "first.txt"}; - const auto second_txt{output_path / "second.txt"}; - - // First run: build first.txt and second.txt (cache miss) - { - sourcemeta::one::Build build{output_path}; - build.refresh(initial_txt); - - const auto result_1 = build.dispatch( - handler_multiply, first_txt, static_cast(2), - initial_txt); - EXPECT_TRUE(result_1); - EXPECT_EQ(read_uint64_t(first_txt), 16); - - const auto result_2 = build.dispatch( - handler_multiply, second_txt, static_cast(3), first_txt); - EXPECT_TRUE(result_2); - EXPECT_EQ(read_uint64_t(second_txt), 48); - - build.finish(); - } - - // Second run: same inputs, should cache hit - { - sourcemeta::one::Build build{output_path}; - - const auto result_1 = build.dispatch( - handler_multiply, first_txt, static_cast(2), - initial_txt); - EXPECT_FALSE(result_1); - EXPECT_EQ(read_uint64_t(first_txt), 16); - - const auto result_2 = build.dispatch( - handler_multiply, second_txt, static_cast(3), first_txt); - EXPECT_FALSE(result_2); - EXPECT_EQ(read_uint64_t(second_txt), 48); - - build.finish(); - } - - // Third run: update initial.txt, should cache miss - write_uint64_t(initial_txt, 7); - std::filesystem::last_write_time( - initial_txt, - std::filesystem::file_time_type::clock::now() + std::chrono::seconds(10)); - - { - sourcemeta::one::Build build{output_path}; - - const auto result_1 = build.dispatch( - handler_multiply, first_txt, static_cast(2), - initial_txt); - EXPECT_TRUE(result_1); - EXPECT_EQ(read_uint64_t(first_txt), 14); - - const auto result_2 = build.dispatch( - handler_multiply, second_txt, static_cast(3), first_txt); - EXPECT_TRUE(result_2); - EXPECT_EQ(read_uint64_t(second_txt), 42); - } -} - -TEST(Build_e2e, dynamic_dependency) { - const auto base_path{std::filesystem::path{BINARY_DIRECTORY} / - "dynamic_dependency"}; - std::filesystem::remove_all(base_path); - const auto output_path{base_path / "output"}; - const auto input_path{base_path / "input"}; - std::filesystem::create_directories(input_path); - write_uint64_t(input_path / "initial.txt", 8); - - const auto initial_txt{input_path / "initial.txt"}; - const auto copy_txt{output_path / "copy.txt"}; - - // First run: cache miss, dynamic dependency registered - { - sourcemeta::one::Build build{output_path}; - build.refresh(initial_txt); - - const auto result = build.dispatch( - handler_mirror_context_node, copy_txt, initial_txt); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(copy_txt), 8); - - build.finish(); - } - - // Second run: cache hit because dynamic dependency was registered - { - sourcemeta::one::Build build{output_path}; - - const auto result = build.dispatch( - handler_mirror_context_node, copy_txt, initial_txt); - EXPECT_FALSE(result); - EXPECT_EQ(read_uint64_t(copy_txt), 8); - } -} - -TEST(Build_e2e, missing_dynamic_dependency) { - const auto base_path{std::filesystem::path{BINARY_DIRECTORY} / - "missing_dynamic_dependency"}; - std::filesystem::remove_all(base_path); - const auto output_path{base_path / "output"}; - const auto input_path{base_path / "input"}; - std::filesystem::create_directories(input_path); - write_uint64_t(input_path / "initial.txt", 8); - - const auto initial_txt{input_path / "initial.txt"}; - const auto copy_txt{output_path / "copy.txt"}; - - // First run: cache miss, dynamic dependency NOT registered - { - sourcemeta::one::Build build{output_path}; - build.refresh(initial_txt); - - const auto result = build.dispatch( - handler_mirror_context_node_without_callback, copy_txt, initial_txt); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(copy_txt), 8); - - build.finish(); - } - - // Second run: cache miss because dynamic dependency was not registered - { - sourcemeta::one::Build build{output_path}; - - const auto result = build.dispatch( - handler_mirror_context_node_without_callback, copy_txt, initial_txt); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(copy_txt), 8); - } -} - -TEST(Build_e2e, new_dependency_invalidates) { - const auto base_path{std::filesystem::path{BINARY_DIRECTORY} / - "new_dependency_invalidates"}; - std::filesystem::remove_all(base_path); - const auto output_path{base_path / "output"}; - const auto input_path{base_path / "input"}; - std::filesystem::create_directories(input_path); - write_uint64_t(input_path / "dep_a.txt", 10); - - const auto dep_a{input_path / "dep_a.txt"}; - const auto dep_b{input_path / "dep_b.txt"}; - const auto target{output_path / "target.txt"}; - - // First run: build with one dependency - { - sourcemeta::one::Build build{output_path}; - build.refresh(dep_a); - - const auto result = - build.dispatch(handler_sum, target, nullptr, dep_a); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(target), 10); - - build.finish(); - } - - // Second run: same dependency, cache hit - { - sourcemeta::one::Build build{output_path}; - - const auto result = - build.dispatch(handler_sum, target, nullptr, dep_a); - EXPECT_FALSE(result); - EXPECT_EQ(read_uint64_t(target), 10); - - build.finish(); - } - - // Third run: add a new dependency, cache miss - write_uint64_t(dep_b, 20); - - { - sourcemeta::one::Build build{output_path}; - build.refresh(dep_b); - - const auto result = build.dispatch(handler_sum, target, - nullptr, dep_a, dep_b); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(target), 30); - } -} - -TEST(Build_e2e, removed_make_dependency_invalidates) { - const auto base_path{std::filesystem::path{BINARY_DIRECTORY} / - "removed_make_dependency_invalidates"}; - std::filesystem::remove_all(base_path); - const auto output_path{base_path / "output"}; - const auto input_path{base_path / "input"}; - std::filesystem::create_directories(input_path); - write_uint64_t(input_path / "dep_a.txt", 10); - write_uint64_t(input_path / "dep_b.txt", 20); - - const auto dep_a{input_path / "dep_a.txt"}; - const auto dep_b{input_path / "dep_b.txt"}; - const auto target{output_path / "target.txt"}; - - // First run: build with two dependencies - { - sourcemeta::one::Build build{output_path}; - build.refresh(dep_a); - build.refresh(dep_b); - - const auto result = build.dispatch(handler_sum, target, - nullptr, dep_a, dep_b); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(target), 30); - - build.finish(); - } - - // Second run: same two dependencies, cache hit - { - sourcemeta::one::Build build{output_path}; - - const auto result = build.dispatch(handler_sum, target, - nullptr, dep_a, dep_b); - EXPECT_FALSE(result); - EXPECT_EQ(read_uint64_t(target), 30); - - build.finish(); - } - - // Third run: remove dep_b, cache miss - { - sourcemeta::one::Build build{output_path}; - - const auto result = - build.dispatch(handler_sum, target, nullptr, dep_a); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(target), 10); - } -} - -TEST(Build_e2e, remove_all_static_dependencies_invalidates) { - const auto base_path{std::filesystem::path{BINARY_DIRECTORY} / - "remove_all_static_deps_invalidates"}; - std::filesystem::remove_all(base_path); - const auto output_path{base_path / "output"}; - const auto input_path{base_path / "input"}; - std::filesystem::create_directories(input_path); - write_uint64_t(input_path / "dep_a.txt", 10); - - const auto dep_a{input_path / "dep_a.txt"}; - const auto target{output_path / "target.txt"}; - - // First run: build with one dependency - { - sourcemeta::one::Build build{output_path}; - build.refresh(dep_a); - - const auto result = - build.dispatch(handler_sum, target, nullptr, dep_a); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(target), 10); - - build.finish(); - } - - // Second run: same dependency, cache hit - { - sourcemeta::one::Build build{output_path}; - - const auto result = - build.dispatch(handler_sum, target, nullptr, dep_a); - EXPECT_FALSE(result); - EXPECT_EQ(read_uint64_t(target), 10); - - build.finish(); - } - - // Third run: remove all static deps, cache miss - { - sourcemeta::one::Build build{output_path}; - - const auto result = - build.dispatch(handler_sum, target, nullptr); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(target), 0); - } -} - -TEST(Build_e2e, replaced_make_dependency_invalidates) { - const auto base_path{std::filesystem::path{BINARY_DIRECTORY} / - "replaced_make_dependency_invalidates"}; - std::filesystem::remove_all(base_path); - const auto output_path{base_path / "output"}; - const auto input_path{base_path / "input"}; - std::filesystem::create_directories(input_path); - write_uint64_t(input_path / "dep_a.txt", 10); - write_uint64_t(input_path / "dep_b.txt", 20); - - const auto dep_a{input_path / "dep_a.txt"}; - const auto dep_b{input_path / "dep_b.txt"}; - const auto target{output_path / "target.txt"}; - - // First run: build with dep_a - { - sourcemeta::one::Build build{output_path}; - build.refresh(dep_a); - build.refresh(dep_b); - - const auto result = - build.dispatch(handler_sum, target, nullptr, dep_a); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(target), 10); - - build.finish(); - } - - // Second run: same dep_a, cache hit - { - sourcemeta::one::Build build{output_path}; - - const auto result = - build.dispatch(handler_sum, target, nullptr, dep_a); - EXPECT_FALSE(result); - EXPECT_EQ(read_uint64_t(target), 10); - - build.finish(); - } - - // Third run: replace dep_a with dep_b, cache miss - { - sourcemeta::one::Build build{output_path}; - - const auto result = - build.dispatch(handler_sum, target, nullptr, dep_b); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(target), 20); - } -} - -TEST(Build_e2e, reordered_static_dependencies_invalidates) { - const auto base_path{std::filesystem::path{BINARY_DIRECTORY} / - "reordered_static_deps_invalidates"}; - std::filesystem::remove_all(base_path); - const auto output_path{base_path / "output"}; - const auto input_path{base_path / "input"}; - std::filesystem::create_directories(input_path); - write_uint64_t(input_path / "dep_a.txt", 10); - write_uint64_t(input_path / "dep_b.txt", 20); - - const auto dep_a{input_path / "dep_a.txt"}; - const auto dep_b{input_path / "dep_b.txt"}; - const auto target{output_path / "target.txt"}; - - // First run: build with {a, b} - { - sourcemeta::one::Build build{output_path}; - build.refresh(dep_a); - build.refresh(dep_b); - - const auto result = build.dispatch(handler_sum, target, - nullptr, dep_a, dep_b); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(target), 30); - - build.finish(); - } - - // Second run: same {a, b}, cache hit - { - sourcemeta::one::Build build{output_path}; - - const auto result = build.dispatch(handler_sum, target, - nullptr, dep_a, dep_b); - EXPECT_FALSE(result); - EXPECT_EQ(read_uint64_t(target), 30); - - build.finish(); - } - - // Third run: reorder to {b, a}, cache miss - { - sourcemeta::one::Build build{output_path}; - - const auto result = build.dispatch(handler_sum, target, - nullptr, dep_b, dep_a); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(target), 30); - } -} - -TEST(Build_e2e, dynamic_deps_do_not_interfere_with_static_comparison) { - const auto base_path{std::filesystem::path{BINARY_DIRECTORY} / - "dynamic_no_interfere_static"}; - std::filesystem::remove_all(base_path); - const auto output_path{base_path / "output"}; - const auto input_path{base_path / "input"}; - std::filesystem::create_directories(input_path); - write_uint64_t(input_path / "dep_static.txt", 10); - write_uint64_t(input_path / "dep_dynamic.txt", 5); - - const auto dep_static{input_path / "dep_static.txt"}; - const auto dep_dynamic{input_path / "dep_dynamic.txt"}; - const auto target{output_path / "target.txt"}; - - // First run: build with one static dep and one dynamic dep - { - sourcemeta::one::Build build{output_path}; - build.refresh(dep_static); - build.refresh(dep_dynamic); - - const auto result = build.dispatch( - handler_sum_with_dynamic, target, dep_dynamic, dep_static); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(target), 15); - - build.finish(); - } - - // Second run: same static dep, cache hit - { - sourcemeta::one::Build build{output_path}; - - const auto result = build.dispatch( - handler_sum_with_dynamic, target, dep_dynamic, dep_static); - EXPECT_FALSE(result); - EXPECT_EQ(read_uint64_t(target), 15); - } -} - -TEST(Build_e2e, persistence_across_runs) { - const auto base_path{std::filesystem::path{BINARY_DIRECTORY} / - "persistence_across_runs"}; - std::filesystem::remove_all(base_path); - const auto output_path{base_path / "output"}; - const auto input_path{base_path / "input"}; - std::filesystem::create_directories(input_path); - write_uint64_t(input_path / "initial.txt", 8); - - const auto initial_txt{input_path / "initial.txt"}; - const auto first_txt{output_path / "first.txt"}; - - // First run: build and write dependencies - { - sourcemeta::one::Build build{output_path}; - build.refresh(initial_txt); - - const auto result = build.dispatch( - handler_multiply, first_txt, static_cast(2), - initial_txt); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(first_txt), 16); - - build.finish(); - } - - // Second run: new instance reads saved state, should cache hit - { - sourcemeta::one::Build build{output_path}; - - const auto result = build.dispatch( - handler_multiply, first_txt, static_cast(2), - initial_txt); - EXPECT_FALSE(result); - EXPECT_EQ(read_uint64_t(first_txt), 16); - } -} - -TEST(Build_e2e, persistence_invalidates_on_change) { - const auto base_path{std::filesystem::path{BINARY_DIRECTORY} / - "persistence_invalidates_on_change"}; - std::filesystem::remove_all(base_path); - const auto output_path{base_path / "output"}; - const auto input_path{base_path / "input"}; - std::filesystem::create_directories(input_path); - write_uint64_t(input_path / "initial.txt", 8); - - const auto initial_txt{input_path / "initial.txt"}; - const auto first_txt{output_path / "first.txt"}; - - // First run - { - sourcemeta::one::Build build{output_path}; - build.refresh(initial_txt); - - const auto result = build.dispatch( - handler_multiply, first_txt, static_cast(2), - initial_txt); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(first_txt), 16); - - build.finish(); - } - - // Modify the input between runs and ensure the timestamp is - // clearly newer, as some filesystems have second-level resolution - write_uint64_t(initial_txt, 100); - std::filesystem::last_write_time( - initial_txt, - std::filesystem::file_time_type::clock::now() + std::chrono::seconds(10)); - - // Second run: should cache miss because initial.txt changed - { - sourcemeta::one::Build build{output_path}; - - const auto result = build.dispatch( - handler_multiply, first_txt, static_cast(2), - initial_txt); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(first_txt), 200); - } -} - -TEST(Build_e2e, dynamic_dependency_stale_invalidates) { - const auto base_path{std::filesystem::path{BINARY_DIRECTORY} / - "dynamic_dep_stale_invalidates"}; - std::filesystem::remove_all(base_path); - const auto output_path{base_path / "output"}; - const auto input_path{base_path / "input"}; - std::filesystem::create_directories(input_path); - write_uint64_t(input_path / "dep_static.txt", 10); - write_uint64_t(input_path / "dep_dynamic.txt", 5); - - const auto dep_static{input_path / "dep_static.txt"}; - const auto dep_dynamic{input_path / "dep_dynamic.txt"}; - const auto target{output_path / "target.txt"}; - - // First run: build - { - sourcemeta::one::Build build{output_path}; - build.refresh(dep_static); - build.refresh(dep_dynamic); - - const auto result = build.dispatch( - handler_sum_with_dynamic, target, dep_dynamic, dep_static); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(target), 15); - - build.finish(); - } - - // Second run: unchanged, cache hit - { - sourcemeta::one::Build build{output_path}; - - const auto result = build.dispatch( - handler_sum_with_dynamic, target, dep_dynamic, dep_static); - EXPECT_FALSE(result); - EXPECT_EQ(read_uint64_t(target), 15); - - build.finish(); - } - - // Update the dynamic dep file between runs - write_uint64_t(dep_dynamic, 100); - std::filesystem::last_write_time( - dep_dynamic, - std::filesystem::file_time_type::clock::now() + std::chrono::seconds(10)); - - // Third run: should cache miss because dynamic dep changed - { - sourcemeta::one::Build build{output_path}; - - const auto result = build.dispatch( - handler_sum_with_dynamic, target, dep_dynamic, dep_static); - EXPECT_TRUE(result); - EXPECT_EQ(read_uint64_t(target), 110); - } -} diff --git a/test/unit/build/build_state_test.cc b/test/unit/build/build_state_test.cc new file mode 100644 index 000000000..919e211d9 --- /dev/null +++ b/test/unit/build/build_state_test.cc @@ -0,0 +1,132 @@ +#include + +#include + +#include // std::chrono::nanoseconds, std::chrono::duration_cast +#include // std::filesystem::path +#include // std::string + +static auto state_path(const std::string &name) -> std::filesystem::path { + return std::filesystem::path{BINARY_DIRECTORY} / "state" / name; +} + +TEST(Build_state, round_trip_empty) { + const auto path{state_path("empty")}; + std::filesystem::create_directories(path.parent_path()); + + const sourcemeta::one::BuildState original_entries; + original_entries.save(path); + + sourcemeta::one::BuildState loaded_entries; + loaded_entries.load(path); + EXPECT_TRUE(loaded_entries.empty()); +} + +TEST(Build_state, round_trip_single_entry_no_deps) { + const auto path{state_path("single_no_deps")}; + std::filesystem::create_directories(path.parent_path()); + + const auto now{std::filesystem::file_time_type::clock::now()}; + sourcemeta::one::BuildState original_entries; + original_entries.emplace("/output/schemas/foo/%/schema.metapack", + {.file_mark = now, .dependencies = {}}); + + original_entries.save(path); + + sourcemeta::one::BuildState loaded_entries; + loaded_entries.load(path); + EXPECT_EQ(loaded_entries.size(), 1); + EXPECT_TRUE(loaded_entries.contains("/output/schemas/foo/%/schema.metapack")); + + const auto *result{ + loaded_entries.entry("/output/schemas/foo/%/schema.metapack")}; + EXPECT_NE(result, nullptr); + EXPECT_TRUE(result->dependencies.empty()); +} + +TEST(Build_state, round_trip_with_file_mark) { + const auto path{state_path("with_mark")}; + std::filesystem::create_directories(path.parent_path()); + + const auto now{std::filesystem::file_time_type::clock::now()}; + sourcemeta::one::BuildState original_entries; + original_entries.emplace("/output/schemas/foo/%/schema.metapack", + {.file_mark = now, .dependencies = {}}); + + original_entries.save(path); + + sourcemeta::one::BuildState loaded_entries; + loaded_entries.load(path); + EXPECT_EQ(loaded_entries.size(), 1); + + const auto *result{ + loaded_entries.entry("/output/schemas/foo/%/schema.metapack")}; + EXPECT_NE(result, nullptr); + + const auto original_ns{std::chrono::duration_cast( + now.time_since_epoch()) + .count()}; + const auto loaded_ns{std::chrono::duration_cast( + result->file_mark.time_since_epoch()) + .count()}; + EXPECT_EQ(original_ns, loaded_ns); +} + +TEST(Build_state, round_trip_with_dependencies) { + const auto path{state_path("with_deps")}; + std::filesystem::create_directories(path.parent_path()); + + const auto now{std::filesystem::file_time_type::clock::now()}; + sourcemeta::one::BuildState original_entries; + original_entries.emplace( + "/output/schemas/foo/%/dependencies.metapack", + {.file_mark = now, + .dependencies = {"/output/schemas/bar/%/schema.metapack", + "/output/schemas/baz/%/schema.metapack", + "/output/schemas/qux/%/schema.metapack"}}); + + original_entries.save(path); + + sourcemeta::one::BuildState loaded_entries; + loaded_entries.load(path); + EXPECT_EQ(loaded_entries.size(), 1); + + const auto *result{ + loaded_entries.entry("/output/schemas/foo/%/dependencies.metapack")}; + EXPECT_NE(result, nullptr); + EXPECT_EQ(result->dependencies.size(), 3); + EXPECT_EQ(result->dependencies[0], "/output/schemas/bar/%/schema.metapack"); + EXPECT_EQ(result->dependencies[1], "/output/schemas/baz/%/schema.metapack"); + EXPECT_EQ(result->dependencies[2], "/output/schemas/qux/%/schema.metapack"); +} + +TEST(Build_state, round_trip_multiple_entries) { + const auto path{state_path("multiple")}; + std::filesystem::create_directories(path.parent_path()); + + const auto now{std::filesystem::file_time_type::clock::now()}; + sourcemeta::one::BuildState original_entries; + original_entries.emplace("/output/schemas/foo/%/schema.metapack", + {.file_mark = now, .dependencies = {}}); + original_entries.emplace( + "/output/schemas/foo/%/dependencies.metapack", + {.file_mark = now, + .dependencies = {"/output/schemas/bar/%/schema.metapack"}}); + original_entries.emplace("/output/configuration.json", + {.file_mark = now, .dependencies = {}}); + + original_entries.save(path); + + sourcemeta::one::BuildState loaded_entries; + loaded_entries.load(path); + EXPECT_EQ(loaded_entries.size(), 3); + EXPECT_TRUE(loaded_entries.contains("/output/schemas/foo/%/schema.metapack")); + EXPECT_TRUE( + loaded_entries.contains("/output/schemas/foo/%/dependencies.metapack")); + EXPECT_TRUE(loaded_entries.contains("/output/configuration.json")); + + const auto *dependencies_entry{ + loaded_entries.entry("/output/schemas/foo/%/dependencies.metapack")}; + EXPECT_NE(dependencies_entry, nullptr); + EXPECT_EQ(dependencies_entry->dependencies.size(), 1); +} diff --git a/test/unit/build/build_test_utils.h b/test/unit/build/build_test_utils.h index 7b02e7f98..6d0224a9b 100644 --- a/test/unit/build/build_test_utils.h +++ b/test/unit/build/build_test_utils.h @@ -1,25 +1,282 @@ -#ifndef SOURCEMETA_ONE_BUILD_TEST_UTILSH_ -#define SOURCEMETA_ONE_BUILD_TEST_UTILSH_ +#ifndef SOURCEMETA_ONE_BUILD_TEST_UTILS_H_ +#define SOURCEMETA_ONE_BUILD_TEST_UTILS_H_ -#include -#include -#include +#include -inline auto READ_FILE(const std::filesystem::path &path) -> std::string { - std::ifstream stream{path}; - std::ostringstream buffer; - buffer << stream.rdbuf(); - return buffer.str(); +#include + +#include // std::sort +#include // std::size_t +#include // std::filesystem::path, std::filesystem::file_time_type +#include // std::set +#include // std::string +#include // std::unordered_map +#include // std::unordered_set +#include // std::vector + +auto MTIME(int seconds) -> std::filesystem::file_time_type { + return std::filesystem::file_time_type{std::chrono::seconds{seconds}}; } -inline auto WRITE_FILE(const std::filesystem::path &path, - const std::string &contents) -> void { - std::filesystem::create_directories(path.parent_path()); - std::ofstream stream{path}; - assert(!stream.fail()); - stream << contents; - stream.flush(); - stream.close(); +static auto +__check_no_intra_wave_dependencies(const sourcemeta::one::BuildPlan &plan) + -> void { + for (std::size_t wave_index{0}; wave_index < plan.waves.size(); + wave_index++) { + std::unordered_set destinations; + for (const auto &action : plan.waves[wave_index]) { + destinations.insert(action.destination.string()); + } + + for (const auto &action : plan.waves[wave_index]) { + for (const auto &dependency : action.dependencies) { + EXPECT_FALSE(destinations.contains(dependency.string())) + << "Wave " << wave_index << ": action " + << action.destination.string() << " depends on " + << dependency.string() << " which is produced in the same wave"; + } + } + } +} + +static auto +__check_no_forward_dependencies(const sourcemeta::one::BuildPlan &plan) + -> void { + for (std::size_t wave_index{0}; wave_index < plan.waves.size(); + wave_index++) { + std::unordered_set future_destinations; + for (std::size_t future{wave_index + 1}; future < plan.waves.size(); + future++) { + for (const auto &action : plan.waves[future]) { + future_destinations.insert(action.destination.string()); + } + } + + for (const auto &action : plan.waves[wave_index]) { + for (const auto &dependency : action.dependencies) { + EXPECT_FALSE(future_destinations.contains(dependency.string())) + << "Wave " << wave_index << ": action " + << action.destination.string() << " depends on " + << dependency.string() << " which is produced in a future wave"; + } + } + } +} + +static auto __is_under(const std::filesystem::path &path, + const std::filesystem::path &base) -> bool { + const auto &path_native{path.native()}; + const auto &base_native{base.native()}; + return path_native.size() > base_native.size() && + path_native.starts_with(base_native); +} + +static auto +__check_no_removed_references(const sourcemeta::one::BuildPlan &plan) -> void { + std::vector remove_destinations; + for (const auto &wave : plan.waves) { + for (const auto &action : wave) { + if (action.type == sourcemeta::one::BuildPlan::Action::Type::Remove) { + remove_destinations.push_back(&action.destination); + } + } + } + + for (const auto &wave : plan.waves) { + for (const auto &action : wave) { + if (action.type == sourcemeta::one::BuildPlan::Action::Type::Remove) { + continue; + } + + for (const auto *remove_path : remove_destinations) { + EXPECT_FALSE(__is_under(action.destination, *remove_path)) + << "Action " << action.destination.string() + << " has destination under removed path " << remove_path->string(); + + for (const auto &dependency : action.dependencies) { + EXPECT_FALSE(__is_under(dependency, *remove_path)) + << "Action " << action.destination.string() << " depends on " + << dependency.string() << " which is under removed path " + << remove_path->string(); + } + } + } + } } +static auto +__check_dependencies_resolvable(const sourcemeta::one::BuildPlan &plan, + const sourcemeta::one::BuildState &entries, + const std::filesystem::path &output) -> void { + const auto &output_prefix{output.native()}; + for (std::size_t wave_index{0}; wave_index < plan.waves.size(); + wave_index++) { + std::unordered_set prior_destinations; + for (std::size_t prior{0}; prior < wave_index; prior++) { + for (const auto &action : plan.waves[prior]) { + prior_destinations.insert(action.destination.string()); + } + } + + for (const auto &action : plan.waves[wave_index]) { + for (const auto &dependency : action.dependencies) { + const auto &dependency_string{dependency.string()}; + if (!dependency.native().starts_with(output_prefix)) { + continue; + } + + EXPECT_TRUE(prior_destinations.contains(dependency_string) || + entries.contains(dependency_string)) + << "Wave " << wave_index << ": action " + << action.destination.string() << " depends on " + << dependency_string << " which is not produced by a previous wave" + << " and not in entries"; + } + } + } +} + +static auto +__check_no_duplicate_destinations(const sourcemeta::one::BuildPlan &plan) + -> void { + std::unordered_set all_destinations; + for (const auto &wave : plan.waves) { + for (const auto &action : wave) { + const auto &destination_string{action.destination.string()}; + EXPECT_FALSE(all_destinations.contains(destination_string)) + << "Duplicate destination: " << destination_string; + all_destinations.insert(destination_string); + } + } +} + +static auto ADD_SCHEMA_ENTRIES(sourcemeta::one::BuildState &entries, + const std::filesystem::path &output, + const std::filesystem::path &relative_output, + const bool evaluate, const bool web, + const std::filesystem::file_time_type mark) + -> void { + const auto base{output / "schemas" / relative_output / "%"}; + const auto explorer_base{output / "explorer" / relative_output / "%"}; + entries.emplace(base / "schema.metapack", + {.file_mark = mark, .dependencies = {}}); + entries.emplace(base / "dependencies.metapack", + {.file_mark = mark, .dependencies = {}}); + entries.emplace(base / "locations.metapack", + {.file_mark = mark, .dependencies = {}}); + entries.emplace(base / "positions.metapack", + {.file_mark = mark, .dependencies = {}}); + entries.emplace(base / "stats.metapack", + {.file_mark = mark, .dependencies = {}}); + entries.emplace(base / "bundle.metapack", + {.file_mark = mark, .dependencies = {}}); + entries.emplace(base / "health.metapack", + {.file_mark = mark, .dependencies = {}}); + entries.emplace(base / "editor.metapack", + {.file_mark = mark, .dependencies = {}}); + entries.emplace(base / "dependents.metapack", + {.file_mark = mark, .dependencies = {}}); + entries.emplace(explorer_base / "schema.metapack", + {.file_mark = mark, .dependencies = {}}); + if (evaluate) { + entries.emplace(base / "blaze-exhaustive.metapack", + {.file_mark = mark, .dependencies = {}}); + entries.emplace(base / "blaze-fast.metapack", + {.file_mark = mark, .dependencies = {}}); + } + if (web) { + entries.emplace(explorer_base / "schema-html.metapack", + {.file_mark = mark, .dependencies = {}}); + entries.emplace(explorer_base / "directory-html.metapack", + {.file_mark = mark, .dependencies = {}}); + } +} + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define EXPECT_CONSISTENT_PLAN(plan, entries, output, build_type, \ + expected_waves, expected_size) \ + do { \ + EXPECT_EQ((plan).output, (output)); \ + EXPECT_EQ((plan).type, sourcemeta::one::BuildPlan::Type::build_type); \ + EXPECT_EQ((plan).waves.size(), (expected_waves)); \ + EXPECT_EQ((plan).size, (expected_size)); \ + __check_no_intra_wave_dependencies(plan); \ + __check_no_forward_dependencies(plan); \ + __check_no_removed_references(plan); \ + __check_dependencies_resolvable(plan, entries, output); \ + __check_no_duplicate_destinations(plan); \ + } while (false) + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define EXPECT_ACTION(plan, wave, index, wave_size, expected_type, \ + expected_dest, expected_data, ...) \ + do { \ + EXPECT_EQ((plan).waves[(wave)].size(), (wave_size)); \ + const auto &action_ref_##wave##_##index{(plan).waves[(wave)][(index)]}; \ + EXPECT_EQ(action_ref_##wave##_##index.type, \ + sourcemeta::one::BuildPlan::Action::Type::expected_type); \ + EXPECT_EQ(action_ref_##wave##_##index.destination, (expected_dest)); \ + EXPECT_EQ(action_ref_##wave##_##index.data, \ + std::string_view{expected_data}); \ + const std::vector expected_deps_##wave##_##index{ \ + __VA_ARGS__}; \ + EXPECT_EQ(action_ref_##wave##_##index.dependencies, \ + expected_deps_##wave##_##index); \ + } while (false) + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define EXPECT_ACTION_UNORDERED(plan, wave, index, wave_size, expected_type, \ + expected_dest, expected_data, ...) \ + do { \ + EXPECT_EQ((plan).waves[(wave)].size(), (wave_size)); \ + const auto &action_ref_##wave##_##index{(plan).waves[(wave)][(index)]}; \ + EXPECT_EQ(action_ref_##wave##_##index.type, \ + sourcemeta::one::BuildPlan::Action::Type::expected_type); \ + EXPECT_EQ(action_ref_##wave##_##index.destination, (expected_dest)); \ + EXPECT_EQ(action_ref_##wave##_##index.data, \ + std::string_view{expected_data}); \ + auto actual_deps_##wave##_##index{ \ + action_ref_##wave##_##index.dependencies}; \ + std::sort(actual_deps_##wave##_##index.begin(), \ + actual_deps_##wave##_##index.end()); \ + std::vector expected_deps_##wave##_##index{ \ + __VA_ARGS__}; \ + std::sort(expected_deps_##wave##_##index.begin(), \ + expected_deps_##wave##_##index.end()); \ + EXPECT_EQ(actual_deps_##wave##_##index, expected_deps_##wave##_##index); \ + } while (false) + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define EXPECT_TOTAL_FILES(plan, entries, ...) \ + do { \ + std::set total_files_result; \ + for (const auto &[total_files_path, total_files_entry] : (entries)) { \ + total_files_result.insert(total_files_path); \ + } \ + for (const auto &total_files_wave : (plan).waves) { \ + for (const auto &total_files_action : total_files_wave) { \ + if (total_files_action.type == \ + sourcemeta::one::BuildPlan::Action::Type::Remove) { \ + total_files_result.erase(total_files_action.destination); \ + const auto total_files_prefix{ \ + total_files_action.destination.string() + "/"}; \ + for (auto total_files_iterator = total_files_result.begin(); \ + total_files_iterator != total_files_result.end();) { \ + if (total_files_iterator->string().starts_with( \ + total_files_prefix)) { \ + total_files_iterator = \ + total_files_result.erase(total_files_iterator); \ + } else { \ + total_files_iterator++; \ + } \ + } \ + } else { \ + total_files_result.insert(total_files_action.destination); \ + } \ + } \ + } \ + const std::set total_files_expected{__VA_ARGS__}; \ + EXPECT_EQ(total_files_result, total_files_expected); \ + } while (false) + #endif diff --git a/test/unit/resolver/resolver_test.cc b/test/unit/resolver/resolver_test.cc index 090898872..abbf989df 100644 --- a/test/unit/resolver/resolver_test.cc +++ b/test/unit/resolver/resolver_test.cc @@ -538,6 +538,18 @@ TEST_F(ResolverTest, meta_draft4_override) { })JSON"); } +TEST_F(ResolverTest, entry_lookup) { + RESOLVER_INIT(resolver); + RESOLVER_IMPORT(resolver, "example", "2020-12-with-id.json"); + + const auto &entry{ + resolver.entry("http://localhost:8000/example/2020-12-with-id")}; + EXPECT_EQ(entry.relative_path, + std::filesystem::path{"example/2020-12-with-id"}); + EXPECT_EQ(entry.original_identifier, + "https://example.com/schemas/2020-12-with-id"); +} + TEST_F(ResolverTest, no_base_anonymous) { RESOLVER_INIT(resolver); RESOLVER_ADD(resolver, "no-base", "anonymous.json",