Add simple_layout test decorator

And add it to test_no_job_project.

So that we can reduce the number of boilerplate git repos initialized
for every (or most) tests, make it simple to specify a single simple
layout file (like we used to do in zuul v2).  The more complicated
scenarios can rely on the full copy-git-repos-from-test-fixtures
behavior, but simple tests can have that automatically generated from
a simple zuul.yaml file.

Change-Id: Ibeec4c526a1097823589f2c38a50e40dc346e0e5
diff --git a/doc/source/developer/testing.rst b/doc/source/developer/testing.rst
index 4a813d0..057ab7e 100644
--- a/doc/source/developer/testing.rst
+++ b/doc/source/developer/testing.rst
@@ -9,6 +9,8 @@
 access to a number of attributes useful for manipulating or inspecting
 the environment being simulated in the test:
 
+.. autofunction:: tests.base.simple_layout
+
 .. autoclass:: tests.base.ZuulTestCase
    :members:
 
diff --git a/tests/base.py b/tests/base.py
index 0c033b5..120920b 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -97,6 +97,30 @@
     raise Exception("Timeout waiting for %s" % purpose)
 
 
+def simple_layout(path):
+    """Specify a layout file for use by a test method.
+
+    :arg str path: The path to the layout file.
+
+    Some tests require only a very simple configuration.  For those,
+    establishing a complete config directory hierachy is too much
+    work.  In those cases, you can add a simple zuul.yaml file to the
+    test fixtures directory (in fixtures/layouts/foo.yaml) and use
+    this decorator to indicate the test method should use that rather
+    than the tenant config file specified by the test class.
+
+    The decorator will cause that layout file to be added to a
+    config-project called "common-config" and each "project" instance
+    referenced in the layout file will have a git repo automatically
+    initialized.
+    """
+
+    def decorator(test):
+        test.__simple_layout__ = path
+        return test
+    return decorator
+
+
 class ChangeReference(git.Reference):
     _common_path_default = "refs/changes"
     _points_to_commits_only = True
@@ -1231,7 +1255,8 @@
         be loaded).  It defaults to the value specified in
         `config_file` but can be overidden by subclasses to obtain a
         different tenant/project layout while using the standard main
-        configuration.
+        configuration.  See also the :py:func:`simple_layout`
+        decorator.
 
     :cvar bool create_project_keys: Indicates whether Zuul should
         auto-generate keys for each project, or whether the test
@@ -1324,7 +1349,6 @@
         self.init_repo("org/conflict-project")
         self.init_repo("org/noop-project")
         self.init_repo("org/experimental-project")
-        self.init_repo("org/no-jobs-project")
 
         self.statsd = FakeStatsd()
         # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
@@ -1452,19 +1476,70 @@
         # obeys the config_file and tenant_config_file attributes.
         self.config = ConfigParser.ConfigParser()
         self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
-        if hasattr(self, 'tenant_config_file'):
-            self.config.set('zuul', 'tenant_config', self.tenant_config_file)
-            git_path = os.path.join(
-                os.path.dirname(
-                    os.path.join(FIXTURE_DIR, self.tenant_config_file)),
-                'git')
-            if os.path.exists(git_path):
-                for reponame in os.listdir(git_path):
-                    project = reponame.replace('_', '/')
-                    self.copyDirToRepo(project,
-                                       os.path.join(git_path, reponame))
+
+        if not self.setupSimpleLayout():
+            if hasattr(self, 'tenant_config_file'):
+                self.config.set('zuul', 'tenant_config',
+                                self.tenant_config_file)
+                git_path = os.path.join(
+                    os.path.dirname(
+                        os.path.join(FIXTURE_DIR, self.tenant_config_file)),
+                    'git')
+                if os.path.exists(git_path):
+                    for reponame in os.listdir(git_path):
+                        project = reponame.replace('_', '/')
+                        self.copyDirToRepo(project,
+                                           os.path.join(git_path, reponame))
         self.setupAllProjectKeys()
 
+    def setupSimpleLayout(self):
+        # If the test method has been decorated with a simple_layout,
+        # use that instead of the class tenant_config_file.  Set up a
+        # single config-project with the specified layout, and
+        # initialize repos for all of the 'project' entries which
+        # appear in the layout.
+        test_name = self.id().split('.')[-1]
+        test = getattr(self, test_name)
+        if hasattr(test, '__simple_layout__'):
+            path = getattr(test, '__simple_layout__')
+        else:
+            return False
+
+        path = os.path.join(FIXTURE_DIR, path)
+        with open(path) as f:
+            layout = yaml.safe_load(f.read())
+        untrusted_projects = []
+        for item in layout:
+            if 'project' in item:
+                name = item['project']['name']
+                untrusted_projects.append(name)
+                self.init_repo(name)
+                self.addCommitToRepo(name, 'initial commit',
+                                     files={'README': ''},
+                                     branch='master', tag='init')
+
+        root = os.path.join(self.test_root, "config")
+        if not os.path.exists(root):
+            os.makedirs(root)
+        f = tempfile.NamedTemporaryFile(dir=root, delete=False)
+        config = [{'tenant':
+                   {'name': 'tenant-one',
+                    'source': {'gerrit':
+                               {'config-projects': ['common-config'],
+                                'untrusted-projects': untrusted_projects}}}}]
+        f.write(yaml.dump(config))
+        f.close()
+        self.config.set('zuul', 'tenant_config',
+                        os.path.join(FIXTURE_DIR, f.name))
+
+        self.init_repo('common-config')
+        with open(path) as f:
+            files = {'zuul.yaml': f.read()}
+        self.addCommitToRepo('common-config', 'add content from fixture',
+                             files, branch='master', tag='init')
+
+        return True
+
     def setupAllProjectKeys(self):
         if self.create_project_keys:
             return
@@ -1956,8 +2031,7 @@
           - org/node-project
           - org/conflict-project
           - org/noop-project
-          - org/experimental-project
-          - org/no-jobs-project\n""" % path)
+          - org/experimental-project\n""" % path)
 
         for repo in untrusted_projects:
             f.write("          - %s\n" % repo)
diff --git a/tests/fixtures/config/single-tenant/git/common-config/zuul.yaml b/tests/fixtures/config/single-tenant/git/common-config/zuul.yaml
index b24f62e..141c78f 100644
--- a/tests/fixtures/config/single-tenant/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/single-tenant/git/common-config/zuul.yaml
@@ -225,9 +225,3 @@
             dependencies: nonvoting-project-merge
         - nonvoting-project-test2:
             dependencies: nonvoting-project-merge
-
-- project:
-    name: org/no-jobs-project
-    check:
-      jobs:
-        - project-testfile
diff --git a/tests/fixtures/config/single-tenant/git/org_no-jobs-project/README b/tests/fixtures/config/single-tenant/git/org_no-jobs-project/README
deleted file mode 100644
index 44f3bac..0000000
--- a/tests/fixtures/config/single-tenant/git/org_no-jobs-project/README
+++ /dev/null
@@ -1 +0,0 @@
-staypuft
diff --git a/tests/fixtures/config/single-tenant/main.yaml b/tests/fixtures/config/single-tenant/main.yaml
index e8a7fcb..8c3e809 100644
--- a/tests/fixtures/config/single-tenant/main.yaml
+++ b/tests/fixtures/config/single-tenant/main.yaml
@@ -20,4 +20,3 @@
           - org/conflict-project
           - org/noop-project
           - org/experimental-project
-          - org/no-jobs-project
diff --git a/tests/fixtures/layouts/no-jobs-project.yaml b/tests/fixtures/layouts/no-jobs-project.yaml
new file mode 100644
index 0000000..803e5a0
--- /dev/null
+++ b/tests/fixtures/layouts/no-jobs-project.yaml
@@ -0,0 +1,23 @@
+- pipeline:
+    name: check
+    manager: independent
+    trigger:
+      gerrit:
+        - event: patchset-created
+    success:
+      gerrit:
+        verified: 1
+    failure:
+      gerrit:
+        verified: -1
+
+- job:
+    name: project-testfile
+    files:
+      - .*-requires
+
+- project:
+    name: org/no-jobs-project
+    check:
+      jobs:
+        - project-testfile
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
index 22ae46d..366354c 100755
--- a/tests/unit/test_scheduler.py
+++ b/tests/unit/test_scheduler.py
@@ -36,6 +36,7 @@
 from tests.base import (
     ZuulTestCase,
     repack_repo,
+    simple_layout,
 )
 
 
@@ -1910,6 +1911,7 @@
         self.assertEqual(A.data['status'], 'MERGED')
         self.assertEqual(A.reported, 2)
 
+    @simple_layout('layouts/no-jobs-project.yaml')
     def test_no_job_project(self):
         "Test that reports with no jobs don't get sent"
         A = self.fake_gerrit.addFakeChange('org/no-jobs-project',