-
Notifications
You must be signed in to change notification settings - Fork 12.1k
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
Track implementation for MC/DC #124144
Comments
@rustbot label +A-code-coverage |
The tracking issue should probably include a (brief) definition of MCDC, as otherwise this is untrackable by anyone who does not already know what the issue is for. |
nit on the terminology, but I'd rather say that conditions are boolean expressions that have no logical operator. |
Excellent, that's much better, thank you! The teams every now and then review tracking issues and it is helpful if whoever is participating in the review can understand what the tracking issue is about without any domain expertise (even if only so they can fetch the appropriate subject-matter expert). |
Thanks for correctness! I should have written "binary logical operators". It's a bit different with normal definition because I think treat conditions + unary operators as condition does not lead to different results in MC/DC. |
Upon implementing mcdc for pattern match, I found that decisions here might be confusing. |
I wonder how should let-chains be instrumented. for example: if let (A | B, C) = value && let Some(X | Y) = other && (x || y) {
} Here, I can't decide whether we should insert 1 or 3 MCDC decision records here
Maybe the 3 decisions would be better and easier to do, but I fear this would add a lot of verbosity to the report, and also increase the number of conditions in decisions, which therefore increase the complexity of the testing. What do you think ? |
I'd tend to 3 decisions as it's more clear and easy to be implemented. Besides, if we want to present consistent results in mcdc, we would better take all refutable pattern matching as decisions or not, otherwise we might face paradox like branch coverage in lazy expressions now. |
That makes sense, indeed. Let's go with this strategy then :) Also, I think it would make sense to allow the user to disable pattern matching instrumentation if needed (for example to reduce the binary size if needed). Maybe we could do this with a flag |
Good idea, |
I have a few cleanup steps planned for after #123409/#124255:
|
…ons, r=oli-obk MCDC coverage: support nested decision coverage rust-lang#123409 provided the initial MCDC coverage implementation. As referenced in rust-lang#124144, it does not currently support "nested" decisions, like the following example : ```rust fn nested_if_in_condition(a: bool, b: bool, c: bool) { if a && if b || c { true } else { false } { say("yes"); } else { say("no"); } } ``` Note that there is an if-expression (`if b || c ...`) embedded inside a boolean expression in the decision of an outer if-expression. This PR proposes a workaround for this cases, by introducing a Decision context stack, and by handing several `temporary condition bitmaps` instead of just one. When instrumenting boolean expressions, if the current node is a leaf condition (i.e. not a `||`/`&&` logical operator nor a `!` not operator), we insert a new decision context, such that if there are more boolean expressions inside the condition, they are handled as separate expressions. On the codegen LLVM side, we allocate as many `temp_cond_bitmap`s as necessary to handle the maximum encountered decision depth.
…ons, r=Zalathar MCDC coverage: support nested decision coverage rust-lang#123409 provided the initial MCDC coverage implementation. As referenced in rust-lang#124144, it does not currently support "nested" decisions, like the following example : ```rust fn nested_if_in_condition(a: bool, b: bool, c: bool) { if a && if b || c { true } else { false } { say("yes"); } else { say("no"); } } ``` Note that there is an if-expression (`if b || c ...`) embedded inside a boolean expression in the decision of an outer if-expression. This PR proposes a workaround for this cases, by introducing a Decision context stack, and by handing several `temporary condition bitmaps` instead of just one. When instrumenting boolean expressions, if the current node is a leaf condition (i.e. not a `||`/`&&` logical operator nor a `!` not operator), we insert a new decision context, such that if there are more boolean expressions inside the condition, they are handled as separate expressions. On the codegen LLVM side, we allocate as many `temp_cond_bitmap`s as necessary to handle the maximum encountered decision depth.
Nested decisions might lead to unexpected results due to sorting mappings happened in llvm. See this patch to llvm for details. Let-chains for pattern matching is affected by this most. |
We treat decisions with only one condition as normal branches now, because mcdc won't give different results from normal branch coverage on such cases. |
I should write something to clarify thoughts on designing mcdc for pattern matching in case someone felt confused about the behaviors. Coverage results of pattern conditions seems to be wrong/unexpected. match tuple {
(A(Some(_)), B | C) => { /*...*/ },
(B | C, A(_)) => { /*...*/ },
(A(None), _) => { /*...*/ },
_ => { /*...*/ }
} rustc would check the first and the third arm first. The coverage results are derived from the actual control flow generated by the compiler, which might be not same as intuition.
|
I am not really convinced that this is something we want to do. In the 2-conditions case, even though the theorem proves it, I find it not trivial to understand that branch coverage implies MCDC. Moreover, in the context of MCDC, I tend to define decisions as boolean expressions composed of several conditions, so in that sense, we don't have to instrument 1-condition decisions by definition, while we still have to instrument decisions with 2+ conditions. At last, while we have historically never instrumented 1-condition decisions for successfully certified code, it may be harder to justify not instrumenting 2-cond. decisions to the certification authorities, even though, again, it is proved to be equivalent. Also, I don't feel like this will bring a consequent improvement for either runtime speed, or memory usage, compared to the gains of not instrumenting 1-cond., mostly because I think 1-cond decisions are the most frequently encountered. |
We might need to make decisions about how to deal with constant folding. const CONSTANT_BOOL: bool = true;
fn foo(a: bool) {
if a && CONSTANT_BOOL {
/*...*/
}
} In such case llvm shows 100% coverage once But once if we changed I'd propose to eliminate these conditions and all other conditions effected by them from MCDC. That says,
And if a decision was always evaluated to solitary value, we remove it and transform its all conditions to normal branches. Let's request some nice insights. |
LLVM 19 has changed the start condition id from 1 to 0 at llvm #81257. This could break all mcdc tests once rustc upgraded its llvm backend. Since llvm 19 is still under development, I plan to deal with it when rust launches process to upgrade the backend . |
Sounds reasonable. Once the compiler switches over to LLVM 19, we might even want to stop supporting MC/DC on LLVM 18, to avoid the complexity of having to support big differences between 18 and 19. 🤔 |
I have several question regarding constant folding. First, when you say " In the case were Also, in the cases Nobody uses constants in decisions intentionnally, so we primarly need to think about generated code. One idea would be to write warning message saying something like "Condition X of the decision was resolved to constant False, this will make MCDC coverage impossible for conditions X,Y, Z". However, I am not really convinced by this approach, because this would introduce compilation "Warnings" that may be completely expected. The other idea would be to add metadata in the mappings, so that fn foo(a: bool) {
if a && false {
println!("success");
}
}
One question that this arises, is how we consider "skipped" conditions. Do we account for them in the whole decision validation, or do we ignore them, and consider the decision is covered if all non-skipped conditions are covered ? Furthermore, the biggest downside of this solution is that is will probably involve a massive refactor of LLVM's ProfileData code. What do you think ? |
That's what I expect: constants are marked as "Folded" while all uncoverable conditions are noted. I've tried a bit on both rustc and llvm. It seems better to implement it on llvm side. Since in rust we know which conditions are folded after injecting statements, it's not elegant for rust to do these stuff. |
It's likely I don't understand the intricacies of constant folding in rust; given its unique use cases, ultimately you can handle these however you wish. However, I want to point out that in clang, we mark conditions that are constant folded so that they can be visualized for the user (both for branch coverage and MC/DC). In MC/DC, these constant folded conditions are excluded from measurement (as you know, they're uncoverable and therefore cannot have any independence pairs). However, a key distinction I want to make is that for MC/DC in clang, we actually do not fold the conditions (we disable this) and therefore we actually instrument the folded conditions in order to facilitate proper coverage of the other conditions in the decision. We have to do this to ensure we match the possible test vectors correctly. |
In my premature thoughts we can avoid remarkable divergences between rust and llvm. The changes I wish to make are:
This method does not invade instruments but only effects how llvm presents for users. For instance, suppose we have a decision With the scenario, we still have 4 mcdc branch mappings: After llvm-cov collects all test vectors, it can show practical test vectors by pick up whose folded conditions bits are expected. Say, in our example, only test vectors in this table are practical: (
As we see, the practical test vectors are whose bit for Next let's find which conditions are uncoverable or folded:
Briefly the scheme is:
I'm going to draft in this way hence not much of existed llvm code need to be refactored. Is it worth? (I've opened an issue on llvm llvm #93560, we can discuss the scheme there) |
Introduction
Modified condition/decision coverage (MC/DC) is a code coverage criterion used widely in safety critical software components and is required by standards such as DO-178B and ISO26262.
Terminology
condition: boolean expressions that have no binary logical operators. For example,
a || b
is not "condition" because it has an or operator whilea==b
is.decision: boolean expressions composed of conditions and binary boolean expressions.
MC/DC requires each condition in a decision is shown to independently affect the outcome of the decision.
e.g Suppose we have code like
Here
(a || b) && c
is a decision anda
,b
,c
are conditions.(a=true, b=false, c=true)
and(a=false, b=false, c=true)
, we saya
can independently affect the decision because the value of decision is changed asa
changes while keepb
andc
unchanged. So that we get 1/3 MC/DC here (1 fora
and 3 fora
,b
,c
).(a=false, b=true, c=true)
and(a=false, b=false, c=false)
also showb
can independently affect the decision because in the later casec
is short-circuited and has no impacts (thus we can view it as same asc=true
). However,c
is not acknowledged due to change ofb
. Plus the two cases before we get 2/3 MC/DC.(a=true,b=false,c=true)
and(a=true,b=false,c=false)
showc
can do the same. By now we get 3/3.Notice that there are duplicate cases, so test cases collection {
(a=true, b=false, c=true)
,(a=false, b=false, c=true)
,(a=false, b=true, c=true)
,(a=false, b=false, c=false)
,(a=true,b=false,c=false)
} are sufficient to prove 3/3 MC/DC.In fact we can use at least n+1 cases to prove 100% MC/DC of decision with n conditions. (In this example, {
(a=true,b=false,c=true)
,(a=false,b=false,c=true)
,(a=false,b=true,c=true)
,(a=true,b=false,c=false)
} are enough)Progress
A basic implementation for MC/DC is filed on #123409 , which has some limits. There are still several cases need to handle:
Support lazy boolean expressions like
let val = a || b
. For now this is impacted by branch coverage and work in a different style with certification standards where usually MC/DC is required. To solve this, 124120 is the prerequisite.Draft: (DRAFT) Proof-of-concept for instrumenting the RHS of lazy logical operators #124644
Support
if-let
pattern. Ferrous suggests that refutable pattern matching should be considered as decisions, which sounds certainly reasonable as these are considered as branches too.This depends on support from branch coverage (see 124118).Draft: Support mcdc analysis for pattern matching #124278
Besides, decisions in match guard might still be left to discuss.
Support nested decisions like
if (a || b) || inner_decision(c || d)
So far only decisions with at most 6 conditions are counted due to resource cost. This limit should be relaxed once if the llvm backend had this optimization.
Notes
LLVM 19 has changed the start condition id from 1 to 0 at llvm #81257. This could break all mcdc tests once rustc upgraded its llvm backend.
The text was updated successfully, but these errors were encountered: