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/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/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();