Browse Source

Add test coverage for Mustache.php 2.0

Update legacy tests for new codebase.
Justin Hileman 13 năm trước cách đây
mục cha
commit
0034bf657b

+ 90 - 0
test/Mustache/Test/BufferTest.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace Mustache\Test;
+
+use Mustache\Mustache;
+use Mustache\Buffer;
+
+/**
+ * @group unit
+ */
+class BufferTest extends \PHPUnit_Framework_TestCase {
+
+	/**
+	 * @dataProvider getConstructorArgs
+	 */
+	public function testConstructor($indent, $charset) {
+		$buffer = new Buffer($indent, $charset);
+		$this->assertEquals($indent, $buffer->getIndent());
+		$this->assertEquals($charset, $buffer->getCharset());
+	}
+
+	public function getConstructorArgs() {
+		return array(
+			array('',       'UTF-8'),
+			array('    ',   'ISO-8859-1'),
+			array("\t\t\t", 'Shift_JIS'),
+		);
+	}
+
+	public function testWrite() {
+		$buffer = new Buffer;
+		$this->assertEquals('', $buffer->flush());
+
+		$buffer->writeLine();
+		$buffer->writeLine();
+		$buffer->writeLine();
+		$this->assertEquals("\n\n\n", $buffer->flush());
+		$this->assertEquals('', $buffer->flush());
+
+		$buffer->write('foo');
+		$buffer->write('bar');
+		$buffer->writeLine();
+		$buffer->write('baz');
+		$this->assertEquals("foobar\nbaz", $buffer->flush());
+		$this->assertEquals('', $buffer->flush());
+
+		$indent = "\t\t";
+		$buffer->setIndent($indent);
+		$buffer->writeText('foo');
+		$buffer->writeLine();
+		$buffer->writeText('bar');
+		$this->assertEquals("\t\tfoo\n\t\tbar", $buffer->flush());
+		$this->assertEquals('', $buffer->flush());
+	}
+
+	/**
+	 * @dataProvider getEscapeAndIndent
+	 */
+	public function testEscapingAndIndenting($text, $escape, $indent, $whitespace, $expected) {
+		$buffer = new Buffer;
+		$buffer->setIndent($whitespace);
+
+		$buffer->write($text, $indent, $escape);
+		$this->assertEquals($expected, $buffer->flush());
+	}
+
+	public function getEscapeAndIndent() {
+		return array(
+			array('> "fun & games" <', false, false, "\t", '> "fun & games" <'),
+			array('> "fun & games" <', true,  false, "\t", '&gt; &quot;fun &amp; games&quot; &lt;'),
+			array('> "fun & games" <', true,  true,  "\t", "\t&gt; &quot;fun &amp; games&quot; &lt;"),
+			array('> "fun & games" <', false, true,  "\t", "\t> \"fun & games\" <"),
+		);
+	}
+
+	public function testChangeIndent() {
+		$indent = "\t\t";
+		$buffer = new Buffer($indent);
+		$this->assertEquals($indent, $buffer->getIndent());
+
+		$indent = "";
+		$buffer->setIndent($indent);
+		$this->assertEquals($indent, $buffer->getIndent());
+
+		$indent = " ";
+		$buffer->setIndent($indent);
+		$this->assertEquals($indent, $buffer->getIndent());
+	}
+
+}

+ 75 - 0
test/Mustache/Test/CompilerTest.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace Mustache\Test;
+
+use Mustache\Compiler;
+use Mustache\Tokenizer;
+
+/**
+ * @group unit
+ */
+class CompilerTest extends \PHPUnit_Framework_TestCase {
+
+	/**
+	 * @dataProvider getCompileValues
+	 */
+	public function testCompile($source, array $tree, $name, $expected) {
+		$compiler = new Compiler;
+
+		$compiled = $compiler->compile($source, $tree, $name);
+		foreach ($expected as $contains) {
+			$this->assertContains($contains, $compiled);
+		}
+	}
+
+	public function getCompileValues() {
+		return array(
+			array('', array(), 'Banana', array(
+				"\nclass Banana extends \Mustache\Template",
+				'$buffer->flush();',
+			)),
+
+			array('', array('TEXT'), 'Monkey', array(
+				"\nclass Monkey extends \Mustache\Template",
+				'$buffer->writeText(\'TEXT\');',
+				'$buffer->flush();',
+			)),
+
+			array(
+				'',
+				array(
+					'foo',
+					"\n",
+					array(
+						Tokenizer::TAG  => '_v',
+						Tokenizer::NAME => 'name',
+					),
+					array(
+						Tokenizer::TAG  => '_v',
+						Tokenizer::NAME => '.',
+					),
+					"'bar'",
+				),
+				'Monkey',
+				array(
+					"\nclass Monkey extends \Mustache\Template",
+					'$buffer->writeText(\'foo\');',
+					'$buffer->writeLine();',
+					'$value = $context->find(\'name\');',
+					'$buffer->writeText($value, true);',
+					'$value = $context->last();',
+					'$buffer->writeText(\'\\\'bar\\\'\');',
+					'$buffer->flush();',
+				)
+			),
+		);
+	}
+
+	/**
+	 * @expectedException InvalidArgumentException
+	 */
+	public function testCompilerThrowsUnknownNodeTypeException() {
+		$compiler = new Compiler;
+		$compiler->compile('', array(array(Tokenizer::TAG => 'invalid')), 'SomeClass');
+	}
+}

+ 150 - 0
test/Mustache/Test/ContextTest.php

@@ -0,0 +1,150 @@
+<?php
+
+namespace Mustache\Test;
+
+use Mustache\Context;
+
+/**
+ * @group unit
+ */
+class ContextTest extends \PHPUnit_Framework_TestCase {
+	public function testConstructor() {
+		$one = new Context;
+		$this->assertSame('', $one->find('foo'));
+		$this->assertSame('', $one->find('bar'));
+
+		$two = new Context(array(
+			'foo' => 'FOO',
+			'bar' => '<BAR>'
+		));
+		$this->assertEquals('FOO', $two->find('foo'));
+		$this->assertEquals('<BAR>', $two->find('bar'));
+
+		$obj = new \StdClass;
+		$obj->name = 'NAME';
+		$three = new Context($obj);
+		$this->assertSame($obj, $three->last());
+		$this->assertEquals('NAME', $three->find('name'));
+	}
+
+	public function testIsTruthy() {
+		$context = new Context;
+
+		$this->assertTrue($context->isTruthy('string'));
+		$this->assertTrue($context->isTruthy(new \StdClass));
+		$this->assertTrue($context->isTruthy(1));
+		$this->assertTrue($context->isTruthy(array('a', 'b')));
+
+		$this->assertFalse($context->isTruthy(null));
+		$this->assertFalse($context->isTruthy(''));
+		$this->assertFalse($context->isTruthy(0));
+		$this->assertFalse($context->isTruthy(array()));
+	}
+
+	public function testIsCallable() {
+		$dummy   = new TestDummy;
+		$context = new Context;
+
+		$this->assertTrue($context->isCallable(function() { return null; }));
+		$this->assertTrue($context->isCallable(array('\Mustache\Test\TestDummy', 'foo')));
+		$this->assertTrue($context->isCallable(array($dummy, 'bar')));
+		$this->assertTrue($context->isCallable($dummy));
+
+		$this->assertFalse($context->isCallable('count'));
+		$this->assertFalse($context->isCallable('TestDummy::foo'));
+		$this->assertFalse($context->isCallable(array('\Mustache\Test\TestDummy', 'name')));
+		$this->assertFalse($context->isCallable(array('NotReallyAClass', 'foo')));
+		$this->assertFalse($context->isCallable(array($dummy, 'name')));
+	}
+
+	/**
+	 * @dataProvider getIterables
+	 */
+	public function testIsIterable($value, $iterable) {
+		$context = new Context;
+		$this->assertEquals($iterable, $context->isIterable($value));
+	}
+
+	public function getIterables() {
+		return array(
+			array(array(0 => 'a', 1 => 'b'), true),
+			array(array(0 => 'a', 2 => 'b'), false),
+			array(array(1 => 'a', 2 => 'b'), false),
+			array(array('a' => 0, 'b' => 1), false),
+			array('some string',             false),
+			array(new \ArrayIterator,        true),
+		);
+	}
+
+	public function testPushPopAndLast() {
+		$context = new Context;
+		$this->assertFalse($context->last());
+
+		$dummy = new TestDummy;
+		$context->push($dummy);
+		$this->assertSame($dummy, $context->last());
+		$this->assertSame($dummy, $context->pop());
+		$this->assertFalse($context->last());
+
+		$obj = new \StdClass;
+		$context->push($dummy);
+		$this->assertSame($dummy, $context->last());
+		$context->push($obj);
+		$this->assertSame($obj, $context->last());
+		$this->assertSame($obj, $context->pop());
+		$this->assertSame($dummy, $context->pop());
+		$this->assertFalse($context->last());
+	}
+
+	public function testFind() {
+		$context = new Context;
+
+		$dummy = new TestDummy;
+
+		$obj = new \StdClass;
+		$obj->name = 'obj';
+
+		$arr = array(
+			'a' => array('b' => array('c' => 'see')),
+			'b' => 'bee',
+		);
+
+		$string = 'some arbitrary string';
+
+		$context->push($dummy);
+		$this->assertEquals('dummy', $context->find('name'));
+
+		$context->push($obj);
+		$this->assertEquals('obj', $context->find('name'));
+
+		$context->pop();
+		$this->assertEquals('dummy', $context->find('name'));
+
+		$dummy->name = 'dummyer';
+		$this->assertEquals('dummyer', $context->find('name'));
+
+		$context->push($arr);
+		$this->assertEquals('bee', $context->find('b'));
+		$this->assertEquals('see', $context->findDot('a.b.c'));
+
+		$dummy->name = 'dummy';
+
+		$context->push($string);
+		$this->assertSame($string, $context->last());
+		$this->assertEquals('dummy', $context->find('name'));
+		$this->assertEquals('see', $context->findDot('a.b.c'));
+		$this->assertEquals('<foo>', $context->find('foo'));
+		$this->assertEquals('<bar>', $context->findDot('bar'));
+	}
+}
+
+class TestDummy {
+	public $name = 'dummy';
+	public function __invoke() {}
+	public static function foo() {
+		return '<foo>';
+	}
+	public function bar() {
+		return '<bar>';
+	}
+}

+ 12 - 5
test/Mustache/Test/Functional/CallTest.php

@@ -1,18 +1,25 @@
 <?php
 
-require_once '../Mustache.php';
+namespace Mustache\Test\Functional;
 
-class MustacheCallTest extends PHPUnit_Framework_TestCase {
+use Mustache\Mustache;
+
+/**
+ * @group magic_methods
+ * @group functional
+ */
+class CallTest extends \PHPUnit_Framework_TestCase {
 
 	public function testCallEatsContext() {
+		$m = new Mustache;
+		$tpl = $m->loadTemplate('{{# foo }}{{ label }}: {{ name }}{{/ foo }}');
+
 		$foo = new ClassWithCall();
 		$foo->name = 'Bob';
 
-		$template = '{{# foo }}{{ label }}: {{ name }}{{/ foo }}';
 		$data = array('label' => 'name', 'foo' => $foo);
-		$m = new Mustache($template, $data);
 
-		$this->assertEquals('name: Bob', $m->render());
+		$this->assertEquals('name: Bob', $tpl->render($data));
 	}
 }
 

+ 4 - 1
test/Mustache/Test/Functional/ExamplesTest.php

@@ -4,12 +4,15 @@ namespace Mustache\Test\Functional;
 
 use Mustache\Mustache;
 
+/**
+ * @group examples
+ * @group functional
+ */
 class ExamplesTest extends \PHPUnit_Framework_TestCase {
 
 	/**
 	 * Test everything in the `examples` directory.
 	 *
-	 * @group examples
 	 * @dataProvider getExamples
 	 *
 	 * @param string $context

+ 58 - 37
test/Mustache/Test/Functional/HigherOrderSectionsTest.php

@@ -1,93 +1,115 @@
 <?php
 
-require_once '../Mustache.php';
+namespace Mustache\Test\Functional;
 
-class MustacheHigherOrderSectionsTest extends PHPUnit_Framework_TestCase {
+use Mustache\Mustache;
+
+/**
+ * @group lambdas
+ * @group functional
+ */
+class HigherOrderSectionsTest extends \PHPUnit_Framework_TestCase {
+
+	private $mustache;
 
 	public function setUp() {
-		$this->foo = new Foo();
+		$this->mustache = new Mustache;
 	}
 
 	public function testAnonymousFunctionSectionCallback() {
-		if (version_compare(PHP_VERSION, '5.3.0', '<')) {
-			$this->markTestSkipped('Unable to test anonymous function section callbacks in PHP < 5.3');
-			return;
-		}
+		$tpl = $this->mustache->loadTemplate('{{#wrapper}}{{name}}{{/wrapper}}');
 
-		$this->foo->wrapper = function($text) {
+		$foo = new Foo;
+		$foo->name = 'Mario';
+		$foo->wrapper = function($text) {
 			return sprintf('<div class="anonymous">%s</div>', $text);
 		};
 
-		$this->assertEquals(
-			sprintf('<div class="anonymous">%s</div>', $this->foo->name),
-			$this->foo->render('{{#wrapper}}{{name}}{{/wrapper}}')
-		);
+		$this->assertEquals(sprintf('<div class="anonymous">%s</div>', $foo->name), $tpl->render($foo));
 	}
 
 	public function testSectionCallback() {
-		$this->assertEquals(sprintf('%s', $this->foo->name), $this->foo->render('{{name}}'));
-		$this->assertEquals(sprintf('<em>%s</em>', $this->foo->name), $this->foo->render('{{#wrap}}{{name}}{{/wrap}}'));
+		$one = $this->mustache->loadTemplate('{{name}}');
+		$two = $this->mustache->loadTemplate('{{#wrap}}{{name}}{{/wrap}}');
+
+		$foo = new Foo;
+		$foo->name = 'Luigi';
+
+		$this->assertEquals($foo->name, $one->render($foo));
+		$this->assertEquals(sprintf('<em>%s</em>', $foo->name), $two->render($foo));
 	}
 
 	public function testRuntimeSectionCallback() {
-		$this->foo->double_wrap = array($this->foo, 'wrapWithBoth');
-		$this->assertEquals(
-			sprintf('<strong><em>%s</em></strong>', $this->foo->name),
-			$this->foo->render('{{#double_wrap}}{{name}}{{/double_wrap}}')
-		);
+		$tpl = $this->mustache->loadTemplate('{{#double_wrap}}{{name}}{{/double_wrap}}');
+
+		$foo = new Foo;
+		$foo->double_wrap = array($foo, 'wrapWithBoth');
+
+		$this->assertEquals(sprintf('<strong><em>%s</em></strong>', $foo->name), $tpl->render($foo));
 	}
 
 	public function testStaticSectionCallback() {
-		$this->foo->trimmer = array(get_class($this->foo), 'staticTrim');
-		$this->assertEquals($this->foo->name, $this->foo->render('{{#trimmer}}    {{name}}    {{/trimmer}}'));
+		$tpl = $this->mustache->loadTemplate('{{#trimmer}}    {{name}}    {{/trimmer}}');
+
+		$foo = new Foo;
+		$foo->trimmer = array(get_class($foo), 'staticTrim');
+
+		$this->assertEquals($foo->name, $tpl->render($foo));
 	}
 
 	public function testViewArraySectionCallback() {
+		$tpl = $this->mustache->loadTemplate('{{#trim}}    {{name}}    {{/trim}}');
+
+		$foo = new Foo;
+
 		$data = array(
 			'name' => 'Bob',
-			'trim' => array(get_class($this->foo), 'staticTrim'),
+			'trim' => array(get_class($foo), 'staticTrim'),
 		);
-		$this->assertEquals($data['name'], $this->foo->render('{{#trim}}    {{name}}    {{/trim}}', $data));
+
+		$this->assertEquals($data['name'], $tpl->render($data));
 	}
 
 	public function testViewArrayAnonymousSectionCallback() {
-		if (version_compare(PHP_VERSION, '5.3.0', '<')) {
-			$this->markTestSkipped('Unable to test anonymous function section callbacks in PHP < 5.3');
-			return;
-		}
+		$tpl = $this->mustache->loadTemplate('{{#wrap}}{{name}}{{/wrap}}');
+
 		$data = array(
 			'name' => 'Bob',
 			'wrap' => function($text) {
 				return sprintf('[[%s]]', $text);
 			}
 		);
+
 		$this->assertEquals(
 			sprintf('[[%s]]', $data['name']),
-			$this->foo->render('{{#wrap}}{{name}}{{/wrap}}', $data)
+			$tpl->render($data)
 		);
 	}
 
 	public function testMonsters() {
+		$tpl = $this->mustache->loadTemplate('{{#title}}{{title}} {{/title}}{{name}}');
+
 		$frank = new Monster();
 		$frank->title = 'Dr.';
 		$frank->name  = 'Frankenstein';
-		$this->assertEquals('Dr. Frankenstein', $frank->render());
+		$this->assertEquals('Dr. Frankenstein', $tpl->render($frank));
 
 		$dracula = new Monster();
 		$dracula->title = 'Count';
 		$dracula->name  = 'Dracula';
-		$this->assertEquals('Count Dracula', $dracula->render());
+		$this->assertEquals('Count Dracula', $tpl->render($dracula));
 	}
 }
 
-class Foo extends Mustache {
+class Foo {
 	public $name = 'Justin';
 	public $lorem = 'Lorem ipsum dolor sit amet,';
 	public $wrap;
 
-	public function __construct($template = null, $view = null, $partials = null) {
-		$this->wrap = array($this, 'wrapWithEm');
-		parent::__construct($template, $view, $partials);
+	public function __construct() {
+		$this->wrap = function($text) {
+			return sprintf('<em>%s</em>', $text);
+		};
 	}
 
 	public function wrapWithEm($text) {
@@ -107,8 +129,7 @@ class Foo extends Mustache {
 	}
 }
 
-class Monster extends Mustache {
-	public $_template = '{{#title}}{{title}} {{/title}}{{name}}';
+class Monster {
 	public $title;
 	public $name;
-}
+}

+ 121 - 121
test/Mustache/Test/Functional/MustacheInjectionTest.php

@@ -1,127 +1,127 @@
 <?php
 
-require_once '../Mustache.php';
+namespace Mustache\Test\Functional;
+
+use Mustache\Mustache;
 
 /**
  * @group mustache_injection
+ * @group functional
  */
-class MustacheInjectionSectionTest extends PHPUnit_Framework_TestCase {
-
-    // interpolation
-
-    public function testInterpolationInjection() {
-        $data = array(
-            'a' => '{{ b }}',
-            'b' => 'FAIL'
-        );
-        $template = '{{ a }}';
-        $output = '{{ b }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data));
-    }
-
-    public function testUnescapedInterpolationInjection() {
-        $data = array(
-            'a' => '{{ b }}',
-            'b' => 'FAIL'
-        );
-        $template = '{{{ a }}}';
-        $output = '{{ b }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data));
-    }
-
-
-    // sections
-
-    public function testSectionInjection() {
-        $data = array(
-            'a' => true,
-            'b' => '{{ c }}',
-            'c' => 'FAIL'
-        );
-        $template = '{{# a }}{{ b }}{{/ a }}';
-        $output = '{{ c }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data));
-    }
-
-    public function testUnescapedSectionInjection() {
-        $data = array(
-            'a' => true,
-            'b' => '{{ c }}',
-            'c' => 'FAIL'
-        );
-        $template = '{{# a }}{{{ b }}}{{/ a }}';
-        $output = '{{ c }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data));
-    }
-
-
-    // partials
-
-    public function testPartialInjection() {
-        $data = array(
-            'a' => '{{ b }}',
-            'b' => 'FAIL'
-        );
-        $template = '{{> partial }}';
-        $partials = array(
-            'partial' => '{{ a }}',
-        );
-        $output = '{{ b }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data, $partials));
-    }
-
-    public function testPartialUnescapedInjection() {
-        $data = array(
-            'a' => '{{ b }}',
-            'b' => 'FAIL'
-        );
-        $template = '{{> partial }}';
-        $partials = array(
-            'partial' => '{{{ a }}}',
-        );
-        $output = '{{ b }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data, $partials));
-    }
-
-
-    // lambdas
-
-    public function testLambdaInterpolationInjection() {
-        $data = array(
-            'a' => array($this, 'interpolationLambda'),
-            'b' => '{{ c }}',
-            'c' => 'FAIL'
-        );
-        $template = '{{ a }}';
-        $output = '{{ c }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data));
-    }
-
-    public function interpolationLambda() {
-        return '{{ b }}';
-    }
-
-    public function testLambdaSectionInjection() {
-        $data = array(
-            'a' => array($this, 'sectionLambda'),
-            'b' => '{{ c }}',
-            'c' => 'FAIL'
-        );
-        $template = '{{# a }}b{{/ a }}';
-        $output = '{{ c }}';
-        $m = new Mustache();
-        $this->assertEquals($output, $m->render($template, $data));
-    }
-
-    public function sectionLambda($content) {
-        return '{{ ' . $content . ' }}';
-    }
-
-}
+class MustacheInjectionTest extends \PHPUnit_Framework_TestCase {
+
+	private $mustache;
+
+	public function setUp() {
+		$this->mustache = new Mustache;
+	}
+
+	// interpolation
+
+	public function testInterpolationInjection() {
+		$tpl = $this->mustache->loadTemplate('{{ a }}');
+
+		$data = array(
+			'a' => '{{ b }}',
+			'b' => 'FAIL'
+		);
+
+		$this->assertEquals('{{ b }}', $tpl->render($data));
+	}
+
+	public function testUnescapedInterpolationInjection() {
+		$tpl = $this->mustache->loadTemplate('{{{ a }}}');
+
+		$data = array(
+			'a' => '{{ b }}',
+			'b' => 'FAIL'
+		);
+
+		$this->assertEquals('{{ b }}', $tpl->render($data));
+	}
+
+
+	// sections
+
+	public function testSectionInjection() {
+		$tpl = $this->mustache->loadTemplate('{{# a }}{{ b }}{{/ a }}');
+
+		$data = array(
+			'a' => true,
+			'b' => '{{ c }}',
+			'c' => 'FAIL'
+		);
+
+		$this->assertEquals('{{ c }}', $tpl->render($data));
+	}
+
+	public function testUnescapedSectionInjection() {
+		$tpl = $this->mustache->loadTemplate('{{# a }}{{{ b }}}{{/ a }}');
+
+		$data = array(
+			'a' => true,
+			'b' => '{{ c }}',
+			'c' => 'FAIL'
+		);
+
+		$this->assertEquals('{{ c }}', $tpl->render($data));
+	}
+
+
+	// partials
+
+	public function testPartialInjection() {
+		$tpl = $this->mustache->loadTemplate('{{> partial }}');
+		$this->mustache->setPartials(array(
+			'partial' => '{{ a }}',
+		));
+
+		$data = array(
+			'a' => '{{ b }}',
+			'b' => 'FAIL'
+		);
+
+		$this->assertEquals('{{ b }}', $tpl->render($data));
+	}
+
+	public function testPartialUnescapedInjection() {
+		$tpl = $this->mustache->loadTemplate('{{> partial }}');
+		$this->mustache->setPartials(array(
+			'partial' => '{{{ a }}}',
+		));
+
+		$data = array(
+			'a' => '{{ b }}',
+			'b' => 'FAIL'
+		);
+
+		$this->assertEquals('{{ b }}', $tpl->render($data));
+	}
+
+
+	// lambdas
+
+	public function testLambdaInterpolationInjection() {
+		$tpl = $this->mustache->loadTemplate('{{ a }}');
+
+		$data = array(
+			'a' => function() { return '{{ b }}'; },
+			'b' => '{{ c }}',
+			'c' => 'FAIL'
+		);
+
+		$this->assertEquals('{{ c }}', $tpl->render($data));
+	}
+
+	public function testLambdaSectionInjection() {
+		$tpl = $this->mustache->loadTemplate('{{# a }}b{{/ a }}');
+
+		$data = array(
+			'a' => function ($text) { return '{{ ' . $text . ' }}'; },
+			'b' => '{{ c }}',
+			'c' => 'FAIL'
+		);
+
+		$this->assertEquals('{{ c }}', $tpl->render($data));
+	}
+}

+ 72 - 60
test/Mustache/Test/Functional/MustacheSpecTest.php

@@ -1,22 +1,30 @@
 <?php
 
-require_once '../Mustache.php';
-require_once './lib/yaml/lib/sfYamlParser.php';
+namespace Mustache\Test\Functional;
+
+use Mustache\Mustache;
+use Mustache\Loader\StringLoader;
 
 /**
  * A PHPUnit test case wrapping the Mustache Spec
  *
  * @group mustache-spec
+ * @group functional
  */
-class MustacheSpecTest extends PHPUnit_Framework_TestCase {
+class MustacheSpecTest extends \PHPUnit_Framework_TestCase {
+
+	private static $mustache;
+
+	public static function setUpBeforeClass() {
+		self::$mustache = new Mustache;
+	}
 
 	/**
 	 * For some reason data providers can't mark tests skipped, so this test exists
 	 * simply to provide a 'skipped' test if the `spec` submodule isn't initialized.
 	 */
 	public function testSpecInitialized() {
-		$spec_dir = dirname(__FILE__) . '/spec/specs/';
-		if (!file_exists($spec_dir)) {
+		if (!file_exists(__DIR__.'/../../../spec/specs/')) {
 			$this->markTestSkipped('Mustache spec submodule not initialized: run "git submodule update --init"');
 		}
 	}
@@ -25,56 +33,72 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase {
 	 * @group comments
 	 * @dataProvider loadCommentSpec
 	 */
-	public function testCommentSpec($desc, $template, $data, $partials, $expected) {
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
+	public function testCommentSpec($desc, $source, $partials, $data, $expected) {
+		$template = self::loadTemplate($source, $partials);
+		$this->assertEquals($expected, $template($data), $desc);
+	}
+
+	public function loadCommentSpec() {
+		return $this->loadSpec('comments');
 	}
 
 	/**
 	 * @group delimiters
 	 * @dataProvider loadDelimitersSpec
 	 */
-	public function testDelimitersSpec($desc, $template, $data, $partials, $expected) {
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
+	public function testDelimitersSpec($desc, $source, $partials, $data, $expected) {
+		$template = self::loadTemplate($source, $partials);
+		$this->assertEquals($expected, $template($data), $desc);
+	}
+
+	public function loadDelimitersSpec() {
+		return $this->loadSpec('delimiters');
 	}
 
 	/**
 	 * @group interpolation
 	 * @dataProvider loadInterpolationSpec
 	 */
-	public function testInterpolationSpec($desc, $template, $data, $partials, $expected) {
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
+	public function testInterpolationSpec($desc, $source, $partials, $data, $expected) {
+		$template = self::loadTemplate($source, $partials);
+		$this->assertEquals($expected, $template($data), $desc);
+	}
+
+	public function loadInterpolationSpec() {
+		return $this->loadSpec('interpolation');
 	}
 
 	/**
+	 * @group inverted
 	 * @group inverted-sections
 	 * @dataProvider loadInvertedSpec
 	 */
-	public function testInvertedSpec($desc, $template, $data, $partials, $expected) {
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
+	public function testInvertedSpec($desc, $source, $partials, $data, $expected) {
+		$template = self::loadTemplate($source, $partials);
+		$this->assertEquals($expected, $template($data), $desc);
+	}
+
+	public function loadInvertedSpec() {
+		return $this->loadSpec('inverted');
 	}
 
 	/**
 	 * @group lambdas
 	 * @dataProvider loadLambdasSpec
 	 */
-	public function testLambdasSpec($desc, $template, $data, $partials, $expected) {
-		if (!version_compare(PHP_VERSION, '5.3.0', '>=')) {
-			$this->markTestSkipped('Unable to test Lambdas spec with PHP < 5.3.');
-		}
+	public function testLambdasSpec($desc, $source, $partials, $data, $expected) {
+		$template = self::loadTemplate($source, $partials);
+		$this->assertEquals($expected, $template($this->prepareLambdasSpec($data)), $desc);
+	}
 
-		$data = $this->prepareLambdasSpec($data);
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
+	public function loadLambdasSpec() {
+		return $this->loadSpec('~lambdas');
 	}
 
 	/**
 	 * Extract and lambdafy any 'lambda' values found in the $data array.
 	 */
-	protected function prepareLambdasSpec($data) {
+	private function prepareLambdasSpec($data) {
 		foreach ($data as $key => $val) {
 			if ($key === 'lambda') {
 				if (!isset($val['php'])) {
@@ -94,42 +118,22 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase {
 	 * @group partials
 	 * @dataProvider loadPartialsSpec
 	 */
-	public function testPartialsSpec($desc, $template, $data, $partials, $expected) {
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
+	public function testPartialsSpec($desc, $source, $partials, $data, $expected) {
+		$template = self::loadTemplate($source, $partials);
+		$this->assertEquals($expected, $template($data), $desc);
+	}
+
+	public function loadPartialsSpec() {
+		return $this->loadSpec('partials');
 	}
 
 	/**
 	 * @group sections
 	 * @dataProvider loadSectionsSpec
 	 */
-	public function testSectionsSpec($desc, $template, $data, $partials, $expected) {
-		$m = new Mustache($template, $data, $partials);
-		$this->assertEquals($expected, $m->render(), $desc);
-	}
-
-	public function loadCommentSpec() {
-		return $this->loadSpec('comments');
-	}
-
-	public function loadDelimitersSpec() {
-		return $this->loadSpec('delimiters');
-	}
-
-	public function loadInterpolationSpec() {
-		return $this->loadSpec('interpolation');
-	}
-
-	public function loadInvertedSpec() {
-		return $this->loadSpec('inverted');
-	}
-
-	public function loadLambdasSpec() {
-		return $this->loadSpec('~lambdas');
-	}
-
-	public function loadPartialsSpec() {
-		return $this->loadSpec('partials');
+	public function testSectionsSpec($desc, $source, $partials, $data, $expected) {
+		$template = self::loadTemplate($source, $partials);
+		$this->assertEquals($expected, $template($data), $desc);
 	}
 
 	public function loadSectionsSpec() {
@@ -144,14 +148,14 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase {
 	 * @access public
 	 * @return array
 	 */
-	protected function loadSpec($name) {
-		$filename = dirname(__FILE__) . '/spec/specs/' . $name . '.yml';
+	private function loadSpec($name) {
+		$filename = __DIR__ . '/../../../spec/specs/' . $name . '.yml';
 		if (!file_exists($filename)) {
 			return array();
 		}
 
 		$data = array();
-		$yaml = new sfYamlParser();
+		$yaml = new \sfYamlParser;
 		$file = file_get_contents($filename);
 
 		// @hack: pre-process the 'lambdas' spec so the Symfony YAML parser doesn't complain.
@@ -160,15 +164,23 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase {
 		}
 
 		$spec = $yaml->parse($file);
+
 		foreach ($spec['tests'] as $test) {
 			$data[] = array(
 				$test['name'] . ': ' . $test['desc'],
 				$test['template'],
-				$test['data'],
 				isset($test['partials']) ? $test['partials'] : array(),
+				$test['data'],
 				$test['expected'],
 			);
 		}
+
 		return $data;
 	}
-}
+
+	private static function loadTemplate($source, $partials) {
+		self::$mustache->setPartials($partials);
+
+		return self::$mustache->loadTemplate($source);
+	}
+}

+ 34 - 18
test/Mustache/Test/Functional/ObjectSectionTest.php

@@ -1,48 +1,64 @@
 <?php
 
-require_once '../Mustache.php';
+namespace Mustache\Test\Functional;
+
+use Mustache\Mustache;
 
 /**
  * @group sections
+ * @group functional
  */
-class MustacheObjectSectionTest extends PHPUnit_Framework_TestCase {
+class ObjectSectionTest extends \PHPUnit_Framework_TestCase {
+	private $mustache;
+
+	public function setUp() {
+		$this->mustache = new Mustache;
+	}
+
 	public function testBasicObject() {
-		$alpha = new Alpha();
-		$this->assertEquals('Foo', $alpha->render('{{#foo}}{{name}}{{/foo}}'));
+		$tpl = $this->mustache->loadTemplate('{{#foo}}{{name}}{{/foo}}');
+		$this->assertEquals('Foo', $tpl->render(new Alpha));
 	}
 
+	/**
+	 * @group magic_methods
+	 */
 	public function testObjectWithGet() {
-		$beta = new Beta();
-		$this->assertEquals('Foo', $beta->render('{{#foo}}{{name}}{{/foo}}'));
+		$tpl = $this->mustache->loadTemplate('{{#foo}}{{name}}{{/foo}}');
+		$this->assertEquals('Foo', $tpl->render(new Beta));
 	}
 
+	/**
+	 * @group magic_methods
+	 */
 	public function testSectionObjectWithGet() {
-		$gamma = new Gamma();
-		$this->assertEquals('Foo', $gamma->render('{{#bar}}{{#foo}}{{name}}{{/foo}}{{/bar}}'));
+		$tpl = $this->mustache->loadTemplate('{{#bar}}{{#foo}}{{name}}{{/foo}}{{/bar}}');
+		$this->assertEquals('Foo', $tpl->render(new Gamma));
 	}
 
 	public function testSectionObjectWithFunction() {
-		$alpha = new Alpha();
-		$alpha->foo = new Delta();
-		$this->assertEquals('Foo', $alpha->render('{{#foo}}{{name}}{{/foo}}'));
+		$tpl = $this->mustache->loadTemplate('{{#foo}}{{name}}{{/foo}}');
+		$alpha = new Alpha;
+		$alpha->foo = new Delta;
+		$this->assertEquals('Foo', $tpl->render($alpha));
 	}
 }
 
-class Alpha extends Mustache {
+class Alpha {
 	public $foo;
 
 	public function __construct() {
-		$this->foo = new StdClass();
+		$this->foo = new \StdClass();
 		$this->foo->name = 'Foo';
 		$this->foo->number = 1;
 	}
 }
 
-class Beta extends Mustache {
+class Beta {
 	protected $_data = array();
 
 	public function __construct() {
-		$this->_data['foo'] = new StdClass();
+		$this->_data['foo'] = new \StdClass();
 		$this->_data['foo']->name = 'Foo';
 		$this->_data['foo']->number = 1;
 	}
@@ -56,7 +72,7 @@ class Beta extends Mustache {
 	}
 }
 
-class Gamma extends Mustache {
+class Gamma {
 	public $bar;
 
 	public function __construct() {
@@ -64,10 +80,10 @@ class Gamma extends Mustache {
 	}
 }
 
-class Delta extends Mustache {
+class Delta {
 	protected $_name = 'Foo';
 
 	public function name() {
 		return $this->_name;
 	}
-}
+}

+ 43 - 0
test/Mustache/Test/Loader/ArrayLoaderTest.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace Mustache\Test\Loader;
+
+use Mustache\Loader\ArrayLoader;
+
+/**
+ * @group unit
+ */
+class ArrayLoaderTest extends \PHPUnit_Framework_TestCase {
+	public function testConstructor() {
+		$loader = new ArrayLoader(array(
+			'foo' => 'bar'
+		));
+
+		$this->assertEquals('bar', $loader->load('foo'));
+	}
+
+	public function testSetAndLoadTemplates() {
+		$loader = new ArrayLoader(array(
+			'foo' => 'bar'
+		));
+		$this->assertEquals('bar', $loader->load('foo'));
+
+		$loader->setTemplate('baz', 'qux');
+		$this->assertEquals('qux', $loader->load('baz'));
+
+		$loader->setTemplates(array(
+			'foo' => 'FOO',
+			'baz' => 'BAZ',
+		));
+		$this->assertEquals('FOO', $loader->load('foo'));
+		$this->assertEquals('BAZ', $loader->load('baz'));
+	}
+
+	/**
+	 * @expectedException \InvalidArgumentException
+	 */
+	public function testMissingTemplatesThrowExceptions() {
+		$loader = new ArrayLoader;
+		$loader->load('not_a_real_template');
+	}
+}

+ 27 - 46
test/Mustache/Test/Loader/FilesystemLoaderTest.php

@@ -1,60 +1,41 @@
 <?php
 
-require_once '../Mustache.php';
-require_once '../MustacheLoader.php';
+namespace Mustache\Test\Loader;
+
+use Mustache\Loader\FilesystemLoader;
 
 /**
- * @group loader
+ * @group unit
  */
-class MustacheLoaderTest extends PHPUnit_Framework_TestCase {
-
-	public function testTheActualFilesystemLoader() {
-		$loader = new MustacheLoader(dirname(__FILE__).'/fixtures');
-		$this->assertEquals(file_get_contents(dirname(__FILE__).'/fixtures/foo.mustache'), $loader['foo']);
-		$this->assertEquals(file_get_contents(dirname(__FILE__).'/fixtures/bar.mustache'), $loader['bar']);
+class FilesystemLoaderTest extends \PHPUnit_Framework_TestCase {
+	public function testConstructor() {
+		$baseDir = realpath(__DIR__.'/../../../fixtures/templates');
+		$loader = new FilesystemLoader($baseDir, array('extension' => '.ms'));
+		$this->assertEquals('alpha contents', $loader->load('alpha'));
+		$this->assertEquals('beta contents', $loader->load('beta.ms'));
 	}
 
-	public function testMustacheUsesFilesystemLoader() {
-		$template = '{{> foo }} {{> bar }}';
-		$data = array(
-			'truthy' => true,
-			'foo'    => 'FOO',
-			'bar'    => 'BAR',
-		);
-		$output = 'FOO BAR';
-		$m = new Mustache();
-		$partials = new MustacheLoader(dirname(__FILE__).'/fixtures');
-		$this->assertEquals($output, $m->render($template, $data, $partials));
+	public function testLoadTemplates() {
+		$baseDir = realpath(__DIR__.'/../../../fixtures/templates');
+		$loader = new FilesystemLoader($baseDir);
+		$this->assertEquals('one contents', $loader->load('one'));
+		$this->assertEquals('two contents', $loader->load('two.mustache'));
 	}
 
-	public function testMustacheUsesDifferentLoadersToo() {
-		$template = '{{> foo }} {{> bar }}';
-		$data = array(
-			'truthy' => true,
-			'foo'    => 'FOO',
-			'bar'    => 'BAR',
-		);
-		$output = 'FOO BAR';
-		$m = new Mustache();
-		$partials = new DifferentMustacheLoader();
-		$this->assertEquals($output, $m->render($template, $data, $partials));
+	/**
+	 * @expectedException \RuntimeException
+	 */
+	public function testMissingBaseDirThrowsException() {
+		$loader = new FilesystemLoader(__DIR__.'/not_a_directory');
 	}
-}
 
-class DifferentMustacheLoader implements ArrayAccess {
-	protected $partials = array(
-		'foo' => '{{ foo }}',
-		'bar' => '{{# truthy }}{{ bar }}{{/ truthy }}',
-	);
+	/**
+	 * @expectedException \InvalidArgumentException
+	 */
+	public function testMissingTemplateThrowsException() {
+		$baseDir = realpath(__DIR__.'/../../../fixtures/templates');
+		$loader = new FilesystemLoader($baseDir);
 
-	public function offsetExists($offset) {
-		return isset($this->partials[$offset]);
+		$loader->load('fake');
 	}
-
-	public function offsetGet($offset) {
-		return $this->partials[$offset];
-	}
-
-	public function offsetSet($offset, $value) {}
-	public function offsetUnset($offset) {}
 }

+ 18 - 0
test/Mustache/Test/Loader/StringLoaderTest.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Mustache\Test\Loader;
+
+use Mustache\Loader\StringLoader;
+
+/**
+ * @group unit
+ */
+class StringLoaderTest extends \PHPUnit_Framework_TestCase {
+	public function testLoadTemplates() {
+		$loader = new StringLoader;
+
+		$this->assertEquals('foo', $loader->load('foo'));
+		$this->assertEquals('{{ bar }}', $loader->load('{{ bar }}'));
+		$this->assertEquals("\n{{! comment }}\n", $loader->load("\n{{! comment }}\n"));
+	}
+}

+ 101 - 418
test/Mustache/Test/MustacheTest.php

@@ -1,464 +1,147 @@
 <?php
 
-require_once '../Mustache.php';
+namespace Mustache\Test;
+
+use Mustache\Compiler;
+use Mustache\Mustache;
+use Mustache\Loader\StringLoader;
+use Mustache\Loader\ArrayLoader;
+use Mustache\Parser;
+use Mustache\Tokenizer;
 
 /**
- * A PHPUnit test case for Mustache.php.
- *
- * This is a very basic, very rudimentary unit test case. It's probably more important to have tests
- * than to have elegant tests, so let's bear with it for a bit.
- *
- * This class assumes an example directory exists at `../examples` with the following structure:
- *
- * @code
- *    examples
- *        foo
- *            Foo.php
- *            foo.mustache
- *            foo.txt
- *        bar
- *            Bar.php
- *            bar.mustache
- *            bar.txt
- * @endcode
- *
- * To use this test:
- *
- *  1. {@link http://www.phpunit.de/manual/current/en/installation.html Install PHPUnit}
- *  2. run phpunit from the `test` directory:
- *        `phpunit MustacheTest`
- *  3. Fix bugs. Lather, rinse, repeat.
- *
- * @extends PHPUnit_Framework_TestCase
+ * @group unit
  */
-class MustacheTest extends PHPUnit_Framework_TestCase {
-
-	const TEST_CLASS = 'Mustache';
-
-	protected $knownIssues = array(
-		// Just the whitespace ones...
-	);
-
-	/**
-	 * Test Mustache constructor.
-	 *
-	 * @access public
-	 * @return void
-	 */
-	public function test__construct() {
-		$template = '{{#mustaches}}{{#last}}and {{/last}}{{type}}{{^last}}, {{/last}}{{/mustaches}}';
-		$data     = array(
-			'mustaches' => array(
-				array('type' => 'Natural'),
-				array('type' => 'Hungarian'),
-				array('type' => 'Dali'),
-				array('type' => 'English'),
-				array('type' => 'Imperial'),
-				array('type' => 'Freestyle', 'last' => 'true'),
-			)
-		);
-		$output = 'Natural, Hungarian, Dali, English, Imperial, and Freestyle';
-
-		$m1 = new Mustache();
-		$this->assertEquals($output, $m1->render($template, $data));
-
-		$m2 = new Mustache($template);
-		$this->assertEquals($output, $m2->render(null, $data));
-
-		$m3 = new Mustache($template, $data);
-		$this->assertEquals($output, $m3->render());
-
-		$m4 = new Mustache(null, $data);
-		$this->assertEquals($output, $m4->render($template));
-	}
-
-	/**
-	 * @dataProvider constructorOptions
-	 */
-	public function testConstructorOptions($options, $charset, $delimiters, $pragmas) {
-		$mustache = new MustacheExposedOptionsStub(null, null, null, $options);
-		$this->assertEquals($charset,    $mustache->getCharset());
-		$this->assertEquals($delimiters, $mustache->getDelimiters());
-		$this->assertEquals($pragmas,    $mustache->getPragmas());
-	}
-
-	public function constructorOptions() {
-		return array(
-			array(
-				array(),
-				'UTF-8',
-				array('{{', '}}'),
-				array(),
-			),
-			array(
-				array(
-					'charset'    => 'UTF-8',
-					'delimiters' => '<< >>',
-					'pragmas'    => array(Mustache::PRAGMA_UNESCAPED => true)
-				),
-				'UTF-8',
-				array('<<', '>>'),
-				array(Mustache::PRAGMA_UNESCAPED => true),
-			),
-			array(
-				array(
-					'charset'    => 'cp866',
-					'delimiters' => array('[[[[', ']]]]'),
-					'pragmas'    => array(Mustache::PRAGMA_UNESCAPED => true)
-				),
-				'cp866',
-				array('[[[[', ']]]]'),
-				array(Mustache::PRAGMA_UNESCAPED => true),
-			),
-		);
-	}
+class MustacheTest extends \PHPUnit_Framework_TestCase {
 
-	/**
-	 * @expectedException MustacheException
-	 */
-	public function testConstructorInvalidPragmaOptionsThrowExceptions() {
-		$mustache = new Mustache(null, null, null, array('pragmas' => array('banana phone' => true)));
-	}
-
-	/**
-	 * Test __toString() function.
-	 *
-	 * @access public
-	 * @return void
-	 */
-	public function test__toString() {
-		$m = new Mustache('{{first_name}} {{last_name}}', array('first_name' => 'Karl', 'last_name' => 'Marx'));
+	private static $tempDir;
 
-		$this->assertEquals('Karl Marx', $m->__toString());
-		$this->assertEquals('Karl Marx', (string) $m);
-
-		$m2 = $this->getMock(self::TEST_CLASS, array('render'), array());
-		$m2->expects($this->once())
-			->method('render')
-			->will($this->returnValue('foo'));
-
-		$this->assertEquals('foo', $m2->render());
-	}
-
-	public function test__toStringException() {
-		$m = $this->getMock(self::TEST_CLASS, array('render'), array());
-		$m->expects($this->once())
-			->method('render')
-			->will($this->throwException(new Exception));
-
-		try {
-			$out = (string) $m;
-		} catch (Exception $e) {
-			$this->fail('__toString should catch all exceptions');
+	public static function setUpBeforeClass() {
+		self::$tempDir = sys_get_temp_dir() . '/mustache_test';
+		if (file_exists(self::$tempDir)) {
+			self::rmdir(self::$tempDir);
 		}
 	}
 
-	/**
-	 * Test render().
-	 *
-	 * @access public
-	 * @return void
-	 */
-	public function testRender() {
-		$m = new Mustache();
-
-		$this->assertEquals('', $m->render(''));
-		$this->assertEquals('foo', $m->render('foo'));
-		$this->assertEquals('', $m->render(null));
-
-		$m2 = new Mustache('foo');
-		$this->assertEquals('foo', $m2->render());
-
-		$m3 = new Mustache('');
-		$this->assertEquals('', $m3->render());
-
-		$m3 = new Mustache();
-		$this->assertEquals('', $m3->render(null));
-	}
-
-	/**
-	 * Test render() with data.
-	 *
-	 * @group interpolation
-	 */
-	public function testRenderWithData() {
-		$m = new Mustache('{{first_name}} {{last_name}}');
-		$this->assertEquals('Charlie Chaplin', $m->render(null, array('first_name' => 'Charlie', 'last_name' => 'Chaplin')));
-		$this->assertEquals('Zappa, Frank', $m->render('{{last_name}}, {{first_name}}', array('first_name' => 'Frank', 'last_name' => 'Zappa')));
-	}
+	private static function rmdir($path) {
+		$path = rtrim($path, '/').'/';
+		$handle = opendir($path);
+		while (($file = readdir($handle)) !== false) {
+			if ($file == '.' || $file == '..') {
+				continue;
+			}
 
-	/**
-	 * @group partials
-	 */
-	public function testRenderWithPartials() {
-		$m = new Mustache('{{>stache}}', null, array('stache' => '{{first_name}} {{last_name}}'));
-		$this->assertEquals('Charlie Chaplin', $m->render(null, array('first_name' => 'Charlie', 'last_name' => 'Chaplin')));
-		$this->assertEquals('Zappa, Frank', $m->render('{{last_name}}, {{first_name}}', array('first_name' => 'Frank', 'last_name' => 'Zappa')));
-	}
+			$fullpath = $path.$file;
+			if (is_dir($fullpath)) {
+				self::rmdir($fullpath);
+			} else {
+				unlink($fullpath);
+			}
+		}
 
-	/**
-	 * @group interpolation
-	 * @dataProvider interpolationData
-	 */
-	public function testDoubleRenderMustacheTags($template, $context, $expected) {
-		$m = new Mustache($template, $context);
-		$this->assertEquals($expected, $m->render());
+		closedir($handle);
+		rmdir($path);
 	}
 
-	public function interpolationData() {
-		return array(
-			array(
-				'{{#a}}{{=<% %>=}}{{b}} c<%={{ }}=%>{{/a}}',
-				array('a' => array(array('b' => 'Do Not Render'))),
-				'{{b}} c'
-			),
-			array(
-				'{{#a}}{{b}}{{/a}}',
-				array('a' => array('b' => '{{c}}'), 'c' => 'FAIL'),
-				'{{c}}'
+	public function testConstructor() {
+		$loader         = new StringLoader;
+		$partialsLoader = new ArrayLoader;
+		$mustache       = new Mustache(array(
+			'template_class_prefix' => '__whot__',
+			'cache' => self::$tempDir,
+			'loader' => $loader,
+			'partials_loader' => $partialsLoader,
+			'partials' => array(
+				'foo' => '{{ foo }}',
 			),
-		);
-	}
-
-	/**
-	 * Mustache should allow newlines (and other whitespace) in comments and all other tags.
-	 *
-	 * @group comments
-	 */
-	public function testNewlinesInComments() {
-		$m = new Mustache("{{! comment \n \t still a comment... }}");
-		$this->assertEquals('', $m->render());
-	}
-
-	/**
-	 * Mustache should return the same thing when invoked multiple times.
-	 */
-	public function testMultipleInvocations() {
-		$m = new Mustache('x');
-		$first = $m->render();
-		$second = $m->render();
-
-		$this->assertEquals('x', $first);
-		$this->assertEquals($first, $second);
-	}
-
-	/**
-	 * Mustache should return the same thing when invoked multiple times.
-	 *
-	 * @group interpolation
-	 */
-	public function testMultipleInvocationsWithTags() {
-		$m = new Mustache('{{one}} {{two}}', array('one' => 'foo', 'two' => 'bar'));
-		$first = $m->render();
-		$second = $m->render();
+			'charset' => 'ISO-8859-1',
+		));
 
-		$this->assertEquals('foo bar', $first);
-		$this->assertEquals($first, $second);
+		$this->assertSame($loader, $mustache->getLoader());
+		$this->assertSame($partialsLoader, $mustache->getPartialsLoader());
+		$this->assertEquals('{{ foo }}', $partialsLoader->load('foo'));
+		$this->assertContains('__whot__', $mustache->getTemplateClassName('{{ foo }}'));
+		$this->assertEquals('ISO-8859-1', $mustache->getCharset());
 	}
 
-	/**
-	 * Mustache should not use templates passed to the render() method for subsequent invocations.
-	 */
-	public function testResetTemplateForMultipleInvocations() {
-		$m = new Mustache('Sirve.');
-		$this->assertEquals('No sirve.', $m->render('No sirve.'));
-		$this->assertEquals('Sirve.', $m->render());
-
-		$m2 = new Mustache();
-		$this->assertEquals('No sirve.', $m2->render('No sirve.'));
-		$this->assertEquals('', $m2->render());
-	}
+	public function testSettingServices() {
+		$loader    = new StringLoader;
+		$tokenizer = new Tokenizer;
+		$parser    = new Parser;
+		$compiler  = new Compiler;
+		$mustache  = new Mustache;
 
-	/**
-	 * Test the __clone() magic function.
-	 *
-	 * @group examples
-	 * @dataProvider getExamples
-	 *
-	 * @param string $class
-	 * @param string $template
-	 * @param string $output
-	 */
-	public function test__clone($class, $template, $output) {
-		if (isset($this->knownIssues[$class])) {
-			return $this->markTestSkipped($this->knownIssues[$class]);
-		}
+		$this->assertNotSame($loader, $mustache->getLoader());
+		$mustache->setLoader($loader);
+		$this->assertSame($loader, $mustache->getLoader());
 
-		$m = new $class;
-		$n = clone $m;
+		$this->assertNotSame($loader, $mustache->getPartialsLoader());
+		$mustache->setPartialsLoader($loader);
+		$this->assertSame($loader, $mustache->getPartialsLoader());
 
-		$n_output = $n->render($template);
+		$this->assertNotSame($tokenizer, $mustache->getTokenizer());
+		$mustache->setTokenizer($tokenizer);
+		$this->assertSame($tokenizer, $mustache->getTokenizer());
 
-		$o = clone $n;
+		$this->assertNotSame($parser, $mustache->getParser());
+		$mustache->setParser($parser);
+		$this->assertSame($parser, $mustache->getParser());
 
-		$this->assertEquals($m->render($template), $n_output);
-		$this->assertEquals($n_output, $o->render($template));
-
-		$this->assertNotSame($m, $n);
-		$this->assertNotSame($n, $o);
-		$this->assertNotSame($m, $o);
+		$this->assertNotSame($compiler, $mustache->getCompiler());
+		$mustache->setCompiler($compiler);
+		$this->assertSame($compiler, $mustache->getCompiler());
 	}
 
 	/**
-	 * Test everything in the `examples` directory.
-	 *
-	 * @group examples
-	 * @dataProvider getExamples
-	 *
-	 * @param string $class
-	 * @param string $template
-	 * @param string $output
+	 * @group functional
 	 */
-	public function testExamples($class, $template, $output) {
-		if (isset($this->knownIssues[$class])) {
-			return $this->markTestSkipped($this->knownIssues[$class]);
-		}
+	public function testCache() {
+		$mustache = new Mustache(array(
+			'template_class_prefix' => '__whot__',
+			'cache' => self::$tempDir,
+		));
 
-		$m = new $class;
-		$this->assertEquals($output, $m->render($template));
+		$source    = '{{ foo }}';
+		$template  = $mustache->loadTemplate($source);
+		$className = $mustache->getTemplateClassName($source);
+		$fileName  = self::$tempDir . '/' . $className . '.php';
+		$this->assertInstanceOf($className, $template);
+		$this->assertFileExists($fileName);
+		$this->assertContains("\nclass $className extends \Mustache\Template", file_get_contents($fileName));
 	}
 
 	/**
-	 * Data provider for testExamples method.
-	 *
-	 * Assumes that an `examples` directory exists inside parent directory.
-	 * This examples directory should contain any number of subdirectories, each of which contains
-	 * three files: one Mustache class (.php), one Mustache template (.mustache), and one output file
-	 * (.txt).
-	 *
-	 * This whole mess will be refined later to be more intuitive and less prescriptive, but it'll
-	 * do for now. Especially since it means we can have unit tests :)
-	 *
-	 * @return array
+	 * @group functional
+	 * @expectedException \RuntimeException
 	 */
-	public function getExamples() {
-		$basedir = dirname(__FILE__) . '/../examples/';
-
-		$ret = array();
-
-		$files = new RecursiveDirectoryIterator($basedir);
-		while ($files->valid()) {
-
-			if ($files->hasChildren() && $children = $files->getChildren()) {
-				$example  = $files->getSubPathname();
-				$class    = null;
-				$template = null;
-				$output   = null;
-
-				foreach ($children as $file) {
-					if (!$file->isFile()) continue;
+	public function testCacheFailsThrowException() {
+		global $mustacheFilesystemRenameHax;
 
-					$filename = $file->getPathname();
-					$info = pathinfo($filename);
+		$mustacheFilesystemRenameHax = true;
 
-					if (isset($info['extension'])) {
-						switch($info['extension']) {
-							case 'php':
-								$class = $info['filename'];
-								include_once($filename);
-								break;
-
-							case 'mustache':
-								$template = file_get_contents($filename);
-								break;
-
-							case 'txt':
-								$output = file_get_contents($filename);
-								break;
-						}
-					}
-				}
-
-				if (!empty($class)) {
-					$ret[$example] = array($class, $template, $output);
-				}
-			}
-
-			$files->next();
-		}
-		return $ret;
+		$mustache = new Mustache(array('cache' => self::$tempDir));
+		$mustache->loadTemplate('{{ foo }}');
 	}
 
 	/**
-	 * @group delimiters
+	 * @expectedException \RuntimeException
 	 */
-	public function testCrazyDelimiters() {
-		$m = new Mustache(null, array('result' => 'success'));
-		$this->assertEquals('success', $m->render('{{=[[ ]]=}}[[ result ]]'));
-		$this->assertEquals('success', $m->render('{{=(( ))=}}(( result ))'));
-		$this->assertEquals('success', $m->render('{{={$ $}=}}{$ result $}'));
-		$this->assertEquals('success', $m->render('{{=<.. ..>=}}<.. result ..>'));
-		$this->assertEquals('success', $m->render('{{=^^ ^^}}^^ result ^^'));
-		$this->assertEquals('success', $m->render('{{=// \\\\}}// result \\\\'));
-	}
+	public function testImmutablePartialsLoadersThrowException() {
+		$mustache = new Mustache(array(
+			'partials_loader' => new StringLoader,
+		));
 
-	/**
-	 * @group delimiters
-	 */
-	public function testResetDelimiters() {
-		$m = new Mustache(null, array('result' => 'success'));
-		$this->assertEquals('success', $m->render('{{=[[ ]]=}}[[ result ]]'));
-		$this->assertEquals('success', $m->render('{{=<< >>=}}<< result >>'));
-		$this->assertEquals('success', $m->render('{{=<% %>=}}<% result %>'));
-	}
-
-	/**
-	 * @group delimiters
-	 */
-	public function testStickyDelimiters() {
-		$m = new Mustache(null, array('result' => 'FAIL'));
-		$this->assertEquals('{{ result }}', $m->render('{{=[[ ]]=}}{{ result }}[[={{ }}=]]'));
-		$this->assertEquals('{{#result}}{{/result}}', $m->render('{{=[[ ]]=}}{{#result}}{{/result}}[[={{ }}=]]'));
-		$this->assertEquals('{{ result }}', $m->render('{{=[[ ]]=}}[[#result]]{{ result }}[[/result]][[={{ }}=]]'));
-		$this->assertEquals('{{ result }}', $m->render('{{#result}}{{=[[ ]]=}}{{ result }}[[/result]][[^result]][[={{ }}=]][[ result ]]{{/result}}'));
+		$mustache->setPartials(array('foo' => '{{ foo }}'));
 	}
+}
 
-	/**
-	 * @group sections
-	 * @dataProvider poorlyNestedSections
-	 * @expectedException MustacheException
-	 */
-	public function testPoorlyNestedSections($template) {
-		$m = new Mustache($template);
-		$m->render();
-	}
 
-	public function poorlyNestedSections() {
-		return array(
-			array('{{#foo}}'),
-			array('{{#foo}}{{/bar}}'),
-			array('{{#foo}}{{#bar}}{{/foo}}'),
-			array('{{#foo}}{{#bar}}{{/foo}}{{/bar}}'),
-			array('{{#foo}}{{/bar}}{{/foo}}'),
-		);
-	}
+// It's prob'ly best if you ignore this bit.
 
-	/**
-	 * Ensure that Mustache doesn't double-render sections (allowing mustache injection).
-	 *
-	 * @group sections
-	 */
-	public function testMustacheInjection() {
-		$template = '{{#foo}}{{bar}}{{/foo}}';
-		$view = array(
-			'foo' => true,
-			'bar' => '{{win}}',
-			'win' => 'FAIL',
-		);
+namespace Mustache;
 
-		$m = new Mustache($template, $view);
-		$this->assertEquals('{{win}}', $m->render());
-	}
-}
+function rename($a, $b) {
+	global $mustacheFilesystemRenameHax;
 
-class MustacheExposedOptionsStub extends Mustache {
-	public function getPragmas() {
-		return $this->_pragmas;
-	}
-	public function getCharset() {
-		return $this->_charset;
-	}
-	public function getDelimiters() {
-		return array($this->_otag, $this->_ctag);
-	}
+	return ($mustacheFilesystemRenameHax) ? false : \rename($a, $b);
 }

+ 157 - 0
test/Mustache/Test/ParserTest.php

@@ -0,0 +1,157 @@
+<?php
+
+namespace Mustache\Test;
+
+use Mustache\Parser;
+use Mustache\Tokenizer;
+
+/**
+ * @group unit
+ */
+class ParserTest extends \PHPUnit_Framework_TestCase {
+
+	/**
+	 * @dataProvider getTokenSets
+	 */
+	public function testParse($tokens, $expected)
+	{
+		$parser = new Parser;
+		$this->assertEquals($expected, $parser->parse($tokens));
+	}
+
+	public function getTokenSets()
+	{
+		return array(
+			array(
+				array(),
+				array()
+			),
+
+			array(
+				array('text'),
+				array('text')
+			),
+
+			array(
+				array(array(
+					Tokenizer::TAG => '_v',
+					Tokenizer::NAME => 'name'
+				)),
+				array(array(
+					Tokenizer::TAG => '_v',
+					Tokenizer::NAME => 'name'
+				)),
+			),
+
+			array(
+				array(
+					'foo',
+					array(
+						Tokenizer::TAG => '^',
+						Tokenizer::INDEX => 123,
+						Tokenizer::NAME => 'parent'
+					),
+					array(
+						Tokenizer::TAG => '_v',
+						Tokenizer::NAME => 'name'
+					),
+					array(
+						Tokenizer::TAG => '/',
+						Tokenizer::INDEX => 456,
+						Tokenizer::NAME => 'parent'
+					),
+					'bar',
+				),
+				array(
+					'foo',
+					array(
+						Tokenizer::TAG => '^',
+						Tokenizer::NAME => 'parent',
+						Tokenizer::INDEX => 123,
+						Tokenizer::END => 456,
+						Tokenizer::NODES => array(
+							array(
+								Tokenizer::TAG => '_v',
+								Tokenizer::NAME => 'name'
+							),
+						),
+					),
+					'bar',
+				),
+			),
+
+		);
+	}
+
+	/**
+	 * @dataProvider getBadParseTrees
+	 * @expectedException \LogicException
+	 */
+	public function testParserThrowsExceptions($tokens) {
+		$parser = new Parser;
+		$parser->parse($tokens);
+	}
+
+	public function getBadParseTrees() {
+		return array(
+			// no close
+			array(
+				array(
+					array(
+						Tokenizer::TAG => '#',
+						Tokenizer::INDEX => 123,
+						Tokenizer::NAME => 'parent'
+					),
+				),
+			),
+
+			// no close inverted
+			array(
+				array(
+					array(
+						Tokenizer::TAG => '^',
+						Tokenizer::INDEX => 123,
+						Tokenizer::NAME => 'parent'
+					),
+				),
+			),
+
+			// no opening inverted
+			array(
+				array(
+					array(
+						Tokenizer::TAG => '/',
+						Tokenizer::INDEX => 123,
+						Tokenizer::NAME => 'parent'
+					),
+				),
+			),
+
+			// weird nesting
+			array(
+				array(
+					array(
+						Tokenizer::TAG => '#',
+						Tokenizer::INDEX => 123,
+						Tokenizer::NAME => 'parent'
+					),
+					array(
+						Tokenizer::TAG => '#',
+						Tokenizer::INDEX => 123,
+						Tokenizer::NAME => 'child'
+					),
+					array(
+						Tokenizer::TAG => '/',
+						Tokenizer::INDEX => 123,
+						Tokenizer::NAME => 'parent'
+					),
+					array(
+						Tokenizer::TAG => '/',
+						Tokenizer::INDEX => 123,
+						Tokenizer::NAME => 'child'
+					),
+				),
+			),
+		);
+	}
+}

+ 43 - 0
test/Mustache/Test/TemplateTest.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace Mustache\Test;
+
+use Mustache\Context;
+use Mustache\Mustache;
+use Mustache\Template;
+
+/**
+ * @group unit
+ */
+class TemplateTest extends \PHPUnit_Framework_TestCase {
+	public function testConstructor() {
+		$mustache = new Mustache;
+		$template = new TemplateStub($mustache);
+		$this->assertSame($mustache, $template->getMustache());
+	}
+
+	public function testRendering() {
+		$rendered = '<< wheee >>';
+		$mustache = new Mustache;
+		$template = new TemplateStub($mustache);
+		$template->rendered = $rendered;
+		$context  = new Context;
+
+		$this->assertEquals($rendered, $template());
+		$this->assertEquals($rendered, $template->render());
+		$this->assertEquals($rendered, $template->renderInternal($context));
+		$this->assertEquals($rendered, $template->render(array('foo' => 'bar')));
+	}
+}
+
+class TemplateStub extends Template {
+	public $rendered;
+
+	public function getMustache() {
+		return $this->mustache;
+	}
+
+	public function renderInternal(Context $context) {
+		return $this->rendered;
+	}
+}

+ 117 - 0
test/Mustache/Test/TokenizerTest.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace Mustache\Test;
+
+use Mustache\Tokenizer;
+
+/**
+ * @group unit
+ */
+class TokenizerTest extends \PHPUnit_Framework_TestCase {
+
+	/**
+	 * @dataProvider getTokens
+	 */
+	public function testScan($text, $delimiters, $expected) {
+		$tokenizer = new Tokenizer;
+		$this->assertSame($expected, $tokenizer->scan($text, $delimiters));
+	}
+
+	public function getTokens() {
+		return array(
+			array(
+				'text',
+				null,
+				array('text'),
+			),
+
+			array(
+				'text',
+				'<<< >>>',
+				array('text'),
+			),
+
+			array(
+				'{{ name }}',
+				null,
+				array(
+					array(
+						Tokenizer::TAG   => '_v',
+						Tokenizer::NAME  => 'name',
+						Tokenizer::OTAG  => '{{',
+						Tokenizer::CTAG  => '}}',
+						Tokenizer::INDEX => 10,
+					)
+				)
+			),
+
+			array(
+				'{{ name }}',
+				'<<< >>>',
+				array('{{ name }}'),
+			),
+
+			array(
+				'<<< name >>>',
+				'<<< >>>',
+				array(
+					array(
+						Tokenizer::TAG   => '_v',
+						Tokenizer::NAME  => 'name',
+						Tokenizer::OTAG  => '<<<',
+						Tokenizer::CTAG  => '>>>',
+						Tokenizer::INDEX => 12,
+					)
+				)
+			),
+
+			array(
+				"{{{ a }}}\n{{# b }}  \n{{= | | =}}| c ||/ b |\n|{ d }|",
+				null,
+				array(
+					array(
+						Tokenizer::TAG   => '{',
+						Tokenizer::NAME  => 'a',
+						Tokenizer::OTAG  => '{{',
+						Tokenizer::CTAG  => '}}',
+						Tokenizer::INDEX => 8,
+					),
+					"\n",
+					array(
+						Tokenizer::TAG   => '#',
+						Tokenizer::NAME  => 'b',
+						Tokenizer::OTAG  => '{{',
+						Tokenizer::CTAG  => '}}',
+						Tokenizer::INDEX => 18,
+					),
+					null,
+					array(
+						Tokenizer::TAG   => '_v',
+						Tokenizer::NAME  => 'c',
+						Tokenizer::OTAG  => '|',
+						Tokenizer::CTAG  => '|',
+						Tokenizer::INDEX => 37,
+					),
+					array(
+						Tokenizer::TAG   => '/',
+						Tokenizer::NAME  => 'b',
+						Tokenizer::OTAG  => '|',
+						Tokenizer::CTAG  => '|',
+						Tokenizer::INDEX => 37,
+					),
+					"\n",
+					array(
+						Tokenizer::TAG   => '{',
+						Tokenizer::NAME  => 'd',
+						Tokenizer::OTAG  => '|',
+						Tokenizer::CTAG  => '|',
+						Tokenizer::INDEX => 51,
+					),
+
+				)
+			),
+
+
+		);
+	}
+}