Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,27 @@

---

## Code Style and Documentation Guidelines

### No Emojis
**Emojis are strictly banned everywhere in the project.**

This includes:
- Source code (comments, strings, documentation)
- Commit messages
- Issue descriptions
- Planning documents (`issues/*.md`)
- Pull request descriptions
- README files
- All other documentation

Use clear, professional text instead. For status indicators in markdown:
- Use text: `Status: Completed`, `Status: In Progress`
- Use checkboxes: `- [x]` for completed, `- [ ]` for pending
- Use symbols if needed: `✓` (checkmark) is acceptable, but text is preferred

---

## Planning Document Structure

Each issue has a corresponding markdown file in `issues/`:
Expand Down
8 changes: 5 additions & 3 deletions backends/dealii/tests/modules.cc
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,14 @@ namespace
if (name.empty())
continue;

const std::string qualified_id = network.get_node_qualified_id(node_id);

std::filesystem::path running_file =
touch_dir / (std::to_string(node_id) + ".running");
touch_dir / (qualified_id + ".running");
std::filesystem::path succeeded_file =
touch_dir / (std::to_string(node_id) + ".succeeded");
touch_dir / (qualified_id + ".succeeded");
std::filesystem::path failed_file =
touch_dir / (std::to_string(node_id) + ".failed");
touch_dir / (qualified_id + ".failed");

// Check that .running file exists
EXPECT_TRUE(std::filesystem::exists(running_file))
Expand Down
157 changes: 157 additions & 0 deletions backends/dealii/tests/network.cc
Original file line number Diff line number Diff line change
Expand Up @@ -573,3 +573,160 @@ TEST(Network, ParseAndExecuteNetwork)
// Verify results
ASSERT_EQ(16, network.get_node(0)->get<Triangulation<2>>().n_active_cells());
}

TEST(Network, DuplicateQualifiedIdThrows)
{
ScopedTestOutputDir output_dir("Network_DuplicateQualifiedId");

coral::NodeObject::register_elementary_type<int>();

// Create a JSON with duplicate qualified_id
json network_json;
network_json["workflow"]["nodes"]["0"]["type"] = "int";
network_json["workflow"]["nodes"]["0"]["value"] = "1";
network_json["workflow"]["nodes"]["0"]["qualified_id"] = "duplicate_id";

network_json["workflow"]["nodes"]["1"]["type"] = "int";
network_json["workflow"]["nodes"]["1"]["value"] = "2";
network_json["workflow"]["nodes"]["1"]["qualified_id"] = "duplicate_id"; // Duplicate!

// Attempting to load this network should throw
coral::Network network;
ASSERT_THROW(
{
try
{
network.from_json(network_json);
}
catch (const coral::DuplicateQualifiedIdException &e)
{
// Verify the duplicate ID is accessible via getter (not by parsing message)
EXPECT_EQ("duplicate_id", e.get_duplicate_id())
<< "Exception should provide the duplicate qualified_id via getter";
throw; // Re-throw for ASSERT_THROW
}
},
coral::DuplicateQualifiedIdException);
}

TEST(Network, AutoGeneratedQualifiedId)
{
ScopedTestOutputDir output_dir("Network_AutoGeneratedQualifiedId");

coral::NodeObject::register_elementary_type<int>();

// Create a JSON WITHOUT qualified_id fields (simulates old JSON or
// programmatically created networks)
json network_json;
network_json["workflow"]["nodes"]["0"]["type"] = "int";
network_json["workflow"]["nodes"]["0"]["value"] = "10";

network_json["workflow"]["nodes"]["1"]["type"] = "int";
network_json["workflow"]["nodes"]["1"]["value"] = "20";

network_json["workflow"]["nodes"]["2"]["type"] = "int";
network_json["workflow"]["nodes"]["2"]["value"] = "30";

// Load the network (should auto-generate qualified_ids)
coral::Network network;
network.from_json(network_json);

// Verify nodes were created
ASSERT_EQ(network.n_nodes(), 3);

// Get the auto-generated qualified_ids (available for runtime use: touch
// files, logs)
auto qid0 = network.get_node_qualified_id(0);
auto qid1 = network.get_node_qualified_id(1);
auto qid2 = network.get_node_qualified_id(2);

// Verify auto-generated IDs have the correct format
EXPECT_TRUE(qid0.find("_auto_") != std::string::npos)
<< "Auto-generated qualified_id should contain '_auto_'. Got: " << qid0;
EXPECT_TRUE(qid1.find("_auto_") != std::string::npos)
<< "Auto-generated qualified_id should contain '_auto_'. Got: " << qid1;
EXPECT_TRUE(qid2.find("_auto_") != std::string::npos)
<< "Auto-generated qualified_id should contain '_auto_'. Got: " << qid2;

// Verify they start with the node_id
EXPECT_EQ(qid0.substr(0, 1), "0")
<< "Auto-generated qualified_id should start with node_id";
EXPECT_EQ(qid1.substr(0, 1), "1")
<< "Auto-generated qualified_id should start with node_id";
EXPECT_EQ(qid2.substr(0, 1), "2")
<< "Auto-generated qualified_id should start with node_id";

// Verify all qualified_ids are unique
EXPECT_NE(qid0, qid1) << "Auto-generated qualified_ids should be unique";
EXPECT_NE(qid1, qid2) << "Auto-generated qualified_ids should be unique";
EXPECT_NE(qid0, qid2) << "Auto-generated qualified_ids should be unique";

// Verify they're accessible from the nodes (for touch files/logs)
EXPECT_EQ(network.get_node(0)->get_qualified_id(), qid0);
EXPECT_EQ(network.get_node(1)->get_qualified_id(), qid1);
EXPECT_EQ(network.get_node(2)->get_qualified_id(), qid2);

// IMPORTANT: Auto-generated qualified_ids should NOT be serialized
// (they're transient, regenerated on each load for backward compatibility)
json output_json = network.to_json();
EXPECT_FALSE(output_json["workflow"]["nodes"]["0"].contains("qualified_id"))
<< "Auto-generated qualified_ids should not be serialized";
EXPECT_FALSE(output_json["workflow"]["nodes"]["1"].contains("qualified_id"))
<< "Auto-generated qualified_ids should not be serialized";
EXPECT_FALSE(output_json["workflow"]["nodes"]["2"].contains("qualified_id"))
<< "Auto-generated qualified_ids should not be serialized";

// Verify roundtrip works: deserialize again and check IDs are regenerated
coral::Network network2;
network2.from_json(output_json);

auto qid0_new = network2.get_node_qualified_id(0);
auto qid1_new = network2.get_node_qualified_id(1);
auto qid2_new = network2.get_node_qualified_id(2);

// New IDs should still have _auto_ marker
EXPECT_TRUE(qid0_new.find("_auto_") != std::string::npos)
<< "Regenerated IDs should still be auto-generated";
EXPECT_TRUE(qid1_new.find("_auto_") != std::string::npos)
<< "Regenerated IDs should still be auto-generated";
EXPECT_TRUE(qid2_new.find("_auto_") != std::string::npos)
<< "Regenerated IDs should still be auto-generated";

// Verify roundtrip produces identical JSON (no qualified_id pollution)
json output_json2 = network2.to_json();
output_json.erase("date_time_utc");
output_json2.erase("date_time_utc");
EXPECT_EQ(output_json, output_json2)
<< "Roundtrip should produce identical JSON";
}

TEST(Network, DuplicateQualifiedIdFromFile)
{
ScopedTestOutputDir output_dir("Network_DuplicateQualifiedIdFromFile");

// Load JSON file with duplicate qualified_ids
std::ifstream input(SOURCE_DIR "/test_files/qualified_id_repeated.json");
ASSERT_TRUE(input.good())
<< "Could not open test_files/qualified_id_repeated.json";

json network_json;
input >> network_json;

// Attempting to load this network should throw DuplicateQualifiedIdException
coral::Network network;
ASSERT_THROW(
{
try
{
network.from_json(network_json);
}
catch (const coral::DuplicateQualifiedIdException &e)
{
// Verify the duplicate ID is "1" (from the test file)
EXPECT_EQ("1", e.get_duplicate_id())
<< "Exception should report the duplicate qualified_id from the file";
throw; // Re-throw for ASSERT_THROW
}
},
coral::DuplicateQualifiedIdException);
}
5 changes: 4 additions & 1 deletion backends/dealii/tests/serialize.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "coral.h"
#include "register_types.h"
#include "test_utils.h"

using namespace dealii;
using namespace coral;
Expand Down Expand Up @@ -39,6 +40,8 @@ TEST(Serialize, Point)

TEST(Serialize, Triangulation)
{
coral_test::ScopedTestOutputDir output_dir("Serialize_Triangulation");

using type = Triangulation<2>;
NodeObject::register_type<type>();
NodeObjectPtr obj = make_node<type>();
Expand All @@ -60,7 +63,7 @@ TEST(Serialize, Triangulation)
ASSERT_NE(&obj->get<type>(), &obj2->get<type>());

// Dump the json to a file
std::ofstream ofs("triangulation.json");
std::ofstream ofs(output_dir.path() / "triangulation.json");
ofs << j.dump(2);
ofs.close();
}
13 changes: 13 additions & 0 deletions core/include/coral.h
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,19 @@ namespace coral
size_t
n_outputs() const;

/**
* Get the qualified_id of this node.
* Returns empty string if qualified_id is not set.
*/
std::string
get_qualified_id() const;

/**
* Set the qualified_id of this node.
*/
void
set_qualified_id(const std::string &id);

private:
/**
* Return true if an output index maps to a pass-through argument.
Expand Down
30 changes: 27 additions & 3 deletions core/include/coral_implementation.h
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,24 @@ namespace coral



CORAL_IMPL_INLINE std::string
NodeObject::get_qualified_id() const
{
const auto &j = initializer.json_serializer;
return j.contains("qualified_id") ? j["qualified_id"].get<std::string>() :
std::string();
}



CORAL_IMPL_INLINE void
NodeObject::set_qualified_id(const std::string &id)
{
initializer.json_serializer["qualified_id"] = id;
}



CORAL_IMPL_INLINE bool
NodeObject::is_passthrough_output(const unsigned int index) const
{
Expand Down Expand Up @@ -819,11 +837,11 @@ namespace coral
const bool is_network = NodeObject::is_network_type(obj->hash());

// If the input JSON contains fields that also exist in the registry
// description, they must match exactly (except for "value", which is
// allowed to differ when provided).
// description, they must match exactly (except for "value" and
// "qualified_id", which are allowed to differ when provided).
for (const auto &[key, val] : j.items())
{
if (key == "value")
if (key == "value" || key == "qualified_id")
continue;
if (is_network &&
(key == "arguments" || key == "inputs" || key == "outputs"))
Expand Down Expand Up @@ -861,6 +879,12 @@ namespace coral
}
}

// Store qualified_id if present in the JSON
if (j.contains("qualified_id"))
{
obj->set_qualified_id(j["qualified_id"].get<std::string>());
}

if (is_network)
{
const bool has_any_interface = j.contains("arguments") ||
Expand Down
29 changes: 29 additions & 0 deletions core/include/coral_network.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,31 @@

namespace coral
{
/**
* Exception thrown when duplicate qualified_id values are detected in a network.
*/
class DuplicateQualifiedIdException : public std::runtime_error
{
private:
std::string duplicate_id;

public:
explicit DuplicateQualifiedIdException(const std::string &id)
: std::runtime_error("Duplicate qualified_id found in network: '" + id +
"'")
, duplicate_id(id)
{}

/**
* Get the duplicate qualified_id that caused this exception.
*/
const std::string &
get_duplicate_id() const
{
return duplicate_id;
}
};

/**
* Directed edge between two nodes and their ports.
*/
Expand Down Expand Up @@ -66,6 +91,7 @@ namespace coral
std::string name;
static size_t n_threads;
static std::filesystem::path touch_file_base_path;
std::set<std::string> auto_generated_qualified_ids; // Track auto-generated IDs

void
rebuild_taskflow();
Expand Down Expand Up @@ -122,6 +148,9 @@ namespace coral
const std::map<unsigned int, std::string> &
get_nodes_name() const;

std::string
get_node_qualified_id(unsigned int id) const;

void
add_connection(unsigned int id, const Connection &conn);

Expand Down
Loading