Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified mantra.db
Binary file not shown.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions mantra/migrations/3_Requirements.sql
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,17 @@ create table RequirementHierarchies (
foreign key (child_product_id, child_req_id) references Requirements (product_id, id) on delete cascade deferrable initially deferred,
foreign key (parent_product_id, parent_req_id) references Requirements (product_id, id) on delete cascade deferrable initially deferred
);

-- Table to map if a requirement replaces others.
-- Replacements are only possible inside the same product.
-- [req("req.replacing")]
create table RequirementReplacements (
last_collect_nr bigint not null references Collections (nr) on delete restrict,
req_id text not null,
product_id text not null,
-- Requirement id that is replaced by the requirement set in "req_id"
replaced_req_id text not null,
constraint RequirementReplacementPk primary key (req_id, product_id, replaced_req_id),
foreign key (req_id, product_id) references Requirements (id, product_id) on delete cascade,
foreign key (replaced_req_id, product_id) references Requirements (id, product_id) on delete cascade deferrable initially deferred
);
49 changes: 48 additions & 1 deletion mantra/src/cmd/collect/requirements/db/aggregate.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::Context;
use anyhow::{Context, anyhow};
use mantra_schema::annotations::TraceKind;

use crate::cmd::collect::Collection;
Expand All @@ -18,6 +18,11 @@ impl<'db> Collection<'db> {
self.update_excluded_requirements()
.await
.context("Failed to update excluded requirements")?;

self.check_replacing_requirements()
.await
.context("Failed checking requirement replacements")?;

self.update_optional_requirements()
.await
.context("Failed to update optional requirements")?;
Expand Down Expand Up @@ -175,6 +180,42 @@ impl<'db> Collection<'db> {
Ok(())
}

async fn check_replacing_requirements(&mut self) -> Result<(), anyhow::Error> {
let collect_nr = self.collect_nr();
let product_id = self.product_id();

let bad_replacements = sqlx::query!(
"
select rr.req_id, rr.replaced_req_id
from RequirementReplacements rr, RequirementDescendants rd
where rr.last_collect_nr = $1 and rr.product_id = $2
and rr.last_collect_nr = rd.last_collect_nr
and rr.product_id = rd.product_id
and rr.product_id = rd.descendant_product_id
and rr.replaced_req_id = rd.id
and rr.req_id = rd.descendant_id
",
collect_nr,
product_id
)
.fetch_all(self.connection_mut())
.await?;

for bad_record in &bad_replacements {
log::error!(
"Requirement '{}' cannot replace its ancestor '{}'",
bad_record.req_id,
bad_record.replaced_req_id
);
}

if bad_replacements.is_empty() {
Ok(())
} else {
Err(anyhow!("Requirement tried to replace its ancestor"))
}
}

async fn update_leaf_requirements(&mut self) -> Result<(), anyhow::Error> {
let collect_nr = self.collect_nr();
let product_id = self.product_id();
Expand Down Expand Up @@ -234,6 +275,12 @@ impl<'db> Collection<'db> {
where deprecated = true
and last_collect_nr = $1
and product_id = $2

union

select product_id, replaced_req_id
from RequirementReplacements
where last_collect_nr = $1 and product_id = $2
),
ParentMarkedDeprecated(product_id, id) as (
select rd.descendant_product_id, rd.descendant_id
Expand Down
55 changes: 55 additions & 0 deletions mantra/src/cmd/collect/requirements/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,25 @@ impl<'db> Collection<'db> {
record.id
)
})?;

sqlx::query!(
"
delete from RequirementReplacements
where product_id = $1 and req_id = $2
and last_collect_nr < $3
",
product_id,
record.id,
collect_nr
)
.execute(self.connection_mut())
.await
.with_context(|| {
format!(
"Failed to delete outdated requirement replacements for requirement '{}'",
record.id
)
})?;
}

// Check bad req-hierarchies before deleting olds.
Expand Down Expand Up @@ -443,6 +462,42 @@ impl<'db> Collection<'db> {
}
}

if let Some(replaced_reqs) = req.replaces {
for replaced_req in replaced_reqs {
sqlx::query!(
"
insert into RequirementReplacements (
last_collect_nr,
product_id,
req_id,
replaced_req_id
)
values (
$1,
$2,
$3,
$4
)
on conflict (product_id, req_id, replaced_req_id)
do update set
last_collect_nr = excluded.last_collect_nr
",
collect_nr,
product_id,
req.id,
replaced_req
)
.execute(self.connection_mut())
.await
.with_context(|| {
format!(
"Failed to update requirement replacement for requirement '{}'",
replaced_req
)
})?;
}
}

Ok(())
}

Expand Down
69 changes: 69 additions & 0 deletions mantra/src/cmd/collect/requirements/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1412,3 +1412,72 @@ mod states {
);
}
}

mod replacements {
use mantra_schema::report::requirement::RequirementState;

use crate::{cmd::collect::test_setup::db_from_dir, db::MantraPool};

#[sqlx::test]
async fn valid_req_replacements(pool: MantraPool) {
let deprecated_state = RequirementState::Deprecated.as_nr();

let db = db_from_dir!(pool, "replacements/valid_replaces").unwrap();

let deprecated_reqs: Vec<String> = sqlx::query!(
"
select id from RequirementVerificationStates
where state = $1
",
deprecated_state
)
.fetch_all(
db.connection()
.await
.expect("Failed to get a connection")
.as_mut(),
)
.await
.unwrap()
.into_iter()
.map(|r| r.id)
.collect();

assert!(
deprecated_reqs.contains(&"req-1".to_string()),
"Expected req-1 to be indirectly deprecated through req-2."
);
assert!(
deprecated_reqs.contains(&"req-3".to_string()),
"Expected req-3 to be indirectly deprecated through req-4."
);
assert!(
deprecated_reqs.contains(&"req-4".to_string()),
"Expected req-4 to be indirectly deprecated through req-5."
);
assert!(
deprecated_reqs.contains(&"req-6".to_string()),
"Expected req-6 to be indirectly deprecated through req-7."
);
assert!(
deprecated_reqs.contains(&"req-7".to_string()),
"Expected req-7 to be directly deprecated."
);
}

#[sqlx::test]
async fn verified_req(pool: MantraPool) {
let db_res = db_from_dir!(pool, "replacements/bad_replaces");

let Err(db_err) = db_res else {
panic!("Failed to detect bad replacement relations")
};

for err in db_err.chain() {
if err.to_string() == "Requirement tried to replace its ancestor" {
return;
}
}
panic!("Failed to detect bad replacement relations");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
products: [{
id: "p1",
name: "test",
requirements: [{
path: "reqs.json5",
source: "schema",
}]
}]
}
Loading