diff --git a/setup/test/lint.php b/setup/test/lint.php
deleted file mode 100644
index 4887856182e23d3acfed19168a92dbc744f4e9e8..0000000000000000000000000000000000000000
--- a/setup/test/lint.php
+++ /dev/null
@@ -1,140 +0,0 @@
-#!/usr/bin/env php
-<?php
-if (php_sapi_name() != 'cli') exit();
-
-function get_osticket_root_path() {
-    # Hop up to the root folder
-    $start = dirname(__file__);
-    for (;;) {
-        if (file_exists($start . '/main.inc.php')) break;
-        $start .= '/..';
-    }
-    return realpath($start);
-}
-
-$root = get_osticket_root_path();
-
-# Check PHP syntax across all php files
-function glob_recursive($pattern, $flags = 0) {
-    $files = glob($pattern, $flags);
-    foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
-        $files = array_merge($files, 
-            glob_recursive($dir.'/'.basename($pattern), $flags));
-    }
-    return $files;
-}
-echo "PHP Syntax Errors: ";
-ob_start();
-$scripts=glob_recursive("$root/*.php");
-$exit=0;
-$syntax_errors="";
-foreach ($scripts as $s) {
-    system("php -l $s", $exit);
-    $line = ob_get_contents();
-    ob_clean();
-    if ($exit !== 0)
-        $syntax_errors .= $line;
-}
-ob_end_clean();
-
-if (strlen($syntax_errors)) {
-    $syntax_errors=str_replace("$root/", '', $syntax_errors);
-    echo "FAIL\n";
-    echo "-------------------------------------------------------\n";
-    echo "$syntax_errors";
-    exit();
-} else {
-    echo "pass\n";
-}
-
-function line_number_for_offset($filename, $offset) {
-    $lines = file($filename);
-    $bytes = $line = 0;
-    while ($bytes < $offset) {
-        $bytes += strlen(array_shift($lines));
-        $line += 1;
-    }
-    return $line;
-}
-echo "Short open tags: ";
-$fails = array();
-foreach ($scripts as $s) {
-    $matches = array();
-    if (preg_match_all('/<\?\s*(?!php|xml).*$/m', file_get_contents($s), $matches,
-            PREG_OFFSET_CAPTURE) > 0) {
-        foreach ($matches[0] as $match)
-            $fails[] = array(
-                str_replace($root.'/', '', $s),
-                $match[0],
-                line_number_for_offset($s, $match[1]));
-    }
-}
-if (count($fails)) {
-    echo "FAIL\n";
-    echo "-------------------------------------------------------\n";
-    foreach ($fails as $f)
-        echo sprintf("In %s, line %d: %s\n", $f[0], $f[2],
-            str_replace("\n", " ", $f[1]));
-    echo "\n";
-} else {
-    echo "pass\n";
-}
-
-# Run phplint across all php files
-echo "Access to unitialized variables: ";
-ob_start();
-# XXX: This won't run well on Windoze
-system("$root/setup/test/lib/phplint.tcl ".implode(" ", $scripts));
-$lint_errors = ob_get_clean();
-
-if (strlen($lint_errors)) {
-    $lint_errors=str_replace("$root/", '', $lint_errors);
-    echo "FAIL\n";
-    echo "-------------------------------------------------------\n";
-    echo "$lint_errors";
-} else {
-    echo "\n";
-}
-
-function find_function_calls($scripts) {
-    $calls=array();
-    foreach ($scripts as $s) {
-        $lines = explode("\n", file_get_contents($s));
-        $lineno=0;
-        foreach (explode("\n", file_get_contents($s)) as $line) {
-            $lineno++; $matches=array();
-            preg_match_all('/-[>]([a-zA-Z0-9]*)\(/', $line, $matches,
-                PREG_SET_ORDER);
-            foreach ($matches as $m) {
-                $calls[] = array($s, $lineno, $line, $m[1]);
-            }
-        }
-    }
-    return $calls;
-}
-
-$php_script_content='';
-foreach ($scripts as $s) {
-    $php_script_content .= file_get_contents($s);
-}
-echo "Access to undefined object methods: ";
-ob_start();
-foreach (find_function_calls($scripts) as $call) {
-    list($file, $no, $line, $func) = $call;
-    if (!preg_match('/^\s*(\/\*[^*]*\*\/)?'."\s*function\s+&?\s*$func\\(/m", 
-            $php_script_content)) {
-        print "$func: Definitely undefined, from $file:$no\n";
-    }
-}
-$undef_func_errors = ob_get_clean();
-
-if (strlen($undef_func_errors)) {
-    $undef_func_errors=str_replace("$root/", '', $undef_func_errors);
-    echo "FAIL\n";
-    echo "-------------------------------------------------------\n";
-    echo "$undef_func_errors";
-    exit();
-} else {
-    echo "\n";
-}
-?>
diff --git a/setup/test/run-tests.php b/setup/test/run-tests.php
new file mode 100644
index 0000000000000000000000000000000000000000..45f8859a3be5f72d6853d862575ff1333966495b
--- /dev/null
+++ b/setup/test/run-tests.php
@@ -0,0 +1,52 @@
+#!/usr/bin/env php
+<?php
+if (php_sapi_name() != 'cli') exit();
+
+require_once "tests/class.test.php";
+
+function get_osticket_root_path() {
+    # Hop up to the root folder
+    $start = dirname(__file__);
+    for (;;) {
+        if (file_exists($start . '/main.inc.php')) break;
+        $start .= '/..';
+    }
+    return realpath($start);
+}
+$root = get_osticket_root_path();
+
+# Check PHP syntax across all php files
+function glob_recursive($pattern, $flags = 0) {
+    $files = glob($pattern, $flags);
+    foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
+        $files = array_merge($files, 
+            glob_recursive($dir.'/'.basename($pattern), $flags));
+    }
+    return $files;
+}
+
+$fails = array();
+foreach (glob_recursive(dirname(__file__)."/tests/test.*.php") as $t) {
+    if (strpos($t,"class.") !== false)
+        continue;
+    $class = (include $t);
+    if (!is_string($class))
+        continue;
+    $test = new $class();
+    echo "Running: " . $test->name . "\n";
+    $test->run();
+    $fails = array_merge($fails, $test->fails);
+    echo " ok\n";
+}
+
+if ($fails) {
+    echo count($fails) . " FAIL(s)\n";
+    echo "-------------------------------------------------------\n";
+    foreach ($fails as $f) {
+        list($test, $script, $line, $message) = $f;
+        $script = str_replace($root.'/', '', $script);
+        print("$test: $message @ $script:$line\n");
+    }
+}
+
+?>
diff --git a/setup/test/tests/class.test.php b/setup/test/tests/class.test.php
new file mode 100644
index 0000000000000000000000000000000000000000..f9cfeb30911d4f688fef4edfab00a29cd54fd48b
--- /dev/null
+++ b/setup/test/tests/class.test.php
@@ -0,0 +1,66 @@
+<?php
+
+class Test {
+    var $fails = array();
+    var $name = "";
+
+    var $third_party_paths = array(
+        '/include/JSON.php',
+        '/include/htmLawed.php',
+        '/include/PasswordHash.php',
+        '/include/pear/',
+    );
+
+    function Test() {
+        call_user_func_array(array($this, '__construct'), func_get_args());
+    }
+    function __construct() {
+        assert_options(ASSERT_CALLBACK, array($this, 'fail'));
+        error_reporting(E_ALL & ~E_WARNING);
+    }
+
+    function setup() {
+    }
+
+    function teardown() {
+    }
+
+    /*static*/ function getAllScripts() {
+        $root = get_osticket_root_path();
+        $scripts = array();
+        foreach (glob_recursive("$root/*.php") as $s) {
+            $found = false;
+            foreach ($this->third_party_paths as $p) {
+                if (strpos($s, $p) !== false) {
+                    $found = true;
+                    break;
+                }
+            }
+            if (!$found)
+                $scripts[] = $s;
+        }
+        return $scripts;
+    }
+
+    function fail($script, $line, $message) {
+        $this->fails[] = array(get_class($this), $script, $line, $message);
+        fputs(STDOUT, 'F');
+    }
+
+    function pass() {
+        fputs(STDOUT, ".");
+    }
+
+    function run() {
+        $rc = new ReflectionClass(get_class($this));
+        foreach ($rc->getMethods() as $m) {
+            if (stripos($m->name, 'test') === 0) {
+                $this->setup();
+                call_user_func(array($this, $m->name));
+                $this->teardown();
+            }
+        }
+    }
+
+}
+?>
diff --git a/setup/test/lib/phplint.tcl b/setup/test/tests/lib/phplint.tcl
similarity index 100%
rename from setup/test/lib/phplint.tcl
rename to setup/test/tests/lib/phplint.tcl
diff --git a/setup/test/tests/test.shortopentags.php b/setup/test/tests/test.shortopentags.php
new file mode 100644
index 0000000000000000000000000000000000000000..aab8e0156a388ac93f9d7bede23adb8da8c392bb
--- /dev/null
+++ b/setup/test/tests/test.shortopentags.php
@@ -0,0 +1,35 @@
+<?php
+require_once "class.test.php";
+
+class ShortOpenTag extends Test {
+    var $name = "PHP Short Open Checks";
+
+    function testFindShortOpens() {
+        foreach ($this->getAllScripts() as $s) {
+            $matches = array();
+            if (preg_match_all('/<\?\s*(?!php|xml).*$/m',
+                    file_get_contents($s), $matches,
+                    PREG_OFFSET_CAPTURE) > 0) {
+                foreach ($matches[0] as $match)
+                    $this->fail(
+                        $s,
+                        line_number_for_offset($s, $match[1]),
+                        $match[0]);
+            }
+            else $this->pass();
+        }
+    }
+}
+
+function line_number_for_offset($filename, $offset) {
+    $lines = file($filename);
+    $bytes = $line = 0;
+    while ($bytes < $offset) {
+        $bytes += strlen(array_shift($lines));
+        $line += 1;
+    }
+    return $line;
+}
+
+return 'ShortOpenTag';
+?>
diff --git a/setup/test/tests/test.syntax.php b/setup/test/tests/test.syntax.php
new file mode 100644
index 0000000000000000000000000000000000000000..774ed964e466a20ce63b352fe7792d68769513d1
--- /dev/null
+++ b/setup/test/tests/test.syntax.php
@@ -0,0 +1,23 @@
+<?php
+require_once "class.test.php";
+
+class SyntaxTest extends Test {
+    var $name = "PHP Syntax Checks";
+
+    function testCompileErrors() {
+        $exit = 0;
+        foreach ($this->getAllScripts() as $s) {
+            ob_start();
+            system("php -l $s", $exit);
+            $line = ob_get_contents();
+            ob_end_clean();
+            if ($exit != 0)
+                $this->fail($s, 0, $line);
+            else
+                $this->pass();
+        }
+    }
+}
+
+return 'SyntaxTest';
+?>
diff --git a/setup/test/tests/test.undefinedmethods.php b/setup/test/tests/test.undefinedmethods.php
new file mode 100644
index 0000000000000000000000000000000000000000..7062ec256b9b062b7858c1b94c0e50e82a3c5f0f
--- /dev/null
+++ b/setup/test/tests/test.undefinedmethods.php
@@ -0,0 +1,42 @@
+<?php
+require_once "class.test.php";
+
+class UndefinedMethods extends Test {
+    var $name = "Access to undefined object methods";
+
+    function testFindShortOpen() {
+        $scripts = $this->getAllScripts();
+        $php_script_content='';
+        foreach ($scripts as $s) {
+            $php_script_content .= file_get_contents($s);
+        }
+        foreach (find_function_calls($scripts) as $call) {
+            list($file, $no, $line, $func) = $call;
+            if (!preg_match('/^\s*(\/\*[^*]*\*\/)?'."\s*function\s+&?\s*$func\\(/m", 
+                    $php_script_content))
+                $this->fail($file, $no, "$func: Definitely undefined");
+            else
+                $this->pass();
+        }
+    }
+}
+
+function find_function_calls($scripts) {
+    $calls=array();
+    foreach ($scripts as $s) {
+        $lines = explode("\n", file_get_contents($s));
+        $lineno=0;
+        foreach ($lines as $line) {
+            $lineno++; $matches=array();
+            preg_match_all('/-[>]([a-zA-Z0-9]*)\(/', $line, $matches,
+                PREG_SET_ORDER);
+            foreach ($matches as $m) {
+                $calls[] = array($s, $lineno, $line, $m[1]);
+            }
+        }
+    }
+    return $calls;
+}
+
+return 'UndefinedMethods';
+?>
diff --git a/setup/test/tests/test.unitialized.php b/setup/test/tests/test.unitialized.php
new file mode 100644
index 0000000000000000000000000000000000000000..09bd0509e155072ccb6044429248965c2e318b36
--- /dev/null
+++ b/setup/test/tests/test.unitialized.php
@@ -0,0 +1,25 @@
+<?php
+require_once "class.test.php";
+
+class UnitializedVars extends Test {
+    var $name = "Access to unitialized variables";
+
+    function testUnitializedUsage() {
+        $scripts = $this->getAllScripts();
+        $matches = array();
+        foreach (range(0, count($scripts), 40) as $start) {
+            $slice = array_slice($scripts, $start, 40);
+            ob_start();
+            # XXX: This won't run well on Windoze
+            system(dirname(__file__)."/lib/phplint.tcl ".implode(" ", $slice));
+            $lint_errors = ob_get_clean();
+            preg_match_all("/\* In (.*) line (\d+): access to uninitialized var '([^']+)'/m",
+                    $lint_errors, $matches, PREG_SET_ORDER);
+            foreach ($matches as $match)
+                $this->fail($match[1], $match[2], "'\${$match[3]}'");
+        }
+    }
+}
+
+return 'UnitializedVars';
+?>