add integration test

sort of horrible, I'm not sure if these are actually good
changes. Partly necessary due to the addition of threadpooling, but
the feedback is poor and they're obviously fragile.
M quiescent/bootstrap.py +4 -4
@@ 42,7 42,7 @@ feed link = feed.atom
     <ul>
     {% for post in all_posts %}
     <li>
-      {{ post.date }} <a href="{{ post.path }}">{{ post.title }}</a>
+      {{ post.date_string }} <a href="{{ post.path }}">{{ post.title }}</a>
     </li>
     {% endfor %}
     </ul>

          
@@ 58,9 58,9 @@ feed link = feed.atom
     <base href='/'></base>
   </head>
   <body>
-    <h1>{{ post.date }}, {{ post.title }}</h1>
+    <h1>{{ post.date_string }}, {{ post.title }}</h1>
     <div>
-    {{ post.body }}
+    {{ post.html_body }}
     </div>
   </body>
 </html>

          
@@ 76,7 76,7 @@ feed link = feed.atom
   <body>
     {% for post in front_posts %}
     <h2><a href={{ post.path }}>{{ post.title }}</a></h2>
-    {{ post.leader }}
+    {{ post.html_leader }}
     {% endfor %}
   </body>
 </html>

          
M quiescent/static.py +23 -19
@@ 78,9 78,7 @@ class StaticGenerator:
     def render_page(self, template_name):
         template_file = os.path.join(self.config.template_dir, template_name)
         with open(template_file, encoding='utf-8') as f:
-            template_text = f.read()
-        template = Templite(template_text)
-        return template
+            return Templite(f.read())
 
     def collect_posts(self, from_dir):
         '''

          
@@ 95,16 93,19 @@ class StaticGenerator:
         return post_files
 
     def find_media_directories(self, directory, media_directory):
-        return [os.path.join(root, dir) for root, directories, _ in os.walk(directory)
-                for dir in directories if dir == media_directory]
+        return (os.path.join(root, dir) for root, directories, _ in os.walk(directory)
+                for dir in directories if dir == media_directory)
 
     def copy_media(self):
         def copies_required(directory):
-            relative_dest_dir = os.path.relpath(directory, self.config.posts_dir)
-            out_path = os.path.join(self.config.output_dir, relative_dest_dir)
-            os.makedirs(out_path, exist_ok=True)
-            return ((os.path.join(directory, filename), out_path)
-                    for filename in os.listdir(directory))
+            try:
+                relative_dest_dir = os.path.relpath(directory, self.config.posts_dir)
+                out_path = os.path.join(self.config.output_dir, relative_dest_dir)
+                os.makedirs(out_path, exist_ok=True)
+                return ((os.path.join(directory, filename), out_path)
+                        for filename in os.listdir(directory))
+            except Exception as e:
+                logger.error(e)
 
         with concurrent.futures.ThreadPoolExecutor() as executor:
             media_dirs = self.find_media_directories(self.config.posts_dir,

          
@@ 122,19 123,22 @@ class StaticGenerator:
                 post = process(parse(text))
                 self.all_posts.append(post)
             except Exception as e:
-                logger.warning('Failed to create post: {post}\n\t{e}'
-                               .format(post=post, e=e))
+                logger.warning('Failed to create post: {name}\n\t{e}'
+                               .format(name=filename, e=e))
         self.all_posts = sorted(self.all_posts, key=lambda p: p.date_time, reverse=True)
 
     def write_generated_files(self):
         def write_post(post):
-            post_page = self.post_template.render({'post': post})
-            output_tree = os.path.dirname(os.path.join(self.config.output_dir, post.path))
-            # reconstitute the input tree in the output directory
-            os.makedirs(output_tree, exist_ok=True)
-            output_path = os.path.join(self.config.output_dir, post.path)
-            with open(output_path, 'w', encoding='utf-8') as f:
-                f.write(post_page)
+            try:
+                post_page = self.post_template.render({'post': post})
+                output_tree = os.path.dirname(os.path.join(self.config.output_dir, post.path))
+                # reconstitute the input tree in the output directory
+                os.makedirs(output_tree, exist_ok=True)
+                output_path = os.path.join(self.config.output_dir, post.path)
+                with open(output_path, 'w', encoding='utf-8') as f:
+                    f.write(post_page)
+            except Exception as e:
+                logger.error(e)
 
         with concurrent.futures.ThreadPoolExecutor() as executor:
             for post in self.all_posts:

          
A => quiescent/tests/golden/2020/example-post.html +15 -0
@@ 0,0 1,15 @@ 
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <base href='/'></base>
+  </head>
+  <body>
+    <h1>2020-01-01, Example Post</h1>
+    <div>
+    <p>This is an example.</p>
+<p>This is the body of the example.</p>
+
+    </div>
+  </body>
+</html>

          
A => quiescent/tests/golden/archive.html +16 -0
@@ 0,0 1,16 @@ 
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <base href='/'></base>
+  </head>
+  <body>
+    <ul>
+    
+    <li>
+      2020-01-01 <a href="2020/example-post.html">Example Post</a>
+    </li>
+    
+    </ul>
+  </body>
+</html>

          
A => quiescent/tests/golden/feed.atom +3 -0
@@ 0,0 1,3 @@ 
+<feed xmlns="http://www.w3.org/2005/Atom"><title>Example Name</title><link href="http://example.com" /><link href="http://example.com" rel="self" /><updated>2020-01-05T18:31:34.424723+00:00</updated><author><name>Example Author</name></author><id>http://example.com</id><entry><title>Example Post</title><link href="http://example.com/2020/example-post.html" /><id>http://example.com/2020/example-post.html</id><updated>2020-01-01T00:00:00+00:00</updated><content type="html">&lt;p&gt;This is an example.&lt;/p&gt;
+&lt;p&gt;This is the body of the example.&lt;/p&gt;
+</content></entry></feed>
  No newline at end of file

          
A => quiescent/tests/golden/index.html +14 -0
@@ 0,0 1,14 @@ 
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <base href='/'></base>
+  </head>
+  <body>
+    
+    <h2><a href=2020/example-post.html>Example Post</a></h2>
+    <p>This is an example.</p>
+
+    
+  </body>
+</html>

          
A => quiescent/tests/test_integration.py +60 -0
@@ 0,0 1,60 @@ 
+import unittest
+import subprocess
+import tempfile
+import filecmp
+import shlex
+import os.path
+import logging
+
+logger = logging.getLogger(__name__)
+
+def directories_same(dir1, dir2):
+    directory_compare = filecmp.dircmp(dir1, dir2)
+    if any([len(differences) > 0 for differences in (directory_compare.left_only,
+                                                     directory_compare.right_only,
+                                                     directory_compare.funny_files)]):
+        logger.error(f'left only: {directory_compare.left_only}\n'
+                     f'right only: {directory_compare.right_only}')
+        return False
+    (_, mismatch, errors) =  filecmp.cmpfiles(dir1, dir2,
+                                              directory_compare.common_files,
+                                              shallow=False)
+
+    # this is terrible, atom feed contains a generation timestamp so
+    # it is always different. the if-clause is necessary due to the
+    # recursion, early checks will remove it, later will error out
+    # without it
+    if 'feed.atom' in mismatch: mismatch.remove('feed.atom')
+
+    if any([len(e) > 0 for e in (mismatch, errors)]):
+        logger.error(f'MISMATCH: {mismatch}')
+        return False
+    for common_dir in directory_compare.common_dirs:
+        new_dir1 = os.path.join(dir1, common_dir)
+        new_dir2 = os.path.join(dir2, common_dir)
+        if not directories_same(new_dir1, new_dir2):
+            return False
+    return True
+
+class IntegrationTests(unittest.TestCase):
+    def test_construct_site(self):
+        with tempfile.TemporaryDirectory() as source:
+            subprocess.run(shlex.split('quiescent --bootstrap'), cwd=source)
+            os.makedirs(os.path.join(source, 'posts', '2020'), exist_ok=True)
+            os.makedirs(os.path.join(source, 'posts', '2020', 'media'), exist_ok=True)
+            with open(os.path.join(source, 'posts', '2020', 'media', 'foo.png'), 'wb') as m:
+                m.write(b'')
+            with open(os.path.join(source, 'posts', '2020', 'example-post.md'), 'w') as f:
+                post_contents = """title: Example Post
+date: 2020-01-01
++++
+
+This is an example.
+
+This is the body of the example.
+"""
+                f.write(post_contents)
+            subprocess.run('quiescent', cwd=source)
+            golden_copy = os.path.abspath(os.path.join(os.path.abspath(__file__), os.pardir, 'golden'))
+            built_output = os.path.join(source, 'build')
+            self.assertTrue(directories_same(golden_copy, built_output))