Files
regorus/tests/azure_policy/parser_tests/mod.rs
Anand Krishnamoorthi 4c92fb4d92 feat(azure_policy): test runner, compiler fixes, and example program (#700)
Adds the YAML test runner that exercises the companion test data PRs, plus
several compiler fixes surfaced during testing:

- Removed parameter register caching that produced wrong results inside
  short-circuiting allOf/anyOf blocks; added literal-index caching for
  parameter defaults to avoid repeated O(n) literal-table scans
- Simplified cross-resource effect details to only emit roleDefinitionIds
  and type (deployment templates are not evaluated for compliance)
- Replaced guid/uniqueString builtins with clear "unsupported" errors
- Normalized datetime output to ISO 8601 with Z suffix
- Added azure_policy parser MAX_COL constant (8192) for long template
  expressions, keeping the global DEFAULT_MAX_COL at 1024
- Added rvm to azure_policy feature dependencies since the compiler
  targets RVM bytecode

Also restructures the example binary into examples/regorus/ with new
azure-policy-eval and azure-policy-aliases subcommands, adds C# alias
normalization tests, and documents Azure Policy support in the README.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-30 13:02:37 -05:00

201 lines
6.7 KiB
Rust

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! YAML-driven test suite for Azure Policy constraint parser.
//!
//! Each YAML file in `tests/azure_policy/parser_tests/cases/` contains a list
//! of test cases. Each case specifies a `policy_rule` JSON string with
//! `"if"` / `"then"` structure. The test runner extracts the `"if"` constraint
//! JSON and parses it with `parse_constraint`.
use anyhow::{bail, Result};
use regorus::languages::azure_policy::parser;
use regorus::Source;
use serde::{Deserialize, Serialize};
use std::fs;
use test_generator::test_resources;
/// A single test case in the YAML file.
#[derive(Serialize, Deserialize, Debug)]
struct TestCase {
/// Short identifier for the test case.
pub note: String,
/// The Azure Policy `policyRule` JSON string.
#[serde(default)]
pub policy_rule: Option<String>,
/// If true, the constraint is expected to fail parsing.
#[serde(default)]
pub want_parse_error: Option<bool>,
/// If true, skip this test case.
#[serde(default)]
pub skip: Option<bool>,
/// Parsing level: `"constraint"` (default) extracts the `"if"` block and
/// calls `parse_constraint`; `"policy_rule"` calls `parse_policy_rule` on
/// the full JSON; `"policy_definition"` calls `parse_policy_definition`
/// on the full JSON.
#[serde(default)]
pub parse_level: Option<String>,
}
/// Top-level YAML test file structure.
#[derive(Serialize, Deserialize, Debug)]
struct YamlTest {
/// Optional global policy rule JSON string.
#[serde(default)]
pub policy_rule: Option<String>,
pub cases: Vec<TestCase>,
}
/// Filter test cases by the `TEST_CASE_FILTER` environment variable.
fn should_run_test_case(case_note: &str) -> bool {
if let Ok(filter) = std::env::var("TEST_CASE_FILTER") {
case_note.contains(&filter)
} else {
true
}
}
/// Extract the `"if"` sub-object from a policy rule JSON string.
///
/// Returns `None` if parsing fails or there is no `"if"` key (the caller
/// should feed the raw string to `parse_constraint` for error tests).
fn extract_if_json(source_json: &str) -> Option<String> {
let v: serde_json::Value = serde_json::from_str(source_json).ok()?;
let if_value = v.get("if")?;
Some(if_value.to_string())
}
/// Run all test cases from a YAML file.
fn yaml_test_impl(file: &str) -> Result<()> {
let yaml_str = fs::read_to_string(file)?;
let test: YamlTest = serde_yaml::from_str(&yaml_str)?;
println!("running {file}");
if let Ok(filter) = std::env::var("TEST_CASE_FILTER") {
println!(" Test case filter active: '{filter}'");
}
let mut executed_count = 0usize;
let mut skipped_count = 0usize;
for case in &test.cases {
if !should_run_test_case(&case.note) {
println!(" case {} filtered out", case.note);
skipped_count += 1;
continue;
}
print!(" case {} ", case.note);
if case.skip == Some(true) {
println!("skipped");
skipped_count += 1;
continue;
}
executed_count += 1;
let expects_parse_error = case.want_parse_error == Some(true);
let source_json = if let Some(ref rule) = case.policy_rule {
rule.clone()
} else if let Some(ref rule) = test.policy_rule {
rule.clone()
} else {
bail!("case '{}': must specify 'policy_rule'", case.note);
};
let parse_level = case.parse_level.as_deref().unwrap_or("constraint");
let parse_result = match parse_level {
"policy_rule" => {
// Parse the full policy_rule JSON with parse_policy_rule.
let source = Source::from_contents(format!("test:{}", case.note), source_json)?;
parser::parse_policy_rule(&source).map(|_| ())
}
"policy_definition" => {
// Parse the full policy definition JSON with parse_policy_definition.
let source = Source::from_contents(format!("test:{}", case.note), source_json)?;
parser::parse_policy_definition(&source).map(|_| ())
}
"constraint" => {
// Extract the "if" constraint JSON. If extraction fails
// (malformed JSON or missing "if" key), feed the raw source
// JSON to parse_constraint — it should fail, matching
// want_parse_error.
let constraint_json =
extract_if_json(&source_json).unwrap_or_else(|| source_json.clone());
let source = Source::from_contents(format!("test:{}", case.note), constraint_json)?;
parser::parse_constraint(&source).map(|_| ())
}
other => {
bail!("case '{}': unknown parse_level '{}'", case.note, other);
}
};
match parse_result {
Ok(()) => {
if expects_parse_error {
bail!(
"case '{}': expected parse error but parsing succeeded",
case.note
);
}
println!("passed (parsed ok)");
}
Err(e) => {
if expects_parse_error {
println!("passed (expected parse error: {})", e);
} else {
bail!("case '{}': unexpected parse error: {}", case.note, e);
}
}
}
}
println!(
" Summary: {executed_count} executed, {skipped_count} skipped, {} total",
test.cases.len()
);
Ok(())
}
#[test_resources("tests/azure_policy/parser_tests/cases/**/*.yaml")]
fn yaml_test(file: &str) {
yaml_test_impl(file).unwrap();
}
/// Test duplicate-key detection directly (bypassing serde_json which
/// silently deduplicates keys).
#[test]
fn duplicate_key_in_condition() {
// Two "field" keys in a single condition object.
let json = r#"{"field": "type", "field": "name", "equals": "X"}"#;
let source = Source::from_contents("test:dup_field".to_string(), json.to_string()).unwrap();
let err = parser::parse_constraint(&source).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("duplicate key"),
"expected duplicate key error, got: {msg}"
);
}
#[test]
fn duplicate_key_in_count() {
// Two "field" keys inside a count block.
let json = r#"{"count": {"field": "a[*]", "field": "b[*]"}, "equals": 0}"#;
let source =
Source::from_contents("test:dup_count_field".to_string(), json.to_string()).unwrap();
let err = parser::parse_constraint(&source).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("duplicate key"),
"expected duplicate key error, got: {msg}"
);
}