Document coverage feature; Convenience query functions (#152)

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2024-02-22 14:59:06 -08:00
committed by GitHub
parent d3d5367fd4
commit 3a86c83827
4 changed files with 149 additions and 4 deletions

View File

@@ -125,3 +125,9 @@ required-features = ["full-opa"]
name="aci"
harness=false
test=false
[package.metadata.docs.rs]
# To build locally:
# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

View File

@@ -175,7 +175,8 @@ $ regorus eval -d examples/example.rego -i examples/input.json data.example --co
```
It produces the following coverage report which shows that all lines are executed except the line that sets `allow` to true.
![coverage.png](https://github.com/microsoft/regorus/blob/main/docs/coverage.png)
![coverage.png](https://github.com/microsoft/regorus/blob/main/docs/coverage.png?raw=true)
See [Engine::get_coverage_report](https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_coverage_report) for details.
Policy coverage information is useful for debugging your policy as well as to write tests for your policy so that all

View File

@@ -265,12 +265,85 @@ impl Engine {
)
}
/// Evaluate a Rego query that produces a boolean value.
///
///
/// This function should be preferred over [`Engine::eval_query`] if just a `true`/`false`
/// value is desired instead of [`QueryResults`].
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// # let mut engine = Engine::new();
///
/// let enable_tracing = false;
/// assert_eq!(engine.eval_bool_query("1 > 2".to_string(), enable_tracing)?, false);
/// assert_eq!(engine.eval_bool_query("1 < 2".to_string(), enable_tracing)?, true);
///
/// // Non boolean queries will raise an error.
/// assert!(engine.eval_bool_query("1+1".to_string(), enable_tracing).is_err());
///
/// // Queries producing multiple values will raise an error.
/// assert!(engine.eval_bool_query("true; true".to_string(), enable_tracing).is_err());
///
/// // Queries producing no values will raise an error.
/// assert!(engine.eval_bool_query("true; false; true".to_string(), enable_tracing).is_err());
/// # Ok(())
/// # }
pub fn eval_bool_query(&mut self, query: String, enable_tracing: bool) -> Result<bool> {
let results = self.eval_query(query, enable_tracing)?;
if results.result.len() != 1 || results.result[0].expressions.len() != 1 {
bail!("query did not produce exactly one value");
match results.result.len() {
0 => bail!("query did not produce any values"),
1 if results.result[0].expressions.len() == 1 => {
results.result[0].expressions[0].value.as_bool().copied()
}
_ => bail!("query produced more than one value"),
}
results.result[0].expressions[0].value.as_bool().copied()
}
/// Evaluate an `allow` query.
///
/// This is a wrapper over [`Engine::eval_bool_query`] that returns true only if the
/// boolean query succeed and produced a `true` value.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// # let mut engine = Engine::new();
///
/// let enable_tracing = false;
/// assert_eq!(engine.eval_allow_query("1 > 2".to_string(), enable_tracing), false);
/// assert_eq!(engine.eval_allow_query("1 < 2".to_string(), enable_tracing), true);
///
/// assert_eq!(engine.eval_allow_query("1+1".to_string(), enable_tracing), false);
/// assert_eq!(engine.eval_allow_query("true; true".to_string(), enable_tracing), false);
/// assert_eq!(engine.eval_allow_query("true; false; true".to_string(), enable_tracing), false);
/// # Ok(())
/// # }
pub fn eval_allow_query(&mut self, query: String, enable_tracing: bool) -> bool {
matches!(self.eval_bool_query(query, enable_tracing), Ok(true))
}
/// Evaluate a `deny` query.
///
/// This is a wrapper over [`Engine::eval_bool_query`] that returns false only if the
/// boolean query succeed and produced a `false` value.
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// # let mut engine = Engine::new();
///
/// let enable_tracing = false;
/// assert_eq!(engine.eval_deny_query("1 > 2".to_string(), enable_tracing), false);
/// assert_eq!(engine.eval_deny_query("1 < 2".to_string(), enable_tracing), true);
///
/// assert_eq!(engine.eval_deny_query("1+1".to_string(), enable_tracing), true);
/// assert_eq!(engine.eval_deny_query("true; true".to_string(), enable_tracing), true);
/// assert_eq!(engine.eval_deny_query("true; false; true".to_string(), enable_tracing), true);
/// # Ok(())
/// # }
pub fn eval_deny_query(&mut self, query: String, enable_tracing: bool) -> bool {
!matches!(self.eval_bool_query(query, enable_tracing), Ok(false))
}
#[doc(hidden)]
@@ -455,16 +528,62 @@ impl Engine {
}
#[cfg(feature = "coverage")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "coverage")))]
/// Get the coverage report.
///
/// ```rust
/// # use regorus::*;
/// # use anyhow::{bail, Result};
/// # fn main() -> Result<()> {
/// let mut engine = Engine::new();
///
/// engine.add_policy(
/// "policy.rego".to_string(),
/// r#"
/// package test # Line 2
///
/// x = y { # Line 4
/// input.a > 2 # Line 5
/// y = 5 # Line 6
/// }
/// "#.to_string()
/// )?;
///
/// // Enable coverage.
/// engine.set_enable_coverage(true);
///
/// engine.eval_query("data".to_string(), false)?;
///
/// let report = engine.get_coverage_report()?;
/// assert_eq!(report.files[0].path, "policy.rego");
///
/// // Only line 5 is evaluated.
/// assert_eq!(report.files[0].covered.iter().cloned().collect::<Vec<u32>>(), vec![5]);
///
/// // Line 4 and 6 are not evaluated.
/// assert_eq!(report.files[0].not_covered.iter().cloned().collect::<Vec<u32>>(), vec![4, 6]);
/// # Ok(())
/// # }
/// ```
///
/// See also [`crate::coverage::Report::to_colored_string`].
pub fn get_coverage_report(&self) -> Result<crate::coverage::Report> {
self.interpreter.get_coverage_report()
}
#[cfg(feature = "coverage")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "coverage")))]
/// Enable/disable policy coverage.
///
/// If `enable` is different from the current value, then any existing coverage
/// information will be cleared.
pub fn set_enable_coverage(&mut self, enable: bool) {
self.interpreter.set_enable_coverage(enable)
}
#[cfg(feature = "coverage")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "coverage")))]
/// Clear the gathered policy coverage data.
pub fn clear_coverage_data(&mut self) {
self.interpreter.clear_coverage_data()
}

View File

@@ -3,6 +3,7 @@
// Use README.md as crate documentation.
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
#![cfg_attr(docsrs, feature(doc_cfg))]
use serde::Serialize;
@@ -340,21 +341,39 @@ impl std::fmt::Debug for dyn Extension {
}
#[cfg(feature = "coverage")]
#[cfg_attr(docsrs, doc(cfg(feature = "coverage")))]
pub mod coverage {
#[derive(Default, serde::Serialize, serde::Deserialize)]
/// Coverage information about a rego policy file.
pub struct File {
/// Path of the policy file.
pub path: String,
/// The rego policy.
pub code: String,
/// Lines that were evaluated.
pub covered: std::collections::BTreeSet<u32>,
/// Lines that were not evaluated.
pub not_covered: std::collections::BTreeSet<u32>,
}
#[derive(Default, serde::Serialize, serde::Deserialize)]
/// Policy coverage report.
pub struct Report {
/// Coverage information for files.
pub files: Vec<File>,
}
impl Report {
/// Produce an ANSI color encoded version of the report.
///
/// Covered lines are green.
/// Lines that are not covered are red.
///
/// <img src="https://github.com/microsoft/regorus/blob/main/docs/coverage.png?raw=true">
pub fn to_colored_string(&self) -> anyhow::Result<String> {
use std::io::Write;
let mut s = Vec::new();