Track actual gameplay sources in async build results
Native and manual checks / native (ubuntu-24.04) (push) Failing after 34s
Native and manual checks / manual (push) Successful in 31s
Native and manual checks / native (windows-2025) (push) Canceled after 0s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
Native and manual checks / native (ubuntu-24.04) (push) Failing after 34s
Native and manual checks / manual (push) Successful in 31s
Native and manual checks / native (windows-2025) (push) Canceled after 0s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
This commit is contained in:
@@ -81,6 +81,13 @@ if(TARGET faset_editor_commands AND TARGET faset_build_service)
|
|||||||
target_link_libraries(faset_editor_session_tests PRIVATE faset_editor_session)
|
target_link_libraries(faset_editor_session_tests PRIVATE faset_editor_session)
|
||||||
target_compile_definitions(faset_editor_session_tests PRIVATE FASET_TEST_ENGINE="${PROJECT_SOURCE_DIR}")
|
target_compile_definitions(faset_editor_session_tests PRIVATE FASET_TEST_ENGINE="${PROJECT_SOURCE_DIR}")
|
||||||
add_test(NAME editor_session_settings COMMAND faset_editor_session_tests)
|
add_test(NAME editor_session_settings COMMAND faset_editor_session_tests)
|
||||||
|
add_executable(faset_editor_schema_signature_tests tests/editor_schema_signature_tests.cpp)
|
||||||
|
target_link_libraries(faset_editor_schema_signature_tests PRIVATE faset_editor_session)
|
||||||
|
target_compile_definitions(faset_editor_schema_signature_tests PRIVATE FASET_TEST_ENGINE="${PROJECT_SOURCE_DIR}")
|
||||||
|
add_dependencies(faset_editor_schema_signature_tests faset_build_schema_tool)
|
||||||
|
add_test(NAME editor_schema_signature
|
||||||
|
COMMAND faset_editor_schema_signature_tests $<TARGET_FILE:faset_build_schema_tool>)
|
||||||
|
set_tests_properties(editor_schema_signature PROPERTIES TIMEOUT 180)
|
||||||
add_executable(faset_editor_autosave_tests tests/editor_autosave_tests.cpp)
|
add_executable(faset_editor_autosave_tests tests/editor_autosave_tests.cpp)
|
||||||
target_link_libraries(faset_editor_autosave_tests PRIVATE faset_editor_session)
|
target_link_libraries(faset_editor_autosave_tests PRIVATE faset_editor_session)
|
||||||
add_test(NAME editor_autosave COMMAND faset_editor_autosave_tests)
|
add_test(NAME editor_autosave COMMAND faset_editor_autosave_tests)
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ class Session {
|
|||||||
std::string pending_play_job_;
|
std::string pending_play_job_;
|
||||||
Json pending_play_scene_;
|
Json pending_play_scene_;
|
||||||
std::unique_ptr<PluginManager> plugins_;
|
std::unique_ptr<PluginManager> plugins_;
|
||||||
std::map<std::string, std::string> submitted_sources_;
|
|
||||||
std::string schema_source_signature_;
|
std::string schema_source_signature_;
|
||||||
bool schema_loaded_ = false;
|
bool schema_loaded_ = false;
|
||||||
std::string schema_error_;
|
std::string schema_error_;
|
||||||
|
|||||||
@@ -501,6 +501,7 @@ struct BuildService::Impl {
|
|||||||
{"schema", path_to_utf8(directory / "schema.json")},
|
{"schema", path_to_utf8(directory / "schema.json")},
|
||||||
{"lua_enabled", job.lua.enabled()},
|
{"lua_enabled", job.lua.enabled()},
|
||||||
{"lua_fingerprint", job.lua.fingerprint},
|
{"lua_fingerprint", job.lua.fingerprint},
|
||||||
|
{"source_signature", inputs.source_hash},
|
||||||
{"fingerprint", fingerprint},
|
{"fingerprint", fingerprint},
|
||||||
{"schema_cache_hit", reused},
|
{"schema_cache_hit", reused},
|
||||||
{"generation_reused", reused},
|
{"generation_reused", reused},
|
||||||
@@ -957,6 +958,7 @@ struct BuildService::Impl {
|
|||||||
{"generation", job.status.id},
|
{"generation", job.status.id},
|
||||||
{"build", built},
|
{"build", built},
|
||||||
{"schema", built.at("schema")},
|
{"schema", built.at("schema")},
|
||||||
|
{"source_signature", built.at("source_signature")},
|
||||||
{"player", built.at("player")},
|
{"player", built.at("player")},
|
||||||
{"build_directory", built.at("build_directory")},
|
{"build_directory", built.at("build_directory")},
|
||||||
{"configuration", built.at("configuration")}};
|
{"configuration", built.at("configuration")}};
|
||||||
|
|||||||
+4
-15
@@ -285,13 +285,10 @@ void Session::poll() {
|
|||||||
if (value.result.contains("schema"))
|
if (value.result.contains("schema"))
|
||||||
try {
|
try {
|
||||||
load_schema(path_from_utf8(value.result.at("schema").get<std::string>()));
|
load_schema(path_from_utf8(value.result.at("schema").get<std::string>()));
|
||||||
// This signature represents the sources submitted with this job, not later
|
// The worker may start after the request and capture newer sources.
|
||||||
// edits.
|
// Only its validated build snapshot can identify this schema.
|
||||||
if (value.result.contains("source_signature"))
|
schema_source_signature_ =
|
||||||
schema_source_signature_ =
|
value.result.at("source_signature").get<std::string>();
|
||||||
value.result.at("source_signature").get<std::string>();
|
|
||||||
else if (submitted_sources_.contains(value.id))
|
|
||||||
schema_source_signature_ = submitted_sources_.at(value.id);
|
|
||||||
atomic_write_json(config_.project_root / ".faset/schema-state.json",
|
atomic_write_json(config_.project_root / ".faset/schema-state.json",
|
||||||
{{"source_signature", schema_source_signature_}});
|
{{"source_signature", schema_source_signature_}});
|
||||||
} catch (const std::exception& error) {
|
} catch (const std::exception& error) {
|
||||||
@@ -496,9 +493,7 @@ void Session::register_commands() {
|
|||||||
"Incrementally compile gameplay and export C++/Lua metadata in separate native "
|
"Incrementally compile gameplay and export C++/Lua metadata in separate native "
|
||||||
"processes. Returns a job ID.",
|
"processes. Returns a job ID.",
|
||||||
schema(Json::object()), [&](const Json&) {
|
schema(Json::object()), [&](const Json&) {
|
||||||
const auto signature = source_signature();
|
|
||||||
const auto id = builds_.start_build();
|
const auto id = builds_.start_build();
|
||||||
submitted_sources_[id] = signature;
|
|
||||||
return Json{{"job", id}};
|
return Json{{"job", id}};
|
||||||
});
|
});
|
||||||
commands_.add(
|
commands_.add(
|
||||||
@@ -509,9 +504,7 @@ void Session::register_commands() {
|
|||||||
schema(Json::object()), [&](const Json&) {
|
schema(Json::object()), [&](const Json&) {
|
||||||
require(scripting::loadLuaProject(config_.project_root).enabled(), "lua.disabled",
|
require(scripting::loadLuaProject(config_.project_root).enabled(), "lua.disabled",
|
||||||
"Declare scripting.lua.scripts in project.faset.json first");
|
"Declare scripting.lua.scripts in project.faset.json first");
|
||||||
const auto signature = source_signature();
|
|
||||||
const auto id = builds_.start_build();
|
const auto id = builds_.start_build();
|
||||||
submitted_sources_[id] = signature;
|
|
||||||
return Json{{"job", id}};
|
return Json{{"job", id}};
|
||||||
});
|
});
|
||||||
commands_.add("faset_lua_reload",
|
commands_.add("faset_lua_reload",
|
||||||
@@ -655,12 +648,10 @@ void Session::register_commands() {
|
|||||||
"output directory. Returns a job ID.",
|
"output directory. Returns a job ID.",
|
||||||
schema({{"document", text}, {"output", text}}, {"document", "output"}),
|
schema({{"document", text}, {"output", text}}, {"document", "output"}),
|
||||||
[&](const Json& args) {
|
[&](const Json& args) {
|
||||||
const auto signature = source_signature();
|
|
||||||
const auto id = builds_.start_export(
|
const auto id = builds_.start_export(
|
||||||
resolved_or_throw(commands_, args.at("document")),
|
resolved_or_throw(commands_, args.at("document")),
|
||||||
project_path(config_.project_root,
|
project_path(config_.project_root,
|
||||||
path_from_utf8(args.at("output").get<std::string>())));
|
path_from_utf8(args.at("output").get<std::string>())));
|
||||||
submitted_sources_[id] = signature;
|
|
||||||
return Json{{"job", id}};
|
return Json{{"job", id}};
|
||||||
});
|
});
|
||||||
commands_.add(
|
commands_.add(
|
||||||
@@ -685,9 +676,7 @@ void Session::register_commands() {
|
|||||||
schema({{"document", text}}, {"document"}), [&](const Json& args) {
|
schema({{"document", text}}, {"document"}), [&](const Json& args) {
|
||||||
stop_player();
|
stop_player();
|
||||||
pending_play_scene_ = resolved_or_throw(commands_, args.at("document"));
|
pending_play_scene_ = resolved_or_throw(commands_, args.at("document"));
|
||||||
const auto signature = source_signature();
|
|
||||||
pending_play_job_ = builds_.start_build();
|
pending_play_job_ = builds_.start_build();
|
||||||
submitted_sources_[pending_play_job_] = signature;
|
|
||||||
return Json{{"job", pending_play_job_}, {"play_pending", true}};
|
return Json{{"job", pending_play_job_}, {"play_pending", true}};
|
||||||
});
|
});
|
||||||
commands_.add(
|
commands_.add(
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#include <faset/core/io.hpp>
|
#include <faset/core/io.hpp>
|
||||||
|
#include <faset/core/process.hpp>
|
||||||
|
#include <chrono>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
// Native stand-in for CMake and SchemaExporter. Tests exercise the real
|
// Native stand-in for CMake and SchemaExporter. Tests exercise the real
|
||||||
// asynchronous BuildService and publication code without compiling a game.
|
// asynchronous BuildService and publication code without compiling a game.
|
||||||
@@ -52,6 +55,15 @@ int tool_main(int argc, char** argv) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (argc > 2 && std::string_view(argv[1]) == "--build") {
|
if (argc > 2 && std::string_view(argv[1]) == "--build") {
|
||||||
|
if (fs::exists("block-native-build")) {
|
||||||
|
atomic_write("build-blocked", "ready\n");
|
||||||
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||||
|
while (fs::exists("block-native-build") &&
|
||||||
|
std::chrono::steady_clock::now() < deadline)
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
if (fs::exists("block-native-build"))
|
||||||
|
throw std::runtime_error("Timed out waiting for build fixture gate");
|
||||||
|
}
|
||||||
if (fs::exists("emit-clang-error-and-fail-build")) {
|
if (fs::exists("emit-clang-error-and-fail-build")) {
|
||||||
for (int index = 0; index < 250; ++index)
|
for (int index = 0; index < 250; ++index)
|
||||||
std::cerr << "/external/library.cpp:1:1: warning: dependency warning "
|
std::cerr << "/external/library.cpp:1:1: warning: dependency warning "
|
||||||
@@ -81,7 +93,9 @@ int tool_main(int argc, char** argv) {
|
|||||||
for (const auto* name : {"sdl3", "entt", "box2d", "box3d", "json", "stb"})
|
for (const auto* name : {"sdl3", "entt", "box2d", "box3d", "json", "stb"})
|
||||||
atomic_write(build / "_deps" / (std::string(name) + "-src") / "LICENSE.txt",
|
atomic_write(build / "_deps" / (std::string(name) + "-src") / "LICENSE.txt",
|
||||||
"Synthetic dependency notice for packaging tests only.\n");
|
"Synthetic dependency notice for packaging tests only.\n");
|
||||||
const auto self = fs::absolute(path_from_utf8(argv[0]));
|
const auto invoked = path_from_utf8(argv[0]);
|
||||||
|
const auto self = invoked.has_parent_path() ? fs::absolute(invoked)
|
||||||
|
: find_executable(argv[0]);
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
constexpr auto suffix = ".exe";
|
constexpr auto suffix = ".exe";
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
#include <faset/core/io.hpp>
|
||||||
|
#include <faset/editor/build_cache.hpp>
|
||||||
|
#include <faset/editor/session.hpp>
|
||||||
|
#include <faset/scripting/project.hpp>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using namespace faset;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void check(bool condition, std::string_view message) {
|
||||||
|
if (!condition)
|
||||||
|
throw std::runtime_error(std::string(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
void prepend_fixture_to_path(const fs::path& directory) {
|
||||||
|
const auto* previous = std::getenv("PATH");
|
||||||
|
const auto value = path_to_utf8(directory) +
|
||||||
|
#ifdef _WIN32
|
||||||
|
";" +
|
||||||
|
#else
|
||||||
|
":" +
|
||||||
|
#endif
|
||||||
|
(previous ? previous : "");
|
||||||
|
#ifdef _WIN32
|
||||||
|
check(_putenv_s("PATH", value.c_str()) == 0, "Could not configure fixture PATH");
|
||||||
|
#else
|
||||||
|
check(setenv("PATH", value.c_str(), 1) == 0, "Could not configure fixture PATH");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool finished(const Json& job) {
|
||||||
|
const auto state = job.at("state").get<std::string>();
|
||||||
|
return state == "succeeded" || state == "failed" || state == "cancelled";
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int test_main(int argc, char** argv) {
|
||||||
|
const auto root = fs::temp_directory_path() / ("Faset signature race " + new_id());
|
||||||
|
try {
|
||||||
|
check(argc == 2, "Expected the native build fixture path");
|
||||||
|
const auto project = root / "project";
|
||||||
|
const auto tools = root / "tools";
|
||||||
|
fs::create_directories(tools);
|
||||||
|
#ifdef _WIN32
|
||||||
|
const auto cmake_fixture = tools / "cmake.exe";
|
||||||
|
#else
|
||||||
|
const auto cmake_fixture = tools / "cmake";
|
||||||
|
#endif
|
||||||
|
fs::copy_file(path_from_utf8(argv[1]), cmake_fixture);
|
||||||
|
prepend_fixture_to_path(tools);
|
||||||
|
|
||||||
|
{
|
||||||
|
editor::Session session({project, path_from_utf8(FASET_TEST_ENGINE), {}});
|
||||||
|
session.scaffold("Source signature race", 2);
|
||||||
|
atomic_write_json(project / "schema-fixture.json",
|
||||||
|
{{"format", "faset.schema"},
|
||||||
|
{"version", 1},
|
||||||
|
{"types", Json::array()}});
|
||||||
|
auto document = session.authoring().create("Export source signature", 2);
|
||||||
|
auto& commands = session.commands();
|
||||||
|
|
||||||
|
const auto before =
|
||||||
|
editor::gameplay_source_hash(project, scripting::loadLuaProject(project));
|
||||||
|
atomic_write(project / "block-native-build", "block\n");
|
||||||
|
const auto blocker = commands.call("faset_build", Json::object()).at("job");
|
||||||
|
const auto gate_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||||
|
while (!fs::exists(project / "build-blocked") &&
|
||||||
|
std::chrono::steady_clock::now() < gate_deadline)
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
if (!fs::exists(project / "build-blocked")) {
|
||||||
|
const auto blocked_job = commands.call("faset_job", {{"id", blocker}});
|
||||||
|
throw std::runtime_error("The first build never reached the controlled worker "
|
||||||
|
"gate: " + blocked_job.dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit both requests while the old source exists and another job owns the worker.
|
||||||
|
// They must publish the later hash actually captured when their turn begins.
|
||||||
|
const auto build = commands.call("faset_build", Json::object()).at("job");
|
||||||
|
const auto exported =
|
||||||
|
commands.call("faset_export",
|
||||||
|
{{"document", document.at("id")}, {"output", "Exports/Race"}})
|
||||||
|
.at("job");
|
||||||
|
atomic_write(project / "Scripts/Gameplay.cpp",
|
||||||
|
read_text(project / "Scripts/Gameplay.cpp") + "\n// queued edit\n");
|
||||||
|
const auto after =
|
||||||
|
editor::gameplay_source_hash(project, scripting::loadLuaProject(project));
|
||||||
|
check(before != after, "Queued source edit did not change the gameplay signature");
|
||||||
|
commands.call("faset_job_cancel", {{"id", blocker}});
|
||||||
|
fs::remove(project / "block-native-build");
|
||||||
|
|
||||||
|
Json build_job, export_job, blocker_job;
|
||||||
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(150);
|
||||||
|
do {
|
||||||
|
session.poll();
|
||||||
|
blocker_job = commands.call("faset_job", {{"id", blocker}});
|
||||||
|
build_job = commands.call("faset_job", {{"id", build}});
|
||||||
|
export_job = commands.call("faset_job", {{"id", exported}});
|
||||||
|
if (finished(blocker_job) && finished(build_job) && finished(export_job))
|
||||||
|
break;
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
} while (std::chrono::steady_clock::now() < deadline);
|
||||||
|
session.poll();
|
||||||
|
check(blocker_job.at("state") == "cancelled", "Blocked build was not cancelled");
|
||||||
|
check(build_job.at("state") == "succeeded",
|
||||||
|
"Queued build failed: " + build_job.value("error", std::string()));
|
||||||
|
check(export_job.at("state") == "succeeded",
|
||||||
|
"Queued export failed: " + export_job.value("error", std::string()));
|
||||||
|
|
||||||
|
const auto status = commands.call("faset_schema_status", Json::object());
|
||||||
|
check(status.at("loaded") == true && status.at("stale") == false,
|
||||||
|
"Editor marked the just-built source schema stale after a queued edit");
|
||||||
|
check(build_job.at("result").value("source_signature", std::string()) == after,
|
||||||
|
"Build result did not report the worker's captured source hash");
|
||||||
|
check(export_job.at("result").value("source_signature", std::string()) == after,
|
||||||
|
"Export result did not report the worker's captured source hash");
|
||||||
|
check(read_json(project / ".faset/schema-state.json").at("source_signature") == after,
|
||||||
|
"Persisted schema provenance differs from the worker's source snapshot");
|
||||||
|
}
|
||||||
|
fs::remove_all(root);
|
||||||
|
std::cout << "Queued Build and Export preserve the actual worker source signature\n";
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
std::cerr << error.what() << "\nFixture retained at " << path_to_utf8(root) << '\n';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
int wmain(int argc, wchar_t** argv) {
|
||||||
|
return run_utf8_main(argc, argv, test_main);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
return test_main(argc, argv);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user