auto merge of #600 : metajack/servo/new-reftest, r=pcwalton

This does not port the existing src/test/html/ref tests to the new framework, as it appears to me that they aren't really reftests in the sense of Gecko's reftest. This new driver uses the Gecko methodology.

Currently this will pop a window for each test due to not having a headless driver yet, and #570 means that servo segfaults when it shuts down so we can't check the exit status.

There's plenty to improve in the future, but this should get us started.
This commit is contained in:
bors-servo 2013-07-18 15:25:10 -07:00
commit 41f7109c63
5 changed files with 138 additions and 168 deletions

View file

@ -22,7 +22,7 @@ servo-test: $(DEPS_servo)
$(RUSTC) $(RFLAGS_servo) --test -o $@ $< $(RUSTC) $(RFLAGS_servo) --test -o $@ $<
reftest: $(S)src/test/harness/reftest/reftest.rs servo reftest: $(S)src/test/harness/reftest/reftest.rs servo
$(RUSTC) $(RFLAGS_servo) -o $@ $< -L . $(RUSTC) -o $@ $<
contenttest: $(S)src/test/harness/contenttest/contenttest.rs servo contenttest: $(S)src/test/harness/contenttest/contenttest.rs servo
$(RUSTC) $(RFLAGS_servo) -o $@ $< -L . $(RUSTC) $(RFLAGS_servo) -o $@ $< -L .
@ -44,15 +44,15 @@ check-all: $(DEPS_CHECK_TARGETS_ALL) check-servo tidy
.PHONY: check-servo .PHONY: check-servo
check-servo: servo-test check-servo: servo-test
./servo-test $(TESTNAME) ./servo-test
.PHONY: check-ref .PHONY: check-ref
check-ref: reftest check-ref: reftest
./reftest --source-dir=$(S)/src/test/html/ref --work-dir=src/test/html/ref $(TESTNAME) ./reftest $(S)src/test/ref/*.list
.PHONY: check-content .PHONY: check-content
check-content: contenttest check-content: contenttest
./contenttest --source-dir=$(S)/src/test/html/content $(TESTNAME) ./contenttest --source-dir=$(S)src/test/html/content $(TESTNAME)
.PHONY: tidy .PHONY: tidy
tidy: tidy:

View file

@ -8,188 +8,141 @@
// except according to those terms. // except according to those terms.
extern mod std; extern mod std;
extern mod servo; extern mod extra;
use std::test::{TestOpts, run_tests_console, TestDesc}; use std::cell::Cell;
use std::getopts::{getopts, reqopt, opt_str, fail_str}; use std::io;
use os::list_dir_path; use std::os;
use servo::run_pipeline_png; use std::run;
use servo::image::base::Image; use extra::digest::{Digest, DigestUtil};
use extra::sha1::Sha1;
use extra::test::{DynTestName, DynTestFn, TestDesc, TestOpts, TestDescAndFn};
use extra::test::run_tests_console;
fn main(args: ~[~str]) { fn main() {
let config = parse_config(args); let args = os::args();
let opts = test_options(config); if args.len() < 2 {
let tests = find_tests(config); println("error: at least one reftest list must be given");
install_rasterize_py(config); os::set_exit_status(1);
run_tests_console(opts, tests); return;
}
struct Config {
source_dir: ~str,
work_dir: ~str,
filter: Option<~str>
}
fn parse_config(args: ~[~str]) -> Config {
let args = args.tail();
let opts = ~[reqopt(~"source-dir"), reqopt(~"work-dir")];
let matches = match getopts(args, opts) {
Ok(m) => m,
Err(f) => fail fail_str(f)
};
Config {
source_dir: opt_str(matches, ~"source-dir"),
work_dir: opt_str(matches, ~"work-dir"),
filter: if matches.free.is_empty() {
None
} else {
Some(matches.free.head())
}
} }
}
fn test_options(config: Config) -> TestOpts { let tests = parse_lists(args.tail());
{ let test_opts = TestOpts {
filter: config.filter, filter: None,
run_ignored: false, run_ignored: false,
logfile: None logfile: None,
} run_tests: true,
} run_benchmarks: false,
save_results: None,
fn find_tests(config: Config) -> ~[TestDesc] { compare_results: None,
let all_files = list_dir_path(&Path(config.source_dir));
let html_files = all_files.filter( |file| file.to_str().ends_with(".html") );
return html_files.map(|file| make_test(config, (*file).to_str()) );
}
fn make_test(config: Config, file: ~str) -> TestDesc {
let directives = load_test_directives(file);
{
name: file,
testfn: fn~() { run_test(config, file) },
ignore: directives.ignore,
should_fail: false
}
}
struct Directives {
ignore: bool
}
fn load_test_directives(file: ~str) -> Directives {
let data = match io::read_whole_file_str(&Path(file)) {
result::Ok(data) => data,
result::Err(e) => fail #fmt("unable to load directives for %s: %s", file, e)
}; };
let mut ignore = false; if !run_tests_console(&test_opts, tests) {
os::set_exit_status(1);
}
}
for str::lines(data).each |line| { enum ReftestKind {
if is_comment(line) { Same,
if line.contains("ignore") { Different,
ignore = true; }
break;
struct Reftest {
name: ~str,
kind: ReftestKind,
left: ~str,
right: ~str,
}
fn parse_lists(filenames: &[~str]) -> ~[TestDescAndFn] {
let mut tests: ~[TestDescAndFn] = ~[];
for filenames.iter().advance |file| {
let file_path = Path(*file);
let contents = match io::read_whole_file_str(&file_path) {
Ok(x) => x,
Err(s) => fail!(s)
};
for contents.line_iter().advance |line| {
let parts: ~[&str] = line.split_iter(' ').filter(|p| !p.is_empty()).collect();
if parts.len() != 3 {
fail!(fmt!("reftest line: '%s' doesn't match 'KIND LEFT RIGHT'", line));
} }
let kind = match parts[0] {
"==" => Same,
"!=" => Different,
_ => fail!(fmt!("reftest line: '%s' has invalid kind '%s'",
line, parts[0]))
};
let src_dir = file_path.dirname();
let file_left = src_dir + "/" + parts[1];
let file_right = src_dir + "/" + parts[2];
let reftest = Reftest {
name: parts[1] + " / " + parts[2],
kind: kind,
left: file_left,
right: file_right,
};
tests.push(make_test(reftest));
} }
} }
tests
}
fn is_comment(line: ~str) -> bool { fn make_test(reftest: Reftest) -> TestDescAndFn {
line.starts_with("<!--") let name = reftest.name.clone();
} let reftest = Cell::new(reftest);
TestDescAndFn {
return Directives { desc: TestDesc {
ignore: ignore name: DynTestName(name),
ignore: false,
should_fail: false,
},
testfn: DynTestFn(|| {
check_reftest(reftest.take());
}),
} }
} }
fn run_test(config: Config, file: ~str) { fn check_reftest(reftest: Reftest) {
let servo_image = render_servo(config, file); let options = run::ProcessOptions::new();
let ref_image = render_ref(config, file); let args = ~[~"-o", ~"/tmp/reftest-left.png", reftest.left.clone()];
let mut process = run::Process::new("./servo", args, options);
let _retval = process.finish();
// assert!(retval == 0);
assert servo_image.width == ref_image.width; let args = ~[~"-o", ~"/tmp/reftest-right.png", reftest.right.clone()];
assert servo_image.height == ref_image.height; let mut process = run::Process::new("./servo", args, options);
#debug("image depth: ref: %?, servo: %?", ref_image.depth, servo_image.depth); let _retval = process.finish();
// assert!(retval == 0);
for uint::range(0, servo_image.height) |h| { // check the pngs are bit equal
for uint::range(0, servo_image.width) |w| { let left_sha = calc_hash(&Path("/tmp/reftest-left.png"));
let i = (h * servo_image.width + w) * 4; let right_sha = calc_hash(&Path("/tmp/reftest-right.png"));
let servo_pixel = ( assert!(left_sha.is_some());
servo_image.data[i + 0], assert!(right_sha.is_some());
servo_image.data[i + 1], match reftest.kind {
servo_image.data[i + 2], Same => assert!(left_sha == right_sha),
servo_image.data[i + 3] Different => assert!(left_sha != right_sha),
); }
let ref_pixel = ( }
ref_image.data[i + 0],
ref_image.data[i + 1],
ref_image.data[i + 2],
ref_image.data[i + 3]
);
#debug("i: %?, x: %?, y: %?, ref: %?, servo: %?", i, w, h, ref_pixel, servo_pixel);
let (sr, sg, sb, sa) = servo_pixel; fn calc_hash(path: &Path) -> Option<~str> {
let (rr, rg, rb, ra) = ref_pixel; match io::file_reader(path) {
Err(*) => None,
if sr != rr Ok(reader) => {
|| sg != rg let mut sha = Sha1::new();
|| sb != rb loop {
|| sa != ra { let bytes = reader.read_bytes(4096);
fail #fmt("mismatched pixel. x: %?, y: %?, ref: %?, servo: %?", w, h, ref_pixel, servo_pixel) sha.input(bytes);
if bytes.len() < 4096 { break; }
} }
Some(sha.result_str())
} }
} }
} }
const WIDTH: uint = 800;
const HEIGHT: uint = 600;
fn render_servo(config: Config, file: ~str) -> Image {
let infile = ~"file://" + os::make_absolute(&Path(file)).to_str();
let outfilename = Path(file).filename().get().to_str() + ".png";
let outfile = Path(config.work_dir).push(outfilename).to_str();
run_pipeline_png(infile, outfile);
return sanitize_image(outfile);
}
fn render_ref(config: Config, file: ~str) -> Image {
let infile = file;
let outfilename = Path(file).filename().get().to_str() + "ref..png";
let outfile = Path(config.work_dir).push(outfilename);
// After we've generated the reference image once, we don't need
// to keep launching Firefox
if !os::path_exists(&outfile) {
let rasterize_path = rasterize_path(config);
let prog = run::start_program("python", ~[rasterize_path, infile, outfile.to_str()]);
prog.finish();
}
return sanitize_image(outfile.to_str());
}
fn sanitize_image(file: ~str) -> Image {
let buf = io::read_whole_file(&Path(file)).get();
let image = servo::image::base::load_from_memory(buf).get();
// I don't know how to precisely control the rendered height of
// the Firefox output, so it is larger than we want. Trim it down.
assert image.width == WIDTH;
assert image.height >= HEIGHT;
let data = vec::slice(image.data, 0, image.width * HEIGHT * 4);
return Image(image.width, HEIGHT, image.depth, data);
}
fn install_rasterize_py(config: Config) {
use io::WriterUtil;
let path = rasterize_path(config);
let writer = io::file_writer(&Path(path), ~[io::Create, io::Truncate]).get();
writer.write_str(rasterize_py());
}
fn rasterize_path(config: Config) -> ~str {
Path(config.work_dir).push(~"rasterize.py").to_str()
}
// This is the script that uses phantom.js to render pages
fn rasterize_py() -> ~str { #include_str("rasterize.py") }

1
src/test/ref/basic.list Normal file
View file

@ -0,0 +1 @@
== hello_a.html hello_b.html

View file

@ -0,0 +1,8 @@
<html>
<head>
<title>hello</title>
</head>
<body>
<strong>Hello!</strong>
</body>
</html>

View file

@ -0,0 +1,8 @@
<html>
<head>
<title>hello</title>
</head>
<body>
<b>Hello!</b>
</body>
</html>