use anyhow::{Context, Result};
use flate2::read::GzDecoder;
use log::*;
use regex::Regex;
use roxmltree::Document;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use crate::parse::{ParserState, ResultParser, TestStatus};
use crate::{BinaryTest, TestCase};
impl TestStatus {
fn from_piglit_str(input: &str) -> TestStatus {
match input {
"pass" => TestStatus::Pass,
"fail" => TestStatus::Fail,
"warn" => TestStatus::CompatibilityWarning,
"crash" => TestStatus::Crash,
"skip" => TestStatus::NotSupported,
"timeout" => TestStatus::Timeout,
_ => {
error!("unknown piglit status '{}'", input);
TestStatus::Crash
}
}
}
}
pub struct PiglitResultParser {
test_name: String,
}
impl PiglitResultParser {
pub fn new(name: &str) -> PiglitResultParser {
PiglitResultParser {
test_name: piglit_sanitize_test_name(name),
}
}
}
impl ResultParser for PiglitResultParser {
fn initialize(&mut self) -> Option<ParserState> {
Some(ParserState::BeginTest(self.test_name.to_owned()))
}
fn parse_line(&mut self, line: &str) -> Result<Option<ParserState>> {
lazy_static! {
static ref STATUS_RE: Regex = Regex::new(r#"PIGLIT: \{"result": "(.*)" \}"#).unwrap();
static ref SUBTEST_RE: Regex =
Regex::new(r#"PIGLIT: \{"subtest": *\{"(.*)" *: *"(.*)"\}\}"#).unwrap();
}
if let Some(cap) = STATUS_RE.captures(line) {
let status = TestStatus::from_piglit_str(&cap[1]);
Ok(Some(ParserState::EndTest(status)))
} else if let Some(cap) = SUBTEST_RE.captures(line) {
let name = piglit_sanitize_test_name(&cap[1]);
let status = TestStatus::from_piglit_str(&cap[2]);
Ok(Some(ParserState::SubTest(name, status)))
} else {
Ok(None)
}
}
}
pub fn read_profile_file(
piglit_folder: &std::path::Path,
profile: &str,
process_isolation: bool,
) -> Result<String> {
if !process_isolation {
let path = piglit_folder.join(Path::new(profile).with_extension("no_isolation.meta.xml"));
if path.exists() {
info!("... using {:?}", &path);
return std::fs::read_to_string(&path).with_context(|| format!("reading {:?}", path));
}
}
{
let path = piglit_folder.join(Path::new(profile).with_extension("meta.xml"));
if path.exists() {
info!("... using {:?}", path);
return std::fs::read_to_string(&path).with_context(|| format!("reading {:?}", path));
}
}
if !process_isolation {
let path = piglit_folder.join(Path::new(profile).with_extension("no_isolation.xml.gz"));
if path.exists() {
info!("... using {:?}", path);
let file = File::open(&path).with_context(|| format!("opening {:?}", path))?;
let mut s = String::new();
GzDecoder::new(file)
.read_to_string(&mut s)
.with_context(|| format!("reading {:?}", path))?;
return Ok(s);
}
}
if !process_isolation {
let path = piglit_folder.join(Path::new(profile).with_extension("no_isolation.xml"));
if path.exists() {
info!("... using {:?}", path);
return std::fs::read_to_string(&path).with_context(|| format!("reading {:?}", path));
}
}
{
let path = piglit_folder.join(Path::new(profile).with_extension("xml.gz"));
if path.exists() {
info!("... using {:?}", path);
let file = File::open(&path).with_context(|| format!("opening {:?}", path))?;
let mut s = String::new();
GzDecoder::new(file)
.read_to_string(&mut s)
.with_context(|| format!("reading {:?}", path))?;
return Ok(s);
}
}
{
let path = piglit_folder.join(Path::new(profile).with_extension("xml"));
std::fs::read_to_string(&path).with_context(|| format!("reading {:?}", path))
}
}
fn get_option_value(node: &roxmltree::Node, name: &str) -> Result<String> {
let mut children = node.children().filter(|x| x.has_tag_name("option"));
let option_node = children
.find(|x| x.attribute("name") == Some(name))
.with_context(|| format!("Getting option {}", name))?;
if children.any(|x| x.attribute("name") == Some(name)) {
anyhow::bail!("More than one option named {}", name);
}
Ok(option_node
.attribute("value")
.with_context(|| format!("getting option {} value", name))?
.to_string())
}
fn parse_piglit_command(
test_name: &str,
test_type: &str,
run_concurrent: bool,
command: &str,
) -> Result<TestCase> {
let len = command.len();
if len < 2 {
anyhow::bail!("command length {} too short", len);
}
let mut all = command[1..(len - 1)]
.split(',')
.map(|arg| arg.replace('\'', ""));
let binary = all.next().context("Getting binary")?;
let mut args: Vec<String> = all
.map(|arg| arg.trim().to_string())
.filter(|arg| !arg.is_empty())
.map(|arg| {
if test_type == "gl_builtin" {
format!("tests/{}", arg)
} else {
arg
}
})
.collect();
match test_type {
"glsl_parser" | "cl" => {}
_ => {
args.push("-auto".to_string());
if run_concurrent {
args.push("-fbo".to_string());
}
}
}
Ok(TestCase::Binary(BinaryTest {
name: test_name.to_string(),
binary,
args,
}))
}
fn piglit_sanitize_test_name(test: &str) -> String {
test.replace(',', "-")
}
pub fn parse_piglit_test(tests: &mut Vec<TestCase>, test: &roxmltree::Node) -> Result<()> {
let test_name = piglit_sanitize_test_name(test.attribute("name").context("getting test name")?);
match test.attribute("type") {
Some("multi_shader") => {
let nodes: Vec<roxmltree::Node> = test
.children()
.filter(|n| n.attribute("name") == Some("files"))
.collect();
if !nodes.is_empty() {
let content: String = nodes[0]
.attribute("value")
.unwrap()
.chars()
.filter(|c| !r#"[]'"#.contains(*c))
.collect();
let all: Vec<&str> = content.split(',').collect();
let mut args = Vec::new();
for a in all {
let m = a.trim();
if !m.is_empty() {
args.push(m.to_string());
}
}
let mut remaining = args.len();
let mut i = 0u32;
while remaining != 0 {
let group_len = usize::min(100, remaining);
remaining -= group_len;
let mut a = args.split_off(remaining);
a.push("-auto".to_string());
a.push("-fbo".to_string());
tests.push(TestCase::Binary(BinaryTest {
name: format!("{}|{}", test_name, i),
binary: "shader_runner".to_string(),
args: a,
}));
i += 1;
}
}
}
Some("asm_parser") => {
tests.push(TestCase::Binary(BinaryTest {
name: test_name,
binary: "asmparsertest".to_string(),
args: vec![
get_option_value(test, "type_")?.replace('\'', ""),
get_option_value(test, "filename")?.replace('\'', ""),
],
}));
}
Some("cl") => {
let command = get_option_value(test, "command").context("parsing command")?;
tests.push(parse_piglit_command(&test_name, "cl", false, &command)?);
}
Some("cl_prog") => {
tests.push(TestCase::Binary(BinaryTest {
name: test_name,
binary: "cl-program-tester".to_string(),
args: vec![get_option_value(test, "filename")?.replace('\'', "")],
}));
}
Some(test_type) => {
let command = get_option_value(test, "command").context("parsing command")?;
let run_concurrent = match get_option_value(test, "run_concurrent")
.context("parsing concurrent")?
.as_str()
{
"True" => true,
"False" => false,
x => anyhow::bail!("Unknown run_concurrent value {}", x),
};
tests.push(parse_piglit_command(
&test_name,
test_type,
run_concurrent,
&command,
)?);
}
None => anyhow::bail!("No test type specified for {}", test_name),
}
Ok(())
}
pub fn parse_piglit_xml_testlist(
folder: &Path,
file_content: &str,
process_isolation: bool,
) -> Result<Vec<crate::TestCase>> {
let doc = Document::parse(file_content).context("reading caselist")?;
let mut tests = Vec::new();
for test in doc.descendants().filter(|n| n.has_tag_name("Profile")) {
if let Some(name) = test.text() {
info!("Found subprofile: {:?}", name);
let content = read_profile_file(folder, name, process_isolation)?;
for t in parse_piglit_xml_testlist(folder, &content, process_isolation)? {
tests.push(t);
}
}
}
for test in doc.descendants().filter(|n| n.has_tag_name("Test")) {
parse_piglit_test(&mut tests, &test)
.with_context(|| format!("parsing test node: {:?}", &test))?;
}
Ok(tests)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::{CaselistResult, TestResult};
use std::path::PathBuf;
use std::time::Duration;
fn parse_immediate_xml(xml: &str) -> Result<Vec<TestCase>> {
let dummy_path = PathBuf::from(".");
parse_piglit_xml_testlist(&dummy_path, xml, false)
}
#[test]
fn parse_command_and_args() {
let xml = r#"
<Test type="gl" name="fast_color_clear@fcc-read-after-clear blit rb">
<option name="command" value="['fcc-read-after-clear', 'blit', 'rb']" />
<option name="run_concurrent" value="True" />
</Test>"#;
assert_eq!(
parse_immediate_xml(xml).unwrap()[0],
TestCase::Binary(BinaryTest {
name: "fast_color_clear@fcc-read-after-clear blit rb".to_string(),
binary: "fcc-read-after-clear".to_string(),
args: vec![
"blit".to_string(),
"rb".to_string(),
"-auto".to_string(),
"-fbo".to_string(),
],
})
);
}
#[test]
fn parse_asmparsertest() {
let xml = r#"
<Test type="asm_parser" name="asmparsertest@arbfp1.0@cos-03.txt">
<option name="type_" value="'ARBfp1.0'" />
<option name="filename" value="'tests/asmparsertest/shaders/ARBfp1.0/cos-03.txt'" />
</Test>"#;
assert_eq!(
parse_immediate_xml(xml).unwrap()[0],
TestCase::Binary(BinaryTest {
name: "asmparsertest@arbfp1.0@cos-03.txt".to_string(),
binary: "asmparsertest".to_string(),
args: vec![
"ARBfp1.0".to_string(),
"tests/asmparsertest/shaders/ARBfp1.0/cos-03.txt".to_string()
],
})
);
}
#[test]
fn parse_glslparsertest() {
let xml = r#"
<Test type="glsl_parser" name="spec@ext_clip_cull_distance@preprocessor@disabled-defined-es.comp">
<option name="shader_version" value="3.1" />
<option name="api" value="'gles3'" />
<option name="command" value="['glslparsertest_gles2', 'generated_tests/spec/ext_clip_cull_distance/preprocessor/disabled-defined-es.comp', 'pass', '3.10 es', '!GL_EXT_clip_cull_distance']" />
<option name="run_concurrent" value="True" />
</Test>"#;
assert_eq!(
parse_immediate_xml(xml).unwrap()[0],
TestCase::Binary(BinaryTest {
name: "spec@ext_clip_cull_distance@preprocessor@disabled-defined-es.comp"
.to_string(),
binary: "glslparsertest_gles2".to_string(),
args: vec![
"generated_tests/spec/ext_clip_cull_distance/preprocessor/disabled-defined-es.comp".to_string(),
"pass".to_string(), "3.10 es".to_string(), "!GL_EXT_clip_cull_distance".to_string()
],
})
);
}
#[test]
fn parse_glx_test() {
let xml = r#"
<Test type="gl" name="glx@glx-query-drawable-glxbaddrawable">
<option name="require_platforms" value="['glx', 'mixed_glx_egl']" />
<option name="command" value="['glx-query-drawable', '--bad-drawable']" />
<option name="run_concurrent" value="False" />
</Test>"#;
assert_eq!(
parse_immediate_xml(xml).unwrap()[0],
TestCase::Binary(BinaryTest {
name: "glx@glx-query-drawable-glxbaddrawable".to_string(),
binary: "glx-query-drawable".to_string(),
args: vec!["--bad-drawable".to_string(), "-auto".to_string()],
})
);
}
#[test]
fn parse_command_with_brackets() -> Result<()> {
let test = parse_piglit_command(
"test",
"gl",
true,
r#"['ext_transform_feedback-output-type', 'vec4', '[2]']"#,
)?;
assert_eq!(
test,
TestCase::Binary(BinaryTest {
name: "test".to_string(),
binary: "ext_transform_feedback-output-type".to_string(),
args: vec!(
"vec4".to_string(),
"[2]".to_string(),
"-auto".to_string(),
"-fbo".to_string(),
)
})
);
Ok(())
}
fn output_as_lines(output: &str) -> Vec<String> {
output.lines().map(|x| x.to_string()).collect()
}
fn parse_piglit_results<B: Read>(output: B) -> CaselistResult {
let parser = PiglitResultParser::new("test");
parser.parse(output).expect("Parser error")
}
fn result(status: TestStatus, subtests: Vec<TestResult>, orig_output: &str) -> CaselistResult {
let result = TestResult {
name: "test".to_owned(),
status,
duration: Duration::new(0, 0),
subtests,
};
CaselistResult {
results: vec![result],
stdout: output_as_lines(orig_output),
}
}
#[test]
fn parse_statuses() {
let output = "
PIGLIT: {\"result\": \"pass\" }";
assert_eq!(
parse_piglit_results(&mut output.as_bytes()),
result(TestStatus::Pass, vec![], output),
);
}
#[test]
fn parse_subtests() {
let output = "
PIGLIT: {\"enumerate subtests\": [\"Check valid integer border color values\", \"Check invalid integer border color values\", \"Check valid float border color values\", \"Check invalid float border color values\"]}
PIGLIT: {\"subtest\": {\"Check valid integer border color values\" : \"pass\"}}
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
PIGLIT: {\"subtest\": {\"Check invalid integer border color values\" : \"fail\"}}
PIGLIT: {\"subtest\": {\"Check valid float border color values\" : \"skip\"}}
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
Mesa: User error: GL_INVALID_OPERATION in glGetTextureSamplerHandleARB(invalid border color)
PIGLIT: {\"subtest\": {\"Check invalid float border color values\" : \"warn\"}}
PIGLIT: {\"result\": \"pass\" }";
let subtests = vec![
TestResult {
name: "Check valid integer border color values".to_owned(),
status: TestStatus::Pass,
duration: Duration::new(0, 0),
subtests: Vec::new(),
},
TestResult {
name: "Check invalid integer border color values".to_owned(),
status: TestStatus::Fail,
duration: Duration::new(0, 0),
subtests: Vec::new(),
},
TestResult {
name: "Check valid float border color values".to_owned(),
status: TestStatus::NotSupported,
duration: Duration::new(0, 0),
subtests: Vec::new(),
},
TestResult {
name: "Check invalid float border color values".to_owned(),
status: TestStatus::CompatibilityWarning,
duration: Duration::new(0, 0),
subtests: Vec::new(),
},
];
assert_eq!(
parse_piglit_results(output.as_bytes()),
result(TestStatus::Pass, subtests, output)
);
}
#[test]
fn parse_crash() {
let output = r#"
PIGLIT: {"subtest": {"Vertex shader/control memory barrier test/modulus=1" : "pass"}}
PIGLIT: {"subtest": {"Tessellation control shader/control memory barrier test/modulus=1" : "pass"}}"#;
let subtests = vec![
TestResult {
name: "Vertex shader/control memory barrier test/modulus=1".to_owned(),
status: TestStatus::Pass,
duration: Duration::new(0, 0),
subtests: vec![],
},
TestResult {
name: "Tessellation control shader/control memory barrier test/modulus=1"
.to_owned(),
status: TestStatus::Pass,
duration: Duration::new(0, 0),
subtests: vec![],
},
];
assert_eq!(
parse_piglit_results(output.as_bytes()),
result(TestStatus::Crash, subtests, output)
);
}
#[test]
fn parse_shader_runner_subtests() {
let output = r#"
PIGLIT TEST: 1 - glsl-fs-swizzle-1
PIGLIT TEST: 1 - glsl-fs-swizzle-1
PIGLIT: {"subtest": {"glsl-fs-swizzle-1" : "pass"}}
PIGLIT TEST: 2 - vs-sign-neg
PIGLIT TEST: 2 - vs-sign-neg
PIGLIT: {"subtest": {"vs-sign-neg" : "pass"}}
"#;
let subtests = vec![
TestResult {
name: "glsl-fs-swizzle-1".to_owned(),
status: TestStatus::Pass,
duration: Duration::new(0, 0),
subtests: vec![],
},
TestResult {
name: "vs-sign-neg".to_owned(),
status: TestStatus::Pass,
duration: Duration::new(0, 0),
subtests: vec![],
},
];
assert_eq!(
parse_piglit_results(output.as_bytes()),
result(TestStatus::Crash, subtests, output)
);
}
#[test]
fn parse_cl() {
let xml = r#"
<Test type="cl" name="api@clgeteventinfo">
<option name="command" value="['cl-api-get-event-info']" />
</Test>"#;
assert_eq!(
parse_immediate_xml(xml).unwrap()[0],
TestCase::Binary(BinaryTest {
name: "api@clgeteventinfo".to_string(),
binary: "cl-api-get-event-info".to_string(),
args: vec![],
})
);
}
#[test]
fn parse_cl_prog() {
let xml = r#"
<Test type="cl_prog" name="program@build@define-gentype">
<option name="filename" value="'tests/cl/program/build/define-GENTYPE.cl'" />
</Test>"#;
assert_eq!(
parse_immediate_xml(xml).unwrap()[0],
TestCase::Binary(BinaryTest {
name: "program@build@define-gentype".to_string(),
binary: "cl-program-tester".to_string(),
args: vec!["tests/cl/program/build/define-GENTYPE.cl".to_string()],
})
);
}
}