-
Notifications
You must be signed in to change notification settings - Fork 195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix #5233 - Validate OSW measures before running #5295
Conversation
"measure_dir_name": "NON_EXISTING_MEASURE_THIS_SHOULD_BE_CAUGHT", | ||
"arguments": {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test 1
"measure_dir_name": "UnloadableMeasure", | ||
"arguments": {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an existing directory (findMeasure works), but it doesn't have a measure.xml
{ | ||
"measure_dir_name": "FakeReport", | ||
"arguments": {} | ||
}, | ||
{ | ||
"measure_dir_name": "FakeModelMeasure", | ||
"arguments": {} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong order
src/cli/CMakeLists.txt
Outdated
# ======================== Workflows should fail ======================== | ||
add_test(NAME OpenStudioCLI.Run_Validate.MissingAMeasure | ||
COMMAND $<TARGET_FILE:openstudio> 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 $<TARGET_FILE:openstudio> 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 $<TARGET_FILE:openstudio> 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 $<TARGET_FILE:openstudio> 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 $<TARGET_FILE:openstudio> 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 $<TARGET_FILE:openstudio> 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 ====================== |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a c++ and a classic (ruby) version of the calling the CLI on these broken workflows. Expect all of them to fail
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I intently pushed just these tests for now, so that CI would report a test failure and highlight the fact that these correctly test the issue #5233
3798 - OpenStudioCLI.Run_Validate.MissingAMeasure (Failed)
3799 - OpenStudioCLI.Run_Validate.UnloadableMeasure (Failed)
3800 - OpenStudioCLI.Run_Validate.WrongMeasureTypeOrder (Failed)
a3c8f24
to
df988e8
Compare
/** Checks that all measures in the Workflow can be found, and are in the correct order (ModelMeasure > EnergyPlusMeasure > ReportingMeasure) */ | ||
bool validateMeasures() const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new WorkflowJSON method. Non-throwy, logs Errors and return a bool
if (!p.empty()) { | ||
setOswPath(p, false); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the std::string& constructor, if it happens to be a path, do call setOswPath like we do for the ctor that takes a openstudio::path.
In Ruby, that means on develop you have a different behavior between these two!
OpenStudio::WorkflowJSON.new("/path/to/workflow.osw")
OpenStudio::WorkflowJSON.new(OpenStudio::toPath("/path/to/workflow.osw"))
I noticed this because if the oswPath isn't set, the setMeasuresTypes (private, impl only) isn't called, and if you try to call workflow.getMeasureStepsWithIndex
or getMeasureSteps
you hit an OS_ASSERT
OS_ASSERT(m_measureTypes.size() == n); |
I think that's pretty broken, but it's outside of the scope of this PR to fix it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as we document this to fix later that's good
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it might seem a bit backwards, but perhaps the openstudio::path
version of the constructor should just forward the call to the string version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it is there seems to be an underiable opportunity to have different behaviors between the two constructors, since the path version has its own implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Meh. You can guess why it's the way it is.
- The string constructor was really only meant to take an actual JSON string in originally
- The path& one was to read from a file on disk
People using ruby especially would try to do OpenStudio::WorkflowJSON.new("/my/workflow.osw")
(instead of OpenStudio::WorkflowJSON.new(OpenStudio::toPath("/my/workflow.osw"))
and would be very confused, so instead the string version when it doesn't parse correctly, it falls back to check if the string argument is actually a path, and if so I'm pretty sure the intent was to do exactly the same thing. C++ constructor chaining is not possible, so instead of bothering to make an internal method fromPath(...)
for a couple of lines, the path version was copy pasted in. This happened in a7efe66 , and probably just forgot the setOswPath since the flow control (with parseSteps etc) was different.
bool WorkflowJSON_Impl::validateMeasures() const { | ||
// TODO: should we exit early, or return all problems found? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@DavidGoldwasser @kbenne thoughts here? I went with the "Find all problems" approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This mimics this workflow-gem validate_measures method: https://github.com/NREL/OpenStudio-workflow-gem/blob/39c84e45f152446a92379a501807e2691dba15ab/lib/openstudio/workflow/util/measure.rb#L97-L151
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the find all problems approach and I think that is what most people will want.
Outside of the scope here, but I'm thinking that perhaps "validation" would be stronger if we actually loaded the measure script into the Python/Ruby engine. Unless I'm mistaken, as it stands, I'm pretty sure that is not happening here. We have encountered some issues (like this) where things go sideways, yet the root cause is that the measure syntax is flawed, but the failure mode misleads the user to thinking there might be an issue within OpenStudio itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rationale of catching this early is perhaps sound, but it's also slow. Not sure what's better.
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<MeasureStep>()) { | ||
// 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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For each step, check we can find the measure directory, and it's a valid BCL Measure.
// 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And ensure the measures are in the right order
// 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"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO:
- Should verify_osw be added to the OSW Schema and to RunOptions?
- Should we maybe not validate it if
fast
is passed? @shorowit might have opinions on this
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 'ModelMeasureRegistersError' called after Energyplus simulation.", sink.logMessages()[0].logMessage()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gtests also added
* One references a non existing measure * One references a folder that can be found but isn't a valid BCLMeasure * One has a ReportingMeasure before a ModelMeasure
… DO run with current C++ CLI to show problem)
…ith current C++ CLI)
…rror messages TODO: should we return early?
Forgot to update the Ctest after using another measure
df988e8
to
4aafb5a
Compare
CI Results for 4aafb5a:
|
if (!p.empty()) { | ||
setOswPath(p, false); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it might seem a bit backwards, but perhaps the openstudio::path
version of the constructor should just forward the call to the string version.
if (!p.empty()) { | ||
setOswPath(p, false); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it is there seems to be an underiable opportunity to have different behaviors between the two constructors, since the path version has its own implementation.
bool WorkflowJSON_Impl::validateMeasures() const { | ||
// TODO: should we exit early, or return all problems found? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the find all problems approach and I think that is what most people will want.
Outside of the scope here, but I'm thinking that perhaps "validation" would be stronger if we actually loaded the measure script into the Python/Ruby engine. Unless I'm mistaken, as it stands, I'm pretty sure that is not happening here. We have encountered some issues (like this) where things go sideways, yet the root cause is that the measure syntax is flawed, but the failure mode misleads the user to thinking there might be an issue within OpenStudio itself.
I'm going to go ahead merge, but I left a few discussion points. |
Pull request overview
Pull Request Author
src/model/test
)src/energyplus/Test
)src/osversion/VersionTranslator.cpp
)Labels:
IDDChange
APIChange
Pull Request - Ready for CI
so that CI builds your PRReview Checklist
This will not be exhaustively relevant to every PR.