Add machine workshop, typed logic, mechanics and sensor-driven ECUs
This commit is contained in:
130
tests/machine_port_tests.cpp
Normal file
130
tests/machine_port_tests.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "FluidSim/Machine.hpp"
|
||||
#include "FluidSim/ScenarioCatalog.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
void check(bool value,const char* why){if(!value)throw std::runtime_error(why);}
|
||||
template<class F>void rejects(F&& operation,const char* why){bool rejected=false;try{operation();}catch(const std::exception&){rejected=true;}check(rejected,why);}
|
||||
struct TempDirectory {
|
||||
std::filesystem::path path=std::filesystem::temp_directory_path()/("fluidsim-port-tests-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
TempDirectory(){std::filesystem::create_directories(path);}
|
||||
~TempDirectory(){std::error_code error;std::filesystem::remove_all(path,error);}
|
||||
};
|
||||
void write(const std::filesystem::path& file,const std::string& value){std::ofstream stream(file);stream.exceptions(std::ios::failbit|std::ios::badbit);stream<<value;}
|
||||
std::string read(const std::filesystem::path& file){std::ifstream stream(file);return {(std::istreambuf_iterator<char>(stream)),{}};}
|
||||
MachinePortDescriptor signal(PortDomain domain,PortDirection direction,SignalQuantity quantity=SignalQuantity::None){MachinePortDescriptor port;port.key="value";port.name="Value";port.domain=domain;port.direction=direction;port.quantity=quantity;port.cardinality=direction==PortDirection::Output?PortCardinality::Multiple:PortCardinality::Single;return port;}
|
||||
}
|
||||
int main()try{
|
||||
MachinePart undeclared;undeclared.kind=MachinePartKind(machine_part_kind_count);
|
||||
rejects([&]{machine_port_count(undeclared);},"Undeclared part silently acquired gas ports");
|
||||
rejects([&]{machine_part_volume(undeclared);},"Undeclared part silently acquired gas storage");
|
||||
for(int kind=0;kind<machine_part_kind_count;++kind){MachineDocument d;const auto& part=d.add(MachinePartKind(kind));std::set<std::string> keys;
|
||||
for(unsigned i=0;i<machine_port_count(part);++i){const auto descriptor=machine_port_descriptor(part,i);validate_port_descriptor(descriptor);check(keys.insert(descriptor.key).second,"Duplicate built-in stable port key");check(machine_port_index(part,descriptor.key)==i,"Stable port lookup changed an endpoint");}
|
||||
rejects([&]{machine_port_descriptor(part,machine_port_count(part));},"Out-of-range port acquired a descriptor");rejects([&]{machine_port_index(part,"unknown");},"Unknown port key was accepted");
|
||||
}
|
||||
auto boolean_out=signal(PortDomain::Boolean,PortDirection::Output),boolean_in=signal(PortDomain::Boolean,PortDirection::Input);
|
||||
auto number_out=signal(PortDomain::Number,PortDirection::Output,SignalQuantity::AngularSpeed),number_in=signal(PortDomain::Number,PortDirection::Input,SignalQuantity::AngularSpeed);
|
||||
check(port_compatibility(boolean_out,boolean_in).allowed&&port_compatibility(boolean_in,boolean_out).allowed,"Boolean input/output pair was rejected");
|
||||
check(port_compatibility(number_out,number_in).allowed,"Compatible numeric pair was rejected");
|
||||
check(!port_compatibility(boolean_out,number_in).allowed,"Implicit Boolean-to-number conversion was accepted");
|
||||
check(!port_compatibility(boolean_out,boolean_out).allowed&&!port_compatibility(boolean_in,boolean_in).allowed,"Signal direction mismatch was accepted");
|
||||
auto wrong_quantity=number_in;wrong_quantity.quantity=SignalQuantity::Pressure;wrong_quantity.pressure_reference=PressureReference::Absolute;check(!port_compatibility(number_out,wrong_quantity).allowed,"RPM connected to pressure");
|
||||
auto pressure_out=wrong_quantity;pressure_out.direction=PortDirection::Output;pressure_out.cardinality=PortCardinality::Multiple;auto gauge_in=wrong_quantity;gauge_in.pressure_reference=PressureReference::Gauge;
|
||||
check(!port_compatibility(pressure_out,gauge_in).allowed,"Absolute and gauge pressure were implicitly converted");
|
||||
auto faulty=number_in;faulty.quantity=SignalQuantity::None;rejects([&]{validate_port_descriptor(faulty);},"Untyped number was accepted");
|
||||
faulty=wrong_quantity;faulty.pressure_reference=PressureReference::None;rejects([&]{validate_port_descriptor(faulty);},"Pressure without a reference was accepted");
|
||||
faulty=boolean_in;faulty.cardinality=PortCardinality::Multiple;rejects([&]{validate_port_descriptor(faulty);},"Multiple input drivers were permitted");
|
||||
faulty=boolean_in;faulty.default_value=0.0;rejects([&]{validate_port_descriptor(faulty);},"Numeric Boolean default was accepted");
|
||||
faulty=boolean_in;faulty.required=true;faulty.default_value=false;rejects([&]{validate_port_descriptor(faulty);},"Required input silently acquired a default");
|
||||
number_in.minimum=0;number_in.maximum=100;number_in.default_value=20.0;validate_port_descriptor(number_in);
|
||||
check(signal_value_valid_for(SignalValue::number(30),number_in)&&!signal_value_valid_for(SignalValue::number(-1),number_in),"Numeric range check failed");
|
||||
check(!signal_value_valid_for(SignalValue::boolean(true),number_in)&&!signal_value_valid_for(SignalValue::number(1),boolean_in),"Signal value type was coerced");
|
||||
const auto nan=std::numeric_limits<double>::quiet_NaN();check(SignalValue::number(nan).validity==SignalValidity::Fault&&!signal_value_valid_for(SignalValue::number(1,nan),number_in),"Nonfinite signal was treated as valid");
|
||||
for(auto domain:{PortDomain::Gas,PortDomain::Rotational,PortDomain::Translational}){MachinePortDescriptor a;a.key="physical";a.name="Physical";a.domain=domain;validate_port_descriptor(a);check(port_compatibility(a,a).allowed,"Physical bidirectional domain was rejected");check(!port_compatibility(a,boolean_in).allowed,"Signal connected to a physical port");}
|
||||
check(std::string(signal_quantity_unit(SignalQuantity::AngularSpeed))=="rad/s","Internal speed units are not SI");
|
||||
|
||||
TempDirectory temp;const auto file=temp.path/"ports.machine.toml";
|
||||
const std::string legacy=R"(format = 4
|
||||
name = 'Legacy control'
|
||||
[[parts]]
|
||||
id = 1
|
||||
kind = 'ecu'
|
||||
[[parts]]
|
||||
id = 2
|
||||
kind = 'cylinder'
|
||||
[[parts]]
|
||||
id = 3
|
||||
kind = 'throttle'
|
||||
[[connections]]
|
||||
a_part = 1
|
||||
a_port = 0
|
||||
b_part = 2
|
||||
b_port = 2
|
||||
method = 4
|
||||
[[connections]]
|
||||
a_part = 1
|
||||
a_port = 0
|
||||
b_part = 3
|
||||
b_port = 2
|
||||
method = 4
|
||||
)";
|
||||
write(file,legacy);auto d=load_machine(file);check(std::count_if(d.links.begin(),d.links.end(),[](const auto& link){return link.kind==MachineConnectionKind::Control;})==2,"Legacy ECU fan-out was lost");
|
||||
check(machine_link_domain(d,d.links[0])==PortDomain::EcuAssignment,"Legacy control was reinterpreted as a signal");save_machine(d,file);const auto saved=read(file);
|
||||
check(saved.find("format = 10")!=std::string::npos&&saved.find("'assignments'")!=std::string::npos&&saved.find("'ecu_assignment'")!=std::string::npos,"Current format did not serialize stable port/method keys");
|
||||
const auto roundtrip=load_machine(file);check(roundtrip.links.size()==d.links.size()&&roundtrip.links[0].a.port==0&&roundtrip.links[0].b.port==2,"Stable key round-trip changed legacy wiring");
|
||||
auto multiple=d;multiple.links.push_back(multiple.links.front());rejects([&]{validate_machine(multiple);},"Duplicate driver was accepted");
|
||||
auto reversed=d;std::swap(reversed.links[0].a,reversed.links[0].b);rejects([&]{validate_machine(reversed);},"Noncanonical ECU connection passed document validation");
|
||||
check(!machine_connection_preview(d,{1,0},{2,0}).allowed,"Preview accepted a gas/control mismatch");
|
||||
|
||||
const auto custom=[](bool reorder){return std::string(R"(format = 5
|
||||
name = 'Stable custom ports'
|
||||
[[parts]]
|
||||
id = 1
|
||||
kind = 'pipe'
|
||||
position_m = [0,0,0]
|
||||
points_m = [[-0.2,0,0],[0,0,0]]
|
||||
point_diameters_m = [0.02,0.02]
|
||||
[[parts]]
|
||||
id = 2
|
||||
kind = 'chamber'
|
||||
position_m = [0,0,0]
|
||||
)")+(reorder?"port_coordinates = [[1,0,0],[0,0,0]]\nport_keys = ['tap_b','tap_a']\n":"port_coordinates = [[0,0,0],[1,0,0]]\nport_keys = ['tap_a','tap_b']\n")+R"([[connections]]
|
||||
a_part = 1
|
||||
a_port = 'end'
|
||||
b_part = 2
|
||||
b_port = 'tap_a'
|
||||
method = 'orifice'
|
||||
)";};
|
||||
write(file,custom(false));const auto original=load_machine(file);write(file,custom(true));const auto reordered=load_machine(file);
|
||||
check(original.links.front().b.port==0&&reordered.links.front().b.port==1,"Stable key did not follow a reordered physical port");
|
||||
check(machine_port_position(*original.find(2),0)==machine_port_position(*reordered.find(2),1),"Reordered port changed its physical endpoint");
|
||||
auto invalid=custom(false);invalid.replace(invalid.find("'tap_a'",invalid.find("[[connections]]")),7,"'unknown'");write(file,invalid);rejects([&]{load_machine(file);},"Unknown serialized port key silently reconnected");
|
||||
invalid=custom(false);invalid.replace(invalid.find("'orifice'"),9,"'unknown'");write(file,invalid);rejects([&]{load_machine(file);},"Unknown serialized method was accepted");
|
||||
auto duplicate=original;duplicate.find(2)->port_keys={"tap_a","tap_a"};rejects([&]{validate_machine(duplicate);},"Duplicate custom identities were accepted");
|
||||
write(file,R"(format = 4
|
||||
[[parts]]
|
||||
id = 1
|
||||
kind = 'pipe'
|
||||
point_diameters_m = [0.04,0.04]
|
||||
[[parts]]
|
||||
id = 2
|
||||
kind = 'junction'
|
||||
junction_method = 1
|
||||
position_m = [0.15,0.25,0]
|
||||
[[connections]]
|
||||
a_part = 1
|
||||
a_port = 1
|
||||
b_part = 2
|
||||
b_port = 5
|
||||
method = 0
|
||||
)");
|
||||
const auto alias=load_machine(file);check(alias.links.front().b.port==0&&machine_port_count(*alias.find(2))==1,"Legacy junction alias did not migrate to its shared port");
|
||||
for(const auto& source:machine_files()){auto machine=load_machine(source);const auto copy=temp.path/source.filename();save_machine(machine,copy);const auto loaded=load_machine(copy);check(loaded.parts.size()==machine.parts.size()&&loaded.links.size()==machine.links.size(),"Existing machine changed topology during migration");}
|
||||
std::cout<<"Typed port domains, quantities, validation and version 5 migration passed\n";return 0;
|
||||
}catch(const std::exception& error){std::cerr<<error.what()<<'\n';return 1;}
|
||||
Reference in New Issue
Block a user