Skip to content

Conversation

@Aurashk
Copy link
Collaborator

@Aurashk Aurashk commented Jan 20, 2026

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

  • Bug fix (non-breaking change to fix an issue)
  • New feature (non-breaking change to add functionality)
  • Refactoring (non-breaking, non-functional change to improve maintainability)
  • Optimization (non-breaking change to speed up the code)
  • Breaking change (whatever its nature)
  • Documentation (improve or add documentation)

Key checklist

  • [ X] All tests pass: $ cargo test
  • [ X] The documentation builds and looks OK: $ cargo doc
  • Update release notes for the latest release if this PR adds a new feature or fixes a bug
    present in the previous release

Further checks

  • [ X] Code is commented, particularly in hard-to-understand areas
  • Tests added that prove fix is effective or that feature works

@codecov
Copy link

codecov bot commented Jan 20, 2026

Codecov Report

❌ Patch coverage is 98.57143% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 85.29%. Comparing base (195c94c) to head (7f06bda).

Files with missing lines Patch % Lines
src/simulation/investment/appraisal.rs 98.48% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Aurashk
Copy link
Collaborator Author

Aurashk commented Jan 20, 2026

@alexdewar @tsmbland
When you get a chance please see if you're happy with how this is structured. It should be flexible enough to give us good coverage over the different types of appraisaloutputs. I've written one simple test for now. A couple of decisions I'm not too sure about though:

  • does it make sense to have appraisal_outputs(..) in fixtures.rs? I'm not sure if it will be used in other places besides these tests.
  • Since investment.rs is already quite big and complicated I'm wondering if we should move sort_appraisal_outputs_by_investment_priority to appraisal.rs instead (and put the tests there). You would have to move some other stuff along with it/expose more stuff to appraisal.rs. But might be more manageable.

Copy link
Collaborator

@tsmbland tsmbland left a 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(..) in fixtures.rs? I'm not sure if it will be used in other places besides these tests.

Personally, I think probably not

  • Since investment.rs is already quite big and complicated I'm wondering if we should move sort_appraisal_outputs_by_investment_priority to appraisal.rs instead (and put the tests there). You would have to move some other stuff along with it/expose more stuff to appraisal.rs. But might be more manageable.

Yeah that seems reasonable to me

@Aurashk Aurashk changed the title add simple lcox metric test add tests for checking appraisal outputs are ordered by investment priority correctly Jan 21, 2026
@Aurashk Aurashk requested a review from alexdewar January 21, 2026 10:27
@Aurashk Aurashk marked this pull request as ready for review January 21, 2026 11:05
Copilot AI review requested due to automatic review settings January 21, 2026 11:05
Copy link
Contributor

Copilot AI left a 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_fallback function 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 Default derive to ObjectiveCoefficients to 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.

Copilot AI review requested due to automatic review settings January 21, 2026 11:21
Copy link
Contributor

Copilot AI left a 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.

Comment on lines 671 to 714
/// 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);
}
Copy link

Copilot AI Jan 21, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

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.

Copy link
Collaborator

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?

Copy link
Collaborator

@alexdewar alexdewar left a 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()
Copy link
Collaborator

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)]
Copy link
Collaborator

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:

Suggested change
#[derive(Clone, Default)]
#[derive(Clone)]
#[cfg_attr(test, derive(Default))]

Comment on lines 544 to 549
#[fixture]
fn appraisal_outputs(
#[default(vec![])] assets: Vec<Asset>,
#[default(vec![])] metrics: Vec<Box<dyn MetricTrait>>,
asset: Asset,
) -> Vec<AppraisalOutput> {
Copy link
Collaborator

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:

Suggested change
#[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.

Comment on lines 671 to 714
/// 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);
}
Copy link
Collaborator

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?

Comment on lines 748 to 752
for (i, &expected_id) in agent_ids.iter().enumerate() {
assert_eq!(
sorted[i].asset.agent_id(),
Some(&AgentID(expected_id.into()))
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about:

Suggested change
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 😉

Copilot AI review requested due to automatic review settings January 23, 2026 09:40
Copy link
Contributor

Copilot AI left a 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.

Copilot AI review requested due to automatic review settings January 23, 2026 11:09
Copy link
Contributor

Copilot AI left a 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.

Comment on lines +670 to +709
/// 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);
}
Copy link

Copilot AI Jan 23, 2026

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.

Copilot uses AI. Check for mistakes.
metrics: Vec<Box<dyn MetricTrait>>,
asset: &Asset,
) -> Vec<AppraisalOutput> {
// If no assets provided, repeat the default asset for each metric.
Copy link

Copilot AI Jan 23, 2026

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."

Suggested change
// If no assets provided, repeat the default asset for each metric.
// Repeat the provided asset for each metric.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

improve treatment of equal-metric appraisals

4 participants