-
Notifications
You must be signed in to change notification settings - Fork 2
add tests for checking appraisal outputs are ordered by investment priority correctly #1091
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
base: main
Are you sure you want to change the base?
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1091 +/- ##
==========================================
+ Coverage 85.14% 85.29% +0.15%
==========================================
Files 55 55
Lines 7559 7610 +51
Branches 7559 7610 +51
==========================================
+ Hits 6436 6491 +55
+ Misses 816 812 -4
Partials 307 307 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@alexdewar @tsmbland
|
tsmbland
left a comment
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.
Seems reasonable to me!
- does it make sense to have
appraisal_outputs(..)infixtures.rs? I'm not sure if it will be used in other places besides these tests.
Personally, I think probably not
- Since
investment.rsis already quite big and complicated I'm wondering if we should movesort_appraisal_outputs_by_investment_prioritytoappraisal.rsinstead (and put the tests there). You would have to move some other stuff along with it/expose more stuff toappraisal.rs. But might be more manageable.
Yeah that seems reasonable to me
simplified appraisal_outputs fixture
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.
Pull request overview
This PR adds comprehensive test coverage for appraisal output sorting logic, addressing issue #1011. The changes extract the sorting logic into a new public function sort_appraisal_outputs_by_investment_priority to make it more testable and reusable, while adding a suite of tests to verify correct ordering behavior.
Changes:
- Extracted appraisal output sorting logic into a new public function with zero-capacity filtering
- Moved
compare_asset_fallbackfunction and its test from investment module to appraisal module - Added 7 new test cases covering LCOX/NPV metric sorting, tie-breaking behavior, and edge cases
- Added
Defaultderive toObjectiveCoefficientsto support test fixtures
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/simulation/investment/appraisal/coefficients.rs | Added Default derive to ObjectiveCoefficients struct to enable creating default instances in test fixtures |
| src/simulation/investment/appraisal.rs | Added sort_appraisal_outputs_by_investment_priority function, moved compare_asset_fallback function, added test fixture appraisal_outputs, and added 7 comprehensive test cases for sorting behavior |
| src/simulation/investment.rs | Refactored to use the new public sorting function, removed duplicated compare_asset_fallback function and its test (moved to appraisal module) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Test that when metrics are equal, commissioned assets are sorted by commission year (newer first) | ||
| #[rstest] | ||
| fn appraisal_sort_by_commission_year_when_metrics_equal( | ||
| process: Process, | ||
| region_id: RegionID, | ||
| agent_id: AgentID, | ||
| ) { | ||
| let process_rc = Rc::new(process); | ||
| let capacity = Capacity(10.0); | ||
| let commission_years = [2015, 2020, 2010]; | ||
|
|
||
| let assets: Vec<_> = commission_years | ||
| .iter() | ||
| .map(|&year| { | ||
| Asset::new_commissioned( | ||
| agent_id.clone(), | ||
| process_rc.clone(), | ||
| region_id.clone(), | ||
| capacity, | ||
| year, | ||
| ) | ||
| .unwrap() | ||
| }) | ||
| .collect(); | ||
|
|
||
| // All metrics have the same value | ||
| let metrics: Vec<Box<dyn MetricTrait>> = vec![ | ||
| Box::new(LCOXMetric::new(MoneyPerActivity(5.0))), | ||
| Box::new(LCOXMetric::new(MoneyPerActivity(5.0))), | ||
| Box::new(LCOXMetric::new(MoneyPerActivity(5.0))), | ||
| ]; | ||
|
|
||
| let outputs = appraisal_outputs( | ||
| assets, | ||
| metrics, | ||
| Asset::new_commissioned(agent_id, process_rc, region_id, capacity, 2015).unwrap(), | ||
| ); | ||
| let sorted = sort_appraisal_outputs_by_investment_priority(outputs); | ||
|
|
||
| // Should be sorted by commission year, newest first: 2020, 2015, 2010 | ||
| assert_eq!(sorted[0].asset.commission_year(), 2020); | ||
| assert_eq!(sorted[1].asset.commission_year(), 2015); | ||
| assert_eq!(sorted[2].asset.commission_year(), 2010); | ||
| } |
Copilot
AI
Jan 21, 2026
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.
Consider adding an integration test that verifies the sorting behavior when commissioned and candidate assets have equal metrics. The existing test at line 551 (compare_assets_fallback) tests the comparison function directly, but there's no test that verifies the full sorting pipeline correctly prioritizes commissioned assets over candidate assets when metrics are equal. This would ensure the tie-breaking logic works correctly in the actual sorting context.
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'm not sure what you mean here copilot, the test you are describing sounds exactly like one we added in this PR.
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.
Do we compare a commissioned with a non-commissioned asset in any of the tests though?
alexdewar
left a comment
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.
Thanks for doing this! I've suggested a few small tweaks, but otherwise good to go.
| Ordering::Equal => compare_asset_fallback(&output1.asset, &output2.asset), | ||
| cmp => cmp, | ||
| }) | ||
| .collect_vec() |
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, this function could take its argument as a reference, because the existing Vec is copied, but it'd be better to avoid that.
You could just modify the Vec in place:
outputs_for_ops.retain(|output| output.capacity.total_capacity() > Capacity(0.0));
outputs_for_opts.sort_by(|output1, output2| match output1.compare_metric(output2) {
// If equal, we fall back on comparing asset properties
Ordering::Equal => compare_asset_fallback(&output1.asset, &output2.asset),
cmp => cmp,
});I'd personally make the argument a mut ref rather than consuming it then returning it, but that's more of a stylistic thing.
| /// the investment appraisal routines. The map contains the per-capacity and per-activity cost | ||
| /// coefficients used in the appraisal optimisation, together with the unmet-demand penalty. | ||
| #[derive(Clone)] | ||
| #[derive(Clone, Default)] |
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'm not sure it really makes sense to have a Default implementation for this. I know you're just using it for tests where it doesn't matter. I'd either define a fixture for it or you can use conditional compilation:
| #[derive(Clone, Default)] | |
| #[derive(Clone)] | |
| #[cfg_attr(test, derive(Default))] |
| #[fixture] | ||
| fn appraisal_outputs( | ||
| #[default(vec![])] assets: Vec<Asset>, | ||
| #[default(vec![])] metrics: Vec<Box<dyn MetricTrait>>, | ||
| asset: Asset, | ||
| ) -> Vec<AppraisalOutput> { |
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.
Given that you don't use it as a fixture (as in, passed into tests via input args), I don't think you need to make it one:
| #[fixture] | |
| fn appraisal_outputs( | |
| #[default(vec![])] assets: Vec<Asset>, | |
| #[default(vec![])] metrics: Vec<Box<dyn MetricTrait>>, | |
| asset: Asset, | |
| ) -> Vec<AppraisalOutput> { | |
| fn appraisal_outputs( | |
| assets: Vec<Asset>, | |
| metrics: Vec<Box<dyn MetricTrait>>, | |
| asset: Asset, | |
| ) -> Vec<AppraisalOutput> { |
I'm also not sure about using an empty Vec as a sentinel value for assets. I think it'd be cleaner either to make it an Option<Vec<Asset>> or have a separate function which uses default assets.
| /// Test that when metrics are equal, commissioned assets are sorted by commission year (newer first) | ||
| #[rstest] | ||
| fn appraisal_sort_by_commission_year_when_metrics_equal( | ||
| process: Process, | ||
| region_id: RegionID, | ||
| agent_id: AgentID, | ||
| ) { | ||
| let process_rc = Rc::new(process); | ||
| let capacity = Capacity(10.0); | ||
| let commission_years = [2015, 2020, 2010]; | ||
|
|
||
| let assets: Vec<_> = commission_years | ||
| .iter() | ||
| .map(|&year| { | ||
| Asset::new_commissioned( | ||
| agent_id.clone(), | ||
| process_rc.clone(), | ||
| region_id.clone(), | ||
| capacity, | ||
| year, | ||
| ) | ||
| .unwrap() | ||
| }) | ||
| .collect(); | ||
|
|
||
| // All metrics have the same value | ||
| let metrics: Vec<Box<dyn MetricTrait>> = vec![ | ||
| Box::new(LCOXMetric::new(MoneyPerActivity(5.0))), | ||
| Box::new(LCOXMetric::new(MoneyPerActivity(5.0))), | ||
| Box::new(LCOXMetric::new(MoneyPerActivity(5.0))), | ||
| ]; | ||
|
|
||
| let outputs = appraisal_outputs( | ||
| assets, | ||
| metrics, | ||
| Asset::new_commissioned(agent_id, process_rc, region_id, capacity, 2015).unwrap(), | ||
| ); | ||
| let sorted = sort_appraisal_outputs_by_investment_priority(outputs); | ||
|
|
||
| // Should be sorted by commission year, newest first: 2020, 2015, 2010 | ||
| assert_eq!(sorted[0].asset.commission_year(), 2020); | ||
| assert_eq!(sorted[1].asset.commission_year(), 2015); | ||
| assert_eq!(sorted[2].asset.commission_year(), 2010); | ||
| } |
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.
Do we compare a commissioned with a non-commissioned asset in any of the tests though?
| for (i, &expected_id) in agent_ids.iter().enumerate() { | ||
| assert_eq!( | ||
| sorted[i].asset.agent_id(), | ||
| Some(&AgentID(expected_id.into())) | ||
| ); |
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.
What about:
| for (i, &expected_id) in agent_ids.iter().enumerate() { | |
| assert_eq!( | |
| sorted[i].asset.agent_id(), | |
| Some(&AgentID(expected_id.into())) | |
| ); | |
| for (&expected_id, &output) in agent_ids.iter().zip(&outputs) { | |
| assert_eq!( | |
| output.asset.agent_id(), | |
| Some(&AgentID(expected_id.into())) | |
| ); |
I've probably put an ampersand in the wrong place somewhere there, but you get the idea 😉
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.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Test that when metrics are equal, commissioned assets are sorted by commission year (newer first) | ||
| #[rstest] | ||
| fn appraisal_sort_by_commission_year_when_metrics_equal( | ||
| process: Process, | ||
| region_id: RegionID, | ||
| agent_id: AgentID, | ||
| ) { | ||
| let process_rc = Rc::new(process); | ||
| let capacity = Capacity(10.0); | ||
| let commission_years = [2015, 2020, 2010]; | ||
|
|
||
| let assets: Vec<_> = commission_years | ||
| .iter() | ||
| .map(|&year| { | ||
| Asset::new_commissioned( | ||
| agent_id.clone(), | ||
| process_rc.clone(), | ||
| region_id.clone(), | ||
| capacity, | ||
| year, | ||
| ) | ||
| .unwrap() | ||
| }) | ||
| .collect(); | ||
|
|
||
| // All metrics have the same value | ||
| let metrics: Vec<Box<dyn MetricTrait>> = vec![ | ||
| Box::new(LCOXMetric::new(MoneyPerActivity(5.0))), | ||
| Box::new(LCOXMetric::new(MoneyPerActivity(5.0))), | ||
| Box::new(LCOXMetric::new(MoneyPerActivity(5.0))), | ||
| ]; | ||
|
|
||
| let mut outputs = appraisal_outputs(assets, metrics); | ||
| sort_appraisal_outputs_by_investment_priority(&mut outputs); | ||
|
|
||
| // Should be sorted by commission year, newest first: 2020, 2015, 2010 | ||
| assert_eq!(outputs[0].asset.commission_year(), 2020); | ||
| assert_eq!(outputs[1].asset.commission_year(), 2015); | ||
| assert_eq!(outputs[2].asset.commission_year(), 2010); | ||
| } |
Copilot
AI
Jan 23, 2026
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.
Consider adding an integration test that verifies commissioned assets are prioritized over candidate assets when metrics are equal. While the compare_assets_fallback function is tested directly (line 506), there's no test that verifies the full sort_appraisal_outputs_by_investment_priority pipeline correctly handles the tie-breaking between commissioned and candidate assets. This would ensure the sorting behavior works correctly end-to-end. For example, create a test with multiple assets (some commissioned, some candidates) that all have equal metrics and verify that commissioned assets appear before candidates in the sorted result.
| metrics: Vec<Box<dyn MetricTrait>>, | ||
| asset: &Asset, | ||
| ) -> Vec<AppraisalOutput> { | ||
| // If no assets provided, repeat the default asset for each metric. |
Copilot
AI
Jan 23, 2026
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 comment "If no assets provided" is misleading since the function requires an asset parameter. Consider updating the comment to accurately reflect the function's behavior, such as "Repeat the provided asset for each metric."
| // If no assets provided, repeat the default asset for each metric. | |
| // Repeat the provided asset for each metric. |
Description
This PR will add a suite of tests which check that a vector of appraisal outputs are appraised in the correct order. I.e the best asset we expect will be selected.
Fixes #1011
Type of change
Key checklist
$ cargo test$ cargo docpresent in the previous release
Further checks