diff --git a/docs/scenario_workflow_translation_plan.md b/docs/scenario_workflow_translation_plan.md index c837b96..a66418b 100644 --- a/docs/scenario_workflow_translation_plan.md +++ b/docs/scenario_workflow_translation_plan.md @@ -154,8 +154,8 @@ Each PR should be independently reviewable and should not implement later PR sco | --- | --- | --- | --- | --- | --- | --- | --- | | 1 | In Progress | Current docs PR | Guiding plan doc | Add this repository plan document. | None | The doc directs PRs 2-18, names acceptance gates, and keeps first-release scope clear. | The doc leaves scenario selection, mapping ownership, warning behavior, or result writeback scope ambiguous. | | 2 | Done | Current PR-2 branch | Scenario data model and discovery | Add reader-returned structures for first-facility report-level package scenarios. Extract scenario ID, name, temporal status, report ID, package ID, reference case ID, measure IDrefs, and linked premises. | PR 1 | Unit tests prove `building_151.xml` discovers baseline plus all package scenarios and existing reader behavior remains unchanged. | Parser assumes a hardcoded namespace, reads the wrong scenario path, or breaks existing reader tests. | -| 3 | Planned | - | Facility measure index | Index facility measures by ID and extract category/name metadata plus linked premises, cost/savings fields, and implementation status. | PR 2 | Tests resolve package `MeasureID` references in `building_151.xml` to parsed measure metadata. | Unresolved refs are silently dropped or measures without `TechnologyCategories` crash parsing. | -| 4 | Planned | - | Parser warning contract | Add structured warnings for missing IDs, unresolved refs, empty packages, missing names, missing categories, and packages with no usable measures. | PR 3 | Tests cover warning cases using `BuildingEQ-1.0.0.xml`, `Golden Test File.xml`, and no-measure fixtures. | Warnings only print to stdout or malformed package data aborts all discovery. | +| 3 | Done | Current PR-3 branch | Measure index | Index first-facility measures by ID and extract category/name metadata plus linked premises, cost/savings fields, and implementation status. | PR 2 | Tests resolve package `MeasureID` references in `building_151.xml` to parsed measure metadata. | Unresolved refs are silently dropped or measures without `TechnologyCategories` crash parsing. | +| 4 | Done | Current PR-4 branch | Parser warning contract | Add structured warnings for missing IDs, unresolved refs, empty packages, missing names, missing categories, and packages with no usable measures. | PR 3 | Tests cover warning cases using `BuildingEQ-1.0.0.xml`, `Golden Test File.xml`, and no-measure fixtures. | Warnings only print to stdout or malformed package data aborts all discovery. | | 5 | Planned | - | Initial mapping JSON | Add `lib/BOSS/scenario_measure_map.json` with verified mappings for an initial supported set from `building_151.xml`. | PR 1 | Mapping JSON is valid, and every included entry has source category/name, target `measure_dir_name`, and arguments. | Legacy mappings are copied blindly without checking current OpenStudio measure dirs/args. | | 6 | Planned | - | Basic `ScenarioMeasureMapper` | Load JSON, normalize lookup keys, and map one parsed BuildingSync measure to OpenStudio step specs using `SystemCategoryAffected` plus `MeasureName`. | PRs 3, 5 | Mapper unit tests return expected steps and structured unmapped warnings. | Mapper mutates reader data, raises on unmapped measures, or hardcodes rules outside JSON. | | 7 | Planned | - | Technology category fallback | Add fallback lookup by technology category plus `MeasureName`. | PR 6 | Tests show fallback mapping works when `SystemCategoryAffected` is absent, while normal lookup priority is preserved. | Fallback changes normal category/name lookup behavior. | @@ -228,6 +228,26 @@ Keep detailed rationale in the relevant PR description or code review thread. Ke - Verification: `bundle exec rspec spec/tests/integration/write_and_run_osws_spec.rb` - completed in 79 minutes 27 seconds with 19 examples and 1 failure. The failure is an unrelated Windows command quoting issue for `Golden Test File` output paths with spaces; 18 final baseline `out.osw` files had `completed_status: Success`. - Follow-up: PR 3 starts facility measure indexing and should keep warning contracts deferred to PR 4. +### PR 3: Measure index + +- Status: Done +- Delivered by: Current PR-3 branch +- Handoff: `BOSS::BuildingSyncReader#get_measures` returns first-facility measures as a hash keyed by `Measure/@ID`, with symbol-keyed metadata for `measure_id`, `system_category_affected`, `technology_category_element_name`, `measure_name`, `custom_measure_name`, `linked_premises_idrefs`, useful cost/savings fields, and `implementation_status`. +- Handoff: Missing optional measure fields return `nil` or `[]`, missing `Measures` returns `{}`, and measures without `TechnologyCategories` remain parseable; PR 4 should add structured warnings for missing IDs, unresolved refs, missing names, and missing categories. +- Verification: `C:\Ruby32-x64\bin\ruby.exe -S bundle exec rspec spec/tests/unit/buildingsync_reader_spec.rb` - 29 examples, 0 failures. +- Verification: `C:\Ruby32-x64\bin\ruby.exe -S bundle exec rubocop --only Lint/UnreachableLoop lib/BOSS/buildingsync_reader/buildingsync_reader.rb` - 1 file inspected, no offenses. Full changed-file RuboCop still reports existing reader/spec style debt and new-cop configuration warnings. +- Follow-up: PR 4 should consume `get_package_measure_scenarios` plus `get_measures` to report unresolved `MeasureID` references and incomplete measure metadata without changing the parser return shapes unless the warning contract requires it. + +### PR 4: Parser warning contract + +- Status: Done +- Delivered by: Current PR-4 branch +- Handoff: `BOSS::BuildingSyncReader#get_parser_warnings` returns structured symbol-keyed warning hashes with `code`, `severity`, `message`, and relevant scenario/package/measure context. Existing scenario and measure reader return shapes remain unchanged. +- Handoff: Parser warning codes cover missing scenario/package/measure IDs, missing `MeasureID` IDrefs, unresolved measure references, packages without `MeasureIDs`, packages with no parser-usable measures, and resolved measures missing `SystemCategoryAffected`, technology category, or `MeasureName`. +- Verification: `C:\Ruby32-x64\bin\ruby.exe -S bundle exec rspec spec/tests/unit/buildingsync_reader_spec.rb` - 33 examples, 0 failures. +- Verification: `C:\Ruby32-x64\bin\ruby.exe -S bundle exec rubocop lib/BOSS/buildingsync_reader/buildingsync_reader.rb spec/tests/unit/buildingsync_reader_spec.rb` - fails on existing reader/spec style debt and new-cop configuration warnings; after PR-4 cleanup, the focused run reports 132 existing offenses, 117 autocorrectable. +- Follow-up: PR 5 can add mapping JSON without expecting parser warnings to identify unmapped OpenStudio measures; mapping-specific warnings remain PR 6/PR 9 scope. + ## Verification Commands Use the narrowest command that verifies the current PR. Broader commands should be run before merging larger integration PRs. diff --git a/lib/BOSS/buildingsync_reader/buildingsync_reader.rb b/lib/BOSS/buildingsync_reader/buildingsync_reader.rb index d7f1c9d..bf0021f 100644 --- a/lib/BOSS/buildingsync_reader/buildingsync_reader.rb +++ b/lib/BOSS/buildingsync_reader/buildingsync_reader.rb @@ -43,6 +43,33 @@ def get_package_measure_scenarios return get_report_scenarios.select { |scenario| scenario[:scenario_type] == :package_of_measures } end + def get_measures + measures = {} + _measure_xmls.each do |measure_xml| + measure = _measure_hash(measure_xml) + measures[measure[:measure_id]] = measure if !measure[:measure_id].nil? + end + + return measures + end + + def get_parser_warnings + warnings = [] + measures = get_measures + + _measure_xmls.each do |measure_xml| + measure = _measure_hash(measure_xml) + warnings << _parser_warning(:missing_measure_id, 'Measure is missing ID.') if _blank?(measure[:measure_id]) + end + + _package_measure_scenario_xmls.each do |report_xml, scenario_xml, package_xml| + scenario = _scenario_hash(report_xml, scenario_xml) + warnings.concat(_package_measure_scenario_warnings(scenario, package_xml, measures)) + end + + return warnings + end + # tries to get weather file from: # 1. given weather file # 2. city state from either building or site @@ -328,6 +355,162 @@ def get_total_weighted_average_load private + def _measure_xmls + measure_xmls = [] + @facility_xml&.elements&.each("#{@ns}Measures/#{@ns}Measure") do |measure_xml| + measure_xmls << measure_xml + end + + return measure_xmls + end + + def _package_measure_scenario_xmls + scenario_xmls = [] + @facility_xml&.elements&.each("#{@ns}Reports/#{@ns}Report") do |report_xml| + report_xml.elements.each("#{@ns}Scenarios/#{@ns}Scenario") do |scenario_xml| + package_xml = _package_of_measures_xml(scenario_xml) + scenario_xmls << [report_xml, scenario_xml, package_xml] if !package_xml.nil? + end + end + + return scenario_xmls + end + + def _measure_hash(measure_xml) + technology_category_xml = _technology_category_xml(measure_xml) + + return { + measure_id: measure_xml.attributes['ID'], + system_category_affected: _element_text(measure_xml, "#{@ns}SystemCategoryAffected"), + technology_category_element_name: _technology_category_element_name(technology_category_xml), + measure_name: _technology_measure_name(technology_category_xml), + custom_measure_name: _element_text(measure_xml, "#{@ns}CustomMeasureName"), + linked_premises_idrefs: _linked_premises_idrefs(measure_xml), + mv_cost: _numeric_element_text(measure_xml, "#{@ns}MVCost"), + useful_life: _numeric_element_text(measure_xml, "#{@ns}UsefulLife"), + measure_total_first_cost: _numeric_element_text(measure_xml, "#{@ns}MeasureTotalFirstCost"), + measure_installation_cost: _numeric_element_text(measure_xml, "#{@ns}MeasureInstallationCost"), + measure_material_cost: _numeric_element_text(measure_xml, "#{@ns}MeasureMaterialCost"), + om_cost_annual_savings: _numeric_element_text(measure_xml, "#{@ns}MeasureSavingsAnalysis/#{@ns}OMCostAnnualSavings"), + implementation_status: _element_text(measure_xml, "#{@ns}ImplementationStatus") + } + end + + def _technology_category_xml(measure_xml) + technology_category_xml = measure_xml.elements["#{@ns}TechnologyCategories/#{@ns}TechnologyCategory"] + + return technology_category_xml&.elements&.[](1) + end + + def _technology_category_element_name(technology_category_xml) + return nil if technology_category_xml.nil? + + return technology_category_xml.name.to_s.split(':').last + end + + def _technology_measure_name(technology_category_xml) + return nil if technology_category_xml.nil? + + return _element_text(technology_category_xml, "#{@ns}MeasureName") + end + + def _numeric_element_text(xml, path) + value = _element_text(xml, path) + return nil if value.nil? || value.strip.empty? + + return Float(value) + rescue ArgumentError + return nil + end + + def _package_measure_scenario_warnings(scenario, package_xml, measures) + warnings = [] + context = _scenario_warning_context(scenario) + measure_idref_xmls = _measure_idref_xmls(package_xml) + + if _blank?(scenario[:scenario_id]) + warnings << _parser_warning(:missing_scenario_id, 'Package scenario is missing ID.', context) + end + if _blank?(scenario[:package_id]) + warnings << _parser_warning(:missing_package_id, 'PackageOfMeasures is missing ID.', context) + end + if measure_idref_xmls.empty? + warnings << _parser_warning(:package_missing_measure_ids, 'Package has no MeasureIDs.', context) + end + + measure_idref_xmls.each do |measure_id_xml| + if _blank?(measure_id_xml.attributes['IDref']) + warnings << _parser_warning(:missing_measure_idref, 'MeasureID is missing IDref.', context) + end + end + + resolved_measures = [] + scenario[:measure_ids].each do |measure_idref| + measure = measures[measure_idref] + if measure.nil? + warnings << _parser_warning( + :unresolved_measure_idref, + 'Package references an unknown measure ID.', + context.merge(measure_idref:) + ) + else + resolved_measures << measure + warnings.concat(_measure_parser_warnings(measure, context)) + end + end + + if resolved_measures.none? { |measure| _usable_package_measure?(measure) } + warnings << _parser_warning(:package_has_no_usable_measures, 'Package has no usable measures.', context) + end + + return warnings + end + + def _measure_parser_warnings(measure, scenario_context) + warnings = [] + context = scenario_context.merge(measure_id: measure[:measure_id]) + + if _blank?(measure[:system_category_affected]) + warnings << _parser_warning(:missing_system_category_affected, 'Measure is missing SystemCategoryAffected.', context) + end + if _blank?(measure[:technology_category_element_name]) + warnings << _parser_warning(:missing_technology_category, 'Measure is missing a usable technology category.', context) + end + if _blank?(measure[:measure_name]) + warnings << _parser_warning(:missing_measure_name, 'Measure is missing MeasureName.', context) + end + + return warnings + end + + def _scenario_warning_context(scenario) + return { + report_id: scenario[:report_id], + scenario_id: scenario[:scenario_id], + package_id: scenario[:package_id] + } + end + + def _parser_warning(code, message, context = {}) + return { + code:, + severity: :warning, + message: + }.merge(context) + end + + def _blank?(value) + return true if value.nil? + + return value.to_s.strip.empty? + end + + def _usable_package_measure?(measure) + return false if _blank?(measure[:measure_name]) + + return !_blank?(measure[:system_category_affected]) || !_blank?(measure[:technology_category_element_name]) + end + def _scenario_hash(report_xml, scenario_xml) package_xml = _package_of_measures_xml(scenario_xml) @@ -365,15 +548,25 @@ def _measure_idrefs(package_xml) return [] if package_xml.nil? measure_ids = [] - package_xml.elements.each("#{@ns}MeasureIDs/#{@ns}MeasureID") do |measure_id_xml| + _measure_idref_xmls(package_xml).each do |measure_id_xml| measure_id = measure_id_xml.attributes['IDref'] measure_ids << measure_id if !measure_id.nil? end return measure_ids end - def _linked_premises_idrefs(scenario_xml) - linked_premises_xml = scenario_xml.elements["#{@ns}LinkedPremises"] + def _measure_idref_xmls(package_xml) + return [] if package_xml.nil? + + measure_idref_xmls = [] + package_xml.elements.each("#{@ns}MeasureIDs/#{@ns}MeasureID") do |measure_id_xml| + measure_idref_xmls << measure_id_xml + end + return measure_idref_xmls + end + + def _linked_premises_idrefs(xml) + linked_premises_xml = xml.elements["#{@ns}LinkedPremises"] return [] if linked_premises_xml.nil? return _descendant_idrefs(linked_premises_xml) diff --git a/spec/files/v2.7.0/expected/building_151_scenario1_resolved_measures.json b/spec/files/v2.7.0/expected/building_151_scenario1_resolved_measures.json new file mode 100644 index 0000000..f5a8488 --- /dev/null +++ b/spec/files/v2.7.0/expected/building_151_scenario1_resolved_measures.json @@ -0,0 +1,38 @@ +{ + "fixture": "spec/files/v2.7.0/building_151.xml", + "measure_count": 16, + "scenario": { + "scenario_id": "Scenario1", + "scenario_name": "LED Only", + "temporal_status": null, + "report_id": "Report1", + "scenario_type": "package_of_measures", + "package_id": "PackageOfMeasures1", + "reference_case_id": "Baseline", + "measure_ids": [ + "Measure1" + ], + "linked_premises_idrefs": [ + "Building151" + ] + }, + "resolved_measures": [ + { + "measure_id": "Measure1", + "system_category_affected": "Lighting", + "technology_category_element_name": "LightingImprovements", + "measure_name": "Retrofit with light emitting diode technologies", + "custom_measure_name": "TBD", + "linked_premises_idrefs": [ + "Building151" + ], + "mv_cost": 0.0, + "useful_life": 12.0, + "measure_total_first_cost": 267390.2, + "measure_installation_cost": 0.0, + "measure_material_cost": 0.0, + "om_cost_annual_savings": null, + "implementation_status": "Proposed" + } + ] +} diff --git a/spec/tests/unit/buildingsync_reader_spec.rb b/spec/tests/unit/buildingsync_reader_spec.rb index c207be1..8fcb3f8 100644 --- a/spec/tests/unit/buildingsync_reader_spec.rb +++ b/spec/tests/unit/buildingsync_reader_spec.rb @@ -5,6 +5,7 @@ # ******************************************************************************* require 'tempfile' +require 'json' require 'BOSS/buildingsync_reader/buildingsync_reader' UNIT_SPEC_FILES_DIR = File.expand_path('../../files', __dir__) @@ -14,6 +15,11 @@ def load_fixture_doc(schema_version, file_name) REXML::Document.new(File.read(xml_path), ignore_whitespace_nodes: :all) end +def load_fixture_json(schema_version, file_name) + json_path = File.join(UNIT_SPEC_FILES_DIR, schema_version, file_name) + JSON.parse(File.read(json_path)) +end + def wrap_in_site(xml) <<~XML @@ -187,6 +193,277 @@ def wrap_in_facility(xml) end end + describe 'get_measures should' do + it 'index measures from building_151 by measure ID' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'building_151.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + measures = buidingsync_reader.get_measures + + # Assert + expected_measure_ids = [ + 'Measure1', + 'Measure3', + 'Measure4', + 'Measure5', + 'Measure6', + 'Measure7', + 'Measure8', + 'Measure9', + 'Measure10', + 'Measure11', + 'Measure12', + 'Measure14', + 'Measure16', + 'Measure18', + 'Measure24', + 'Measure25' + ] + + expect(measures.keys).to eq expected_measure_ids + end + + it 'extract measure category, name, linked premises, cost, savings, and status metadata' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'building_151.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + measure = buidingsync_reader.get_measures['Measure1'] + + # Assert + expect(measure).to include( + measure_id: 'Measure1', + system_category_affected: 'Lighting', + technology_category_element_name: 'LightingImprovements', + measure_name: 'Retrofit with light emitting diode technologies', + custom_measure_name: 'TBD', + linked_premises_idrefs: ['Building151'], + mv_cost: 0.0, + useful_life: 12.0, + measure_total_first_cost: 267390.2, + measure_installation_cost: 0.0, + measure_material_cost: 0.0, + om_cost_annual_savings: nil, + implementation_status: 'Proposed' + ) + end + + it 'resolves package MeasureID references against parsed measure metadata' do + # Set Up + fixture = File.join('spec', 'files', 'v2.7.0', 'building_151.xml') + doc = load_fixture_doc('v2.7.0', 'building_151.xml') + expected_output = load_fixture_json('v2.7.0', File.join('expected', 'building_151_scenario1_resolved_measures.json')) + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + measures = buidingsync_reader.get_measures + scenario = buidingsync_reader.get_package_measure_scenarios.find { |package_scenario| package_scenario[:scenario_id] == 'Scenario1' } + parsed_output = { + fixture: fixture, + measure_count: measures.length, + scenario: scenario, + resolved_measures: scenario[:measure_ids].map { |measure_id| measures[measure_id] } + } + + # Assert + expect(JSON.parse(JSON.generate(parsed_output))).to eq expected_output + end + + it 'uses the document namespace prefix when indexing measures' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'building_151_n1.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + measure = buidingsync_reader.get_measures['Measure1'] + + # Assert + expect(measure).to include( + technology_category_element_name: 'LightingImprovements', + measure_name: 'Retrofit with light emitting diode technologies' + ) + end + + it 'returns an empty hash when no measures exist' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'building_151_no_measures.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + + # Assert + expect(buidingsync_reader.get_measures).to eq({}) + end + + it 'extracts measure-owned savings metadata when present' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'BuildingEQ-1.0.0.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + measure = buidingsync_reader.get_measures['MeasureType-70023838998340'] + + # Assert + expect(measure).to include( + measure_id: 'MeasureType-70023838998340', + system_category_affected: 'Cooking', + technology_category_element_name: 'FutureOtherECMs', + measure_name: 'Other', + linked_premises_idrefs: ['BuildingType-70023826271140'], + useful_life: 50.0, + measure_total_first_cost: 75242.0, + om_cost_annual_savings: 260.0 + ) + end + + it 'keeps incomplete measure metadata nil-safe for later warning handling' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'Golden Test File.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + measure = buidingsync_reader.get_measures['Building1RemovePV'] + + # Assert + expect(measure).to include( + measure_id: 'Building1RemovePV', + system_category_affected: nil, + technology_category_element_name: nil, + measure_name: nil, + custom_measure_name: nil, + linked_premises_idrefs: ['Building1'], + mv_cost: nil, + useful_life: nil, + measure_total_first_cost: nil, + measure_installation_cost: nil, + measure_material_cost: nil, + om_cost_annual_savings: nil, + implementation_status: nil + ) + end + end + + describe 'get_parser_warnings should' do + it 'return no warnings for complete package scenarios' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'BuildingEQ-1.0.0.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + + # Assert + expect(buidingsync_reader.get_parser_warnings).to eq [] + end + + it 'return no warnings when no package scenarios exist' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'building_151_no_measures.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + + # Assert + expect(buidingsync_reader.get_parser_warnings).to eq [] + end + + it 'warn for incomplete package measure metadata' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'Golden Test File.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + warnings = buidingsync_reader.get_parser_warnings + + # Assert + expect(warnings.map { |warning| warning[:severity] }.uniq).to eq [:warning] + expect(warnings.map { |warning| warning[:code] }).to contain_exactly( + :missing_system_category_affected, + :missing_technology_category, + :missing_measure_name, + :package_has_no_usable_measures, + :missing_system_category_affected, + :missing_technology_category, + :missing_measure_name, + :package_has_no_usable_measures + ) + + building_measure_warning = warnings.find do |warning| + warning[:measure_id] == 'Building1RemovePV' && warning[:code] == :missing_measure_name + end + expect(building_measure_warning).to include( + report_id: 'Report-c1857e54-836b-4674-95f4-2e6e9c8510b4', + scenario_id: 'Scenario1', + package_id: 'PackageOfMeasures-b9ca1b63-acd6-4d8a-9d8f-f39a96fd8dac' + ) + end + + it 'warn for malformed package scenario references' do + # Set Up + doc = REXML::Document.new wrap_in_facility(<<~XML) + + + Lighting + + + + Install lighting controls + + + + + + + + + + + + + + + + + + + + + + + + + + + XML + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + warnings = buidingsync_reader.get_parser_warnings + + # Assert + expect(warnings.map { |warning| warning[:code] }).to contain_exactly( + :missing_measure_id, + :missing_scenario_id, + :missing_package_id, + :missing_measure_idref, + :unresolved_measure_idref, + :package_has_no_usable_measures, + :package_missing_measure_ids, + :package_has_no_usable_measures + ) + + unresolved_warning = warnings.find { |warning| warning[:code] == :unresolved_measure_idref } + expect(unresolved_warning).to include( + report_id: 'ReportA', + scenario_id: nil, + package_id: nil, + measure_idref: 'UnknownMeasure' + ) + end + end + describe 'get_climate_zone should' do it "get from site" do # Set Up