[Bf-blender-cvs] SVN commit: /data/svn/bf-blender [21067] branches/soc-2009-jaguarandi: Initial code commit of test script

André Pinto andresusanopinto at gmail.com
Sun Jun 21 22:01:47 CEST 2009


Revision: 21067
          http://projects.blender.org/plugins/scmsvn/viewcvs.php?view=rev&root=bf-blender&revision=21067
Author:   jaguarandi
Date:     2009-06-21 22:01:47 +0200 (Sun, 21 Jun 2009)

Log Message:
-----------
Initial code commit of test script

created as part of:
	"Raytrace optimization" a Google Summer of Code 2009 project by Andr?\195?\169 Susano Pinto

http://wiki.blender.org/index.php/User:Jaguarandi/SummerOfCode2009

Objectives of blender-test:
 * make it easier to run render tests
 * other people can run tests and send results, which can be used to compare
 * easy to add test cases
 * easy to add builds
 * run tests must be aware of the machine it runs on
 * be flexible as possible for future usage and extension

Characteristics:
 * Coded in python (default scripting language inside blender community)
 * Directory based database for easy managing
 * allow html reports/comparisons of scenes/build/machines

Added Paths:
-----------
    branches/soc-2009-jaguarandi/test/
    branches/soc-2009-jaguarandi/test/Test.py
    branches/soc-2009-jaguarandi/test/blendertest.py
    branches/soc-2009-jaguarandi/test/config.py
    branches/soc-2009-jaguarandi/test/html.py
    branches/soc-2009-jaguarandi/test/persistent.py
    branches/soc-2009-jaguarandi/test/util.py

Added: branches/soc-2009-jaguarandi/test/Test.py
===================================================================
--- branches/soc-2009-jaguarandi/test/Test.py	                        (rev 0)
+++ branches/soc-2009-jaguarandi/test/Test.py	2009-06-21 20:01:47 UTC (rev 21067)
@@ -0,0 +1,123 @@
+import os
+import string
+import config
+import time
+import subprocess
+import persistent
+
+class Case(dict):
+	def __init__(self, arg = {}):
+		dict.__init__(self, arg)
+		
+	def run(self, res):
+		print self["name"]+":","test not implemented",self["type"]
+		return TestRun.RESULT_NONE
+		
+	def run_cmd(self, cmd, test):
+		dt = time.time()
+		proc = subprocess.Popen(cmd,stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+		out, err = proc.communicate()
+		dt = time.time()-dt
+
+		test["cmdline"] = cmd
+		test["time"] = dt
+		test["stderr"] = err
+		test["stdout"] = out
+		test["exit_status"] = proc.returncode
+		
+		return proc.returncode
+
+
+class Render(Case):
+	__name__ = "render"
+	
+	def __init__(self, arg = {}):
+		Case.__init__(self, arg)
+		
+	def run(self, test):
+		#prepare cmdline
+		filename = os.path.join(test.path, self["filename"])
+		cmd = [ test["build"]["path"], "-b", self["path"], "-o", filename, "-F", "PNG", "-x", "0", "-f", "%d"%self["frame"]]
+
+		if self.run_cmd(cmd, test) == 0:
+			test["result"] = TestRun.RESULT_OK
+		else:
+			test["result"] = TestRun.RESULT_FAIL
+
+
+################################
+class TestRun(dict):
+	RESULT_NONE = "none"
+	RESULT_OK	= "ok"
+	RESULT_FAIL	= "fail"
+
+	def __init__(self, machine, build, case):
+		self.path = os.path.join( config.run_path, machine["hash"], build["hash"], case["hash"] )
+		self.result_path = os.path.join( self.path, "result.xml" )
+		
+		if os.path.isfile(self.result_path):
+			dict.__init__(self, persistent.load(file(self.result_path, "r")))
+			if "result" not in self:
+				self["result"] = TestRun.RESULT_NONE
+		else:
+			self["machine"]	= machine
+			self["build"]	= build
+			self["case"]	= case
+			self["result"]	= TestRun.RESULT_NONE
+		
+	# Save results
+	def save_results(self):
+		dirname = os.path.dirname( self.result_path )
+		if not os.path.isdir(dirname):
+			os.makedirs(dirname)
+		persistent.dump( dict(self), file(self.result_path, "w") )
+
+
+	def getResult(self):
+		return self["result"]
+	
+	# get a path to an image generated by the test run
+	def getImageResult(self):
+		img  = os.path.join(self.path,'1.png')
+		if os.path.isfile( img ):
+			return img
+		return None
+
+
+	#Short run description:
+	# MM:SS.mmm <name> <result description>
+	def getResultDescription(self):
+		if self["result"] == TestRun.RESULT_NONE:
+			return self["case"]["name"] + " (not available)"
+		res = ""
+
+		if "time" in self:
+			time = self["time"]*1000
+			res += "%02d:%02d.%03d "%(time/60000, (time/1000)%60, time%1000)
+		res += self["case"]["name"]
+		
+		if "returncode" in self and self["returncode"] != 0:
+			res += " (non zero return)"
+		if "sdderr" in self and len(self["stderr"].splitlines()):
+			res += " (%d stderr lines)"%(len(self["stderr"].splitlines()))
+		return res
+
+	
+	# TODO should be atomic (if it crashs while saving result it can create a wrong xml file)
+	def run(self, force = False):
+		try:	
+			if self["result"] != TestRun.RESULT_NONE and force == False:
+				return
+		
+			if self["case"]["type"] == Render.__name__:
+				Render(self["case"]).run(self)
+
+			self.save_results()
+		except:
+			#TODO print exception
+			print "Failed to process test run:",self.result_path
+			os._exit(-1)
+	
+##########################################################################################
+def get(machine,build,case):
+	return TestRun(machine,build,case)

Added: branches/soc-2009-jaguarandi/test/blendertest.py
===================================================================
--- branches/soc-2009-jaguarandi/test/blendertest.py	                        (rev 0)
+++ branches/soc-2009-jaguarandi/test/blendertest.py	2009-06-21 20:01:47 UTC (rev 21067)
@@ -0,0 +1,158 @@
+#!/usr/bin/python
+"""
+File structure:
+	case/<path>.blend
+	build/<path>.bin
+	
+	run/<machine-hash>/<build_hash>/<case_hash>/result.xml
+	run/<machine-hash>/<build_hash>/<case_hash>/1.png
+"""
+import sys
+import os
+import re
+import config
+
+import Test
+import persistent
+import html
+import util
+
+OK = "\033[92m"
+WARNING = "\033[93m"
+FAIL = "\033[91m"
+INFO = "\033[95m"
+ENDC = "\033[0m"
+
+### Build ###
+_valid_build = re.compile(r'.*\.bin$')
+def is_valid_build(arg):
+	return _valid_build.match( arg )
+	
+def load_build(path):
+	#TODO test the existence of a .xml file with information about this build
+	conf = dict(config.default_build_config)
+
+	conf["path"] = path
+	conf["name"] = os.path.splitext(os.path.basename(path))[0]
+	conf["hash"] = util.get_hash(open(path, 'rb'))
+		
+	return conf
+
+### Test Case ###	
+_valid_scene = re.compile(r'.*\.blend$')
+def is_valid_case(arg):
+	return _valid_scene.match( arg )
+
+def load_case(path):
+	#TODO test the existence of a .xml file with diferent configuration for this test case
+	conf = dict(config.default_case_config)
+	
+	conf["path"] = path
+	conf["name"] = os.path.splitext(os.path.basename(path))[0]
+	conf["hash"] = util.get_hash(open(path, 'rb'))
+		
+	return conf
+
+
+
+#############################################
+result_color = { "ok" : OK, "fail": FAIL, "none" : FAIL }
+
+def get_color(trun):
+	if "result" in trun and trun["result"] in result_color:
+		return result_color[trun["result"]]
+	return WARNING
+
+def update_test_runs( builds, cases ):
+	tot = len(cases)
+	for build in builds:
+		i = 1
+		print INFO+"=== "+build["path"]+" ==="+ENDC
+		for case in cases:
+			res = Test.get( config.machine, build, case )
+			res.run()
+			print get_color(res)+"[%2d/%d] %s" % (i,tot,res.getResultDescription())+ENDC
+			i += 1
+				
+				
+#############################################
+def gen_html( builds, cases ):
+	return html.generate( [config.machine], builds, cases)
+	
+	
+#############################################
+def do_help():
+	print """Usage:
+blender_test.py [action] [<paths for test cases or builds>]
+
+actions:
+	--update	runs tests cases between the given builds and cases
+				test results are saved under:
+					<config.run_path>/<machine-hash>/<build-hash>/<case-hash>/
+
+	--html		outputs html comparison table to standart output
+				test results are loaded from <config.run_path>
+				auxiliary html files are created under <config.html_path>
+	
+	if no action is given it will just print information on the cases
+	and builds found on the given paths
+
+paths:
+	All given paths will be recursively searched for files matching:
+		*.blend to represent test scenes
+		*.bin to represent builds
+	
+	if no test case is given it will scan directory "case"
+	if no build is given it will scan directory "build"
+"""
+
+
+builds = []
+cases = []
+
+def load_path(arg):
+	if os.path.isdir(arg):
+		for root, dirs, files in os.walk(arg):
+			 for file in files:
+			 	load_path(os.path.join(root, file))
+	else:
+		if is_valid_case(arg):
+			cases.append( load_case(arg) )		
+		elif is_valid_build(arg):
+			builds.append( load_build(arg) )
+
+
+do_update = False
+do_html = False
+do_info = True
+
+if len(sys.argv) > 1:
+
+	for arg in sys.argv[1:]:
+		if arg == "--help":
+			do_help()
+			os._exit(-1)			
+		if arg == "--update":
+			do_update = True
+		if arg == "--html":
+			do_info = False
+			do_html = True
+		else:
+			load_path(arg)
+			
+if len(builds) == 0:
+	load_path( "build" )
+
+if len(cases) == 0:
+	load_path( "case" )
+
+if do_info:
+	print INFO," Machine   : ",config.machine,ENDC
+	print INFO," test-cases: ",len(cases),ENDC
+	print INFO," builds    : ",len(builds),ENDC
+
+if do_update:
+	update_test_runs( builds, cases )
+
+if do_html:
+	gen_html( builds, cases )		


Property changes on: branches/soc-2009-jaguarandi/test/blendertest.py
___________________________________________________________________
Name: svn:executable
   + *

Added: branches/soc-2009-jaguarandi/test/config.py
===================================================================
--- branches/soc-2009-jaguarandi/test/config.py	                        (rev 0)
+++ branches/soc-2009-jaguarandi/test/config.py	2009-06-21 20:01:47 UTC (rev 21067)
@@ -0,0 +1,21 @@
+import os
+
+run_path = "run"
+html_path = "html"
+
+default_case_config	= {
+		"type":			"Render",
+		"filename":		"#.png",
+		"frame":		1,
+	}
+
+default_build_config = { }
+
+machine = {
+		"hostname":		os.uname()[1],
+#		"cpu":			"Intel(R) Pentium(R) 4 CPU 2.80GHz",
+		"hash":			os.uname()[1]
+	}
+
+print "config.py: please configure your machine description and comment this line!"
+

Added: branches/soc-2009-jaguarandi/test/html.py
===================================================================
--- branches/soc-2009-jaguarandi/test/html.py	                        (rev 0)
+++ branches/soc-2009-jaguarandi/test/html.py	2009-06-21 20:01:47 UTC (rev 21067)
@@ -0,0 +1,168 @@
+import os
+
+import config
+import Test
+import util
+
+	
+def dict2html(stream, dic):
+	print >>stream,'<dl>'
+	for key in dic:
+		print >>stream,'<dt>',key,'</dt><dd>'
+		if type(dic[key]) == type({}):
+			dict2html(stream,dic[key])
+		else:
+			print >>stream,dic[key]
+		print >>stream,'</dd>'
+	print >>stream,'</dl>'
+		
+
+def generate( machines, builds, cases ):
+	html_root = config.html_path
+
+	print """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+	<head>
+		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+		<title>Blender test compare</title>
+		<link href="style.css" rel="stylesheet" type="text/css" />
+	</head>
+	<body>
+"""
+	valid_configs = []
+	for machine in machines:
+		for build in builds:
+			for case in cases:
+				if Test.get(machine,build,case):
+					valid_configs.append( [machine,build] )
+					break
+					
+	def make_detail_page(run):
+		if run == None:
+			return
+		
+		path = os.path.join( config.html_path, run["machine"]["hash"], run["build"]["hash"], run["case"]["hash"] )
+		if not os.path.isdir(path):
+			os.makedirs( path )
+		stream = open( os.path.join(path, "index.html"), "w" )
+		print >>stream, """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+	<head>
+		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

@@ Diff output truncated at 10240 characters. @@




More information about the Bf-blender-cvs mailing list