Files
regorus/tests/lexer/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

360 lines
10 KiB
Rust

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use anyhow::{bail, Result};
use regorus::unstable::*;
use serde::{Deserialize, Serialize};
use test_generator::test_resources;
fn get_tokens(source: &Source) -> Result<Vec<Token>> {
let mut tokens = vec![];
let mut lex = Lexer::new(source);
loop {
let tok = lex.next_token()?;
tokens.push(tok.clone());
if tok.0 == TokenKind::Eof {
break;
}
}
Ok(tokens)
}
fn check_loc(tok: &Token) -> Result<()> {
let msg = tok.1.source.message(tok.1.line, tok.1.col, "", "");
let lines: Vec<&str> = msg.split('\n').collect();
let source_line = lines[3];
let caret_line = lines[4];
let mut idx = 0usize;
let mut source_idx = idx;
loop {
match source_idx < source_line.len() && idx < caret_line.len() {
true => (),
// Handle Eof
false if tok.0 == TokenKind::Eof && source_idx >= source_line.len() => return Ok(()),
// Handle case where a raw string's first char is a newline.
false if tok.0 == TokenKind::RawString && &tok.1.text()[0..1] == "\n" => return Ok(()),
_ => {
bail!("could not find caret for {tok:#?} {msg}");
}
}
match &caret_line[idx..idx + 1] {
"^" => {
let span_str = tok.1.text();
let span_str = span_str.split('\n').collect::<Vec<&str>>()[0];
let source_str = &source_line[source_idx..];
assert!(
source_str.starts_with(span_str) || span_str.starts_with(source_str),
"location mismatch for {tok:#?} {msg}\n{span_str}\n{source_str}"
);
return Ok(());
}
_ if &source_line[source_idx..source_idx + 1] == "\t" => idx += 4,
_ => idx += 1,
}
source_idx += 1;
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Case {
pub rego: String,
pub note: String,
pub tokens: Vec<String>,
pub kinds: Option<Vec<String>>,
pub error: Option<String>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Test {
cases: Vec<Case>,
}
fn yaml_test_impl(file: &str) -> Result<()> {
println!("\nrunning {file}");
let yaml = std::fs::read_to_string(file)?;
let test: Test = serde_yaml::from_str(&yaml)?;
for case in &test.cases {
let source = Source::from_contents("case.rego".to_string(), case.rego.clone())?;
print!("case {} ", &case.note);
match get_tokens(&source) {
Ok(tokens) => {
for (idx, tok) in tokens.iter().enumerate() {
if idx >= case.tokens.len() {
break;
}
assert_eq!(
*tok.1.text(),
case.tokens[idx],
"{} Expected token `{}` not found",
source.message(tok.1.line, tok.1.col, "mismatch-error", &case.tokens[idx]),
&case.tokens[idx]
);
if let Some(k) = &case.kinds {
if idx >= k.len() {
break;
}
assert_eq!(
format!("{:?}", tok.0),
k[idx],
"{}",
source.message(
tok.1.line,
tok.1.col,
"mismatch-error",
"token kind mismatch"
)
);
}
check_loc(tok)?;
}
assert_eq!(
tokens.len(),
case.tokens.len(),
"\n. Token count mismatch.\nLexed tokens:{tokens:?}"
);
if let Some(k) = &case.kinds {
assert_eq!(
tokens.len(),
k.len(),
"\n. Kind count mismatch.\nLexed tokens:{tokens:?}"
);
}
}
Err(actual) => match &case.error {
Some(expected) => {
let actual = actual.to_string();
if !actual.contains(expected) {
bail!("Error message\n`{actual}\n`\ndoes not contain `{expected}`");
}
}
_ => return Err(actual),
},
}
println!("passed");
}
println!("{} cases passed.", test.cases.len());
Ok(())
}
fn yaml_test(file: &str) -> Result<()> {
match yaml_test_impl(file) {
Ok(_) => Ok(()),
Err(e) => {
// If Err is returned, it doesn't always get printed by cargo test.
// Therefore, panic with the error.
panic!("{}", e);
}
}
}
#[test_resources("tests/lexer/**/*.yaml")]
fn run(path: &str) {
yaml_test(path).unwrap()
}
#[test]
fn debug() -> Result<()> {
let rego = "\"This string is 35 characters long.\"\"short string\"";
let source = Source::from_contents("case.rego".to_string(), rego.to_string())?;
let mut lexer = Lexer::new(&source);
let tok = lexer.next_token()?;
check_loc(&tok)?;
assert_eq!(
format!("{:?}", tok.1),
"1:2:1:35, \"This string is 35 characters lon...\"",
"long span not truncated correctly"
);
let tok = lexer.next_token()?;
check_loc(&tok)?;
assert_eq!(format!("{:?}", tok.1), "1:38:37:49, \"short string\"");
Ok(())
}
#[test]
fn tab() -> Result<()> {
let rego = r#" "This string is 35 characters long."`raw string`p"#;
let source = Source::from_contents("case.rego".to_string(), rego.to_string())?;
let mut lexer = Lexer::new(&source);
// read first tab and string.
let tok = lexer.next_token()?;
check_loc(&tok)?;
assert_eq!(tok.1.col, 6, "tab not accounted correctly.");
// read raw string which contains tab.
let tok = lexer.next_token()?;
check_loc(&tok)?;
assert_eq!(tok.1.col, 42, "raw string not positioned correctly");
// read next token (ident)
let tok = lexer.next_token()?;
check_loc(&tok)?;
println!("{:?}", &tok);
println!("{}", source.message(tok.1.line, tok.1.col, "", ""));
assert_eq!(
tok.1.col, 56,
"tab within rawstring not accounted correctly"
);
Ok(())
}
#[test]
fn invalid_line() -> Result<()> {
let rego = "";
let source = Source::from_contents("case.rego".to_string(), rego.to_string())?;
assert_eq!(
source.message(2, 0, "", ""),
"case.rego: invalid line 2 specified"
);
Ok(())
}
#[test]
fn invalid_span_text_fallbacks() -> Result<()> {
let rego = "abc";
let source = Source::from_contents("case.rego".to_string(), rego.to_string())?;
let ss = SourceStr::new(source.clone(), 100, 200);
assert_eq!(
ss.text(),
"<invalid-span>",
"SourceStr should return fallback for out-of-bounds span"
);
let span = Span {
source: source.clone(),
line: 1,
col: 1,
start: 5,
end: 2,
};
assert_eq!(
span.text(),
"<invalid-span>",
"Span should return fallback for malformed span"
);
Ok(())
}
#[test]
#[cfg(feature = "std")]
fn file_more_than_64_kb_size() -> Result<()> {
let source = Source::from_file("tests/kata/data/large.rego")?;
let mut lexer = Lexer::new(&source);
let mut count = 0;
// Read tokens until EOF.
loop {
let token = lexer.next_token()?;
count += 1;
if token.0 == TokenKind::Eof {
break;
}
}
assert_eq!(count, 8789);
Ok(())
}
#[test]
fn default_limits_reject_oversized_file() {
let big = "x".repeat(1_048_577);
let err = Source::from_contents("big.rego".into(), big).unwrap_err();
assert!(err
.to_string()
.contains("exceeds maximum allowed policy file size"));
}
#[test]
fn custom_limits_accept_larger_file() {
use core::num::NonZeroUsize;
let big = "x".repeat(1_048_577);
assert!(Source::from_contents_with_limits(
"big.rego".into(),
big,
NonZeroUsize::new(2_097_152).unwrap(),
NonZeroUsize::new(1).unwrap()
)
.is_ok());
}
#[test]
fn custom_limits_reject_line_count() {
use core::num::NonZeroUsize;
let err = Source::from_contents_with_limits(
"lines.rego".into(),
"x\n".repeat(6),
NonZeroUsize::new(100).unwrap(),
NonZeroUsize::new(5).unwrap(),
)
.unwrap_err();
assert!(err
.to_string()
.contains("exceeds maximum allowed line count"));
}
#[test]
fn engine_policy_length_config_flows_through() -> Result<()> {
use core::num::NonZeroUsize;
use regorus::{Engine, PolicyLengthConfig};
let mut engine = Engine::new();
engine.set_policy_length_config(PolicyLengthConfig {
max_file_bytes: NonZeroUsize::new(10).unwrap(),
..Default::default()
});
let err = engine
.add_policy("test.rego".into(), "package test".into())
.unwrap_err();
assert!(err
.to_string()
.contains("exceeds maximum allowed policy file size"));
engine.clear_policy_length_config();
engine.add_policy("test.rego".into(), "package test".into())?;
Ok(())
}
#[test]
fn custom_max_col_allows_wide_line() -> Result<()> {
use core::num::NonZeroU32;
use regorus::{Engine, PolicyLengthConfig};
// A line wider than the default 1024 columns.
let wide = format!("package test\na := \"{}\"", "x".repeat(9000));
let mut engine = Engine::new();
// Should fail with default limits.
let err = engine
.add_policy("wide.rego".into(), wide.clone())
.unwrap_err();
assert!(err.to_string().contains("maximum column width"));
// Should succeed with a raised max_col.
engine.set_policy_length_config(PolicyLengthConfig {
max_col: NonZeroU32::new(16384).unwrap(),
..Default::default()
});
engine.add_policy("wide.rego".into(), wide)?;
Ok(())
}