diff --git a/.github/workflows/buildCSharp.yml b/.github/workflows/buildCSharp.yml index 67bd5d7d547..ffaeafc2ad7 100644 --- a/.github/workflows/buildCSharp.yml +++ b/.github/workflows/buildCSharp.yml @@ -23,9 +23,9 @@ jobs: name: [Ubuntu, macOS, macOS_arm64, Windows64, Windows32] include: - name: Ubuntu - os: ubuntu-20.04 + os: ubuntu-22.04 - name: macOS - os: macos-11 + os: macos-13 - name: macOS_arm64 os: macos-14 - name: Windows64 diff --git a/CMakeLists.txt b/CMakeLists.txt index 05fe5faf2f9..2d833b0f85c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,7 +161,7 @@ get_directory_property(hasParent PARENT_DIRECTORY) # TODO: Modify the more specific variables as needed to indicate prerelease, etc # Keep in beta in-between release cycles. Set to empty string (or comment out) for official) -set(PROJECT_VERSION_PRERELEASE "rc2") +set(PROJECT_VERSION_PRERELEASE "rc3") # OpenStudio version: Only include Major.Minor.Patch, eg "3.0.0", even if you have a prerelease tag set(OPENSTUDIO_VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") @@ -638,11 +638,15 @@ endif() if(UNIX) if(APPLE) + # TODO: temp for #5281 - Resigned with a different entitlements to avoid an issue when we pip install stuff into the E+ dir with native comps (numpy for eg) + set(ENERGYPLUS_REPO "jmarrec") + set(ENERGYPLUS_RELEASE_NAME "v24.2.0a-entitlements") + if (ARCH MATCHES "arm64") - set(ENERGYPLUS_EXPECTED_HASH f36afc055a675ae95523d6f41ec478c0) + set(ENERGYPLUS_EXPECTED_HASH 29616d6aa23e2fc0f71362da9794ef08) set(ENERGYPLUS_PLATFORM "Darwin-macOS13-arm64") else() - set(ENERGYPLUS_EXPECTED_HASH b2003c461277c0bd4e91a4c003b350ac) + set(ENERGYPLUS_EXPECTED_HASH 21bdad40fbc560b4b382469ef9f96936) set(ENERGYPLUS_PLATFORM "Darwin-macOS12.1-x86_64") endif() elseif(LSB_RELEASE_ID_SHORT MATCHES "CentOS") @@ -843,7 +847,7 @@ if(BUILD_CLI) set(OPENSTUDIO_GEMS_BASEURL "http://openstudio-resources.s3.amazonaws.com/dependencies") # TODO: temp - set(OPENSTUDIO_GEMS_BASEURL "https://github.com/NREL/openstudio-gems/releases/download/v3.9.0-RC2") + set(OPENSTUDIO_GEMS_BASEURL "https://github.com/NREL/openstudio-gems/releases/download/v3.9.0-RC3") # To use the package produced by a PR to https://github.com/NREL/openstudio-gems set(USE_OPENSTUDIO_GEMS_PR FALSE) @@ -855,19 +859,19 @@ if(BUILD_CLI) if(UNIX) if(APPLE) if (ARCH MATCHES arm64) - set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20241101-darwin_arm64-3.2.2.tar.gz") - set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "c4cf7754444937e5df496f23af6d76ad") + set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20241112-darwin_arm64-3.2.2.tar.gz") + set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "cb6a884366780c8ba5da2852ca60e98a") else() - set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20241101-darwin-3.2.2.tar.gz") - set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "5754b6ba7cff435d33f6c4dcdcd6bfc4") + set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20241112-darwin-3.2.2.tar.gz") + set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "ce81fcf1bf707281082cde3d87dac114") endif() else() if (ARCH MATCHES "arm64") set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20240517-linux_arm64-3.2.2.tar.gz") set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "f6d094a3bdaf1476ce7911e74276993b") else() - set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20241101-linux-3.2.2.tar.gz") - set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "972a9246fd2a6d43bc7eb23e2ad2b715") + set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20241112-linux-3.2.2.tar.gz") + set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "7551691c842a86cc8198959b5d0f1ee9") endif() if (USE_OPENSTUDIO_GEMS_PR) set(OPENSTUDIO_GEMS_BASEURL "${OPENSTUDIO_GEMS_BASEURL}/openstudio-gems-linux/${OPENSTUDIO_GEMS_PR_NUMBER}") @@ -875,8 +879,8 @@ if(BUILD_CLI) endif() elseif(WIN32) # OpenStudio gems are only supported on 64 bit windows - set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20241101-windows-3.2.2.tar.gz") - set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "a5f1fde7ab2555e736d3d21ba5beb683") + set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20241112-windows-3.2.2.tar.gz") + set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "8f1cdcca664a1d3d80908230a00981ef") if (USE_OPENSTUDIO_GEMS_PR) set(OPENSTUDIO_GEMS_BASEURL "${OPENSTUDIO_GEMS_BASEURL}/openstudio-gems-windows/${OPENSTUDIO_GEMS_PR_NUMBER}") endif() diff --git a/ProjectMacros.cmake b/ProjectMacros.cmake index cf944fa06a2..3ca827748ed 100644 --- a/ProjectMacros.cmake +++ b/ProjectMacros.cmake @@ -532,6 +532,7 @@ macro(MAKE_SWIG_TARGET NAME SIMPLENAME KEY_I_FILE I_FILES PARENT_TARGET PARENT_S set( model_names OpenStudioMeasure + OpenStudioAlfalfa OpenStudioModel OpenStudioModelAirflow OpenStudioModelAvailabilityManager diff --git a/csharp/CMakeLists.txt b/csharp/CMakeLists.txt index 995b65987ff..4bffd5e51ac 100644 --- a/csharp/CMakeLists.txt +++ b/csharp/CMakeLists.txt @@ -83,6 +83,7 @@ set(translator_wrappers set(model_wrappers csharp_OpenStudioMeasure_wrap.cxx + csharp_OpenStudioAlfalfa_wrap.cxx csharp_OpenStudioModel_wrap.cxx csharp_OpenStudioModelAirflow_wrap.cxx csharp_OpenStudioModelAvailabilityManager_wrap.cxx diff --git a/resources/workflow/invalid_measures/missing_a_measure.osw b/resources/workflow/invalid_measures/missing_a_measure.osw new file mode 100644 index 00000000000..2f78c0633ba --- /dev/null +++ b/resources/workflow/invalid_measures/missing_a_measure.osw @@ -0,0 +1,19 @@ +{ + "weather_file": "../../Examples/compact_osw/files/srrl_2013_amy.epw", + "seed_file": "../example_model.osm", + "measure_paths": ["../measures/"], + "steps": [ + { + "measure_dir_name": "FakeModelMeasure", + "arguments": {} + }, + { + "measure_dir_name": "NON_EXISTING_MEASURE_THIS_SHOULD_BE_CAUGHT", + "arguments": {} + }, + { + "measure_dir_name": "FakeReport", + "arguments": {} + } + ] +} diff --git a/resources/workflow/invalid_measures/unloadable_measure.osw b/resources/workflow/invalid_measures/unloadable_measure.osw new file mode 100644 index 00000000000..cb5d1c88178 --- /dev/null +++ b/resources/workflow/invalid_measures/unloadable_measure.osw @@ -0,0 +1,19 @@ +{ + "weather_file": "../../Examples/compact_osw/files/srrl_2013_amy.epw", + "seed_file": "../example_model.osm", + "measure_paths": ["../measures/"], + "steps": [ + { + "measure_dir_name": "FakeModelMeasure", + "arguments": {} + }, + { + "measure_dir_name": "UnloadableMeasure", + "arguments": {} + }, + { + "measure_dir_name": "FakeReport", + "arguments": {} + } + ] +} diff --git a/resources/workflow/invalid_measures/wrong_measure_type_order.osw b/resources/workflow/invalid_measures/wrong_measure_type_order.osw new file mode 100644 index 00000000000..01b28e49177 --- /dev/null +++ b/resources/workflow/invalid_measures/wrong_measure_type_order.osw @@ -0,0 +1,15 @@ +{ + "weather_file": "../../Examples/compact_osw/files/srrl_2013_amy.epw", + "seed_file": "../example_model.osm", + "measure_paths": ["../measures/"], + "steps": [ + { + "measure_dir_name": "FakeReport", + "arguments": {} + }, + { + "measure_dir_name": "FakeModelMeasure", + "arguments": {} + } + ] +} diff --git a/resources/workflow/measures/FakeModelMeasure/measure.rb b/resources/workflow/measures/FakeModelMeasure/measure.rb new file mode 100644 index 00000000000..68a978a9096 --- /dev/null +++ b/resources/workflow/measures/FakeModelMeasure/measure.rb @@ -0,0 +1,42 @@ +class FakeModelMeasure < OpenStudio::Measure::ModelMeasure + # human readable name + def name + # Measure name should be the title case of the class name. + return 'A dumb ModelMeasure' + end + + # human readable description + def description + return 'Does nothing' + end + + # human readable description of modeling approach + def modeler_description + return 'Just for testing' + end + + # define the arguments that the user will input + def arguments(model) + args = OpenStudio::Measure::OSArgumentVector.new + + return args + end + + # define what happens when the measure is run + def run(model, runner, user_arguments) + super(model, runner, user_arguments) # Do **NOT** remove this line + + # use the built-in error checking + if !runner.validateUserArguments(arguments(model), user_arguments) + return false + end + + # report final condition of model + runner.registerFinalCondition("The FakeModelMeasure run.") + + return true + end +end + +# register the measure to be used by the application +FakeModelMeasure.new.registerWithApplication diff --git a/resources/workflow/measures/FakeModelMeasure/measure.xml b/resources/workflow/measures/FakeModelMeasure/measure.xml new file mode 100644 index 00000000000..a1fd30f66f0 --- /dev/null +++ b/resources/workflow/measures/FakeModelMeasure/measure.xml @@ -0,0 +1,44 @@ + + + 3.1 + fake_model_measure + 677b8fd3-2627-4516-b090-f6e47dc99fea + 162465e5-b6f6-419f-ab5f-c2fc4bd549e0 + 2024-11-07T11:55:10Z + 82D8F881 + FakeModelMeasure + A dumb ModelMeasure + Does nothing + Just for testing + + + + + Envelope.Form + + + + Measure Type + ModelMeasure + string + + + Measure Language + Ruby + string + + + + + + OpenStudio + 3.9.0 + 3.9.0 + + measure.rb + rb + script + DDA977B0 + + + diff --git a/resources/workflow/measures/UnloadableMeasure/README.md b/resources/workflow/measures/UnloadableMeasure/README.md new file mode 100644 index 00000000000..4459d7e1aa3 --- /dev/null +++ b/resources/workflow/measures/UnloadableMeasure/README.md @@ -0,0 +1 @@ +This doesnt have a xml diff --git a/resources/workflow/measures/UnloadableMeasure/measure.rb b/resources/workflow/measures/UnloadableMeasure/measure.rb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/alfalfa/Alfalfa.i b/src/alfalfa/Alfalfa.i index 1c3956b2b5b..ebe639c05a7 100644 --- a/src/alfalfa/Alfalfa.i +++ b/src/alfalfa/Alfalfa.i @@ -6,6 +6,8 @@ #endif %include +#define ALFALFA_API + %include %import %import @@ -13,7 +15,6 @@ %ignore openstudio::alfalfa::detail; %{ - #include #include #include #include @@ -33,15 +34,14 @@ using namespace openstudio::alfalfa; %} -%ignore openstudio::alfalfa::ComponentBase; +%ignore openstudio::alfalfa::AlfalfaComponentBase; %ignore openstudio::alfalfa::AlfalfaActuator::clone; %ignore openstudio::alfalfa::AlfalfaConstant::clone; %ignore openstudio::alfalfa::AlfalfaMeter::clone; %ignore openstudio::alfalfa::AlfalfaGlobalVariable::clone; %ignore openstudio::alfalfa::AlfalfaOutputVariable::clone; -%include -%include +%include %include %include %include diff --git a/src/alfalfa/AlfalfaActuator.hpp b/src/alfalfa/AlfalfaActuator.hpp index 9bc82dccb29..1ce5c05df2c 100644 --- a/src/alfalfa/AlfalfaActuator.hpp +++ b/src/alfalfa/AlfalfaActuator.hpp @@ -2,13 +2,13 @@ #define ALFALFA_COMPONENT_ACTUATOR_HPP #include "AlfalfaAPI.hpp" -#include "ComponentBase.hpp" +#include "AlfalfaComponentBase.hpp" #include "../utilities/idf/IdfObject.hpp" namespace openstudio { namespace alfalfa { - class ALFALFA_API AlfalfaActuator : public ComponentBase + class ALFALFA_API AlfalfaActuator : public AlfalfaComponentBase { public: /** @@ -26,15 +26,15 @@ namespace alfalfa { Json::Value toJSON() const override; - ComponentCapability capability() const override { - return ComponentCapability::Bidirectional; + AlfalfaComponentCapability capability() const override { + return AlfalfaComponentCapability::Bidirectional; } - ComponentType type() const override { - return ComponentType::Actuator; + AlfalfaComponentType type() const override { + return AlfalfaComponentType::Actuator; } - std::unique_ptr clone() const override { + std::unique_ptr clone() const override { return std::make_unique(*this); } diff --git a/src/alfalfa/AlfalfaComponent.cpp b/src/alfalfa/AlfalfaComponent.cpp index 6b10d5bf702..bde3cae7039 100644 --- a/src/alfalfa/AlfalfaComponent.cpp +++ b/src/alfalfa/AlfalfaComponent.cpp @@ -6,11 +6,11 @@ namespace alfalfa { return m_component->toJSON(); } - ComponentCapability AlfalfaComponent::capability() const { + AlfalfaComponentCapability AlfalfaComponent::capability() const { return m_component->capability(); } - ComponentType AlfalfaComponent::type() const { + AlfalfaComponentType AlfalfaComponent::type() const { return m_component->type(); } diff --git a/src/alfalfa/AlfalfaComponent.hpp b/src/alfalfa/AlfalfaComponent.hpp index b87b3ef5102..951ed4516f9 100644 --- a/src/alfalfa/AlfalfaComponent.hpp +++ b/src/alfalfa/AlfalfaComponent.hpp @@ -5,14 +5,14 @@ #include #include "AlfalfaAPI.hpp" -#include "ComponentBase.hpp" +#include "AlfalfaComponentBase.hpp" namespace openstudio { namespace alfalfa { class ALFALFA_API AlfalfaComponent { public: - template ::value, bool> = true> + template ::value, bool> = true> AlfalfaComponent(T component) : m_component(std::make_unique(std::move(component))) {} AlfalfaComponent(const AlfalfaComponent& other) : m_component(other.m_component->clone()) {} @@ -32,9 +32,9 @@ namespace alfalfa { Json::Value toJSON() const; - ComponentCapability capability() const; + AlfalfaComponentCapability capability() const; - ComponentType type() const; + AlfalfaComponentType type() const; std::string typeName() const; @@ -46,7 +46,7 @@ namespace alfalfa { private: AlfalfaComponent() = default; - std::unique_ptr m_component; + std::unique_ptr m_component; }; inline bool operator==(const AlfalfaComponent& lhs, const AlfalfaComponent& rhs) { diff --git a/src/alfalfa/AlfalfaComponentBase.cpp b/src/alfalfa/AlfalfaComponentBase.cpp new file mode 100644 index 00000000000..1e114f07ced --- /dev/null +++ b/src/alfalfa/AlfalfaComponentBase.cpp @@ -0,0 +1,15 @@ +#include "AlfalfaComponentBase.hpp" + +namespace openstudio { +namespace alfalfa { + + bool AlfalfaComponentBase::canInput() const { + return capability() == AlfalfaComponentCapability::Bidirectional || capability() == AlfalfaComponentCapability::Input; + } + + bool AlfalfaComponentBase::canOutput() const { + return capability() == AlfalfaComponentCapability::Bidirectional || capability() == AlfalfaComponentCapability::Output; + } + +} // namespace alfalfa +} // namespace openstudio diff --git a/src/alfalfa/ComponentBase.hpp b/src/alfalfa/AlfalfaComponentBase.hpp similarity index 52% rename from src/alfalfa/ComponentBase.hpp rename to src/alfalfa/AlfalfaComponentBase.hpp index c85ba317886..b5239deb298 100644 --- a/src/alfalfa/ComponentBase.hpp +++ b/src/alfalfa/AlfalfaComponentBase.hpp @@ -3,27 +3,23 @@ #include "AlfalfaAPI.hpp" -#include "../utilities/core/Enum.hpp" +#include "../utilities/data/DataEnums.hpp" #include namespace openstudio { namespace alfalfa { - OPENSTUDIO_ENUM(ComponentCapability, ((Input))((Output))((Bidirectional))) - - OPENSTUDIO_ENUM(ComponentType, ((Actuator))((Constant))((Meter))((OutputVariable))((GlobalVariable))) - - class ALFALFA_API ComponentBase + class ALFALFA_API AlfalfaComponentBase { public: - virtual ~ComponentBase() = default; + virtual ~AlfalfaComponentBase() = default; virtual Json::Value toJSON() const = 0; - virtual ComponentCapability capability() const = 0; + virtual AlfalfaComponentCapability capability() const = 0; - virtual ComponentType type() const = 0; + virtual AlfalfaComponentType type() const = 0; virtual std::string typeName() const { return type().valueName(); @@ -31,7 +27,7 @@ namespace alfalfa { virtual std::string deriveName() const = 0; - virtual std::unique_ptr clone() const = 0; + virtual std::unique_ptr clone() const = 0; virtual bool canInput() const; diff --git a/src/alfalfa/AlfalfaConstant.hpp b/src/alfalfa/AlfalfaConstant.hpp index 1d74e8b6226..e07403fe387 100644 --- a/src/alfalfa/AlfalfaConstant.hpp +++ b/src/alfalfa/AlfalfaConstant.hpp @@ -3,11 +3,11 @@ #include "AlfalfaAPI.hpp" -#include "ComponentBase.hpp" +#include "AlfalfaComponentBase.hpp" namespace openstudio { namespace alfalfa { - class ALFALFA_API AlfalfaConstant : public ComponentBase + class ALFALFA_API AlfalfaConstant : public AlfalfaComponentBase { public: /** @@ -19,15 +19,15 @@ namespace alfalfa { Json::Value toJSON() const override; - ComponentCapability capability() const override { - return ComponentCapability::Output; + AlfalfaComponentCapability capability() const override { + return AlfalfaComponentCapability::Output; } - ComponentType type() const override { - return ComponentType::Constant; + AlfalfaComponentType type() const override { + return AlfalfaComponentType::Constant; } - std::unique_ptr clone() const override { + std::unique_ptr clone() const override { return std::make_unique(*this); } diff --git a/src/alfalfa/AlfalfaGlobalVariable.hpp b/src/alfalfa/AlfalfaGlobalVariable.hpp index 3f389bb08a4..e62efa171c0 100644 --- a/src/alfalfa/AlfalfaGlobalVariable.hpp +++ b/src/alfalfa/AlfalfaGlobalVariable.hpp @@ -3,13 +3,13 @@ #include "AlfalfaAPI.hpp" -#include "ComponentBase.hpp" +#include "AlfalfaComponentBase.hpp" #include "../utilities/idf/IdfObject.hpp" namespace openstudio { namespace alfalfa { - class ALFALFA_API AlfalfaGlobalVariable : public ComponentBase + class ALFALFA_API AlfalfaGlobalVariable : public AlfalfaComponentBase { public: /** @@ -27,15 +27,15 @@ namespace alfalfa { Json::Value toJSON() const override; - ComponentCapability capability() const override { - return ComponentCapability::Bidirectional; + AlfalfaComponentCapability capability() const override { + return AlfalfaComponentCapability::Bidirectional; } - ComponentType type() const override { - return ComponentType::GlobalVariable; + AlfalfaComponentType type() const override { + return AlfalfaComponentType::GlobalVariable; } - std::unique_ptr clone() const override { + std::unique_ptr clone() const override { return std::make_unique(*this); } diff --git a/src/alfalfa/AlfalfaJSON.cpp b/src/alfalfa/AlfalfaJSON.cpp index 518b09946a7..9fa41f5324e 100644 --- a/src/alfalfa/AlfalfaJSON.cpp +++ b/src/alfalfa/AlfalfaJSON.cpp @@ -83,9 +83,9 @@ namespace alfalfa { Json::Value AlfalfaJSON_Impl::toJSON() const { Json::Value root; - for (const auto& point : m_points) { + for (Json::ArrayIndex i = 0; const auto& point : points()) { // No guard here as the toJSON call will throw an exception if the id does not exist. - root[point.id().get()] = point.toJSON(); + root[i++] = point.toJSON(); } return root; } diff --git a/src/alfalfa/AlfalfaMeter.hpp b/src/alfalfa/AlfalfaMeter.hpp index 51001bf47af..8d2593764b0 100644 --- a/src/alfalfa/AlfalfaMeter.hpp +++ b/src/alfalfa/AlfalfaMeter.hpp @@ -3,13 +3,13 @@ #include "AlfalfaAPI.hpp" -#include "ComponentBase.hpp" +#include "AlfalfaComponentBase.hpp" #include "../utilities/idf/IdfObject.hpp" namespace openstudio { namespace alfalfa { - class ALFALFA_API AlfalfaMeter : public ComponentBase + class ALFALFA_API AlfalfaMeter : public AlfalfaComponentBase { public: /** @@ -27,15 +27,15 @@ namespace alfalfa { Json::Value toJSON() const override; - ComponentCapability capability() const override { - return ComponentCapability::Output; + AlfalfaComponentCapability capability() const override { + return AlfalfaComponentCapability::Output; } - ComponentType type() const override { - return ComponentType::Meter; + AlfalfaComponentType type() const override { + return AlfalfaComponentType::Meter; } - std::unique_ptr clone() const override { + std::unique_ptr clone() const override { return std::make_unique(*this); } diff --git a/src/alfalfa/AlfalfaOutputVariable.hpp b/src/alfalfa/AlfalfaOutputVariable.hpp index bf6b010a86b..c8d85d8ea71 100644 --- a/src/alfalfa/AlfalfaOutputVariable.hpp +++ b/src/alfalfa/AlfalfaOutputVariable.hpp @@ -3,13 +3,13 @@ #include "AlfalfaAPI.hpp" -#include "ComponentBase.hpp" +#include "AlfalfaComponentBase.hpp" #include "../utilities/idf/IdfObject.hpp" namespace openstudio { namespace alfalfa { - class ALFALFA_API AlfalfaOutputVariable : public ComponentBase + class ALFALFA_API AlfalfaOutputVariable : public AlfalfaComponentBase { public: /** @@ -27,15 +27,15 @@ namespace alfalfa { Json::Value toJSON() const override; - ComponentCapability capability() const override { - return ComponentCapability::Output; + AlfalfaComponentCapability capability() const override { + return AlfalfaComponentCapability::Output; } - ComponentType type() const override { - return ComponentType::OutputVariable; + AlfalfaComponentType type() const override { + return AlfalfaComponentType::OutputVariable; } - std::unique_ptr clone() const override { + std::unique_ptr clone() const override { return std::make_unique(*this); } diff --git a/src/alfalfa/AlfalfaPoint.cpp b/src/alfalfa/AlfalfaPoint.cpp index b3dfb301187..9ffd9a03c59 100644 --- a/src/alfalfa/AlfalfaPoint.cpp +++ b/src/alfalfa/AlfalfaPoint.cpp @@ -7,12 +7,11 @@ #include #include +#include namespace openstudio { namespace alfalfa { - static constexpr std::string_view ID_VALID_CHARS_MSG = "IDs can only contain letters, numbers, and the following special characters _-[]():"; - namespace detail { AlfalfaPoint_Impl::AlfalfaPoint_Impl(const std::string& display_name) { @@ -22,11 +21,7 @@ namespace alfalfa { Json::Value AlfalfaPoint_Impl::toJSON() const { Json::Value point; - if (auto id_ = id()) { - point["id"] = *id_; - } else { - throw std::runtime_error(fmt::format("Point requires a valid ID for export. {}", ID_VALID_CHARS_MSG)); - } + point["id"] = id(); point["name"] = displayName(); if (auto input_ = input()) { point["input"]["type"] = input_->typeName(); @@ -80,24 +75,17 @@ namespace alfalfa { } void AlfalfaPoint_Impl::setId(const std::string& id) { - if (isValidId(id)) { - m_id = id; - } else { - throw std::runtime_error(ID_VALID_CHARS_MSG.data()); + if (id.empty()) { + throw std::runtime_error("Id must have non-zero length"); } + m_id = toIdString(id); } - boost::optional AlfalfaPoint_Impl::id() const { - boost::optional result = m_id; - if (!result.is_initialized()) { - std::string id = toIdString(displayName()); - if (isValidId(id)) { - result = id; - } else { - LOG(Warn, fmt::format("Display name '{}' does not produce a valid point ID. Manually set a valid ID or export will fail.", displayName())); - } + std::string AlfalfaPoint_Impl::id() const { + if (m_id.empty()) { + return toIdString(displayName()); } - return result; + return m_id; } void AlfalfaPoint_Impl::setDisplayName(const std::string& display_name) { @@ -105,12 +93,6 @@ namespace alfalfa { throw std::runtime_error("Display name must have non-zero length"); } m_display_name = display_name; - if (!m_id.is_initialized()) { - const std::string id = toIdString(display_name); - if (!isValidId(id)) { - LOG(Warn, fmt::format("Display name '{}' does not produce a valid point ID. Manually set a valid ID or export will fail.", display_name)); - } - } } std::string AlfalfaPoint_Impl::displayName() const { @@ -125,17 +107,13 @@ namespace alfalfa { return m_optional; } - bool AlfalfaPoint_Impl::isValidId(const std::string& id) { - return !id.empty() && boost::regex_match(id, boost::regex(R"(^[A-Za-z0-9_\-\[\]:()]*$)")); - } - std::string AlfalfaPoint_Impl::toIdString(const std::string& str) { - return boost::regex_replace(str, boost::regex(" "), "_"); + std::string id_string = str; + std::replace(id_string.begin(), id_string.end(), ' ', '_'); + return id_string; } } // namespace detail - // AlfalfaPoint::AlfalfaPoint(const AlfalfaPoint& point) : m_impl(point.m_impl) {} - AlfalfaPoint::AlfalfaPoint(const std::string& display_name) : m_impl(std::make_shared(display_name)) {} void AlfalfaPoint::setInput(const AlfalfaComponent& component) { @@ -162,7 +140,7 @@ namespace alfalfa { return m_impl->units(); } - boost::optional AlfalfaPoint::id() const { + std::string AlfalfaPoint::id() const { return m_impl->id(); } diff --git a/src/alfalfa/AlfalfaPoint.hpp b/src/alfalfa/AlfalfaPoint.hpp index 3ed3ee51589..1ca63e380a4 100644 --- a/src/alfalfa/AlfalfaPoint.hpp +++ b/src/alfalfa/AlfalfaPoint.hpp @@ -69,7 +69,7 @@ namespace alfalfa { /** * Get id of point. By default this will be a version of the display name with spaces removed. */ - boost::optional id() const; + std::string id() const; /** * Set id of point. This is the component which will uniquely identify the point in the API. diff --git a/src/alfalfa/AlfalfaPoint_Impl.hpp b/src/alfalfa/AlfalfaPoint_Impl.hpp index f6d0810a65c..bc311434e5f 100644 --- a/src/alfalfa/AlfalfaPoint_Impl.hpp +++ b/src/alfalfa/AlfalfaPoint_Impl.hpp @@ -32,7 +32,7 @@ namespace alfalfa { boost::optional units() const; - boost::optional id() const; + std::string id() const; void setId(const std::string& id); @@ -47,13 +47,11 @@ namespace alfalfa { private: static std::string toIdString(const std::string& str); - static bool isValidId(const std::string& id); - boost::optional m_input; boost::optional m_output; std::string m_display_name; boost::optional m_units; - boost::optional m_id; + std::string m_id; bool m_optional = true; // configure logging diff --git a/src/alfalfa/CMakeLists.txt b/src/alfalfa/CMakeLists.txt index c87da18b12d..9fe7e350d15 100644 --- a/src/alfalfa/CMakeLists.txt +++ b/src/alfalfa/CMakeLists.txt @@ -5,8 +5,8 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR}/) set(${target_name}_src AlfalfaAPI.hpp - ComponentBase.cpp - ComponentBase.hpp + AlfalfaComponentBase.cpp + AlfalfaComponentBase.hpp AlfalfaComponent.hpp AlfalfaComponent.cpp AlfalfaConstant.hpp @@ -58,4 +58,4 @@ if(BUILD_TESTING) endif() -MAKE_SWIG_TARGET(OpenStudioAlfalfa alfalfa "${CMAKE_CURRENT_SOURCE_DIR}/Alfalfa.i" "${${target_name}_swig_src}" ${target_name} OpenStudioModel) +MAKE_SWIG_TARGET(OpenStudioAlfalfa alfalfa "${CMAKE_CURRENT_SOURCE_DIR}/Alfalfa.i" "${${target_name}_swig_src}" ${target_name} OpenStudioOSVersion) diff --git a/src/alfalfa/ComponentBase.cpp b/src/alfalfa/ComponentBase.cpp deleted file mode 100644 index 4ed339d05da..00000000000 --- a/src/alfalfa/ComponentBase.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "ComponentBase.hpp" - -namespace openstudio { -namespace alfalfa { - - bool ComponentBase::canInput() const { - return capability() == ComponentCapability::Bidirectional || capability() == ComponentCapability::Input; - } - - bool ComponentBase::canOutput() const { - return capability() == ComponentCapability::Bidirectional || capability() == ComponentCapability::Output; - } - -} // namespace alfalfa -} // namespace openstudio diff --git a/src/alfalfa/test/AlfalfaJSON_GTest.cpp b/src/alfalfa/test/AlfalfaJSON_GTest.cpp index 6b5e568871d..6a623d7c409 100644 --- a/src/alfalfa/test/AlfalfaJSON_GTest.cpp +++ b/src/alfalfa/test/AlfalfaJSON_GTest.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include "../AlfalfaJSON.hpp" @@ -194,74 +195,46 @@ TEST(AlfalfaJSON, json_serialization) { const bool parsing_success = Json::parseFromStream(r_builder, ifs, &root, &formatted_errors); EXPECT_TRUE(parsing_success); EXPECT_EQ(alfalfa.toJSON(), root); - for (const AlfalfaPoint& point : alfalfa.points()) { - EXPECT_EQ(root[point.id().get()], point.toJSON()); + for (Json::ArrayIndex i = 0; const auto& point : alfalfa.points()) { + EXPECT_EQ(root[i++], point.toJSON()); } } TEST(AlfalfaJSON, point_exceptions_logging) { - const std::string ID_VALID_CHARS_MSG = "IDs can only contain letters, numbers, and the following special characters _-[]():"; - const std::string DISPLAY_NAME_VALID_CHARS_MSG = "Display name '{}' does not produce a valid point ID. Manually set a valid ID or export will fail."; + const std::string DISPLAY_NAME_EMPTY_ERROR = "Display name must have non-zero length"; + const std::string ID_EMPTY_ERROR = "Id must have non-zero length"; const std::string LOG_CHANNEL = "openstudio.AlfalfaPoint"; StringStreamLogSink ss; ss.setLogLevel(Warn); - const AlfalfaPoint point("Point"); + AlfalfaPoint point("Point"); ASSERT_EQ(0, ss.logMessages().size()); point.id(); ASSERT_EQ(0, ss.logMessages().size()); //Test logging in constructor - AlfalfaPoint invalid_point("Point$$$"); - ASSERT_EQ(1, ss.logMessages().size()); - LogMessage invalid_id_msg = ss.logMessages().at(0); - EXPECT_EQ(invalid_id_msg.logMessage(), fmt::format(fmt::runtime(DISPLAY_NAME_VALID_CHARS_MSG), "Point$$$")); - EXPECT_EQ(invalid_id_msg.logLevel(), Warn); - EXPECT_EQ(invalid_id_msg.logChannel(), LOG_CHANNEL); - ss.resetStringStream(); - ASSERT_EQ(ss.logMessages().size(), 0); - - // Test logging when getting id() - const boost::optional invalid_point_id = invalid_point.id(); - EXPECT_FALSE(invalid_point_id.is_initialized()); - ASSERT_EQ(ss.logMessages().size(), 1); - invalid_id_msg = ss.logMessages().at(0); - EXPECT_EQ(invalid_id_msg.logMessage(), fmt::format(fmt::runtime(DISPLAY_NAME_VALID_CHARS_MSG), "Point$$$")); - EXPECT_EQ(invalid_id_msg.logLevel(), Warn); - EXPECT_EQ(invalid_id_msg.logChannel(), LOG_CHANNEL); - ss.resetStringStream(); - ASSERT_EQ(ss.logMessages().size(), 0); - - // Test exception handling in setId() EXPECT_THROW( { try { - invalid_point.setId("Point_123_$$$"); + const AlfalfaPoint invalid_point(""); } catch (const std::runtime_error& error) { - EXPECT_EQ(error.what(), ID_VALID_CHARS_MSG); + EXPECT_EQ(error.what(), DISPLAY_NAME_EMPTY_ERROR); throw; } }, std::runtime_error); - ASSERT_EQ(ss.logMessages().size(), 0); - // Test exception handling in toJSON() + // Test exception handling in setId() EXPECT_THROW( { try { - Json::Value root = invalid_point.toJSON(); + point.setId(""); } catch (const std::runtime_error& error) { - EXPECT_EQ(error.what(), "Point requires a valid ID for export. " + ID_VALID_CHARS_MSG); + EXPECT_EQ(error.what(), ID_EMPTY_ERROR); throw; } }, std::runtime_error); - ASSERT_EQ(ss.logMessages().size(), 1); - invalid_id_msg = ss.logMessages().at(0); - EXPECT_EQ(invalid_id_msg.logMessage(), fmt::format(fmt::runtime(DISPLAY_NAME_VALID_CHARS_MSG), "Point$$$")); - EXPECT_EQ(invalid_id_msg.logLevel(), Warn); - EXPECT_EQ(invalid_id_msg.logChannel(), LOG_CHANNEL); - ss.resetStringStream(); ASSERT_EQ(ss.logMessages().size(), 0); //Test Calls work when provided with legal input @@ -277,8 +250,7 @@ TEST(AlfalfaJSON, point_exceptions_logging) { // Test that changing display name changes the ID if an ID has not been set. valid_point.setDisplayName("Another Good Point"); ASSERT_EQ(ss.logMessages().size(), 0); - ASSERT_TRUE(valid_point.id().is_initialized()); - ASSERT_EQ(valid_point.id().get(), "Another_Good_Point"); + ASSERT_EQ(valid_point.id(), "Another_Good_Point"); ASSERT_EQ(ss.logMessages().size(), 0); Json::Value valid_json = valid_point.toJSON(); @@ -288,8 +260,7 @@ TEST(AlfalfaJSON, point_exceptions_logging) { const std::string new_id = "Another_Valid_Point(123)"; valid_point.setId(new_id); ASSERT_EQ(ss.logMessages().size(), 0); - ASSERT_TRUE(valid_point.id().is_initialized()); - ASSERT_EQ(valid_point.id().get(), new_id); + ASSERT_EQ(valid_point.id(), new_id); valid_json = valid_point.toJSON(); ASSERT_EQ(ss.logMessages().size(), 0); @@ -297,25 +268,25 @@ TEST(AlfalfaJSON, point_exceptions_logging) { // Test that once an ID is set, setting a new display name won't throw a warning. valid_point.setDisplayName("Valid Name, but Invalid Id $$$"); ASSERT_EQ(ss.logMessages().size(), 0); - ASSERT_EQ(valid_point.id().get(), new_id); + ASSERT_EQ(valid_point.id(), new_id); valid_json = valid_point.toJSON(); ASSERT_EQ(ss.logMessages().size(), 0); } -class InputComponent : public ComponentBase +class InputComponent : public AlfalfaComponentBase { public: InputComponent() = default; - ComponentCapability capability() const override { - return ComponentCapability::Input; + AlfalfaComponentCapability capability() const override { + return AlfalfaComponentCapability::Input; } - openstudio::alfalfa::ComponentType type() const override { - return openstudio::alfalfa::ComponentType::Constant; + AlfalfaComponentType type() const override { + return AlfalfaComponentType::Constant; } - std::unique_ptr clone() const override { + std::unique_ptr clone() const override { return std::make_unique(*this); } diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index 179c35f7a04..e79684fcdfc 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -270,6 +270,63 @@ if(BUILD_TESTING) PASS_REGULAR_EXPRESSION "HI FROM ERB PYTHON PLUGIN[\r\n\t ]*HI FROM JINJA PYTHON PLUGIN" ) + # ======================== Workflows should fail ======================== + add_test(NAME OpenStudioCLI.Run_Validate.MissingAMeasure + COMMAND $ run --show-stdout -w missing_a_measure.osw + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/workflow/invalid_measures/" + ) + set_tests_properties(OpenStudioCLI.Run_Validate.MissingAMeasure PROPERTIES + WILL_FAIL TRUE + RESOURCE_LOCK "invalid_measures" + ) + + add_test(NAME OpenStudioCLI.Run_Validate.UnloadableMeasure + COMMAND $ run --show-stdout -w unloadable_measure.osw + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/workflow/invalid_measures/" + ) + set_tests_properties(OpenStudioCLI.Run_Validate.UnloadableMeasure PROPERTIES + WILL_FAIL TRUE + RESOURCE_LOCK "invalid_measures" + ) + + add_test(NAME OpenStudioCLI.Run_Validate.WrongMeasureTypeOrder + COMMAND $ run --show-stdout -w wrong_measure_type_order.osw + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/workflow/invalid_measures/" + ) + set_tests_properties(OpenStudioCLI.Run_Validate.WrongMeasureTypeOrder PROPERTIES + WILL_FAIL TRUE + RESOURCE_LOCK "invalid_measures" + ) + + # Classic + add_test(NAME OpenStudioCLI.Classic.Run_Validate.MissingAMeasure + COMMAND $ classic run --show-stdout -w missing_a_measure.osw + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/workflow/invalid_measures/" + ) + set_tests_properties(OpenStudioCLI.Classic.Run_Validate.MissingAMeasure PROPERTIES + WILL_FAIL TRUE + RESOURCE_LOCK "invalid_measures" + ) + + add_test(NAME OpenStudioCLI.Classic.Run_Validate.UnloadableMeasure + COMMAND $ classic run --show-stdout -w unloadable_measure.osw + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/workflow/invalid_measures/" + ) + set_tests_properties(OpenStudioCLI.Classic.Run_Validate.UnloadableMeasure PROPERTIES + WILL_FAIL TRUE + RESOURCE_LOCK "invalid_measures" + ) + + add_test(NAME OpenStudioCLI.Classic.Run_Validate.WrongMeasureTypeOrder + COMMAND $ classic run --show-stdout -w wrong_measure_type_order.osw + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/workflow/invalid_measures/" + ) + set_tests_properties(OpenStudioCLI.Classic.Run_Validate.WrongMeasureTypeOrder PROPERTIES + WILL_FAIL TRUE + RESOURCE_LOCK "invalid_measures" + ) + # ====================== End Workflows should fail ====================== + if (Pytest_AVAILABLE) add_test(NAME OpenStudioCLI.test_loglevel COMMAND ${Python_EXECUTABLE} -m pytest --verbose ${Pytest_XDIST_OPTS} --os-cli-path $ "${CMAKE_CURRENT_SOURCE_DIR}/test/test_loglevel.py" diff --git a/src/gltf/GltfMaterialData.cpp b/src/gltf/GltfMaterialData.cpp index 589737fec35..3a7dc4c0263 100644 --- a/src/gltf/GltfMaterialData.cpp +++ b/src/gltf/GltfMaterialData.cpp @@ -26,7 +26,7 @@ #include -#include +#include #include #include #include diff --git a/src/gltf/GltfMaterialData.hpp b/src/gltf/GltfMaterialData.hpp index 36eeb9f30ce..675ad6783c5 100644 --- a/src/gltf/GltfMaterialData.hpp +++ b/src/gltf/GltfMaterialData.hpp @@ -8,7 +8,7 @@ #include "GltfAPI.hpp" -#include +#include #include #include diff --git a/src/measure/CMakeLists.txt b/src/measure/CMakeLists.txt index 36b249dd111..03c3c771d8e 100644 --- a/src/measure/CMakeLists.txt +++ b/src/measure/CMakeLists.txt @@ -67,4 +67,4 @@ endif() CREATE_TEST_TARGETS(${target_name} "${${target_name}_test_src}" "${${target_name}_test_depends}") -MAKE_SWIG_TARGET(OpenStudioMeasure measure "${CMAKE_CURRENT_SOURCE_DIR}/Measure.i" "${${target_name}_swig_src}" ${target_name} "OpenStudioOSVersion;OpenStudioAlfalfa") +MAKE_SWIG_TARGET(OpenStudioMeasure measure "${CMAKE_CURRENT_SOURCE_DIR}/Measure.i" "${${target_name}_swig_src}" ${target_name} OpenStudioAlfalfa) diff --git a/src/measure/Measure.i b/src/measure/Measure.i index 3b074944e14..18b1d5e8419 100644 --- a/src/measure/Measure.i +++ b/src/measure/Measure.i @@ -7,10 +7,10 @@ %include -%include #define MODEL_API #define STANDARDSINTERFACE_API #define MEASURE_API +#define ALFALFA_API %include %import diff --git a/src/utilities/data/Data.i b/src/utilities/data/Data.i index 348095d4fab..ea034806c3f 100644 --- a/src/utilities/data/Data.i +++ b/src/utilities/data/Data.i @@ -37,6 +37,10 @@ %template(OptionalAppGFuelType) boost::optional; %template(ComponentTypeVector) std::vector; %template(OptionalComponentType) boost::optional; +%template(AlfalfaComponentTypeVector) std::vector; +%template(OptionalAlfalfaComponentType) boost::optional; +%template(AlfalfaComponentCapabilityVector) std::vector; +%template(OptionalAlfalfaComponentCapability) boost::optional; %include %include diff --git a/src/utilities/data/DataEnums.hpp b/src/utilities/data/DataEnums.hpp index c64d977243d..cbe9d7e9921 100644 --- a/src/utilities/data/DataEnums.hpp +++ b/src/utilities/data/DataEnums.hpp @@ -290,6 +290,32 @@ using OptionalComponentType = boost::optional; /** \relates ComponentType */ using ComponentTypeVector = std::vector; + +OPENSTUDIO_ENUM(AlfalfaComponentCapability, + ((Input)) + ((Output)) + ((Bidirectional)) +) +/** \relates AlfalfaComponentCapability */ +using OptionalAlfalfaComponentCapability = boost::optional; + +/** \relates AlfalfaComponentCapability */ +using AlfalfaComponentCapabilityVector = std::vector; + + +OPENSTUDIO_ENUM(AlfalfaComponentType, + ((Actuator)) + ((Constant)) + ((Meter)) + ((OutputVariable)) + ((GlobalVariable)) +) +/** \relates AlfalfaComponentType */ +using OptionalAlfalfaComponentType = boost::optional; + +/** \relates AlfalfaComponentType */ +using AlfalfaComponentTypeVector = std::vector; + // clang-format on inline UTILITIES_API AppGFuelType convertFuelTypeToAppG(FuelType fuelType) { diff --git a/src/utilities/filetypes/WorkflowJSON.cpp b/src/utilities/filetypes/WorkflowJSON.cpp index 1ba116ceea1..4bbc8a60393 100644 --- a/src/utilities/filetypes/WorkflowJSON.cpp +++ b/src/utilities/filetypes/WorkflowJSON.cpp @@ -46,10 +46,12 @@ namespace detail { std::string formattedErrors; bool parsingSuccessful = Json::parseFromStream(rbuilder, ss, &m_value, &formattedErrors); + openstudio::path p; + if (!parsingSuccessful) { // see if this is a path - openstudio::path p = toPath(s); + p = toPath(s); if (boost::filesystem::exists(p) && boost::filesystem::is_regular_file(p)) { // open file std::ifstream ifs(openstudio::toSystemFilename(p)); @@ -65,6 +67,10 @@ namespace detail { parseSteps(); parseRunOptions(); + + if (!p.empty()) { + setOswPath(p, false); + } } WorkflowJSON_Impl::WorkflowJSON_Impl(const openstudio::path& p) { @@ -851,6 +857,65 @@ namespace detail { } } + bool WorkflowJSON_Impl::validateMeasures() const { + // TODO: should we exit early, or return all problems found? + + bool result = true; + MeasureType state = MeasureType::ModelMeasure; + + for (size_t i = 0; const auto& step : m_steps) { + LOG(Debug, "Validating step " << i); + if (auto step_ = step.optionalCast()) { + // Not calling getBCLMeasure because I want to mimic workflow-gem and be as explicit as possible about what went wrong + const auto measureDirName = step_->measureDirName(); + auto measurePath_ = findMeasure(measureDirName); + if (!measurePath_) { + LOG(Error, "Cannot find measure '" << measureDirName << "'"); + result = false; + continue; + } + auto bclMeasure_ = BCLMeasure::load(*measurePath_); + if (!bclMeasure_) { + LOG(Error, "Cannot load measure '" << measureDirName << "' at '" << *measurePath_ << "'"); + result = false; + continue; + } + + // Ensure that measures are in order, i.e. no OS after E+, E+ or OS after Reporting + const auto measureType = bclMeasure_->measureType(); + + if (measureType == MeasureType::ModelMeasure) { + if (state == MeasureType::EnergyPlusMeasure) { + LOG(Error, "OpenStudio measure '" << measureDirName << "' called after transition to EnergyPlus."); + result = false; + } + if (state == MeasureType::ReportingMeasure) { + LOG(Error, "OpenStudio measure '" << measureDirName << "' called after Energyplus simulation."); + result = false; + } + + } else if (measureType == MeasureType::EnergyPlusMeasure) { + if (state == MeasureType::ReportingMeasure) { + LOG(Error, "EnergyPlus measure '" << measureDirName << "' called after Energyplus simulation."); + result = false; + } + if (state == MeasureType::ModelMeasure) { + state = MeasureType::EnergyPlusMeasure; + } + + } else if (measureType == MeasureType::ReportingMeasure) { + state = MeasureType::ReportingMeasure; + + } else { + LOG(Error, "MeasureType " << measureType.valueName() << " of measure '" << measureDirName << "' is not supported"); + result = false; + } + } + ++i; + } + + return result; + } } // namespace detail WorkflowJSON::WorkflowJSON() : m_impl(std::shared_ptr(new detail::WorkflowJSON_Impl())) {} @@ -1123,6 +1188,10 @@ void WorkflowJSON::resetRunOptions() { getImpl()->resetRunOptions(); } +bool WorkflowJSON::validateMeasures() const { + return getImpl()->validateMeasures(); +} + std::ostream& operator<<(std::ostream& os, const WorkflowJSON& workflowJSON) { os << workflowJSON.string(); return os; diff --git a/src/utilities/filetypes/WorkflowJSON.hpp b/src/utilities/filetypes/WorkflowJSON.hpp index 1ac097baa7f..5d6c4dd693b 100644 --- a/src/utilities/filetypes/WorkflowJSON.hpp +++ b/src/utilities/filetypes/WorkflowJSON.hpp @@ -233,6 +233,9 @@ class UTILITIES_API WorkflowJSON /** Reset RunOptions for this workflow. */ void resetRunOptions(); + /** Checks that all measures in the Workflow can be found, and are in the correct order (ModelMeasure > EnergyPlusMeasure > ReportingMeasure) */ + bool validateMeasures() const; + protected: // get the impl template diff --git a/src/utilities/filetypes/WorkflowJSON_Impl.hpp b/src/utilities/filetypes/WorkflowJSON_Impl.hpp index 65d65c52e83..03be7a07339 100644 --- a/src/utilities/filetypes/WorkflowJSON_Impl.hpp +++ b/src/utilities/filetypes/WorkflowJSON_Impl.hpp @@ -153,6 +153,8 @@ namespace detail { // Emitted on any change Nano::Signal onChange; + bool validateMeasures() const; + private: REGISTER_LOGGER("openstudio.WorkflowJSON"); diff --git a/src/utilities/filetypes/test/WorkflowJSON_GTest.cpp b/src/utilities/filetypes/test/WorkflowJSON_GTest.cpp index d88b5b8247a..48e2eb5a070 100644 --- a/src/utilities/filetypes/test/WorkflowJSON_GTest.cpp +++ b/src/utilities/filetypes/test/WorkflowJSON_GTest.cpp @@ -15,9 +15,11 @@ #include "../../time/DateTime.hpp" +#include "../../core/Filesystem.hpp" #include "../../core/Exception.hpp" #include "../../core/System.hpp" #include "../../core/Checksum.hpp" +#include "../../core/StringStreamLogSink.hpp" #include @@ -1436,3 +1438,48 @@ TEST(Filetypes, RunOptions_overrideValuesWith) { ASSERT_FALSE(ftOptions.excludeSpaceTranslation()); ASSERT_TRUE(ftOptions.isExcludeSpaceTranslationDefaulted()); } + +TEST(Filetypes, WorkflowJSON_ValidateMeasures_Ok) { + auto p = resourcesPath() / toPath("utilities/Filetypes/full.osw"); + WorkflowJSON w(p); + EXPECT_TRUE(w.validateMeasures()); +} + +TEST(Filetypes, WorkflowJSON_ValidateMeasures_Missing) { + auto p = resourcesPath() / toPath("workflow/invalid_measures/missing_a_measure.osw"); + ASSERT_TRUE(boost::filesystem::is_regular_file(p)); + WorkflowJSON w(p); + StringStreamLogSink sink; + sink.setLogLevel(Error); + EXPECT_FALSE(w.validateMeasures()); + ASSERT_EQ(1, sink.logMessages().size()); + EXPECT_EQ("Cannot find measure 'NON_EXISTING_MEASURE_THIS_SHOULD_BE_CAUGHT'", sink.logMessages()[0].logMessage()); +} + +TEST(Filetypes, WorkflowJSON_ValidateMeasures_Unloadable) { + auto p = resourcesPath() / toPath("workflow/invalid_measures/unloadable_measure.osw"); + ASSERT_TRUE(boost::filesystem::is_regular_file(p)); + WorkflowJSON w(p); + StringStreamLogSink sink; + sink.setLogLevel(Error); + EXPECT_FALSE(w.validateMeasures()); + auto logMessages = sink.logMessages(); + ASSERT_EQ(3, logMessages.size()); + EXPECT_EQ("utilities.bcl.BCLXML", logMessages.at(0).logChannel()); + EXPECT_EQ("utilities.bcl.BCLMeasure", logMessages.at(1).logChannel()); + EXPECT_EQ("openstudio.WorkflowJSON", logMessages.at(2).logChannel()); + auto logMessage = sink.logMessages()[2].logMessage(); + EXPECT_TRUE(logMessage.find("Cannot load measure 'UnloadableMeasure' at '") != std::string::npos) << logMessage; +} + +TEST(Filetypes, WorkflowJSON_ValidateMeasures_WrongOrder) { + auto p = resourcesPath() / toPath("workflow/invalid_measures/wrong_measure_type_order.osw"); + ASSERT_TRUE(boost::filesystem::is_regular_file(p)); + WorkflowJSON w(p); + StringStreamLogSink sink; + sink.setLogLevel(Error); + EXPECT_FALSE(w.validateMeasures()); + ASSERT_EQ(1, sink.logMessages().size()); + + EXPECT_EQ("OpenStudio measure 'FakeModelMeasure' called after Energyplus simulation.", sink.logMessages()[0].logMessage()); +} diff --git a/src/workflow/RunInitialization.cpp b/src/workflow/RunInitialization.cpp index a35ba499a64..0d691b2dba5 100644 --- a/src/workflow/RunInitialization.cpp +++ b/src/workflow/RunInitialization.cpp @@ -54,10 +54,17 @@ void OSWorkflow::runInitialization() { } }); - // TODO: create the runner with our WorkflowJSON (workflow gem uses datapoint/analysis too?!) - // TODO: Validate the OSW measures if the flag is set to true, (the default state) - // Note JM 2022-11-07: Is it better to try and load all measures once, instead of crashing later? + // There isn't a 'verify_osw' key in the RunOptions, so always do it for now. Maybe don't if `fast`? + { + LOG(Info, "Attempting to validate the measure workflow"); + + if (!workflowJSON.validateMeasures()) { + LOG_AND_THROW("Workflow is invalid"); + } + + LOG(Info, "Validated the measure workflow"); + } LOG(Debug, "Finding and loading the seed file"); auto seedPath_ = workflowJSON.seedFile();